Post-Effekt-Kette als Gerüst, Dither als erstes Glied
Bildweite Effekte gehören nicht in den Szenen-Shader. Cel-Ramp und Sobel-Outline sollen dazukommen, ohne einander (oder scene.wgsl) zu kennen — also eine Kette gleichrangiger Fullscreen-Pässe zwischen Szene und Blit, Ping-Pong über zwei interne Targets. Der Vertrag ist für alle gleich: Farbtextur rein (group 0/binding 0), Farbe raus. Die Vertex-Stufe teilen sich alle über fullscreen.wgsl, damit eine Effekt-Datei nur aus ihrem fs_main besteht. Ein neuer Effekt ist eine .wgsl plus eine Zeile in EFFECTS. Bei 640×480 kostet ein eigener Pass je Effekt praktisch nichts — deshalb kein Über-Shader. Erster Bewohner ist die RGB555-Quantisierung samt Bayer-Dither, aus scene.wgsl herausgelöst. Das UI zeichnet jetzt in einem eigenen Pass hinter der Kette, sonst würde der Dither auch den Text quantisieren, den er bisher nicht anfasst; damit braucht SpritePass kein Tiefen-Attachment mehr. Geschaltet wird über den exec-Trichter (`post`, `post <e> on|off`). Die Namen kennt nur das Frontend: es meldet seine Kette beim Start in engine::post an, wie render::audio seine Emitter aus engine::audio zieht. Der Kern bekommt dadurch keine GPU zu sehen. Das Bild soll unverändert bleiben — gleicher Bayer-Kernel, gleiche Koordinaten, gleiche Reihenfolge relativ zum UI. Verifiziert sind Tests, clippy und ein Start ohne Validation-Fehler; ein Pixelvergleich steht aus. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+74
-53
@@ -1,10 +1,12 @@
|
||||
//! wgpu-Zustand: Surface, Device und der zweistufige Render-Pfad
|
||||
//! aus dem Renderer-Plan:
|
||||
//! wgpu-Zustand: Surface, Device und der interne Render-Pfad:
|
||||
//!
|
||||
//! Pass 1 (intern): 640×480 RGBA8 + Depth — hier entsteht das Bild
|
||||
//! ([`ScenePass`], PS1-Shader).
|
||||
//! Pass 2 (Fenster): Nearest-Upscale des internen Targets mit
|
||||
//! 4:3-Letterbox (via Viewport) auf die Surface.
|
||||
//! Szene (intern): 640×480 RGBA8 + Depth — Geometrie und Textur
|
||||
//! ([`ScenePass`]).
|
||||
//! Post (intern): die Effekt-Kette ([`Post`]), Ping-Pong zwischen zwei
|
||||
//! Targets; jeder Effekt einzeln schaltbar.
|
||||
//! Overlay (intern): das UI ([`SpritePass`]) — *nach* der Kette, damit
|
||||
//! Bild-Effekte nur die Welt treffen, nie den Text.
|
||||
//! Blit (Fenster): Nearest-Upscale mit 4:3-Letterbox (via Viewport).
|
||||
//!
|
||||
//! 640×480 (VGA) statt 320×240: das Spiel ist text-getrieben — die höhere
|
||||
//! Auflösung trägt den 80-Spalten-Textmodus-Look (EGA 8×14), der Lo-Fi-Rest
|
||||
@@ -14,8 +16,10 @@ use std::sync::Arc;
|
||||
|
||||
use winit::window::Window;
|
||||
|
||||
use crate::engine::post::PostChain;
|
||||
use crate::engine::tga::Image;
|
||||
use crate::render::math::Mat4;
|
||||
use crate::render::post::Post;
|
||||
use crate::render::scene::{Mesh, ScenePass};
|
||||
use crate::render::sprite::SpritePass;
|
||||
use crate::render::ui::Ui;
|
||||
@@ -34,13 +38,14 @@ pub struct Gpu {
|
||||
queue: wgpu::Queue,
|
||||
config: wgpu::SurfaceConfiguration,
|
||||
|
||||
internal_view: wgpu::TextureView,
|
||||
depth_view: wgpu::TextureView,
|
||||
|
||||
scene: ScenePass,
|
||||
post: Post,
|
||||
sprite: SpritePass,
|
||||
blit_pipeline: wgpu::RenderPipeline,
|
||||
blit_bind: wgpu::BindGroup,
|
||||
/// Blit-Bind-Group je Post-Target — welche gilt, sagt [`Post::run`].
|
||||
blit_binds: [wgpu::BindGroup; 2],
|
||||
}
|
||||
|
||||
impl Gpu {
|
||||
@@ -98,19 +103,20 @@ impl Gpu {
|
||||
};
|
||||
surface.configure(&device, &config);
|
||||
|
||||
let (internal_view, depth_view) = make_internal_targets(&device);
|
||||
let depth_view = make_depth_target(&device);
|
||||
|
||||
// Pass 1: die Szene. Geometrie + Bilder kommen fertig vom Aufrufer
|
||||
// (Decode in render::run); hier wird nur hochgeladen.
|
||||
// Szene: Geometrie + Bilder kommen fertig vom Aufrufer (Decode in
|
||||
// render::run); hier wird nur hochgeladen.
|
||||
let scene = ScenePass::new(&device, &queue, INTERNAL_FORMAT, DEPTH_FORMAT, mesh, images);
|
||||
let post = Post::new(&device, INTERNAL_FORMAT, [INTERNAL_W, INTERNAL_H]);
|
||||
|
||||
// Overlay-Pass: zeichnet nach der Szene ins selbe interne Target.
|
||||
// Overlay: eigener Pass hinter der Kette, ohne Tiefe.
|
||||
let sprite = SpritePass::new(
|
||||
&device, &queue, INTERNAL_FORMAT, DEPTH_FORMAT,
|
||||
&device, &queue, INTERNAL_FORMAT,
|
||||
[INTERNAL_W as f32, INTERNAL_H as f32], ui_textures,
|
||||
);
|
||||
|
||||
// Pass 2: internes Target nearest-gesampelt auf die Surface.
|
||||
// Blit: das fertige interne Bild nearest-gesampelt auf die Surface.
|
||||
let blit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("blit"),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("blit.wgsl").into()),
|
||||
@@ -142,25 +148,29 @@ impl Gpu {
|
||||
min_filter: wgpu::FilterMode::Nearest,
|
||||
..Default::default()
|
||||
});
|
||||
let blit_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("blit"),
|
||||
layout: &blit_pipeline.get_bind_group_layout(0),
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&internal_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(&sampler),
|
||||
},
|
||||
],
|
||||
});
|
||||
let blit_layout = blit_pipeline.get_bind_group_layout(0);
|
||||
let blit_bind = |view: &wgpu::TextureView| {
|
||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("blit"),
|
||||
layout: &blit_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(&sampler),
|
||||
},
|
||||
],
|
||||
})
|
||||
};
|
||||
let blit_binds = [blit_bind(post.view(0)), blit_bind(post.view(1))];
|
||||
|
||||
Self {
|
||||
surface, device, queue, config,
|
||||
internal_view, depth_view,
|
||||
scene, sprite, blit_pipeline, blit_bind,
|
||||
depth_view,
|
||||
scene, post, sprite, blit_pipeline, blit_binds,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +192,7 @@ impl Gpu {
|
||||
/// Einen Frame rendern. Die View-Matrix kommt vom Aufrufer;
|
||||
/// die Projektion gehört hierher, weil sie an der internen Auflösung
|
||||
/// hängt.
|
||||
pub fn frame(&mut self, view: &Mat4, ui: &Ui) {
|
||||
pub fn frame(&mut self, view: &Mat4, ui: &Ui, post: &PostChain) {
|
||||
let proj = Mat4::perspective(
|
||||
60f32.to_radians(),
|
||||
INTERNAL_W as f32 / INTERNAL_H as f32,
|
||||
@@ -208,12 +218,12 @@ impl Gpu {
|
||||
let surface_view = frame.texture.create_view(&Default::default());
|
||||
let mut enc = self.device.create_command_encoder(&Default::default());
|
||||
|
||||
// Pass 1: intern.
|
||||
// Szene ins erste Post-Target.
|
||||
{
|
||||
let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("internal"),
|
||||
label: Some("scene"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &self.internal_view,
|
||||
view: self.post.scene_target(),
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
@@ -234,11 +244,33 @@ impl Gpu {
|
||||
multiview_mask: None,
|
||||
});
|
||||
self.scene.draw(&mut pass);
|
||||
// Overlay obenauf, im selben Pass (kein Tiefentest).
|
||||
}
|
||||
|
||||
// Effekt-Kette; sie sagt, welches Target das Bild jetzt trägt.
|
||||
let out = self.post.run(&mut enc, post);
|
||||
|
||||
// Overlay obendrauf — nach der Kette, also von keinem Effekt berührt.
|
||||
{
|
||||
let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("overlay"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: self.post.view(out),
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
self.sprite.draw(&mut pass, &ui.batches);
|
||||
}
|
||||
|
||||
// Pass 2: Letterbox-Blit aufs Fenster.
|
||||
// Letterbox-Blit aufs Fenster.
|
||||
{
|
||||
let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("blit"),
|
||||
@@ -260,7 +292,7 @@ impl Gpu {
|
||||
let (vx, vy, vw, vh) = letterbox(self.config.width, self.config.height);
|
||||
pass.set_viewport(vx, vy, vw, vh, 0.0, 1.0);
|
||||
pass.set_pipeline(&self.blit_pipeline);
|
||||
pass.set_bind_group(0, &self.blit_bind, &[]);
|
||||
pass.set_bind_group(0, &self.blit_binds[out], &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
|
||||
@@ -269,23 +301,12 @@ impl Gpu {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_internal_targets(device: &wgpu::Device) -> (wgpu::TextureView, wgpu::TextureView) {
|
||||
let size = wgpu::Extent3d {
|
||||
width: INTERNAL_W, height: INTERNAL_H, depth_or_array_layers: 1,
|
||||
};
|
||||
let color = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("internal color"),
|
||||
size,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: INTERNAL_FORMAT,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
fn make_depth_target(device: &wgpu::Device) -> wgpu::TextureView {
|
||||
let depth = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("internal depth"),
|
||||
size,
|
||||
size: wgpu::Extent3d {
|
||||
width: INTERNAL_W, height: INTERNAL_H, depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
@@ -293,7 +314,7 @@ fn make_internal_targets(device: &wgpu::Device) -> (wgpu::TextureView, wgpu::Tex
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
(color.create_view(&Default::default()), depth.create_view(&Default::default()))
|
||||
depth.create_view(&Default::default())
|
||||
}
|
||||
|
||||
/// Größtes 4:3-Rechteck, das ins Fenster passt, zentriert.
|
||||
|
||||
Reference in New Issue
Block a user