MONKEY (polish): per-view post uniforms written at prepare, not allocated per frame

Colour grading and sun shafts built a fresh `UniformBuffer` (a new GPU buffer)
inside their render-graph node every frame. Each view now owns one small
UNIFORM | COPY_DST buffer, created the first frame the effect is on and
rewritten in place with `RenderQueue::write_buffer` in
`RenderSystems::PrepareResources`, the same shape as bloom's prepare-time
uniform. The node only binds it. The pass still gates on the extracted view
component, so the kept buffer never runs a disabled pass.

Verified: cargo check -p benilla-app; rustfmt clean on both files. No GPU
here; built on Linux only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6yUWgPosSakvL23gtgvpH
This commit is contained in:
Claude
2026-09-25 17:46:00 +00:00
parent 42635eeeff
commit 78d7e94433
2 changed files with 63 additions and 15 deletions
+38 -8
View File
@@ -25,7 +25,7 @@ use bevy::{
renderer::{RenderContext, RenderDevice, RenderQueue},
texture::GpuImage,
view::ViewTarget,
RenderApp, RenderStartup,
Render, RenderApp, RenderStartup, RenderSystems,
},
};
use std::collections::{HashMap, HashSet};
@@ -73,6 +73,10 @@ impl Plugin for GradingPlugin {
render_app
.insert_resource(GradeShader(shader))
.add_systems(RenderStartup, init_pipeline)
.add_systems(
Render,
prepare_uniforms.in_set(RenderSystems::PrepareResources),
)
.add_render_graph_node::<ViewNodeRunner<GradeNode>>(Core3d, GradeLabel)
.add_render_graph_edges(
Core3d,
@@ -344,17 +348,47 @@ fn init_pipeline(
});
}
/// MONKEY (polish): the view's uniform, created once and rewritten in prepare, like bloom's.
#[derive(Component)]
struct GradeUniformBuffer(Buffer);
fn prepare_uniforms(
mut commands: Commands,
device: Res<RenderDevice>,
queue: Res<RenderQueue>,
views: Query<(Entity, &GradeView, Option<&GradeUniformBuffer>)>,
) {
for (entity, grade, buffer) in &views {
let rows = [grade.control.to_array()];
match buffer {
Some(buffer) => queue.write_buffer(&buffer.0, 0, bytemuck::cast_slice(&rows)),
None => {
let buffer = device.create_buffer_with_data(&BufferInitDescriptor {
label: Some("post_grading_uniform"),
contents: bytemuck::cast_slice(&rows),
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
});
commands.entity(entity).insert(GradeUniformBuffer(buffer));
}
}
}
}
#[derive(Default)]
struct GradeNode;
impl ViewNode for GradeNode {
type ViewQuery = (&'static ViewTarget, &'static GradeView);
type ViewQuery = (
&'static ViewTarget,
&'static GradeView,
&'static GradeUniformBuffer,
);
fn run<'w>(
&self,
_graph: &mut RenderGraphContext,
context: &mut RenderContext<'w>,
(target, grade): QueryItem<'w, '_, Self::ViewQuery>,
(target, grade, uniform): QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let settings = world.resource::<GradePipeline>();
@@ -367,10 +401,6 @@ impl ViewNode for GradeNode {
return Ok(());
};
let device = context.render_device();
let mut uniform = UniformBuffer::from(GradeUniform {
control: grade.control,
});
uniform.write_buffer(device, world.resource::<RenderQueue>());
let out = target.post_process_write();
let bind = device.create_bind_group(
"post_grading",
@@ -381,7 +411,7 @@ impl ViewNode for GradeNode {
&day.texture_view,
&night.texture_view,
&settings.lut_sampler,
uniform.binding().unwrap(),
uniform.0.as_entire_binding(),
)),
);
let diagnostics = context.diagnostic_recorder();
+25 -7
View File
@@ -165,6 +165,10 @@ const MASK_FORMAT: TextureFormat = TextureFormat::R8Unorm;
#[derive(Component)]
struct ShaftMask(CachedTexture);
/// MONKEY (polish): the view's uniform, created once and rewritten in prepare, like bloom's.
#[derive(Component)]
struct ShaftUniform(Buffer);
#[derive(Component)]
struct ViewShaftPipeline(CachedRenderPipelineId);
@@ -277,17 +281,31 @@ fn prepare_pipelines(
}
}
/// The half-size mask target, rounded up so an odd edge column still has a texel.
/// The half-size mask target, rounded up so an odd edge column still has a texel, and the
/// view's uniform, written in place.
fn prepare_masks(
mut commands: Commands,
mut textures: ResMut<TextureCache>,
device: Res<RenderDevice>,
views: Query<(Entity, &ExtractedCamera), With<ShaftView>>,
queue: Res<RenderQueue>,
views: Query<(Entity, &ExtractedCamera, &ShaftView, Option<&ShaftUniform>)>,
) {
for (entity, camera) in &views {
for (entity, camera, shaft, uniform) in &views {
let Some(size) = camera.physical_viewport_size else {
continue;
};
let rows = [shaft.sun.to_array(), shaft.color.to_array()];
match uniform {
Some(uniform) => queue.write_buffer(&uniform.0, 0, bytemuck::cast_slice(&rows)),
None => {
let buffer = device.create_buffer_with_data(&BufferInitDescriptor {
label: Some("post_sun_shafts_uniform"),
contents: bytemuck::cast_slice(&rows),
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
});
commands.entity(entity).insert(ShaftUniform(buffer));
}
}
let mask = textures.get(
&device,
TextureDescriptor {
@@ -316,16 +334,18 @@ impl ViewNode for ShaftNode {
type ViewQuery = (
&'static ViewTarget,
&'static ViewDepthTexture,
// Gates the pass: the mask and uniform outlive a disabled frame.
&'static ShaftView,
&'static ViewShaftPipeline,
&'static ShaftMask,
&'static ShaftUniform,
);
fn run<'w>(
&self,
_graph: &mut RenderGraphContext,
context: &mut RenderContext<'w>,
(target, depth, shaft, id, mask): QueryItem<'w, '_, Self::ViewQuery>,
(target, depth, _, id, mask, uniform): QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let cache = world.resource::<PipelineCache>();
@@ -350,8 +370,6 @@ impl ViewNode for ShaftNode {
&cache.get_bind_group_layout(&settings.mask_layouts[multisampled]),
&BindGroupEntries::single(depth.view()),
);
let mut uniform = UniformBuffer::from(*shaft);
uniform.write_buffer(device, world.resource::<RenderQueue>());
let out = target.post_process_write();
let bind = device.create_bind_group(
"post_sun_shafts",
@@ -360,7 +378,7 @@ impl ViewNode for ShaftNode {
out.source,
&settings.sampler,
&mask.0.default_view,
uniform.binding().unwrap(),
uniform.0.as_entire_binding(),
)),
);
let diagnostics = context.diagnostic_recorder();