From 33ce1061ba6fe8b93cfbbaece1af026faf5a5fe9 Mon Sep 17 00:00:00 2001 From: xfy Date: Wed, 15 Jul 2026 09:42:19 +0800 Subject: [PATCH] fix(render): resolve validation crash from egui camera + brightpass uniform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two runtime validation errors crashed the app on startup: 1. BrightPassMaterial used #[uniform(0)] threshold: f32 (bare f32 = 4-byte binding), but the WGSL BrightPassUniform struct is 16 bytes (threshold + 3 pads). The pipeline-layout mismatch ("Buffer structure size 16... greater than min_binding_size, which is 4") killed the opaque_mesh2d pipeline. Fix: wrap threshold in a BrightPassUniform struct matching the WGSL layout, same pattern as BlurUniform/CompositeUniform. 2. bevy_egui 0.41's auto-context assigns PrimaryEguiContext to the FIRST Added — with the 7-camera bloom pipeline, that's the offscreen camera (renders to Rgba16Float Image). egui's pipeline expects the window's Rgba8UnormSrgb surface → format mismatch crash. Fix: disable auto_create_primary_context (PreStartup system) and explicitly tag the composite (window) camera with PrimaryEguiContext. Both fixes verified: the app now runs without validation errors for 8+ seconds. Physics tests (20/20) unaffected. --- src/render/material.rs | 14 +++++++++++++- src/render/plugin.rs | 28 +++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/render/material.rs b/src/render/material.rs index 573f5f5..2a65bef 100644 --- a/src/render/material.rs +++ b/src/render/material.rs @@ -123,12 +123,24 @@ impl Material2d for BlackHoleMaterial { } } +/// Uniform for the bright-pass (bloom stage [2]). +/// Must be a struct (not bare f32) so the Rust binding size matches the +/// WGSL `BrightPassUniform` (16 bytes: threshold + 3 pads). A bare f32 +/// registers a 4-byte min_binding_size, causing a pipeline-layout mismatch. +#[derive(Clone, ShaderType, Default)] +pub struct BrightPassUniform { + pub threshold: f32, + pub _pad0: f32, + pub _pad1: f32, + pub _pad2: f32, +} + /// Extracts luminance above a threshold from the HDR offscreen into a /// half-res float texture (bloom stage [2]). Soft-knee, not hard cut. #[derive(Asset, TypePath, AsBindGroup, Clone)] pub struct BrightPassMaterial { #[uniform(0)] - pub threshold: f32, + pub uniform: BrightPassUniform, #[texture(1)] #[sampler(2)] pub source: Handle, diff --git a/src/render/plugin.rs b/src/render/plugin.rs index 18f01f5..f83284f 100644 --- a/src/render/plugin.rs +++ b/src/render/plugin.rs @@ -80,6 +80,15 @@ pub struct BloomFinalTarget(pub Handle); #[derive(Resource)] pub struct AppliedBloomQuality(pub crate::params::BloomQuality); +/// Disables bevy_egui's auto-context creation so we can explicitly assign +/// `PrimaryEguiContext` to the composite (window) camera. Without this, +/// bevy_egui assigns to the first spawned camera — the offscreen camera, +/// which renders to an Rgba16Float Image that egui's Rgba8UnormSrgb pipeline +/// can't render into. +fn disable_egui_auto_context(mut settings: ResMut) { + settings.auto_create_primary_context = false; +} + pub struct BlackHolePlugin; impl Plugin for BlackHolePlugin { @@ -92,6 +101,14 @@ impl Plugin for BlackHolePlugin { .add_plugins(Material2dPlugin::::default()) .add_plugins(Material2dPlugin::::default()) .add_plugins(bevy_egui::EguiPlugin::default()) + // The bloom pipeline spawns 7 Camera2d entities (offscreen + brightpass + // + 4 blur + composite). bevy_egui's auto-context assigns PrimaryEguiContext + // to the FIRST Added — the offscreen camera, which renders to an + // Rgba16Float Image. egui's pipeline expects the window's Rgba8UnormSrgb + // surface → format mismatch crash. Disable auto-assignment and explicitly + // tag the composite (window) camera with PrimaryEguiContext (see spawn). + // Runs in PreStartup, before setup_primary_egui_context_system. + .add_systems(bevy::prelude::PreStartup, disable_egui_auto_context) .add_systems(Startup, spawn_fullscreen_quad) .add_systems(Startup, crate::scene::planets::spawn_default_planet) .add_systems( @@ -238,7 +255,10 @@ fn spawn_fullscreen_quad( CompositeQuad, RenderLayers::layer(1), )); - commands.spawn((Camera2d, Msaa::Off, UpscaleCamera, Nudgable, RenderLayers::layer(1))); + // The composite camera renders to the window (Rgba8UnormSrgb surface). + // PrimaryEguiContext tells bevy_egui to render the UI onto THIS camera, + // not the offscreen/bloom cameras (which render to Rgba16Float Images). + commands.spawn((Camera2d, Msaa::Off, UpscaleCamera, Nudgable, bevy_egui::PrimaryEguiContext, RenderLayers::layer(1))); } /// Spawns the bloom pipeline (brightpass + blur pyramid + bloom_final). @@ -272,7 +292,9 @@ fn spawn_bloom_pipeline( commands.spawn(( Mesh2d(meshes.add(Rectangle::new(2.0, 2.0))), MeshMaterial2d(bp_materials.add(crate::render::material::BrightPassMaterial { - threshold: 1.0, + uniform: crate::render::material::BrightPassUniform { + threshold: 1.0, _pad0: 0.0, _pad1: 0.0, _pad2: 0.0, + }, source: offscreen.clone(), })), Transform::default().with_scale(Vec3::new(bw as f32 / 2.0, bh as f32 / 2.0, 1.0)), @@ -601,7 +623,7 @@ fn mirror_params( } // Update brightpass threshold (live-tunable). for (_, mat) in brightpass_materials.iter_mut() { - mat.threshold = params.bloom_threshold; + mat.uniform.threshold = params.bloom_threshold; } // Update composite material uniforms (bloom strength + exposure live-tunable). for (_, mat) in composite_materials.iter_mut() {