mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Keep HUD render target stable and refresh resized texture binding
This commit is contained in:
1 parent
d194ceff05
commit
d70de2625d
2 files changed
+138
-10
No files matched your search
@@ -64,6 +64,8 @@ struct Slot {
|
||||
#[derive(Resource)]
|
||||
struct Hud {
|
||||
target: Handle<Image>,
|
||||
composite: Handle<HudComposite>,
|
||||
rebind_after_resize: bool,
|
||||
runtime: hud_runtime::Runtime,
|
||||
source: serde_json::Value,
|
||||
shapes: apt_scene::Shapes,
|
||||
@@ -157,6 +159,8 @@ fn setup(
|
||||
}
|
||||
Ok(Hud {
|
||||
target: Handle::default(),
|
||||
composite: Handle::default(),
|
||||
rebind_after_resize: false,
|
||||
runtime,
|
||||
source,
|
||||
shapes,
|
||||
@@ -193,8 +197,9 @@ fn setup(
|
||||
RenderLayers::layer(31),
|
||||
Msaa::Off,
|
||||
));
|
||||
hud.composite = composites.add(HudComposite { image: target });
|
||||
commands.spawn((
|
||||
MaterialNode(composites.add(HudComposite { image: target })),
|
||||
MaterialNode(hud.composite.clone()),
|
||||
UiTargetCamera(output),
|
||||
GlobalZIndex(1),
|
||||
Pickable::IGNORE,
|
||||
@@ -214,23 +219,136 @@ fn setup(
|
||||
// Rasterize at output pixel resolution; retain the original 1280x720 APT
|
||||
// coordinate space. This avoids a second enlargement of every glyph/glow.
|
||||
fn resize_target(
|
||||
hud: Option<Res<Hud>>,
|
||||
hud: Option<ResMut<Hud>>,
|
||||
window: Single<&Window, With<PrimaryWindow>>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
mut composites: ResMut<Assets<HudComposite>>,
|
||||
) {
|
||||
let Some(hud) = hud else {
|
||||
return;
|
||||
};
|
||||
let Some(image) = images.get_mut(&hud.target) else {
|
||||
let Some(mut hud) = hud else {
|
||||
return;
|
||||
};
|
||||
// Bevy 0.18's UI material preparation has no dependency on GpuImage
|
||||
// preparation. Retry once on the following frame, when the replacement
|
||||
// image is available regardless of preparation order during the resize.
|
||||
if hud.rebind_after_resize {
|
||||
if let Some(material) = composites.get_mut(&hud.composite) {
|
||||
material.image = hud.target.clone();
|
||||
}
|
||||
}
|
||||
let size = Extent3d {
|
||||
width: window.physical_width().max(1),
|
||||
height: window.physical_height().max(1),
|
||||
depth_or_array_layers: 1,
|
||||
};
|
||||
if image.texture_descriptor.size != size {
|
||||
image.resize(size);
|
||||
hud.rebind_after_resize = resize_image(
|
||||
&mut images,
|
||||
&mut composites,
|
||||
&hud.target,
|
||||
&hud.composite,
|
||||
size,
|
||||
);
|
||||
}
|
||||
fn resize_image(
|
||||
images: &mut Assets<Image>,
|
||||
composites: &mut Assets<HudComposite>,
|
||||
target: &Handle<Image>,
|
||||
composite: &Handle<HudComposite>,
|
||||
size: Extent3d,
|
||||
) -> bool {
|
||||
if images
|
||||
.get(target)
|
||||
.is_some_and(|image| image.texture_descriptor.size != size)
|
||||
{
|
||||
// Assets::get_mut emits Modified even without a write. Doing that
|
||||
// every frame recreates the GPU target behind the compositor's
|
||||
// cached bind group. Only invalidate the image on a real resize.
|
||||
if let Some(image) = images.get_mut(target) {
|
||||
image.resize(size);
|
||||
}
|
||||
// The UI material retains its bind group. A real target resize must
|
||||
// reprepare it so it samples the replacement GPU texture view.
|
||||
if let Some(material) = composites.get_mut(composite) {
|
||||
material.image = target.clone();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hud_target_changes_only_on_resize_and_refreshes_composite() {
|
||||
// Asset scheduling only: no render plugin, window or gameplay systems.
|
||||
let mut app = App::new();
|
||||
app.add_plugins((MinimalPlugins, bevy::asset::AssetPlugin::default()))
|
||||
.init_asset::<Image>()
|
||||
.init_asset::<HudComposite>();
|
||||
let target =
|
||||
app.world_mut()
|
||||
.resource_mut::<Assets<Image>>()
|
||||
.add(Image::new_target_texture(
|
||||
1280,
|
||||
720,
|
||||
TextureFormat::Rgba8UnormSrgb,
|
||||
None,
|
||||
));
|
||||
let composite = app
|
||||
.world_mut()
|
||||
.resource_mut::<Assets<HudComposite>>()
|
||||
.add(HudComposite {
|
||||
image: target.clone(),
|
||||
});
|
||||
app.update();
|
||||
app.world_mut()
|
||||
.resource_mut::<Messages<AssetEvent<Image>>>()
|
||||
.clear();
|
||||
app.world_mut()
|
||||
.resource_mut::<Messages<AssetEvent<HudComposite>>>()
|
||||
.clear();
|
||||
for (width, height, changed) in
|
||||
[(1280, 720, false), (1920, 1080, true), (1920, 1080, false)]
|
||||
{
|
||||
app.world_mut()
|
||||
.resource_scope(|world, mut images: Mut<Assets<Image>>| {
|
||||
let mut composites = world.resource_mut::<Assets<HudComposite>>();
|
||||
resize_image(
|
||||
&mut images,
|
||||
&mut composites,
|
||||
&target,
|
||||
&composite,
|
||||
Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
});
|
||||
app.update();
|
||||
let images: Vec<_> = app
|
||||
.world_mut()
|
||||
.resource_mut::<Messages<AssetEvent<Image>>>()
|
||||
.drain()
|
||||
.collect();
|
||||
let materials: Vec<_> = app
|
||||
.world_mut()
|
||||
.resource_mut::<Messages<AssetEvent<HudComposite>>>()
|
||||
.drain()
|
||||
.collect();
|
||||
assert_eq!(
|
||||
images
|
||||
.iter()
|
||||
.any(|e| matches!(e, AssetEvent::Modified { id } if *id == target.id())),
|
||||
changed
|
||||
);
|
||||
assert_eq!(
|
||||
materials
|
||||
.iter()
|
||||
.any(|e| matches!(e, AssetEvent::Modified { id } if *id == composite.id())),
|
||||
changed
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn reset(
|
||||
|
||||
@@ -98,8 +98,8 @@ The copied build and launcher are in ignored `logs/scoring/build`. The launcher
|
||||
uses this worktree's HUD cache and the existing owned asset installation and
|
||||
starts paused. It checks required paths before running and retains failures on
|
||||
screen. The executable SHA256 is
|
||||
`cecfff20b647a56253f0aad3e9a37655143121a1ae8c883e7fc3dfc88e6e3de7`.
|
||||
The launcher now selects `skate3rust-hud-quality.exe`.
|
||||
`6853a0035cb3a8693681e574f1fae5829f2caec694657536b92217c6d8f7c63b`.
|
||||
The launcher now selects `skate3rust-hud-score-fix.exe`.
|
||||
|
||||
The missing-HUD startup defect is fixed: HUD setup now depends on presentation
|
||||
setup, so Bevy applies the deferred camera spawn before the HUD queries it.
|
||||
@@ -178,6 +178,16 @@ Air452 suspension in addition to landing lifetime, expiry and cancellation.
|
||||
Both pass, and the static-CRT release build succeeds. The game was not launched;
|
||||
pixel appearance and GPU resize behavior still require interactive verification.
|
||||
|
||||
The first window-resolution build introduced a render-target invalidation
|
||||
regression: Bevy 0.18.1 `Assets::get_mut` queues a Modified event even for a
|
||||
read-only inspection. The size check therefore recreated the GPU image each
|
||||
frame while the UI material retained its earlier texture binding. The check
|
||||
now uses immutable access; a real resize refreshes the compositor material,
|
||||
including a following-frame refresh to account for independent image/material
|
||||
preparation order. An asset-only regression test checks that an unchanged
|
||||
target emits no Modified event and a resize invalidates both image and material.
|
||||
The test uses no window, renderer or gameplay systems.
|
||||
|
||||
## Native parity gaps
|
||||
|
||||
These are implementation gaps, not merely missing gameplay validation:
|
||||
|
||||
Reference in new issue
Block a user