Window init

This commit is contained in:
2026-06-13 03:16:59 +02:00
parent 2dd338c890
commit 0a8861a5b1
7 changed files with 2652 additions and 11 deletions
+295
View File
@@ -0,0 +1,295 @@
//! wgpu-Zustand: Surface, Device und der zweistufige Render-Pfad
//! aus dem Renderer-Plan:
//!
//! Pass 1 (intern): 320×240 RGBA8 + Depth — hier entsteht das Bild.
//! Aktuell nur ein Testmuster-Shader; Schritt 3
//! ersetzt ihn durch die Szenen-Pipeline.
//! 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;
pub const INTERNAL_W: u32 = 320;
pub const INTERNAL_H: u32 = 240;
/// 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,
pattern_pipeline: wgpu::RenderPipeline,
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: Testmuster auf das interne Target.
let pattern_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("pattern"),
source: wgpu::ShaderSource::Wgsl(include_str!("pattern.wgsl").into()),
});
let pattern_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("pattern"),
layout: None,
vertex: wgpu::VertexState {
module: &pattern_shader,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &pattern_shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(INTERNAL_FORMAT.into())],
}),
primitive: wgpu::PrimitiveState::default(),
// Depth hängt am Pass (Schritt 3 braucht es); das Muster
// selbst schreibt/testet nicht.
depth_stencil: Some(wgpu::DepthStencilState {
format: DEPTH_FORMAT,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Always),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
// 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,
pattern_pipeline, 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);
}
pub fn frame(&mut self) {
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,
});
pass.set_pipeline(&self.pattern_pipeline);
pass.draw(0..3, 0..1);
}
// 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)
}