Remove unused gamepad backend and avoid redundant render preparation

This commit is contained in:
chazmm committed 2026-09-09 16:34:43 +01:00
1 parent d8de12f280
commit 842b4adb2f
5 files changed
+198 -26

No files matched your search

+5 -1
View File
@@ -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)
+52 -18
View File
@@ -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(())
+69 -5
View File
@@ -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
};
});
}
}
+60
View File
@@ -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(())