Skip to content

fix(render): honour render_scale from the first frame on every host#88

Merged
proggeramlug merged 1 commit into
mainfrom
fix/render-target-boot-scale
Jul 11, 2026
Merged

fix(render): honour render_scale from the first frame on every host#88
proggeramlug merged 1 commit into
mainfrom
fix/render-target-boot-scale

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

The bug

Renderer::new_impl sizes the depth/HDR/G-buffer targets to the full surface, but the pass chain runs at render_extent() (surface * render_scale, 0.5 by default). resize() is the only thing that reconciles the two — and the only caller that ran it at boot was the Windows crate, which patched itself in place (native/windows/src/lib.rs:678).

Every other host boots through attach_enginemacOS, iOS/iPadOS, tvOS, Linux, Android, visionOS — and never resizes on frame 1: those platforms poll the OS size and resize only when it changes, which is false on a plain windowed launch. Nothing in the engine's TS boot path calls setTaaEnabled/setRenderScale either, so a default game stays in the mismatched state.

Consequences on those platforms:

  • Post-FX silently drops out — compute passes dispatch at half extent over full-size targets.
  • Fatal first-frame wgpu validation error once a scene-reading translucent material is in view: the depth-snapshot copy spans a partial depth texture. This is exactly the crash the Windows fix was chasing; it was never fixed anywhere else.

Evidence (M1 Max / Metal)

Before this change, the macOS boot path reported render_extent 128×128 against 256×256 targets, and rendering the same scene through the macOS boot path vs. the (resized) Windows boot path differed by mean 1.076/255, max 191, with the bloom/glow highlights missing entirely on macOS.

The fix

Do it at the root: Renderer::new_impl now runs resize() before returning, so construction-time targets always honour render_scale and every platform inherits it. The Windows band-aid is removed — it was fixing one platform and hiding the bug for the other six.

Tests

The golden suite structurally cannot catch this: its harness calls set_taa_enabled(false) immediately after construction, which internally resizes and repairs the invariant before anything renders.

So this adds native/shared/tests/render_targets.rs, asserting render_target_extent() == render_extent() straight out of the constructor (the real frame-1 state), and that the invariant survives set_render_scale / resize / set_taa_enabled. Needs one new accessor, Renderer::render_target_extent().

  • 100 unit tests, 7/7 goldens (all unchanged), 2 new invariant tests — green on Metal.
  • All 7 goldens passing unchanged is the evidence this is pixel-neutral wherever a resize already happened, and only changes behaviour where it was genuinely broken.
  • macos, ios, tvos crates and the shared WASM target all compile; validate-ffi clean.

Also

Files EN-024: iOS/iPadOS/tvOS report pixels as the logical size while macOS reports points. That makes the physical-resolution glyph work (60ba704) inert on Apple mobile (dpi = physical / logical is always 1.0 there) and diverges game-facing coordinates across Apple targets. Left as a decision rather than folded in here — both resolutions are breaking changes.

Note: pre-existing CI red on main

tools/check-file-lines.js already fails on clean main, independently of this PR:

  • native/shared/src/renderer/mod.rs: 12118 lines vs. its grandfathered 11985 baseline
  • native/shared/src/renderer/material_system.rs: 2054 lines vs. the 2000 limit

This PR adds 23 lines to mod.rs (already over). Fixing that debt is a split/refactor that doesn't belong in a bug fix — flagging it so it isn't mistaken for fallout from this change.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed initial rendering so display scaling is applied correctly from the first frame.
    • Improved consistency of render resolution during window resizing, render-scale changes, and graphics option updates.
    • Prevented mismatches between the configured render extent and allocated rendering targets.
  • Documentation

    • Added documentation outlining Apple platform logical-size reporting differences and their impact on rendering and game coordinate conventions.

`Renderer::new_impl` sizes the depth/HDR/G-buffer targets to the full
surface, but the pass chain runs at `render_extent()` (surface *
render_scale, 0.5 by default). `resize()` is the only thing that
reconciles the two, and until now the only caller that ran it at boot
was the Windows crate, which patched itself in-place.

Every other host boots through `attach_engine` (macOS, iOS/iPadOS,
tvOS, Linux, Android, visionOS) and never resizes on frame 1: those
platforms poll the OS size and only resize when it *changes*, which is
false on a plain windowed launch. They rendered against targets that
ignored render_scale — post-FX silently dropped out, and the
depth-snapshot copy spans a partial depth texture, which wgpu rejects
outright once a scene-reading translucent material is in view (the
fatal first-frame validation error the Windows fix was chasing).

Verified on an M1 Max: before this change the macOS boot path rendered
visibly differently from the resized Windows path (mean 1.076/255, max
191, bloom/glow highlights missing); render_extent reported 128x128
against 256x256 targets.

