Skip to content

fix(game-ui): preserve expand layout in viewport previews - #111

Closed
SuperSuperPepper wants to merge 1 commit into
renzora:mainfrom
SuperSuperPepper:fix-ui-canvas-expand-preview
Closed

SuperSuperPepper wants to merge 1 commit into
renzora:mainfrom
SuperSuperPepper:fix-ui-canvas-expand-preview

Conversation

@SuperSuperPepper

Copy link
Copy Markdown

Render preview UI through a transparent camera with its own image target scale factor so editor UiScale does not force post-layout root scaling.

Assisted-by: OpenAI GPT-5 (gpt-5)

Summary

Fix Expand canvas previews so they use the same pre-layout scaling semantics as the runtime.

The previous preview path applied UiTransform.scale to the canvas root after layout. That could resize the rendered tree, but it could not change the logical width used during layout, so aspect ratios narrower than the reference resolution produced a different layout from the shipped game.

This change renders viewport UI through a dedicated transparent UI-only camera. Its image render target uses a per-target scale factor, allowing Bevy to resolve the preview layout with the correct logical size while leaving the editor shell's global UiScale unchanged.

Changes

  • Add an isolated transparent UI preview camera that composites onto the existing viewport image.
  • Route play-mode and Show Game UI canvases through that camera while preserving the existing offscreen UI-editor and world-space canvas paths.
  • Derive the image target scale factor from the canvas scale mode and cancel the editor's global UiScale.
  • Add a regression test for Expand, Fit, and Constant scale calculations, including the 1280x720 reference rendered into a 1225x551 target from [Bug] UiCanvas expand mode scales the full-size root in Editor viewport previe #110.

Related Issues

Closes #110

AI Assistance

Assisted-by: OpenAI GPT-5 (gpt-5)

Checklist

  • Code compiles cleanly (renzora check)
  • All existing tests pass (renzora test)
  • New tests added (if applicable)
  • No unrelated formatting or refactoring changes
  • Branch is up to date with main
  • I have read every line of this diff and can explain it in review
  • Any AI assistance is disclosed above and in an Assisted-by: commit trailer

Testing

Automated checks completed:

cargo test --profile dist -p renzora_ember_editor
3 passed; 0 failed

CI test scope from .github/workflows/test.yml, run with --profile dist
passed

CI workspace Clippy scope from .github/workflows/test.yml, run with --profile dist
passed

cargo clippy --manifest-path xtask/Cargo.toml -- -D warnings
passed

cargo clippy --manifest-path tools/updater/Cargo.toml -- -D warnings
passed

cargo renzora dist
passed; fresh Windows editor staged successfully

git diff --check; branch compared with upstream/main
passed; ahead 1, behind 0

The renzora CLI and Docker are not installed on the validation host, so the
documented Cargo commands from the repository's current CI workflow were run
directly instead. The workspace test and Clippy invocations used the workflow's
vendored-crate exclusion lists.

Manual editor verification is still required before submission. The freshly
staged editor launches successfully. With an Expand canvas using a 1280x720
reference resolution, verify the Scene overlay, Editor Play, standalone runtime,
save/reload, and the UI editor panel at:

  • 1280x720
  • 1920x1080
  • 1920x1200
  • 1225x551

Confirm that the preview matches runtime layout, the 64px test bar remains
flush with the viewport bottom, the scene remains visible behind the UI, the UI
editor preview is unchanged, and world-space canvases are unaffected.

Render preview UI through a transparent camera with its own image target scale factor so editor UiScale does not force post-layout root scaling.

Assisted-by: OpenAI GPT-5 (gpt-5)
@RenzoraEngine

Copy link
Copy Markdown
Contributor

Tested on Windows at 18cae67. The Expand fix is correct — verified at several aspect ratios in the editor and the standalone runtime. But it can't merge yet: it blanks the editor viewport and adds a crash.

Root cause

In Bevy 0.19 the scale factor is part of the render target's identity, not metadata on it:

impl PartialEq for ImageRenderTarget {
    fn eq(&self, other: &Self) -> bool {
        self.handle == other.handle && FloatOrd(self.scale_factor) == FloatOrd(other.scale_factor)
    }
}

Hash and Ord include it too, and both maps that decide where a camera draws are keyed on that identity (ViewTargetAttachments, MainTextureKey).

A second camera with ClearColorConfig::None composites over the first only because they share a main texture. The editor camera targets Image(handle, 1.0); the preview camera targets Image(handle, 0.6167). Different key, so it gets its own empty texture and writes that out over the image, replacing the scene instead of drawing on top of it.

The per-target scale factor is therefore both what makes the fix work and what breaks compositing.

Symptoms

  1. Black backdrop. The scene disappears behind the preview UI in the editor. The runtime is unaffected, since this camera is editor-only.
  2. Constant works. It is the only mode whose factor is 1.0, so the only mode that shares the target. That is the theory confirmed from the other direction.
  3. Constant + resizing the viewport panel crashes. Set Scale Mode to constant, resize the panel:
