Skip to content

Upload tile delta to GPU#4159

Open
evanpelle wants to merge 1 commit into
mainfrom
territory-perf
Open

Upload tile delta to GPU#4159
evanpelle wants to merge 1 commit into
mainfrom
territory-perf

Conversation

@evanpelle
Copy link
Copy Markdown
Collaborator

@evanpelle evanpelle commented Jun 4, 2026

Description

Reduces the amount of tile data sent to the gpu each tick, roughly ~10fps rate increase on 10 year old chromebook.

Two changes to the territory rendering path:

1. Split passEnabled.mapOverlay into four flags

The single mapOverlay toggle controlled four unrelated passes (territory fill, border compute, border stamp, trail). Splits it into territory, borderCompute, borderStamp, trail so each can be toggled independently in the debug GUI. Pure rename — default behavior is unchanged (all four default to true).

2. GPU scatter for per-frame tile texture updates

Replaces the dirty-row bbox texSubImage2D upload in TerritoryPass with a new TileScatterPass that uploads a small attribute buffer of (x, y, state) patches and runs a single POINTS draw into an FBO bound to tileTex. Each patch rasterizes as a 1×1 point into exactly its target texel.

Why: the old path's cost scaled with the bounding box of the dirty rows, not the number of changed tiles. In typical play, tile changes are spread across the whole map (multiple players fighting in different regions, scattered trails/fallout), so the bbox covered most of the map's rows and we re-uploaded mostly-unchanged data every frame. The new path is constant cost in patch count regardless of spatial distribution, and no longer scales with map size.

The full-upload path (initial load / seek / spawn-phase flush) is unchanged. fullUploadPending correctly supersedes any queued scatter patches.

Please complete the following:

  • I have added screenshots for all UI updates (N/A — no UI changes)
  • I process any text displayed to the user through translateText() and I've added it to the en.json file (N/A — no user-facing text)
  • I have added relevant tests to the test directory (renderer code, not covered by unit tests; verified visually)

Please put your Discord username so you can be contacted if a bug or regression is found:

evan

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 5, 2026

Review Change Stack

Walkthrough

This PR refactors the render pass enable system by splitting a shared mapOverlay flag into four independent toggles and introduces a new TileScatterPass GPU utility for efficient incremental territory tile updates. TerritoryPass is refactored to use scatter-based patching instead of dirty-row tracking.

Changes

Render pass routing and scatter-based tile uploads

Layer / File(s) Summary
Configuration and pass enable flags
src/client/render/gl/RenderSettings.ts, src/client/render/gl/render-settings.json
RenderSettings.passEnabled interface adds territory, borderCompute, borderStamp, and trail boolean fields while removing mapOverlay. Default configuration is updated to enable these new passes.
TileScatterPass GPU rendering pass
src/client/render/gl/passes/TileScatterPass.ts
New exported TileScatterPass class manages per-tile patch accumulation in CPU buffers, GPU VBO uploads with capacity growth, and rendering via shader into tile textures. Provides push(x, y, state) queueing, flush() execution, count query, clear(), and dispose() cleanup.
TerritoryPass scatter-based tile updates
src/client/render/gl/passes/TerritoryPass.ts
Refactored to use TileScatterPass for incremental uploads. Introduces fullUploadPending flag to distinguish full refreshes (seek/spawn/live reset) from per-frame drip updates. Removes old dirty-row min/max tracking and texSubImage2D partial uploads; flushTileTexture now routes through scatter patches or full texture uploads based on fullUploadPending state.
Renderer pass enable routing
src/client/render/gl/Renderer.ts
Updated computeTextures(), drawBaseLayer(), and renderOverlays() to gate each pass on its dedicated flag: borderPassborderCompute, territoryPassterritory, borderStampPassborderStamp, trailPasstrail.
Debug UI pass enable toggles
src/client/render/gl/debug/Layout.ts
Added debug UI toggles for territory, borderCompute, and borderStamp in the "Pass Enables" folder, replacing the single mapOverlay toggle.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • openfrontio/OpenFrontIO#3966: Modifies TerritoryPass and Renderer to render territory patterns via new textures and uniforms; overlaps with the main PR's refactoring of both files.
  • openfrontio/OpenFrontIO#3973: Extends TerritoryPass drip/bucket tile update mechanism that the main PR refactors to use TileScatterPass.
  • openfrontio/OpenFrontIO#3968: Modifies TerritoryPass tile upload logic including dirty-row tracking and full-upload detection that the main PR replaces with fullUploadPending and scatter-based routing.

