Window init
This commit is contained in:
Generated
+2217
-6
File diff suppressed because it is too large
Load Diff
@@ -5,3 +5,6 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bladeink = "1.2.5"
|
bladeink = "1.2.5"
|
||||||
|
pollster = "0.4.0"
|
||||||
|
wgpu = "29.0.3"
|
||||||
|
winit = "0.30.13"
|
||||||
|
|||||||
+10
-5
@@ -2,17 +2,22 @@
|
|||||||
//!
|
//!
|
||||||
//! Aufbau: `engine/` ist der headless Kern (Signal/Action-Dispatcher,
|
//! Aufbau: `engine/` ist der headless Kern (Signal/Action-Dispatcher,
|
||||||
//! KV-Store, Ink-Stories); Frontends konsumieren ihn über `StoryState`
|
//! KV-Store, Ink-Stories); Frontends konsumieren ihn über `StoryState`
|
||||||
//! und die `Action`-Queue. Aktuell einziges Frontend: die CLI-REPL in
|
//! und die `Action`-Queue. Frontends: das Fenster (`render`, Default)
|
||||||
//! `cli`. Der Renderer kommt als zweites dazu (notes/renderer-plan.md).
|
//! und die Konsolen-REPL (`cli`, via `--cli`).
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
mod engine;
|
mod engine;
|
||||||
|
mod render;
|
||||||
|
|
||||||
use engine::game::Game;
|
use engine::game::Game;
|
||||||
use engine::{assets, signals};
|
use engine::{assets, signals};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let signals_path = assets::path("assets/signals.toml");
|
if std::env::args().any(|a| a == "--cli") {
|
||||||
let game = Game::new(signals::load_signals(&signals_path));
|
let signals_path = assets::path("assets/signals.toml");
|
||||||
cli::run(game, &signals_path);
|
let game = Game::new(signals::load_signals(&signals_path));
|
||||||
|
cli::run(game, &signals_path);
|
||||||
|
} else {
|
||||||
|
render::run();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Upscale-Pass: internes 320×240-Target nearest-gesampelt auf die
|
||||||
|
// Surface. Das 4:3-Letterbox-Rechteck setzt der Rust-Code als Viewport;
|
||||||
|
// hier ist es ein simples Fullscreen-Dreieck mit UVs.
|
||||||
|
|
||||||
|
struct VsOut {
|
||||||
|
@builtin(position) pos: vec4f,
|
||||||
|
@location(0) uv: vec2f,
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(@builtin(vertex_index) i: u32) -> VsOut {
|
||||||
|
var pos = array<vec2f, 3>(
|
||||||
|
vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0),
|
||||||
|
);
|
||||||
|
var out: VsOut;
|
||||||
|
out.pos = vec4f(pos[i], 0.0, 1.0);
|
||||||
|
// NDC (y hoch) → UV (v runter).
|
||||||
|
out.uv = vec2f(pos[i].x * 0.5 + 0.5, 0.5 - pos[i].y * 0.5);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(0) @binding(0) var internal_tex: texture_2d<f32>;
|
||||||
|
@group(0) @binding(1) var internal_smp: sampler;
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VsOut) -> @location(0) vec4f {
|
||||||
|
return textureSample(internal_tex, internal_smp, in.uv);
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
//! Fenster-Frontend: winit-Loop um den wgpu-Renderer.
|
||||||
|
//!
|
||||||
|
//! Stand: Schritt 1+2 des Renderer-Plans (notes/renderer-plan.md) —
|
||||||
|
//! Fenster, internes 320×240-Target mit Testmuster, Nearest-Upscale mit
|
||||||
|
//! 4:3-Letterbox. Szene, PS1-Shader und Flycam folgen als nächste Schritte.
|
||||||
|
//!
|
||||||
|
//! Gleiche Schicht-Regel wie `cli`: Geschwister von `engine`, konsumiert
|
||||||
|
//! dessen Schnittstellen (sobald hier eine Szene läuft) — nie umgekehrt.
|
||||||
|
|
||||||
|
mod gpu;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use winit::application::ApplicationHandler;
|
||||||
|
use winit::event::{ElementState, KeyEvent, WindowEvent};
|
||||||
|
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||||
|
use winit::keyboard::{KeyCode, PhysicalKey};
|
||||||
|
use winit::window::{Window, WindowId};
|
||||||
|
|
||||||
|
use gpu::Gpu;
|
||||||
|
|
||||||
|
pub fn run() {
|
||||||
|
let event_loop = EventLoop::new().expect("winit: Event-Loop");
|
||||||
|
// Poll statt Wait: wir rendern kontinuierlich (Spiel, kein Editor).
|
||||||
|
event_loop.set_control_flow(ControlFlow::Poll);
|
||||||
|
let mut app = App::default();
|
||||||
|
event_loop.run_app(&mut app).expect("winit: run");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct App {
|
||||||
|
window: Option<Arc<Window>>,
|
||||||
|
gpu: Option<Gpu>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationHandler for App {
|
||||||
|
/// winit liefert das Fenster erst hier, nicht beim Loop-Start —
|
||||||
|
/// deshalb sind `window`/`gpu` Options statt Konstruktor-Felder.
|
||||||
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
if self.window.is_some() { return; }
|
||||||
|
let attrs = Window::default_attributes()
|
||||||
|
.with_title("wds")
|
||||||
|
// 3× interne Auflösung als Startgröße; frei resizebar.
|
||||||
|
.with_inner_size(winit::dpi::LogicalSize::new(
|
||||||
|
gpu::INTERNAL_W * 3, gpu::INTERNAL_H * 3));
|
||||||
|
let window = Arc::new(event_loop.create_window(attrs).expect("winit: Fenster"));
|
||||||
|
self.gpu = Some(Gpu::new(window.clone(), event_loop.owned_display_handle()));
|
||||||
|
self.window = Some(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
|
||||||
|
match event {
|
||||||
|
WindowEvent::CloseRequested => event_loop.exit(),
|
||||||
|
WindowEvent::KeyboardInput {
|
||||||
|
event: KeyEvent {
|
||||||
|
physical_key: PhysicalKey::Code(KeyCode::Escape),
|
||||||
|
state: ElementState::Pressed, ..
|
||||||
|
}, ..
|
||||||
|
} => event_loop.exit(),
|
||||||
|
WindowEvent::Resized(size) => {
|
||||||
|
if let Some(gpu) = &mut self.gpu {
|
||||||
|
gpu.resize(size.width, size.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::RedrawRequested => {
|
||||||
|
if let Some(gpu) = &mut self.gpu { gpu.frame(); }
|
||||||
|
// Sofort den nächsten Frame anfordern (Vsync drosselt).
|
||||||
|
if let Some(w) = &self.window { w.request_redraw(); }
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// Testmuster für Schritt 2: 8px-Schachbrett mit Farbverlauf, damit
|
||||||
|
// Auflösung (320×240) und Orientierung des internen Targets sichtbar
|
||||||
|
// sind. Wird in Schritt 3 durch die Szenen-Pipeline ersetzt.
|
||||||
|
|
||||||
|
// Fullscreen-Dreieck ohne Vertex-Buffer: überdeckt (-1,-1)..(1,1).
|
||||||
|
@vertex
|
||||||
|
fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
|
||||||
|
var pos = array<vec2f, 3>(
|
||||||
|
vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0),
|
||||||
|
);
|
||||||
|
return vec4f(pos[i], 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(@builtin(position) p: vec4f) -> @location(0) vec4f {
|
||||||
|
// p.xy = Pixelkoordinaten im internen Target (0..320, 0..240).
|
||||||
|
let checker = (u32(p.x / 8.0) + u32(p.y / 8.0)) % 2u;
|
||||||
|
let base = select(0.35, 0.65, checker == 1u);
|
||||||
|
// Verlauf: rot wächst nach rechts, grün nach unten.
|
||||||
|
return vec4f(
|
||||||
|
base * (0.5 + 0.5 * p.x / 320.0),
|
||||||
|
base * (0.5 + 0.5 * p.y / 240.0),
|
||||||
|
base * 0.5,
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user