Render HUD at window resolution and restore authored clean glow

This commit is contained in:
chazmm committed 2026-09-08 18:25:54 +01:00
1 parent 142fe97a6d
commit d194ceff05
7 files changed
+123 -14

No files matched your search

+7
View File
@@ -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| {
@@ -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(())
}
+3 -1
View File
@@ -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)),
+1
View File
@@ -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);
+41 -5
View File
@@ -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<Image>,
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<Config>,
skater: Res<SkaterRuntime>,
cameras: Query<Entity, With<IsDefaultUiCamera>>,
window: Single<&Window, With<PrimaryWindow>>,
mut images: ResMut<Assets<Image>>,
mut composites: ResMut<Assets<HudComposite>>,
) {
@@ -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<Res<Hud>>,
window: Single<&Window, With<PrimaryWindow>>,
mut images: ResMut<Assets<Image>>,
) {
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<ResMut<Hud>>,
skater: Res<SkaterRuntime>,
+19 -5
View File
@@ -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::<f32>()).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;
}
+35 -2
View File
@@ -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: