summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMicha White <botahamec@outlook.com>2022-09-17 18:41:21 -0400
committerMicha White <botahamec@outlook.com>2022-09-17 18:41:21 -0400
commita005620119b9c1d18c750552d0a707f36f407ea1 (patch)
treeddbc39c56189a1818a80440e2233e95a06df6db2
renderer stuff
-rw-r--r--.gitignore2
-rw-r--r--Cargo.toml18
-rw-r--r--examples/black.rs40
-rw-r--r--src/lib.rs9
-rw-r--r--src/renderer.rs250
5 files changed, 319 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4fffb2f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/target
+/Cargo.lock
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..03a4a5e
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "alligator_render"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+winit = "0.27"
+log = "0.4"
+wgpu = "0.13"
+thiserror = "1"
+
+[dev-dependencies]
+pollster = "0.2"
+
+[[example]]
+name = "black" \ No newline at end of file
diff --git a/examples/black.rs b/examples/black.rs
new file mode 100644
index 0000000..b39f6ed
--- /dev/null
+++ b/examples/black.rs
@@ -0,0 +1,40 @@
+use alligator_render::Renderer;
+use wgpu::PowerPreference;
+use winit::{
+ event::{Event, WindowEvent},
+ event_loop::{ControlFlow, EventLoop},
+ window::Window,
+};
+
+fn main() {
+ // initialize a window
+ let event_loop = EventLoop::new();
+ let window = Window::new(&event_loop).unwrap();
+ window.set_title("Black Screen.exe");
+ window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
+
+ // initialize the renderer
+ let mut renderer =
+ pollster::block_on(Renderer::new(&window, PowerPreference::LowPower)).unwrap();
+
+ event_loop.run(move |event, _, control_flow| match event {
+ Event::WindowEvent { window_id, event } => {
+ if window_id == window.id() {
+ match event {
+ WindowEvent::Resized(size) => renderer.resize(size),
+ WindowEvent::CloseRequested => *control_flow = ControlFlow::ExitWithCode(0),
+ _ => (),
+ }
+ }
+ }
+ Event::RedrawRequested(window_id) => {
+ if window_id == window.id() {
+ _ = renderer.render();
+ }
+ }
+ Event::MainEventsCleared => {
+ window.request_redraw();
+ }
+ _ => {}
+ })
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..a902878
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,9 @@
+#![feature(let_else)]
+#![feature(nonzero_min_max)]
+#![warn(clippy::pedantic)]
+#![warn(clippy::nursery)]
+#![allow(clippy::module_name_repetitions)]
+
+pub mod renderer;
+
+pub use renderer::Renderer;
diff --git a/src/renderer.rs b/src/renderer.rs
new file mode 100644
index 0000000..f937183
--- /dev/null
+++ b/src/renderer.rs
@@ -0,0 +1,250 @@
+use std::num::NonZeroU32;
+
+use thiserror::Error;
+use winit::{
+ dpi::{LogicalSize, PhysicalSize},
+ error::OsError,
+ event_loop::EventLoop,
+ window::{Fullscreen, Window, WindowBuilder},
+};
+
+/// No device could be found which supports the given surface
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
+#[error("No GPU could be found on this machine")]
+pub struct NoGpuError {
+ /// Prevents this type from being constructed
+ _priv: (),
+}
+
+impl NoGpuError {
+ /// Create a new error
+ const fn new() -> Self {
+ Self { _priv: () }
+ }
+}
+
+#[derive(Debug, Error)]
+pub enum NewRendererError {
+ #[error(transparent)]
+ NoGpu(#[from] NoGpuError),
+ #[error(transparent)]
+ WindowInitError(#[from] OsError),
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct Resizable {
+ pub min_width: Option<NonZeroU32>,
+ pub min_height: Option<NonZeroU32>,
+ pub max_width: Option<NonZeroU32>,
+ pub max_height: Option<NonZeroU32>,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct WindowInfo {
+ pub default_x: i32,
+ pub default_y: i32,
+ pub resizability: Option<Resizable>,
+ pub default_maximized: bool,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum WindowMode {
+ Windowed(WindowInfo),
+ // TODO support choosing a monitor
+ BorderlessFullscreen, // TODO exclusive fullscreen
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+// TODO to consider: don't allow the x and y to be too big
+// TODO window icon
+pub struct RendererConfig {
+ pub default_width: NonZeroU32,
+ pub default_height: NonZeroU32,
+ pub size: WindowMode,
+ pub title: Option<Box<str>>,
+ pub low_power: bool,
+ pub vsync: bool,
+}
+
+pub struct Renderer {
+ surface: wgpu::Surface,
+ device: wgpu::Device,
+ queue: wgpu::Queue,
+ config: wgpu::SurfaceConfiguration,
+ window: Window,
+ event_loop: EventLoop<()>,
+}
+
+// TODO make this more complete
+impl Renderer {
+ /// Initializes the renderer
+ ///
+ /// # Errors
+ ///
+ /// Returns a [`NoGpu`] error if no device could be detected that can
+ /// display to the window
+ // TODO make it possible to use without winit (ie, use a bitmap in memory as a surface)
+ // TODO make PowerPreference a configuration option
+ // TODO check for zero size windows
+ #[allow(clippy::missing_panics_doc)]
+ pub async fn new(config: RendererConfig) -> Result<Self, NewRendererError> {
+ let mut builder = WindowBuilder::new()
+ .with_title(config.title.unwrap_or_else(|| "Alligator Game".into()))
+ .with_inner_size(LogicalSize::new(
+ config.default_width.get(),
+ config.default_height.get(),
+ ));
+
+ match config.size {
+ WindowMode::Windowed(size) => {
+ builder = builder.with_maximized(size.default_maximized);
+
+ if let Some(resizing_options) = size.resizability {
+ if resizing_options.max_height.is_some() || resizing_options.max_width.is_some()
+ {
+ builder = builder.with_max_inner_size(LogicalSize::new(
+ resizing_options.max_width.unwrap_or(NonZeroU32::MAX).get(),
+ resizing_options.max_height.unwrap_or(NonZeroU32::MAX).get(),
+ ));
+ }
+
+ if resizing_options.min_height.is_some() || resizing_options.min_width.is_some()
+ {
+ builder = builder.with_min_inner_size(LogicalSize::new(
+ resizing_options.min_width.unwrap_or(NonZeroU32::MAX).get(),
+ resizing_options.min_height.unwrap_or(NonZeroU32::MAX).get(),
+ ));
+ }
+ } else {
+ builder = builder.with_resizable(false);
+ }
+ }
+ WindowMode::BorderlessFullscreen => {
+ builder = builder.with_fullscreen(Some(Fullscreen::Borderless(None)));
+ }
+ }
+
+ let event_loop = EventLoop::new();
+ let window = builder.build(&event_loop)?;
+
+ // the instance's main purpose is to create an adapter and a surface
+ let instance = wgpu::Instance::new(wgpu::Backends::all());
+
+ // the surface is the part of the screen we'll draw to
+ // TODO guarantee the window to stay open longer than the surface
+ let surface = unsafe { instance.create_surface(&window) };
+
+ let power_preference = if config.low_power {
+ wgpu::PowerPreference::LowPower
+ } else {
+ wgpu::PowerPreference::HighPerformance
+ };
+
+ // the adapter is the handle to the GPU
+ let adapter = instance
+ .request_adapter(&wgpu::RequestAdapterOptions {
+ power_preference,
+ compatible_surface: Some(&surface),
+ force_fallback_adapter: false,
+ })
+ .await;
+ let adapter = adapter.or_else(|| {
+ instance
+ .enumerate_adapters(wgpu::Backends::all())
+ .find(|adapter| !surface.get_supported_formats(adapter).is_empty())
+ });
+ let Some(adapter) = adapter else { return Err(NoGpuError::new().into()) };
+
+ // gets a connection to the device, as well as a handle to its command queue
+ // the options chosen here ensure that this is guaranteed to not panic
+ let (device, queue) = adapter
+ .request_device(
+ &wgpu::DeviceDescriptor {
+ label: None,
+ features: wgpu::Features::empty(),
+ limits: wgpu::Limits::default(),
+ },
+ None,
+ )
+ .await
+ .unwrap();
+
+ // configuration for the surface
+ let config = wgpu::SurfaceConfiguration {
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
+ format: surface.get_supported_formats(&adapter)[0], // TODO make this configurable
+ width: config.default_width.get(),
+ height: config.default_height.get(),
+ present_mode: wgpu::PresentMode::Mailbox, // TODO make this configurable
+ };
+
+ surface.configure(&device, &config);
+
+ Ok(Self {
+ surface,
+ device,
+ queue,
+ config,
+ window,
+ event_loop,
+ })
+ }
+
+ pub const fn size(&self) -> PhysicalSize<u32> {
+ PhysicalSize::new(self.config.width, self.config.height)
+ }
+
+ // TODO return error for zero-sized windows
+ pub fn resize(&mut self, size: PhysicalSize<u32>) {
+ if size.width > 0 && size.height > 0 {
+ self.config.height = size.height;
+ self.config.width = size.width;
+ self.surface.configure(&self.device, &self.config);
+ }
+ }
+
+ /// Renders a new frame to the window
+ ///
+ /// # Errors
+ ///
+ /// A number of problems could occur here. A timeout could occur while
+ /// trying to acquire the next frame. There may also be no more memory left
+ /// that can be used for the new frame.
+ pub fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
+ // the new texture we can render to
+ let output = self.surface.get_current_texture()?;
+ let view = output
+ .texture
+ .create_view(&wgpu::TextureViewDescriptor::default());
+
+ // this will allow us to send commands to the gpu
+ let mut encoder = self
+ .device
+ .create_command_encoder(&wgpu::CommandEncoderDescriptor {
+ label: Some("Render Encoder"),
+ });
+
+ {
+ let _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("Render Pass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: &view,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
+ store: true,
+ },
+ })],
+ depth_stencil_attachment: None,
+ });
+ }
+ // the encoder can't finish building the command buffer until the
+ // render pass is dropped
+
+ // submit the command buffer to the GPU
+ self.queue.submit(std::iter::once(encoder.finish()));
+ output.present();
+
+ Ok(())
+ }
+}