Suggested labels

UI/UX

Poem

🎨 Four flags bloom where one did grow,
Scatter patches steal the show,
Territory tiles now dance with grace,
Each pass controls its own small space! 🌍

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ⚠️ Warning The title 'Upload tile delta to GPU' describes a specific implementation detail, but the changeset shows broader refactoring: the addition of TileScatterPass, updates to multiple render passes, configuration changes, and debug UI expansion. Consider a more comprehensive title like 'Refactor territory rendering with TileScatterPass for GPU tile delta updates' that captures the main architectural changes, or clarify if the PR focuses specifically on tile delta uploads.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description clearly explains the changes: splitting mapOverlay into four flags and implementing GPU scatter for tile texture updates to improve performance.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/client/render/gl/passes/TileScatterPass.ts (1)

53-62: ⚡ Quick win

Add FBO completeness check after attaching texture.

If tileTex has an incompatible format or is incomplete, the FBO will silently fail. A quick check helps catch shader/texture setup bugs early.

🛡️ Suggested FBO completeness check
     gl.framebufferTexture2D(
       gl.FRAMEBUFFER,
       gl.COLOR_ATTACHMENT0,
       gl.TEXTURE_2D,
       tileTex,
       0,
     );
+    if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
+      console.error("TileScatterPass: FBO incomplete");
+    }
     gl.bindFramebuffer(gl.FRAMEBUFFER, null);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/render/gl/passes/TileScatterPass.ts` around lines 53 - 62, After
attaching tileTex to the framebuffer in TileScatterPass (this.fbo), call
gl.checkFramebufferStatus(gl.FRAMEBUFFER) and verify it equals
gl.FRAMEBUFFER_COMPLETE; if not, log or throw a clear error including the
returned status so texture/format incompatibilities are detected early. Add this
check right after the framebufferTexture2D call and before unbinding the
framebuffer (use the this.fbo and tileTex symbols to locate the code), and
ensure the code cleans up or fails fast when the status is not COMPLETE.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/render/gl/passes/TileScatterPass.ts`:
- Around line 114-127: The flush() method in TileScatterPass sets
gl.viewport(0,0,this.mapW,this.mapH) for the FBO but never restores the previous
viewport, causing subsequent draws (e.g. drawBaseLayer()) to render with the
wrong viewport; before calling gl.viewport save the current viewport (via
gl.getParameter(gl.VIEWPORT) or equivalent), perform the FBO draw as currently
implemented (bindFramebuffer, viewport, drawArrays, unbind), then restore the
saved viewport with gl.viewport(...) after gl.bindFramebuffer(gl.FRAMEBUFFER,
null) so the canvas rendering state is unchanged; update the method handling in
TileScatterPass.flush() (and related fbo/viewport logic) to use this
save/restore sequence.

---

Nitpick comments:
In `@src/client/render/gl/passes/TileScatterPass.ts`:
- Around line 53-62: After attaching tileTex to the framebuffer in
TileScatterPass (this.fbo), call gl.checkFramebufferStatus(gl.FRAMEBUFFER) and
verify it equals gl.FRAMEBUFFER_COMPLETE; if not, log or throw a clear error
including the returned status so texture/format incompatibilities are detected
early. Add this check right after the framebufferTexture2D call and before
unbinding the framebuffer (use the this.fbo and tileTex symbols to locate the
code), and ensure the code cleans up or fails fast when the status is not
COMPLETE.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 608fc816-26fc-4ff6-af61-f65f5abdabc4

📥 Commits

Reviewing files that changed from the base of the PR and between 2c2390d and 052ff1c.

