9eead719b0
Full project snapshot migrated to new Gitea remote without history: engine, editor, physics, script, examples, tests, docs, and assets. Relicensed from GPLv3 to MIT and updated repo URLs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
214 lines
8.3 KiB
Rust
214 lines
8.3 KiB
Rust
//! Window surface rendering: swapchain configuration, resize, clear loop.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use winit::window::Window;
|
|
|
|
use super::{clear_view, Gpu, RenderError};
|
|
use crate::math::Color;
|
|
use crate::window::RenderCtx;
|
|
|
|
/// Renders to a window surface.
|
|
///
|
|
/// Owns the [`Gpu`] plus the window's [`wgpu::Surface`] and its
|
|
/// configuration. Stage 2 scope: every frame is cleared to
|
|
/// [`clear_color`](Self::clear_color); draw passes come in later stages.
|
|
pub struct RenderContext {
|
|
gpu: Gpu,
|
|
surface: wgpu::Surface<'static>,
|
|
config: wgpu::SurfaceConfiguration,
|
|
clear_color: Color,
|
|
}
|
|
|
|
impl RenderContext {
|
|
/// Acquires the GPU and configures a surface for `window`.
|
|
///
|
|
/// The window is held by `Arc` so the surface (which borrows it) can be
|
|
/// `'static`, as winit hands out windows from its event loop.
|
|
///
|
|
/// To run on any device, several render backends are tried in turn — the
|
|
/// default (env-selected Vulkan/Metal/DX12), then GL, then a software
|
|
/// adapter — and the first that produces a *configurable* surface wins.
|
|
/// This is what lets the engine survive drivers that report a GPU but
|
|
/// cannot present to the window's surface (e.g. old NVIDIA on Wayland under
|
|
/// Vulkan, where `surface.configure` would otherwise fail).
|
|
pub fn new(window: Arc<Window>) -> Result<Self, RenderError> {
|
|
// (label, backend override, force a software adapter)
|
|
let attempts: [(&str, Option<wgpu::Backends>, bool); 3] = [
|
|
("default", None, false),
|
|
("GL", Some(wgpu::Backends::GL), false),
|
|
("software", None, true),
|
|
];
|
|
|
|
let mut last_err: Option<RenderError> = None;
|
|
for (i, &(label, backends, force_fallback)) in attempts.iter().enumerate() {
|
|
match Self::try_backend(&window, backends, force_fallback) {
|
|
Ok(ctx) => {
|
|
if i > 0 {
|
|
log::warn!("render backend fell back to '{label}'");
|
|
}
|
|
return Ok(ctx);
|
|
}
|
|
Err(err) => {
|
|
log::warn!("render backend '{label}' unavailable: {err}");
|
|
last_err = Some(err);
|
|
}
|
|
}
|
|
}
|
|
Err(last_err.unwrap_or(RenderError::NoWorkingBackend))
|
|
}
|
|
|
|
/// Attempts one backend: build an instance (optionally forcing `backends`),
|
|
/// create the surface, acquire an adapter/device (optionally a software
|
|
/// one), and configure the surface. Any failure returns `Err` so the caller
|
|
/// can try the next backend rather than aborting the process.
|
|
fn try_backend(
|
|
window: &Arc<Window>,
|
|
backends: Option<wgpu::Backends>,
|
|
force_fallback_adapter: bool,
|
|
) -> Result<Self, RenderError> {
|
|
let size = window.inner_size();
|
|
// The window doubles as the display handle (needed by GL/X11-style
|
|
// backends); `from_env` keeps backend/flags overridable via WGPU_*.
|
|
let mut desc =
|
|
wgpu::InstanceDescriptor::new_with_display_handle_from_env(Box::new(window.clone()));
|
|
if let Some(backends) = backends {
|
|
desc.backends = backends;
|
|
}
|
|
let instance = wgpu::Instance::new(desc);
|
|
let surface = instance.create_surface(window.clone())?;
|
|
let gpu = Gpu::with_instance(instance, Some(&surface), force_fallback_adapter)?;
|
|
|
|
let config = surface
|
|
.get_default_config(gpu.adapter(), size.width.max(1), size.height.max(1))
|
|
.ok_or(RenderError::UnsupportedSurface)?;
|
|
configure_surface(gpu.device(), &surface, &config)?;
|
|
log::info!(
|
|
"surface configured: {}x{} {:?} ({:?}) on {:?}",
|
|
config.width,
|
|
config.height,
|
|
config.format,
|
|
config.present_mode,
|
|
gpu.adapter().get_info().backend,
|
|
);
|
|
|
|
Ok(Self {
|
|
gpu,
|
|
surface,
|
|
config,
|
|
clear_color: Color::BLACK,
|
|
})
|
|
}
|
|
|
|
/// Reconfigures the surface for a new window size. Zero dimensions
|
|
/// (minimized window) are clamped to 1 so the surface stays valid.
|
|
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.gpu.device(), &self.config);
|
|
}
|
|
|
|
/// Current surface size in physical pixels.
|
|
pub fn size(&self) -> (u32, u32) {
|
|
(self.config.width, self.config.height)
|
|
}
|
|
|
|
/// The surface's texture format. Apps need this to build render pipelines
|
|
/// (or UI integrations) whose output matches the surface.
|
|
pub fn surface_format(&self) -> wgpu::TextureFormat {
|
|
self.config.format
|
|
}
|
|
|
|
/// The color the surface is cleared to each frame.
|
|
pub fn clear_color(&self) -> Color {
|
|
self.clear_color
|
|
}
|
|
|
|
/// Sets the clear color; takes effect on the next rendered frame.
|
|
pub fn set_clear_color(&mut self, color: Color) {
|
|
self.clear_color = color;
|
|
}
|
|
|
|
/// Renders one frame: acquires the next surface texture, clears it, and
|
|
/// presents. Equivalent to [`render_frame_with`](Self::render_frame_with)
|
|
/// with an empty draw hook.
|
|
pub fn render_frame(&mut self, window: &Window) -> Result<(), RenderError> {
|
|
self.render_frame_with(window, |_| {})
|
|
}
|
|
|
|
/// Renders one frame, invoking `draw` after the clear and before present.
|
|
///
|
|
/// The surface texture is acquired and cleared to
|
|
/// [`clear_color`](Self::clear_color), then `draw` is handed a
|
|
/// [`RenderCtx`] so it can record additional passes into the same view
|
|
/// (use `LoadOp::Load` to preserve the clear), and finally the frame is
|
|
/// presented.
|
|
///
|
|
/// Lost or outdated surfaces (e.g. mid-resize) are reconfigured and the
|
|
/// frame skipped; timed-out or occluded acquires skip the frame. All are
|
|
/// normal transient conditions and not reported as errors.
|
|
pub fn render_frame_with(
|
|
&mut self,
|
|
window: &Window,
|
|
draw: impl FnOnce(&RenderCtx<'_>),
|
|
) -> Result<(), RenderError> {
|
|
use wgpu::CurrentSurfaceTexture;
|
|
let frame = match self.surface.get_current_texture() {
|
|
// A suboptimal frame is still presentable; the next resize event
|
|
// reconfigures the surface anyway.
|
|
CurrentSurfaceTexture::Success(frame) | CurrentSurfaceTexture::Suboptimal(frame) => {
|
|
frame
|
|
}
|
|
CurrentSurfaceTexture::Lost | CurrentSurfaceTexture::Outdated => {
|
|
self.surface.configure(self.gpu.device(), &self.config);
|
|
return Ok(());
|
|
}
|
|
CurrentSurfaceTexture::Timeout | CurrentSurfaceTexture::Occluded => return Ok(()),
|
|
CurrentSurfaceTexture::Validation => return Err(RenderError::SurfaceValidation),
|
|
};
|
|
let view = frame
|
|
.texture
|
|
.create_view(&wgpu::TextureViewDescriptor::default());
|
|
clear_view(self.gpu.device(), self.gpu.queue(), &view, self.clear_color);
|
|
|
|
let ctx = RenderCtx {
|
|
gpu: &self.gpu,
|
|
view: &view,
|
|
window,
|
|
surface_format: self.config.format,
|
|
size: (self.config.width, self.config.height),
|
|
};
|
|
draw(&ctx);
|
|
|
|
frame.present();
|
|
Ok(())
|
|
}
|
|
|
|
/// The underlying GPU handle.
|
|
pub fn gpu(&self) -> &Gpu {
|
|
&self.gpu
|
|
}
|
|
}
|
|
|
|
/// Configures `surface`, capturing any validation error instead of letting it
|
|
/// reach wgpu's default (fatal, process-aborting) error handler.
|
|
///
|
|
/// `surface.configure` returns `()` and reports failures through the device's
|
|
/// error sink, which by default panics. Wrapping it in a validation error scope
|
|
/// turns "Invalid surface" (and similar) into a recoverable [`Result`] so the
|
|
/// caller can fall back to another backend.
|
|
fn configure_surface(
|
|
device: &wgpu::Device,
|
|
surface: &wgpu::Surface<'static>,
|
|
config: &wgpu::SurfaceConfiguration,
|
|
) -> Result<(), RenderError> {
|
|
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
|
|
surface.configure(device, config);
|
|
// `pop()` consumes the guard and yields any captured error. On native
|
|
// backends the future is already resolved; `block_on` just unwraps it.
|
|
if let Some(err) = pollster::block_on(scope.pop()) {
|
|
return Err(RenderError::SurfaceConfigure(err.to_string()));
|
|
}
|
|
Ok(())
|
|
}
|