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:
Homer Simpson
2026-07-05 20:41:02 +02:00
parent 2afb56b329
commit 9eead719b0
157 changed files with 47270 additions and 2 deletions
+213
View File
@@ -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(())
}