⛔ Files ignored due to path filters (2)
  • src/client/render/gl/shaders/map-overlay/tile-scatter.frag.glsl is excluded by !**/*.glsl
  • src/client/render/gl/shaders/map-overlay/tile-scatter.vert.glsl is excluded by !**/*.glsl
📒 Files selected for processing (6)
  • src/client/render/gl/RenderSettings.ts
  • src/client/render/gl/Renderer.ts
  • src/client/render/gl/debug/Layout.ts
  • src/client/render/gl/passes/TerritoryPass.ts
  • src/client/render/gl/passes/TileScatterPass.ts
  • src/client/render/gl/render-settings.json

Comment on lines +114 to +127
gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo);
gl.viewport(0, 0, this.mapW, this.mapH);
gl.disable(gl.BLEND);

gl.useProgram(this.program);
gl.uniform2f(this.uMapSize, this.mapW, this.mapH);

gl.bindVertexArray(this.vao);
gl.drawArrays(gl.POINTS, 0, this.patchCount);

gl.bindFramebuffer(gl.FRAMEBUFFER, null);

this.patchCount = 0;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Viewport is not restored after FBO draw.

flush() sets viewport to (0, 0, mapW, mapH) for the tile texture FBO, but doesn't restore it. Per Renderer.ts, flushTileTexture() runs during the texture upload phase before drawBaseLayer(). If the canvas size differs from the map size, subsequent draws will use the wrong viewport.

🐛 Save and restore viewport
     gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo);
+    const oldViewport = gl.getParameter(gl.VIEWPORT);
     gl.viewport(0, 0, this.mapW, this.mapH);
     gl.disable(gl.BLEND);

     gl.useProgram(this.program);
     gl.uniform2f(this.uMapSize, this.mapW, this.mapH);

     gl.bindVertexArray(this.vao);
     gl.drawArrays(gl.POINTS, 0, this.patchCount);

     gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+    gl.viewport(oldViewport[0], oldViewport[1], oldViewport[2], oldViewport[3]);

     this.patchCount = 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo);
gl.viewport(0, 0, this.mapW, this.mapH);
gl.disable(gl.BLEND);
gl.useProgram(this.program);
gl.uniform2f(this.uMapSize, this.mapW, this.mapH);
gl.bindVertexArray(this.vao);
gl.drawArrays(gl.POINTS, 0, this.patchCount);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
this.patchCount = 0;
}
gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo);
const oldViewport = gl.getParameter(gl.VIEWPORT);
gl.viewport(0, 0, this.mapW, this.mapH);
gl.disable(gl.BLEND);
gl.useProgram(this.program);
gl.uniform2f(this.uMapSize, this.mapW, this.mapH);
gl.bindVertexArray(this.vao);
gl.drawArrays(gl.POINTS, 0, this.patchCount);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(oldViewport[0], oldViewport[1], oldViewport[2], oldViewport[3]);
this.patchCount = 0;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/render/gl/passes/TileScatterPass.ts` around lines 114 - 127, The
flush() method in TileScatterPass sets gl.viewport(0,0,this.mapW,this.mapH) for
the FBO but never restores the previous viewport, causing subsequent draws (e.g.
drawBaseLayer()) to render with the wrong viewport; before calling gl.viewport
save the current viewport (via gl.getParameter(gl.VIEWPORT) or equivalent),
perform the FBO draw as currently implemented (bindFramebuffer, viewport,
drawArrays, unbind), then restore the saved viewport with gl.viewport(...) after
gl.bindFramebuffer(gl.FRAMEBUFFER, null) so the canvas rendering state is
unchanged; update the method handling in TileScatterPass.flush() (and related
fbo/viewport logic) to use this save/restore sequence.

@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management Jun 5, 2026
@evanpelle evanpelle added this to the v32 milestone Jun 5, 2026
@evanpelle evanpelle changed the title territory Upload tile delta to GPU Jun 5, 2026
@evanpelle evanpelle marked this pull request as ready for review June 5, 2026 00:40
@evanpelle evanpelle requested a review from a team as a code owner June 5, 2026 00:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

1 participant