Import Oxide engine (Stages 0–10) under MIT license
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>
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
//! 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user