Fix it at the root instead: `Renderer::new_impl` now runs `resize()`
before returning, so construction-time targets are always correct and
every platform inherits it. The Windows band-aid is removed — it was
masking the bug for one platform and hiding it for the rest.

The golden suite structurally cannot catch this: its harness calls
`set_taa_enabled(false)` right after construction, which internally
resizes and repairs the invariant before anything renders. So add
tests/render_targets.rs, which asserts render_target_extent() ==
render_extent() straight out of the constructor (the real frame-1
state) and that it survives set_render_scale / resize / set_taa_enabled.
Needs one new accessor, `Renderer::render_target_extent()`.

All 7 goldens pass unchanged, which is the evidence this is
pixel-neutral wherever a resize already happened and only changes
behaviour where it was genuinely broken.

Also files EN-024: iOS/iPadOS/tvOS report pixels as the logical size
(macOS reports points), which makes the physical-resolution glyph work
inert on Apple mobile and diverges game-facing coordinates. Left as a
decision, not folded in here — both resolutions are breaking.
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Renderer construction now synchronizes render targets with the effective render extent, Windows initialization uses this behavior, GPU tests verify sizing invariants, and documentation adds an Apple pixel-convention decision ticket.

Changes

Renderer target synchronization

Layer / File(s) Summary
Initialize and expose render target extent
native/shared/src/renderer/mod.rs, native/shared/tests/render_targets.rs
Renderer::new routes initial dimensions through resize, exposes render_target_extent(), and tests synchronization across construction, scaling, resizing, and TAA changes.
Use renderer-managed initialization
native/windows/src/lib.rs
Windows initialization removes the explicit construction-time resize and relies on Renderer::new.

Apple pixel convention decision

Layer / File(s) Summary
Document Apple pixel conventions
docs/tickets.md
Adds EN-024 describing Apple platform pixel conventions, rendering and coordinate consequences, and the pending behavior decision.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: initializing render targets with render_scale from the first frame.
Description check ✅ Passed The description covers the summary, test evidence, and reviewer notes, so it satisfies the template's intent.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/render-target-boot-scale

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
native/shared/src/renderer/mod.rs (1)

6533-6547: 🚀 Performance & Scalability | 🔵 Trivial

Verify boot-time cost of the now-duplicated render-target allocation.

This correctly fixes the render-scale bug, but note every render-resolution target (depth/HDR/G-buffer/bloom/SSAO/hiz/SSR/SSGI/probe history, plus the surface reconfigure / headless-target rebuild) is now allocated twice at construction: once at full surface size inside the constructor body, then again at render_extent() size via this resize() call. This is one-time-per-boot cost on every platform (previously only Windows paid an equivalent cost), so it's worth confirming it doesn't introduce a noticeable startup-latency regression on resource-constrained targets (mobile GPUs on iOS/Android), since the PR notes compile validation for those platforms but not boot-time profiling.

🤖 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 `@native/shared/src/renderer/mod.rs` around lines 6533 - 6547, Verify boot-time
allocation cost for the renderer initialization followed by renderer.resize,
profiling representative iOS and Android devices and comparing startup latency
against the prior implementation. Confirm that the duplicate render-target
allocation does not cause a noticeable regression; if it does, initialize render
targets at render_extent() during construction so the first resize does not
rebuild them.
🤖 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.

Nitpick comments:
In `@native/shared/src/renderer/mod.rs`:
- Around line 6533-6547: Verify boot-time allocation cost for the renderer
initialization followed by renderer.resize, profiling representative iOS and
Android devices and comparing startup latency against the prior implementation.
Confirm that the duplicate render-target allocation does not cause a noticeable
regression; if it does, initialize render targets at render_extent() during
construction so the first resize does not rebuild them.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dd239e9e-adf3-4457-b252-3c9b8bbd47a1

📥 Commits

Reviewing files that changed from the base of the PR and between 2b3a299 and 4340c50.

📒 Files selected for processing (4)
  • docs/tickets.md
  • native/shared/src/renderer/mod.rs
  • native/shared/tests/render_targets.rs
  • native/windows/src/lib.rs

@proggeramlug proggeramlug merged commit 8370313 into main Jul 11, 2026
10 checks passed
@proggeramlug proggeramlug deleted the fix/render-target-boot-scale branch July 11, 2026 11:05
proggeramlug pushed a commit that referenced this pull request Jul 11, 2026
PR #87/#88's web work pushed native/web/src/lib.rs to 2034 lines
(limit 2000). Pure code motion, same pattern as input_ffi.rs: the
Phase-1c material-system FFI section (compile/params/draw submission,
instance buffers, texture arrays, reflection probes) moves to
material_ffi.rs. lib.rs 2034 -> 1546; cargo check for
wasm32-unknown-unknown green; validate-ffi passes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant