mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-27 19:54:40 +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)]
|
#[derive(Resource)]
|
||||||
struct Hud {
|
struct Hud {
|
||||||
target: Handle<Image>,
|
target: Handle<Image>,
|
||||||
|
composite: Handle<HudComposite>,
|
||||||
|
rebind_after_resize: bool,
|
||||||
runtime: hud_runtime::Runtime,
|
runtime: hud_runtime::Runtime,
|
||||||
source: serde_json::Value,
|
source: serde_json::Value,
|
||||||
shapes: apt_scene::Shapes,
|
shapes: apt_scene::Shapes,
|
||||||
@@ -157,6 +159,8 @@ fn setup(
|
|||||||
}
|
}
|
||||||
Ok(Hud {
|
Ok(Hud {
|
||||||
target: Handle::default(),
|
target: Handle::default(),
|
||||||
|
composite: Handle::default(),
|
||||||
|
rebind_after_resize: false,
|
||||||
runtime,
|
runtime,
|
||||||
source,
|
source,
|
||||||
shapes,
|
shapes,
|
||||||
@@ -193,8 +197,9 @@ fn setup(
|
|||||||
RenderLayers::layer(31),
|
RenderLayers::layer(31),
|
||||||
Msaa::Off,
|
Msaa::Off,
|
||||||
));
|
));
|
||||||
|
hud.composite = composites.add(HudComposite { image: target });
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
MaterialNode(composites.add(HudComposite { image: target })),
|
MaterialNode(hud.composite.clone()),
|
||||||
UiTargetCamera(output),
|
UiTargetCamera(output),
|
||||||
GlobalZIndex(1),
|
GlobalZIndex(1),
|
||||||
Pickable::IGNORE,
|
Pickable::IGNORE,
|
||||||
@@ -214,23 +219,136 @@ fn setup(
|
|||||||
// Rasterize at output pixel resolution; retain the original 1280x720 APT
|
// Rasterize at output pixel resolution; retain the original 1280x720 APT
|
||||||
// coordinate space. This avoids a second enlargement of every glyph/glow.
|
// coordinate space. This avoids a second enlargement of every glyph/glow.
|
||||||
fn resize_target(
|
fn resize_target(
|
||||||
hud: Option<Res<Hud>>,
|
hud: Option<ResMut<Hud>>,
|
||||||
window: Single<&Window, With<PrimaryWindow>>,
|
window: Single<&Window, With<PrimaryWindow>>,
|
||||||
mut images: ResMut<Assets<Image>>,
|
mut images: ResMut<Assets<Image>>,
|
||||||
|
mut composites: ResMut<Assets<HudComposite>>,
|
||||||
) {
|
) {
|
||||||
let Some(hud) = hud else {
|
let Some(mut hud) = hud else {
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(image) = images.get_mut(&hud.target) else {
|
|
||||||
return;
|
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 {
|
let size = Extent3d {
|
||||||
width: window.physical_width().max(1),
|
width: window.physical_width().max(1),
|
||||||
height: window.physical_height().max(1),
|
height: window.physical_height().max(1),
|
||||||
depth_or_array_layers: 1,
|
depth_or_array_layers: 1,
|
||||||
};
|
};
|
||||||
if image.texture_descriptor.size != size {
|
hud.rebind_after_resize = resize_image(
|
||||||
image.resize(size);
|
&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(
|
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
|
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
|
starts paused. It checks required paths before running and retains failures on
|
||||||
screen. The executable SHA256 is
|
screen. The executable SHA256 is
|
||||||
`cecfff20b647a56253f0aad3e9a37655143121a1ae8c883e7fc3dfc88e6e3de7`.
|
`6853a0035cb3a8693681e574f1fae5829f2caec694657536b92217c6d8f7c63b`.
|
||||||
The launcher now selects `skate3rust-hud-quality.exe`.
|
The launcher now selects `skate3rust-hud-score-fix.exe`.
|
||||||
|
|
||||||
The missing-HUD startup defect is fixed: HUD setup now depends on presentation
|
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.
|
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;
|
Both pass, and the static-CRT release build succeeds. The game was not launched;
|
||||||
pixel appearance and GPU resize behavior still require interactive verification.
|
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
|
## Native parity gaps
|
||||||
|
|
||||||
These are implementation gaps, not merely missing gameplay validation:
|
These are implementation gaps, not merely missing gameplay validation:
|
||||||
|
|||||||
Reference in new issue
Block a user