From d194ceff05281d115e73da6b5b033fac2e003890 Mon Sep 17 00:00:00 2001 From: chazmm Date: Tue, 8 Sep 2026 18:25:54 +0100 Subject: [PATCH] Render HUD at window resolution and restore authored clean glow --- crates/skate-data/examples/hud_data.rs | 7 +++ .../skate-data/examples/scoring_flow_data.rs | 18 +++++++- crates/skate-game/src/hud_runtime.rs | 4 +- crates/skate-game/src/physics/frame.rs | 1 + crates/skate-game/src/scoring_hud.rs | 46 +++++++++++++++++-- crates/skate-game/src/scoring_runtime.rs | 24 ++++++++-- docs/native-scoring-hud.md | 37 ++++++++++++++- 7 files changed, 123 insertions(+), 14 deletions(-) diff --git a/crates/skate-data/examples/hud_data.rs b/crates/skate-data/examples/hud_data.rs index fdacfb4a..4740079d 100644 --- a/crates/skate-data/examples/hud_data.rs +++ b/crates/skate-data/examples/hud_data.rs @@ -61,6 +61,13 @@ fn main() -> Result<(), String> { runtime .update(input.clone(), new_trick, false, tick % 180 == 120) .map_err(|e| format!("Data frame {tick}: {e}"))?; + if tick == 10 { + if !runtime.vm.get(runtime.controller, "mLastClean").truth() + || runtime.vm.get(runtime.controller, "mLastSketchy").truth() + { + return Err("Clean landing selected the sketchy color timeline".into()); + } + } let draws = apt_scene::draw(&runtime.bindings.movie, &runtime.vm, &shapes)?; max_draws = max_draws.max(draws.len()); saw_native_shadow_pair |= draws.windows(2).any(|pair| { diff --git a/crates/skate-data/examples/scoring_flow_data.rs b/crates/skate-data/examples/scoring_flow_data.rs index a305427c..2c577f1e 100644 --- a/crates/skate-data/examples/scoring_flow_data.rs +++ b/crates/skate-data/examples/scoring_flow_data.rs @@ -36,6 +36,7 @@ fn frame( fakie: false, nollie: false, body_flip: false, + suspend_air: false, landing: Default::default(), teleported: false, reverting: false, @@ -131,8 +132,23 @@ fn main() -> Result<(), String> { if expired.hud_input().sequence_score != 0 || expired.hud_input().line_time != 0. { return Err("Expired line retained its HUD score".into()); } + // Native Air452 suspends continuous air metrics without removing the carrier. + let mut suspended = scoring_runtime::Runtime::load(&data)?; + suspended.advance(frame(0, FilteredCategory::Ground, None))?; + for tick in 1..61 { + suspended.advance(frame(tick, FilteredCategory::Air, Some(kickflip)))?; + } + let held_score = suspended.hud_input().sequence_score; + let mut freeze = frame(61, FilteredCategory::Air, Some(kickflip)); + freeze.suspend_air = true; + freeze.position = [100., 100., 100.]; + freeze.body_flip = true; + suspended.advance(freeze)?; + if suspended.hud_input().sequence_score != held_score { + return Err("Air452 suspension accrued distance/height reward".into()); + } println!( - "Scoring data audit: authored kickflip credited once; landing display persists; timer uses seconds; line expiry clears score; teleport cancels pending rewards and multiplier" + "Scoring data audit: authored kickflip credited once; landing display persists; timer uses seconds; line expiry clears score; teleport cancels pending rewards and multiplier; Air452 freezes continuous metrics" ); Ok(()) } diff --git a/crates/skate-game/src/hud_runtime.rs b/crates/skate-game/src/hud_runtime.rs index afa55279..66da1da8 100644 --- a/crates/skate-game/src/hud_runtime.rs +++ b/crates/skate-game/src/hud_runtime.rs @@ -106,8 +106,10 @@ impl Host for Bindings { [ Value::Number(self.input.sequence_score as f64), Value::Text(score(self.input.sequence_score)), - Value::Bool(self.input.clean), + // 825C2D90 exposes backend152/153; the shipped + // UpdateTrickScoring reads [2] as sketchy, [3] as clean. Value::Bool(self.input.sketchy), + Value::Bool(self.input.clean), Value::Number(self.input.sequence_timer as f64), Value::Number(self.input.line_score as f64), Value::Text(score(self.input.line_score)), diff --git a/crates/skate-game/src/physics/frame.rs b/crates/skate-game/src/physics/frame.rs index 78c04ccd..62b15169 100644 --- a/crates/skate-game/src/physics/frame.rs +++ b/crates/skate-game/src/physics/frame.rs @@ -237,6 +237,7 @@ pub(super) fn advance( forward:deck_frame.basis.columns[2], switch:skater.animation.packet.riding_switch,fakie:skater.animation.packet.riding_fakie, nollie:skater.animation.packet.weight_forwards,body_flip:skater.player_input.physical.air.flag_441!=0, + suspend_air:skater.player_input.physical.air.use_air_reckoning_452!=0, landing:skater.landing_quality,teleported,reverting:skater.player_state.state_flags[70-52], })?; super::climbing::approach::advance(physics, skater, controls); diff --git a/crates/skate-game/src/scoring_hud.rs b/crates/skate-game/src/scoring_hud.rs index 47a5074e..f4de6174 100644 --- a/crates/skate-game/src/scoring_hud.rs +++ b/crates/skate-game/src/scoring_hud.rs @@ -2,7 +2,7 @@ use crate::{apt_scene, config::Config, hud_runtime, physics::SkaterRuntime}; use bevy::{ asset::{RenderAssetUsages, embedded_asset}, - camera::{RenderTarget, visibility::RenderLayers}, + camera::{RenderTarget, ScalingMode, visibility::RenderLayers}, prelude::*, render::render_resource::{ AsBindGroup, BlendState, Extent3d, PrimitiveTopology, RenderPipelineDescriptor, ShaderType, @@ -11,6 +11,7 @@ use bevy::{ shader::ShaderRef, sprite_render::{AlphaMode2d, Material2d, Material2dPlugin, MeshMaterial2d}, ui_render::UiMaterialPlugin, + window::PrimaryWindow, }; use std::{collections::BTreeMap, path::PathBuf}; @@ -62,6 +63,7 @@ struct Slot { } #[derive(Resource)] struct Hud { + target: Handle, runtime: hud_runtime::Runtime, source: serde_json::Value, shapes: apt_scene::Shapes, @@ -91,7 +93,9 @@ impl Plugin for ScoringHudPlugin { ) .add_systems( Update, - (reset, render).chain().after(crate::app::FrameSet::Physics), + (resize_target, reset, render) + .chain() + .after(crate::app::FrameSet::Physics), ); } } @@ -100,6 +104,7 @@ fn setup( config: Res, skater: Res, cameras: Query>, + window: Single<&Window, With>, mut images: ResMut>, mut composites: ResMut>, ) { @@ -151,6 +156,7 @@ fn setup( textures.insert(path, images.add(image)); } Ok(Hud { + target: Handle::default(), runtime, source, shapes, @@ -161,15 +167,23 @@ fn setup( }) })(); match result { - Ok(hud) => { + Ok(mut hud) => { let target = images.add(Image::new_target_texture( - 1280, - 720, + window.physical_width().max(1), + window.physical_height().max(1), TextureFormat::Rgba8UnormSrgb, None, )); + hud.target = target.clone(); commands.spawn(( Camera2d, + Projection::Orthographic(OrthographicProjection { + scaling_mode: ScalingMode::Fixed { + width: 1280., + height: 720., + }, + ..OrthographicProjection::default_2d() + }), Camera { order: -1, clear_color: ClearColorConfig::Custom(Color::NONE), @@ -197,6 +211,28 @@ fn setup( Err(error) => error!("Original scoring HUD could not load: {error}"), } } +// 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>, + window: Single<&Window, With>, + mut images: ResMut>, +) { + let Some(hud) = hud else { + return; + }; + let Some(image) = images.get_mut(&hud.target) else { + return; + }; + 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); + } +} fn reset( hud: Option>, skater: Res, diff --git a/crates/skate-game/src/scoring_runtime.rs b/crates/skate-game/src/scoring_runtime.rs index e2569faf..b28324c6 100644 --- a/crates/skate-game/src/scoring_runtime.rs +++ b/crates/skate-game/src/scoring_runtime.rs @@ -36,6 +36,7 @@ pub(crate) struct Frame { pub fakie: bool, pub nollie: bool, pub body_flip: bool, + pub suspend_air: bool, pub landing: skate_core::animation::landing_quality::Output, pub teleported: bool, pub reverting: bool, @@ -48,6 +49,7 @@ pub(crate) struct Runtime { held: [f32; 4], distance: [f32; 4], metric_rewards: [f32; 4], + metric_started: [bool; 4], start: [f32; 3], previous: [f32; 3], previous_heading: f32, @@ -83,6 +85,7 @@ impl Runtime { held: [0.; 4], distance: [0.; 4], metric_rewards: [0.; 4], + metric_started: [false; 4], start: [0.; 3], previous: [0.; 3], previous_heading: 0., @@ -175,6 +178,7 @@ impl Runtime { } } if !keep_metric { + self.metric_started[slot] = false; self.held[slot] = 0.; self.distance[slot] = 0.; self.metric_rewards[slot] = 0.; @@ -269,8 +273,16 @@ impl Runtime { self.air_repetition_set = true; } } - if c.announced { - self.held[slot] += f.dt; + let distance_metric = self.collector == Collector::Grind + || self.collector == Collector::Ground && slot < 3; + let was_active = self.metric_started[slot]; + self.metric_started[slot] = true; + if c.announced && !(self.collector == Collector::Air && f.suspend_air) { + // 82DB0588 starts a distance collector at time/distance zero. + // Every active frame still updates its reference position. + if !distance_metric || was_active { + self.held[slot] += f.dt; + } let previous_distance = self.distance[slot]; let displacement = std::array::from_fn::<_, 3, _>(|i| f.position[i] - self.previous[i]); @@ -283,7 +295,7 @@ impl Runtime { } else { (displacement.iter().map(|x| x * x).sum::()).sqrt() }; - if delta > f32::from_bits(0x3ba3d70a) { + if was_active && delta > f32::from_bits(0x3ba3d70a) { self.distance[slot] += delta; } let curve = match self.collector { @@ -409,7 +421,9 @@ impl Runtime { } } let mut ids = [None; 4]; - self.collector_ticks = self.collector_ticks.saturating_add(1); + if !(self.collector == Collector::Air && f.suspend_air) { + self.collector_ticks = self.collector_ticks.saturating_add(1); + } match self.collector { Collector::Ground => { ids[0] = if f.flags & 0x80000000 != 0 { @@ -459,7 +473,7 @@ impl Runtime { for (slot, id) in ids.into_iter().enumerate() { self.carrier(slot, id, &f)?; } - if self.collector == Collector::Air { + if self.collector == Collector::Air && !f.suspend_air { if self.collector_ticks > 5 || f.flags & 0x01000000 != 0 { self.sequence_active = true; } diff --git a/docs/native-scoring-hud.md b/docs/native-scoring-hud.md index 591508d5..6ce0065d 100644 --- a/docs/native-scoring-hud.md +++ b/docs/native-scoring-hud.md @@ -69,7 +69,7 @@ It is a usable integration for testing, **not a finished native-parity port**. The HUD executes the owned trickdisplay ActionScript and timelines, including score, multiplier, line meter, stance and trick-name updates. It renders original -shape atlases and font glyphs through a separate 1280x720 camera with inherited +shape atlases and font glyphs through a separate camera with inherited multiplicative/additive colors. Movie objects are collected and mesh/material slots reused. Fixed updates pause with gameplay; map-generation changes recreate the movie even while paused. Missing assets or unsupported actions are logged. @@ -98,7 +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 -`9b3458045e7078dcd6daca3e5bea89d16298315b751b4cac4f82f8a6531cd7c0`. +`cecfff20b647a56253f0aad3e9a37655143121a1ae8c883e7fc3dfc88e6e3de7`. +The launcher now selects `skate3rust-hud-quality.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. @@ -145,6 +146,38 @@ finite geometry (33 maximum batches). Release compilation passes. User screenshots guided the investigation but did not supply rendering constants; no game, reference executable or GPU capture was launched for validation. +## Output resolution and authored blue glow + +The HUD target now uses the window's physical pixel dimensions and resizes with +the window. Its orthographic projection retains the authored 1280x720 coordinate +space. Previously the adapter rasterized at 720p and enlarged the finished image, +adding blur on larger windows. This change improves rasterization without +inventing higher-resolution source artwork. + +A data-only inventory of `fedata.big`, `fedynamic.big`, `fetexture.big` and +`miscboot.big` found byte-identical duplicate trickdisplay APT/CONST/GEO/RX2 +assets and 512x512 Futura font atlases. No higher-resolution variant was found +in those banks. The inventory and hashes are retained in ignored +`logs/scoring/hud-resolution-inventory.json`. + +`GetGeneralInfo` had clean and sketchy reversed. The original +`UpdateTrickScoring` action stream reads slot2 as sketchy (0199D2) and slot3 +as clean (019A30), matching backend bytes152/153 returned by `825C2D90`. +Correcting those slots selects the authored clean color animation. The native +FontPS/Font8PS programs contain texture/color modulation; the glow is supplied +by the authored layers, not a replacement procedural effect. + +Additional collector fixes follow `82DA8A70`/`82DA93D8`: published Air452 +suspends continuous air metrics and grab accrual while preserving the carrier. +Distance collectors also start their first active frame at zero elapsed +time/distance, following `82DB0588`. + +Validation: the 1,800-frame supplied-data HUD audit asserts that clean input +selects mLastClean rather than mLastSketchy; the scoring-flow audit verifies +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. + ## Native parity gaps These are implementation gaps, not merely missing gameplay validation: