mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-27 19:54:40 +00:00
Remove unused gamepad backend and avoid redundant render preparation
This commit is contained in:
1 parent
d8de12f280
commit
842b4adb2f
5 files changed
+198
-26
No files matched your search
@@ -65,7 +65,11 @@ pub(crate) fn build(
|
||||
..default()
|
||||
}),
|
||||
..default()
|
||||
}).build().disable::<bevy::log::LogPlugin>(),
|
||||
}).build().disable::<bevy::log::LogPlugin>()
|
||||
// Gameplay and menu navigation both use raw XInput. No game system
|
||||
// consumes Bevy gamepad events/rumble; its second device backend can
|
||||
// stall PreUpdate (70.68 ms in the University capture).
|
||||
.disable::<bevy::gilrs::GilrsPlugin>(),
|
||||
)
|
||||
.insert_resource(bevy::winit::WinitSettings {focused_mode:bevy::winit::UpdateMode::Continuous,unfocused_mode:bevy::winit::UpdateMode::Continuous})
|
||||
.insert_resource(config)
|
||||
|
||||
@@ -19,6 +19,7 @@ use bevy::{
|
||||
view::ViewTarget,
|
||||
},
|
||||
};
|
||||
use std::{collections::VecDeque, sync::Mutex};
|
||||
|
||||
#[derive(Resource, Clone, ExtractResource)]
|
||||
struct Settings {
|
||||
@@ -56,6 +57,7 @@ pub(super) fn install(app: &mut App) {
|
||||
render
|
||||
.add_systems(RenderStartup, initialize)
|
||||
.add_systems(Render, upload.in_set(RenderSystems::PrepareResources))
|
||||
.add_systems(Render, prune_bindings.in_set(RenderSystems::PrepareBindGroups))
|
||||
.add_render_graph_node::<ViewNodeRunner<ExposureNode>>(Core3d, ExposureLabel)
|
||||
.add_render_graph_edges(
|
||||
Core3d,
|
||||
@@ -112,6 +114,10 @@ struct Pipeline {
|
||||
settings: Buffer,
|
||||
state: Buffer,
|
||||
sampler: Sampler,
|
||||
// The buffers, sampler and layouts are immutable for this Pipeline's life.
|
||||
// Source views can alternate, resize or belong to different cameras. Keep a
|
||||
// bounded cache so retired targets cannot accumulate across map/size changes.
|
||||
bindings: Mutex<VecDeque<(TextureViewId, BindGroup, BindGroup)>>,
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
@@ -189,6 +195,7 @@ fn initialize(
|
||||
min_filter: FilterMode::Linear,
|
||||
..default()
|
||||
}),
|
||||
bindings: default(),
|
||||
});
|
||||
}
|
||||
fn bytes<const N: usize>(values: [Vec4; N]) -> Vec<u8> {
|
||||
@@ -199,13 +206,29 @@ fn bytes<const N: usize>(values: [Vec4; N]) -> Vec<u8> {
|
||||
.collect()
|
||||
}
|
||||
fn upload(settings: Res<Settings>, pipeline: Res<Pipeline>, queue: Res<RenderQueue>) {
|
||||
let mut data = [0u8; 32];
|
||||
for (chunk, value) in data.chunks_exact_mut(4).zip(
|
||||
[settings.tuning, settings.timing].into_iter().flat_map(|v| v.to_array()),
|
||||
) {
|
||||
chunk.copy_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
queue.write_buffer(
|
||||
&pipeline.settings,
|
||||
0,
|
||||
&bytes([settings.tuning, settings.timing]),
|
||||
&data,
|
||||
);
|
||||
}
|
||||
|
||||
fn prune_bindings(pipeline: Res<Pipeline>, views: Query<&ViewTarget, With<super::RetailTone>>) {
|
||||
// Bind groups retain their textures. Release retired camera/resize targets
|
||||
// before rendering, including when there are no exposure views left.
|
||||
pipeline.bindings.lock().unwrap().retain(|(id, _, _)| {
|
||||
views.iter().any(|view| {
|
||||
*id == view.main_texture_view().id() || *id == view.main_texture_other_view().id()
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
|
||||
struct ExposureLabel;
|
||||
#[derive(Default)]
|
||||
@@ -230,21 +253,32 @@ impl ViewNode for ExposureNode {
|
||||
let post = view.post_process_write();
|
||||
use bevy::render::diagnostic::RecordDiagnostics;
|
||||
let diagnostics = context.diagnostic_recorder();
|
||||
let meter = context.render_device().create_bind_group(
|
||||
"retail meter",
|
||||
&cache.get_bind_group_layout(&p.compute_layout),
|
||||
&BindGroupEntries::sequential((
|
||||
post.source,
|
||||
&p.sampler,
|
||||
p.settings.as_entire_binding(),
|
||||
p.state.as_entire_binding(),
|
||||
)),
|
||||
);
|
||||
let output = context.render_device().create_bind_group(
|
||||
"retail exposed tone",
|
||||
&cache.get_bind_group_layout(&p.tone_layout),
|
||||
&BindGroupEntries::sequential((post.source, &p.sampler, p.state.as_entire_binding())),
|
||||
);
|
||||
let mut bindings = p.bindings.lock().unwrap();
|
||||
let index = if let Some(index) = bindings.iter().position(|(id, _, _)| *id == post.source.id()) {
|
||||
index
|
||||
} else {
|
||||
let meter = context.render_device().create_bind_group(
|
||||
"retail meter",
|
||||
&cache.get_bind_group_layout(&p.compute_layout),
|
||||
&BindGroupEntries::sequential((
|
||||
post.source,
|
||||
&p.sampler,
|
||||
p.settings.as_entire_binding(),
|
||||
p.state.as_entire_binding(),
|
||||
)),
|
||||
);
|
||||
let output = context.render_device().create_bind_group(
|
||||
"retail exposed tone",
|
||||
&cache.get_bind_group_layout(&p.tone_layout),
|
||||
&BindGroupEntries::sequential((post.source, &p.sampler, p.state.as_entire_binding())),
|
||||
);
|
||||
if bindings.len() == 8 {
|
||||
bindings.pop_front();
|
||||
}
|
||||
bindings.push_back((post.source.id(), meter, output));
|
||||
bindings.len() - 1
|
||||
};
|
||||
let (_, meter, output) = &bindings[index];
|
||||
{
|
||||
let mut pass = context
|
||||
.command_encoder()
|
||||
@@ -254,7 +288,7 @@ impl ViewNode for ExposureNode {
|
||||
});
|
||||
pass.set_pipeline(compute);
|
||||
let span = diagnostics.pass_span(&mut pass, "retail_exposure_meter");
|
||||
pass.set_bind_group(0, &meter, &[]);
|
||||
pass.set_bind_group(0, meter, &[]);
|
||||
pass.dispatch_workgroups(1, 1, 1);
|
||||
span.end(&mut pass);
|
||||
}
|
||||
@@ -272,7 +306,7 @@ impl ViewNode for ExposureNode {
|
||||
});
|
||||
pass.set_render_pipeline(tone);
|
||||
let span = diagnostics.pass_span(&mut pass, "retail_exposed_tone");
|
||||
pass.set_bind_group(0, &output, &[]);
|
||||
pass.set_bind_group(0, output, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
span.end(&mut pass);
|
||||
Ok(())
|
||||
|
||||
@@ -17,6 +17,11 @@ struct Params {
|
||||
second: Vec4,
|
||||
fade: Vec4,
|
||||
}
|
||||
impl Params {
|
||||
fn bits(&self) -> [[u32; 4]; 4] {
|
||||
[self.scroll, self.first, self.second, self.fade].map(|v| v.to_array().map(f32::to_bits))
|
||||
}
|
||||
}
|
||||
#[derive(Asset, TypePath, AsBindGroup, Clone, Debug)]
|
||||
struct Effect {
|
||||
#[uniform(0)]
|
||||
@@ -47,6 +52,57 @@ mod shader_path_tests {
|
||||
let ShaderRef::Path(path) = Effect::fragment_shader() else { panic!("Expected embedded shader path") };
|
||||
assert_eq!(path, AssetPath::from(embedded_path!("effect.wgsl")).with_source("embedded"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_noise_keeps_its_sequence_and_publishes_before_becoming_visible() {
|
||||
use bevy::ecs::system::RunSystemOnce;
|
||||
let mut app = App::new();
|
||||
app.add_plugins((MinimalPlugins, AssetPlugin::default()))
|
||||
.init_asset::<Effect>()
|
||||
.init_resource::<super::super::SessionMarker>();
|
||||
let initial = Params { scroll: Vec4::ZERO, first: Vec4::X, second: Vec4::X, fade: Vec4::ZERO };
|
||||
let material = app.world_mut().resource_mut::<Assets<Effect>>().add(Effect {
|
||||
params: initial.clone(), texture: default(),
|
||||
});
|
||||
let mut noise = Noise::default();
|
||||
noise.texture();
|
||||
app.insert_resource(Runtime { noise, material: material.clone(), time: 0. });
|
||||
app.world_mut().spawn(Window::default());
|
||||
let screen = app.world_mut().spawn((Screen, Transform::default(), Visibility::Hidden)).id();
|
||||
app.update();
|
||||
app.world_mut().resource_mut::<Messages<AssetEvent<Effect>>>().clear();
|
||||
let mut reference = Noise::default();
|
||||
reference.texture();
|
||||
let mut reference_time = 0.;
|
||||
let mut published = initial;
|
||||
for (dt, progress) in [(0.016, 0.), (0.25, 0.), (0., 0.5), (0., 0.5), (0.02, 0.7), (0.05, 0.), (0., 0.4)] {
|
||||
app.world_mut().resource_mut::<Time<Real>>().advance_by(std::time::Duration::from_secs_f64(dt));
|
||||
app.world_mut().resource_mut::<super::super::SessionMarker>().progress = progress;
|
||||
reference_time += dt;
|
||||
while reference_time >= 1. / 60. {
|
||||
reference_time -= 1. / 60.;
|
||||
reference.advance();
|
||||
}
|
||||
let expected = Params {
|
||||
scroll: Vec4::from_array(reference.scroll),
|
||||
first: Vec4::from_array(Noise::weights(reference.phases[0])),
|
||||
second: Vec4::from_array(Noise::weights(reference.phases[1])),
|
||||
fade: Vec4::new(progress, 0., 0., 0.),
|
||||
};
|
||||
let modified = progress > 0. && expected.bits() != published.bits();
|
||||
app.world_mut().run_system_once(present).unwrap();
|
||||
if progress > 0. { published = expected; }
|
||||
assert_eq!(app.world().resource::<Assets<Effect>>().get(&material).unwrap().params.bits(), published.bits());
|
||||
let runtime = app.world().resource::<Runtime>();
|
||||
assert_eq!(runtime.noise.scroll.map(f32::to_bits), reference.scroll.map(f32::to_bits));
|
||||
assert_eq!(runtime.noise.phases.map(f32::to_bits), reference.phases.map(f32::to_bits));
|
||||
assert_eq!(*app.world().get::<Visibility>(screen).unwrap(), if progress > 0. { Visibility::Inherited } else { Visibility::Hidden });
|
||||
app.update();
|
||||
let modifications = app.world_mut().resource_mut::<Messages<AssetEvent<Effect>>>().drain()
|
||||
.filter(|e| matches!(e, AssetEvent::Modified { id } if *id == material.id())).count();
|
||||
assert_eq!(modifications, usize::from(modified));
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Component)]
|
||||
struct Screen;
|
||||
@@ -116,20 +172,28 @@ fn present(
|
||||
runtime.time -= 1. / 60.;
|
||||
runtime.noise.advance();
|
||||
}
|
||||
if let Some(material) = materials.get_mut(&runtime.material) {
|
||||
material.params = Params {
|
||||
// Advance the native noise stream even while hidden, then publish the current
|
||||
// values on the first visible frame. Hidden assets need no GPU preparation.
|
||||
if session.progress > 0. {
|
||||
let params = Params {
|
||||
scroll: Vec4::from_array(runtime.noise.scroll),
|
||||
first: Vec4::from_array(Noise::weights(runtime.noise.phases[0])),
|
||||
second: Vec4::from_array(Noise::weights(runtime.noise.phases[1])),
|
||||
fade: Vec4::new(session.progress, 0., 0., 0.),
|
||||
};
|
||||
if materials.get(&runtime.material).is_some_and(|m| m.params.bits() != params.bits()) {
|
||||
materials.get_mut(&runtime.material).unwrap().params = params;
|
||||
}
|
||||
}
|
||||
for (mut transform, mut visibility) in &mut screen {
|
||||
transform.scale = Vec3::new(window.width(), window.height(), 1.);
|
||||
*visibility = if session.progress > 0. {
|
||||
let scale = Vec3::new(window.width(), window.height(), 1.);
|
||||
if transform.scale != scale {
|
||||
transform.scale = scale;
|
||||
}
|
||||
visibility.set_if_neq(if session.progress > 0. {
|
||||
Visibility::Inherited
|
||||
} else {
|
||||
Visibility::Hidden
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -375,3 +375,63 @@ comparison was checked against event-level five-second windows and long slices.
|
||||
This capture supports retaining the exact lighting-update optimization, with no
|
||||
claim of an overall FPS improvement. User visual confirmation remains separate
|
||||
from timing evidence; the agent did not launch gameplay or alter quality settings.
|
||||
|
||||
## Follow-up source audit and optimization order
|
||||
|
||||
The third capture ranks the next work as follows. Times below are inclusive CPU
|
||||
wall time amortized over main frames, unless labeled GPU; nested and parallel
|
||||
rows must not be added. The 25 us filter omits short executions.
|
||||
|
||||
| Priority | Area and observed cost | Concrete opportunity / decision |
|
||||
|---|---|---|
|
||||
| 1 | Command generation 2.004 ms/frame; main opaque scope 1.772 ms/frame | Draw traversal accounts for 0.418 ms opaque plus 0.141 ms masked. Added `main_opaque_pass_close` and `main_opaque_encoder_finish` scopes to locate the remaining cost before changing batching or submission. |
|
||||
| 2 | Directional shadow visibility 0.421 ms/frame | Bevy scans eligible meshes for each light/view and tests every cascade. A conservative static-mesh hierarchy or layer-based candidate index could reduce scans without changing final tests. It needs equivalence tests against the existing entity sets across moving cameras, map swaps, dynamic casters and near-plane exceptions. No culling replacement yet. |
|
||||
| 3 | Controller-event hitch: 70.68 ms in one call | Disable the unused Gilrs backend. Both gameplay and menu navigation use `input::platform::poll`; the repository has no Bevy gamepad-event or rumble consumer. Raw XInput state/capability calls, slot selection, errors and fixed publication remain unchanged. |
|
||||
| 4 | Raw controller polling 0.242 ms/frame; menu navigation 0.028 ms/frame | Menu and gameplay make separate OS polls. Sharing a snapshot would change their sample time; throttling disconnected slots or caching capabilities changes reconnect/error semantics. Keep this path unchanged under the native-input constraint. |
|
||||
| 5 | Character preparation 0.064 ms/frame; material bindings 0.034 ms/frame | Retain the existing bit-exact SH update guard. Further gains during changing lighting would require a separate per-character uniform/storage update path, preserving distinct materials and current-frame lighting. That is a renderer design change, not a safe skipped update. |
|
||||
| 6 | Hidden session effect: material preparation 0.020 ms/frame; specialization checks 0.035 ms/frame | `present` modified its material, scale and visibility every frame even when hidden. Skip hidden material publication; keep noise/time advancement and publish current values before making it visible. Guard identical visible parameters and unchanged scale/visibility. |
|
||||
| 7 | Render buffer preparation: indirect parameters 0.098 ms/frame; instances 0.086 ms/frame | Existing capacity reservation already prevents routine capacity churn. Static/dynamic partitioning could reduce uploads but needs explicit removal, previous-frame and camera-cut handling. Do not mark moving data static or disable motion history. |
|
||||
| 8 | Main visibility 0.134 ms/frame; transforms 0.059 ms/frame | Avoid unnecessary component change ticks first (session effect fixed). A shared static hierarchy is a future option; freezing visibility or transforms is not equivalent. |
|
||||
| 9 | Physics advance 0.152 ms/frame, including collision/solve 0.089 ms/frame | Existing collision hierarchy preserves source ordering. Reusing query scratch is possible but needs per-call ownership/reentrancy and result-order tests. These costs do not justify changing solver behavior or fixed cadence. |
|
||||
| 10 | Exposure GPU meter 0.00770 ms; tone 0.01548 ms | Preserve both passes every frame. Reuse their CPU bind groups and replace the per-frame 32-byte heap payload with a stack array. No change to dispatch count, uniform bytes or exposure adaptation. |
|
||||
|
||||
The source review also checked map construction/transitions, texture deduplication,
|
||||
world/sky material writes and existing render-capacity management. The world and
|
||||
sky material loops run at scene construction or an explicit diagnostic keypress,
|
||||
not continuously. There is no evidence here for per-frame map parsing, ongoing
|
||||
texture reloads or pipeline compilation. Loading/memory peaks require a separate
|
||||
startup/transition capture; steady-state timings cannot rank those costs.
|
||||
|
||||
### Implemented follow-up changes and limits
|
||||
|
||||
Gilrs removal follows the additional consumer audit above. The earlier decision
|
||||
to leave input unchanged concerned the *used raw XInput path*, which remains
|
||||
untouched. Removing an unused backend removes this specific stall source; it does
|
||||
not guarantee that OS/controller stalls can never occur elsewhere. Keyboard and
|
||||
mouse input plugins remain enabled.
|
||||
|
||||
The session effect retains the same 60 Hz noise advancement, including hidden
|
||||
frames and multi-step deltas. A headless regression test compares its sequence
|
||||
with continuous reference advancement through hidden/visible transitions and
|
||||
checks material modification events and the first visible payload. Visible
|
||||
parameters use exact float-bit comparison; no epsilon or effect quality reduction
|
||||
is introduced. Hidden materials may deliberately retain older parameters until
|
||||
the frame that displays them.
|
||||
|
||||
Exposure caches at most eight `(source TextureViewId, meter bind group, tone bind
|
||||
group)` entries inside its Pipeline resource. Buffers, sampler and layouts are
|
||||
immutable for that resource's lifetime, and replacing the resource drops the cache.
|
||||
Alternating post-process sources have separate entries. Before rendering, prune
|
||||
entries against both current source views of every exposure camera, so camera
|
||||
removal/resizing releases retired textures rather than retaining large targets.
|
||||
Resized/recreated targets have new source identities. The
|
||||
destination is a render attachment, not a bind-group input, so it does not belong
|
||||
in the key. Buffer contents still update every frame. GPU execution/resize checks
|
||||
remain manual because this task does not authorize the agent to start the game.
|
||||
|
||||
The strongest larger opportunity remains reducing CPU rendering work with exact
|
||||
output equivalence. A blanket 32 m map split would quadruple the estimated groups,
|
||||
so it is not an appropriate default for this capture. Bindless material access is
|
||||
a possible later experiment, but must preserve each texture's format, sampler,
|
||||
role and alpha ordering and cannot be inferred to help from material counts alone.
|
||||
No FPS gain is claimed for the follow-up changes before a matched user capture.
|
||||
@@ -133,8 +133,18 @@ impl ViewNode for MainOpaquePass3dNode {
|
||||
}
|
||||
|
||||
pass_span.end(&mut render_pass);
|
||||
drop(render_pass);
|
||||
command_encoder.finish()
|
||||
{
|
||||
// wgpu may perform substantial validation/encoding when the pass
|
||||
// closes. Separate that CPU work from draw-list traversal.
|
||||
#[cfg(feature = "trace")]
|
||||
let _span = info_span!("main_opaque_pass_close").entered();
|
||||
drop(render_pass);
|
||||
}
|
||||
{
|
||||
#[cfg(feature = "trace")]
|
||||
let _span = info_span!("main_opaque_encoder_finish").entered();
|
||||
command_encoder.finish()
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in new issue
Block a user