Files
wds/src/render/gpu.rs
T
2026-06-13 13:44:02 +02:00

279 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! wgpu-Zustand: Surface, Device und der zweistufige Render-Pfad
//! aus dem Renderer-Plan:
//!
//! Pass 1 (intern): 320×240 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.
use std::sync::Arc;
use winit::window::Window;
use crate::render::math::Mat4;
use crate::render::scene::ScenePass;
pub const INTERNAL_W: u32 = 480;
pub const INTERNAL_H: u32 = 360;
/// D16 reicht für PS1-Geometrieskalen und ist das älteste, überall
/// (auch GL-Fallback) unterstützte Depth-Format.
const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth16Unorm;
const INTERNAL_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
pub struct Gpu {
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
internal_view: wgpu::TextureView,
depth_view: wgpu::TextureView,
scene: ScenePass,
blit_pipeline: wgpu::RenderPipeline,
blit_bind: wgpu::BindGroup,
}
impl Gpu {
pub fn new(window: Arc<Window>, display: winit::event_loop::OwnedDisplayHandle) -> Self {
let size = window.inner_size();
// Display-Handle mitgeben: für den GL-Fallback (v.a. Wayland)
// Pflicht; Vulkan ignoriert es. `with_env` erlaubt Overrides wie
// WGPU_BACKEND=gl zum Testen des Fallback-Pfads.
let instance = wgpu::Instance::new(
wgpu::InstanceDescriptor::new_with_display_handle(Box::new(display)).with_env(),
);
let surface = instance.create_surface(window).expect("wgpu: Surface");
let adapter = pollster::block_on(instance.request_adapter(
&wgpu::RequestAdapterOptions {
compatible_surface: Some(&surface),
..Default::default()
},
)).expect("wgpu: kein Adapter");
let info = adapter.get_info();
println!("[gpu] {} ({:?}, {:?})", info.name, info.backend, info.device_type);
// Baseline-Limits/-Features: alles, was der Plan braucht, ist
// WebGPU-Kern — nichts anfordern, dann läuft es auch auf HD 5500.
let (device, queue) = pollster::block_on(
adapter.request_device(&wgpu::DeviceDescriptor::default()),
).expect("wgpu: Device");
// Nicht-sRGB-8-Bit-Surface bevorzugen: der Fragment-Shader
// quantisiert später selbst auf RGB555 — die Werte sollen
// unverändert auf den Schirm, ohne Gamma-Umkodierung beim Blit.
// Explizite Liste statt „erstes nicht-sRGB": Treiber bieten auch
// 16-Bit-Formate an, die extra Device-Features bräuchten.
let caps = surface.get_capabilities(&adapter);
let format = [wgpu::TextureFormat::Bgra8Unorm, wgpu::TextureFormat::Rgba8Unorm]
.into_iter()
.find(|f| caps.formats.contains(f))
.unwrap_or(caps.formats[0]);
println!("[gpu] Surface-Format {format:?}");
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
width: size.width.max(1),
height: size.height.max(1),
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode: caps.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &config);
let (internal_view, depth_view) = make_internal_targets(&device);
// Pass 1: die Szene.
let scene = ScenePass::new(&device, INTERNAL_FORMAT, DEPTH_FORMAT);
// Pass 2: internes Target 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()),
});
let blit_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("blit"),
layout: None,
vertex: wgpu::VertexState {
module: &blit_shader,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &blit_shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(config.format.into())],
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("blit nearest"),
mag_filter: wgpu::FilterMode::Nearest,
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),
},
],
});
Self {
surface, device, queue, config,
internal_view, depth_view,
scene, blit_pipeline, blit_bind,
}
}
pub fn resize(&mut self, width: u32, height: u32) {
self.config.width = width.max(1);
self.config.height = height.max(1);
self.surface.configure(&self.device, &self.config);
}
/// Einen Frame rendern. `t` = Sekunden seit Start (treibt vorerst
/// die Orbit-Kamera; ab Schritt 4 kommt die Kamera vom Aufrufer).
pub fn frame(&mut self, t: f32) {
// Hartkodierte Orbit-Kamera um den Testwürfel — macht Vertex-Snap
// und Dither in Bewegung sichtbar. Die Flycam ersetzt sie.
let yaw = t * 0.4;
let eye = [2.2 * yaw.sin(), 1.3, 2.2 * yaw.cos()];
let view = Mat4::view(eye, yaw, -0.5);
let proj = Mat4::perspective(
60f32.to_radians(),
INTERNAL_W as f32 / INTERNAL_H as f32,
0.1, 100.0,
);
self.scene.prepare(&self.queue, &proj.mul(&view));
use wgpu::CurrentSurfaceTexture as Cst;
let frame = match self.surface.get_current_texture() {
Cst::Success(f) | Cst::Suboptimal(f) => f,
// Surface veraltet (Resize/Compositor): neu konfigurieren,
// diesen Frame auslassen.
Cst::Outdated | Cst::Lost => {
self.surface.configure(&self.device, &self.config);
return;
}
// Fenster verdeckt/minimiert oder Treiber-Timeout: auslassen.
Cst::Timeout | Cst::Occluded => return,
Cst::Validation => panic!("wgpu: Surface-Validation-Fehler"),
};
let surface_view = frame.texture.create_view(&Default::default());
let mut enc = self.device.create_command_encoder(&Default::default());
// Pass 1: intern.
{
let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("internal"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &self.internal_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
self.scene.draw(&mut pass);
}
// Pass 2: Letterbox-Blit aufs Fenster.
{
let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("blit"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &surface_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
// Schwarz = die Letterbox-Balken.
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
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.draw(0..3, 0..1);
}
self.queue.submit([enc.finish()]);
frame.present();
}
}
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: &[],
});
let depth = device.create_texture(&wgpu::TextureDescriptor {
label: Some("internal depth"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: DEPTH_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
(color.create_view(&Default::default()), depth.create_view(&Default::default()))
}
/// Größtes 4:3-Rechteck, das ins Fenster passt, zentriert.
/// Nicht-ganzzahlige Skalierung ist gewollt (volle Fensterausnutzung);
/// die leicht ungleichen Pixel passen zum CRT-Vorbild.
fn letterbox(win_w: u32, win_h: u32) -> (f32, f32, f32, f32) {
let (w, h) = (win_w as f32, win_h as f32);
let scale = (w / INTERNAL_W as f32).min(h / INTERNAL_H as f32);
let vw = INTERNAL_W as f32 * scale;
let vh = INTERNAL_H as f32 * scale;
((w - vw) * 0.5, (h - vh) * 0.5, vw, vh)
}