Unrecoverable GPU render error (Validation):
  Attachments have differing sizes: the depth attachment's texture view has
  extent (1269, 622, 1) but is followed by the color attachment at index 0's
  texture view which has (1269, 623, 1)

Sharing a target is what makes an attachment-size disagreement possible, so 2 and 3 are the same coin.

Two directions

  • Render the preview UI into its own image and composite that over the viewport as a UI node. The offscreen UI-editor path already renders a canvas to a dedicated target.
  • Keep scale_factor at 1.0 so the camera shares the scene's target, and obtain the reduced logical size another way.

Smaller items

  • Missing RELEASE_NOTES.md line.
  • A preview-derived UiTransform already saved into a scene is never reset, so it compounds with the new scaling. Pre-existing, happens on main too, but this PR is where it starts to double up.
  • camera.is_active is written unguarded every frame, marking Camera changed every frame. The render-target write next to it is guarded.
  • order: 1 is hardcoded. A game camera authored at order 1 or above draws over the preview UI.
  • Tried and did not fix it, so nobody repeats them: Msaa::Off, Hdr, and binding the render target before the camera activates.

RenzoraEngine added a commit that referenced this pull request Sep 18, 2026
A screen canvas previewed over the viewport was scaled only after layout,
so `Expand` could not work. Its root is already `100% x 100%`, and a
transform can resize the rendered tree but cannot change the width layout
resolved against. A 64px bar anchored to the bottom was laid out against
the true viewport and then shrunk about its centre, leaving it floating
clear of the edge it was pinned to; above the reference resolution the
same arithmetic pushed the edges off-screen instead.

The runtime does this by moving the global `UiScale` before layout. The
editor cannot, because one resource drives every bevy_ui tree in the
process, so the preview now reproduces both halves per-canvas: lay the
root out in a box of `target / scale` logical pixels, then scale that box
by `scale` and translate it so the centre-scaled result stays flush with
the target corner. `Fit` is untouched, since its root is the reference box
a centre-scale is meant to move, and `Constant` scales by 1.0.

The root rect is normally an invariant re-established every frame, so the
preview marks the canvases it owns. That marker is deliberately not
`Reflect`: a saved scene must never come back exempt from the healer.

Also heals the root `UiTransform` back to identity. The old preview wrote
its scale there and scene save serializes reflected components, so the
factor was baked into `.bsn` files and a shipped game applied it on top of
its own `UiScale`. Healing rather than migrating means an already-poisoned
scene repairs itself on load.

Issue #110 was diagnosed and first fixed by SuperSuperPepper in #111. That
approach gave the preview its own camera with a per-target scale factor,
which is sound for layout but cannot composite: a scale factor is part of a
render target's identity in Bevy 0.19, and two cameras draw onto one image
only by sharing a main texture keyed on that identity. The preview camera
got its own texture and wrote it out over the scene, blanking the viewport.

Closes #110
@RenzoraEngine

Copy link
Copy Markdown
Contributor

Thanks for this, and sorry to close it rather than merge it.

Your diagnosis of #110 was right, and so was the fix: the editor was scaling the tree after layout when the runtime changes the logical size before it, and Expand cannot survive that. I've landed a fix in 3e8d951 that takes the same insight to a different place, and credited you in the commit message.

What changed is only how the preview composites. Giving the preview its own camera with a per-target scale factor is correct for layout, but it cannot draw over the viewport. In Bevy 0.19 a scale factor is part of a render target's identity (ImageRenderTarget compares and hashes on it), and two cameras composite onto one image only by sharing a main texture, which is keyed on that identity. So a preview camera at any factor but 1.0 got its own texture and wrote it out over the scene instead of onto it. That showed up as a black viewport in every mode except constant, which is the one mode whose factor is 1.0, and as a GPU validation crash when constant shared the target and a resize left the depth and colour attachments a pixel apart.

The landed version keeps the root full-target and reproduces the runtime's semantics in the layout instead: lay the root out in a box of target / scale logical pixels, then scale that box and translate it so the centre-scaled result stays flush with the corner. No second camera, so nothing has to share anything.

It also picked up the persistence risk the issue raised. The old preview wrote its scale to the root UiTransform and scene save serializes reflected components, so the factor was being baked into .bsn files. There's now a healer that resets it, which repairs already-affected scenes on load.

Two things from your PR that I'd have missed on my own: projection.set_changed() being required because camera_system doesn't watch Changed<RenderTarget>, and the regression test pinning the 1225x551 case from the issue. The equivalent test on main uses your worked example.

Please do send more. This was a good catch and a good read of the problem.

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.

[Bug] UiCanvas expand mode scales the full-size root in Editor viewport previe

2 participants