Update graphics demo; Restructure code;

This commit is contained in:
Gabriel Bjørnager Jensen 2025-03-04 21:21:14 +01:00
parent 48a8fe94a2
commit 6ef261be61
18 changed files with 573 additions and 232 deletions

View file

@ -3,6 +3,11 @@
This is the changelog of Bedrock.
See `README.md` for more information.
## 0.4.0-0
* Update graphics demo
* Restructure code
## 0.3.1
* Add desktop entry

View file

@ -1,19 +1,20 @@
[package]
name = "bedrock"
version = "0.3.1"
version = "0.4.0-0"
authors = ["Achernar", "Gabriel Bjørnager Jensen"]
edition = "2024"
repository = "ssh://git@ssh.mandelbrot.dk:bedrock"
[dependencies]
ctrlc = "3.4"
pollster = "0.4"
rand = "0.9"
toml = "0.8"
wgpu = "24.0"
winit = "0.30"
ctrlc = "3.4"
pollster = "0.4"
rand = "0.9"
toml = "0.8"
wgpu = "24.0"
winit = "0.30"
serde = { version = "1.0", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
zerocopy = { version = "0.8", features = ["derive", "simd"] }
[workspace.lints.clippy]
arc_with_non_send_sync = "forbid"

View file

@ -3,11 +3,11 @@
<svg height="96" width="96" xmlns="http://www.w3.org/2000/svg">
<rect fill="#060606" height="100%" width="100%" x="0" y="0" /> <!-- 0% -->
<polygon fill="#222222" points="96,0 96,80 48,48" />
<polygon fill="#414141" points="48,48 96,48 96,96 80,96" />
<polygon fill="#636363" points="48,48 96,96 24,96" />
<polygon fill="#878787" points="48,48 48,96 0,96 0,80" />
<polygon fill="#AEAEAE" points="0,24 48,48 0,96" />
<polygon fill="#D6D6D6" points="0,0 24,0 48,48 0,48" />
<polygon fill="#FFFFFF" points="2,0 48,0 48,48" />
<polygon fill="#222222" points="96,0 48,48 96,80" />
<polygon fill="#414141" points="48,48 80,96 96,96 96,48" />
<polygon fill="#636363" points="48,48 24,96 96,96" />
<polygon fill="#878787" points="48,48 0,80 0,96 48,96" />
<polygon fill="#AEAEAE" points="0,24 0,96 48,48" />
<polygon fill="#D6D6D6" points="0,0 0,48 48,48 24,0" />
<polygon fill="#FFFFFF" points="2,0 48,48 48,0" />
</svg>

Before

Width:  |  Height:  |  Size: 591 B

After

Width:  |  Height:  |  Size: 591 B

View file

@ -1,8 +1,8 @@
// Copyright 2025 Gabriel Bjørnager Jensen.
use crate::app::{App, Event, GraphicsContext};
use crate::app::{App, Event};
use crate::graphics::GraphicsContext;
use pollster::block_on;
use winit::application::ApplicationHandler;
use winit::event::{StartCause, WindowEvent};
use winit::event_loop::ActiveEventLoop;
@ -12,7 +12,7 @@ impl ApplicationHandler<Event> for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.graphics_context.is_some() { return };
let graphics_context = block_on(GraphicsContext::new(event_loop));
let graphics_context = GraphicsContext::new(event_loop);
self.graphics_context = Some(graphics_context);
}
@ -36,7 +36,7 @@ impl ApplicationHandler<Event> for App {
WindowEvent::RedrawRequested => {
let graphics_context = self.graphics_context.as_mut().unwrap();
graphics_context.render();
graphics_context.render(&self.map);
}
WindowEvent::Resized(size) => {

View file

@ -8,8 +8,9 @@ mod regenerate_level;
mod run;
mod tick;
use crate::app::{Config, GraphicsContext};
use crate::config::Config;
use crate::error::{Error, Result};
use crate::graphics::GraphicsContext;
use crate::level::Map;
use std::fs::{create_dir_all, write};
@ -98,31 +99,8 @@ impl App {
eprintln!("writing test level to \"{}\"", test_level_path.display());
let _ = write(test_level_path, TEST_LEVEL);
let _ = write(test_level_path, include_str!("test_level.toml"));
Ok(data_dir)
}
}
const TEST_LEVEL: &str =
r#"[level]
name = "test"
creatour = "Achernar"
description = "A test level."
[[chunk]]
terrain_height = 0.0625
ground = "sand"
[[chunk]]
terrain_height = 0.125
ground = "dirt"
[[chunk]]
terrain_height = 0.25
ground = "stone"
"#;

View file

@ -0,0 +1,19 @@
[level]
name = "test"
creatour = "Achernar"
description = "A test level."
[[chunk]]
terrain_height = 0.0625
ground = "sand"
[[chunk]]
terrain_height = 0.125
ground = "dirt"
[[chunk]]
terrain_height = 0.25
ground = "stone"

View file

@ -1,159 +0,0 @@
// Copyright 2025 Gabriel Bjørnager Jensen.
mod render;
use crate::version::Version;
use std::pin::Pin;
use wgpu::{
Device,
Instance,
PresentMode,
PowerPreference,
Queue,
RequestAdapterOptions,
Surface,
SurfaceConfiguration,
SurfaceTargetUnsafe,
TextureUsages,
};
use winit::dpi::PhysicalSize;
use winit::event_loop::ActiveEventLoop;
use winit::window::{Window, WindowAttributes};
#[derive(Debug)]
pub struct GraphicsContext {
surface_config: SurfaceConfiguration,
queue: Queue,
device: Device,
surface: Surface<'static>,
window: Pin<Box<Window>>,
}
impl GraphicsContext {
#[must_use]
pub async fn new(event_loop: &ActiveEventLoop) -> Self {
eprintln!("creating graphics context");
let size = PhysicalSize {
width: 0x200,
height: 0x180,
};
eprintln!("opening window");
let window = {
let title = format!("Bedrock {}", Version::CURRENT);
let attrs = WindowAttributes::default()
.with_inner_size(size)
.with_min_inner_size(size)
.with_title(&title);
match event_loop.create_window(attrs) {
Ok(window) => Pin::new(Box::new(window)),
Err(e) => panic!("unable to open window: {e}"),
}
};
eprintln!("creating wgpu instance");
let instance = Instance::new(&Default::default());
eprintln!("creating surface");
let surface = unsafe {
let target = match SurfaceTargetUnsafe::from_window(&*window) {
Ok(target) => target,
Err(e) => panic!("unable to create surface target: {e}"),
};
match instance.create_surface_unsafe(target) {
Ok(surface) => surface,
Err(e) => panic!("unable to create surface: {e}"),
}
};
eprintln!("creating adapter");
let adapter = {
let options = RequestAdapterOptions {
power_preference: PowerPreference::LowPower,
compatible_surface: Some(&surface),
..Default::default()
};
instance
.request_adapter(&options)
.await
.expect("no adapter available")
};
let surface_capabilities = surface.get_capabilities(&adapter);
let surface_format = surface_capabilities
.formats
.iter()
.find(|f| f.is_srgb())
.copied()
.expect("unable to find srgb surface format");
eprintln!("creating device and queue");
let (device, queue) = {
match adapter.request_device(&Default::default(), None).await {
Ok((device, queue)) => (device, queue),
Err(e) => panic!("unable to find device: {e}"),
}
};
eprintln!("configuring surface");
let surface_config = SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width,
height: size.height,
present_mode: PresentMode::Fifo,
desired_maximum_frame_latency: 0x2,
alpha_mode: surface_capabilities.alpha_modes[0x0],
view_formats: Default::default(),
};
surface.configure(&device, &surface_config);
Self {
surface_config,
queue,
device,
surface,
window,
}
}
#[inline]
pub fn resize(&mut self, width: u32, height: u32) {
self.surface_config.width = width;
self.surface_config.height = height;
eprintln!("resizing graphics context to `{width}*{height}`");
self.surface.configure(&self.device, &self.surface_config);
}
#[inline(always)]
pub fn request_redraw(&mut self) {
self.window.request_redraw();
}
}

View file

@ -1,11 +1,7 @@
// Copyright 2025 Gabriel Bjørnager Jensen.
mod app;
mod config;
mod event;
mod graphics_context;
pub use app::App;
pub use config::Config;
pub use event::Event;
pub use graphics_context::GraphicsContext;

5
src/config/mod.rs Normal file
View file

@ -0,0 +1,5 @@
// Copyright 2025 Gabriel Bjørnager Jensen.
mod config;
pub use config::Config;

View file

@ -0,0 +1,302 @@
// Copyright 2025 Gabriel Bjørnager Jensen.
mod render;
use crate::graphics::{MAIN_SHADER, Vec4, Vertex};
use crate::version::Version;
use pollster::block_on;
use std::pin::Pin;
use wgpu::{
BlendState,
Buffer,
BufferUsages,
ColorTargetState,
ColorWrites,
Device,
Face,
FragmentState,
FrontFace,
Instance,
PolygonMode,
PowerPreference,
PresentMode,
PrimitiveState,
PrimitiveTopology,
Queue,
RenderPipeline,
RenderPipelineDescriptor,
RequestAdapterOptions,
Surface,
SurfaceConfiguration,
SurfaceTargetUnsafe,
TextureUsages,
VertexState,
};
use wgpu::util::{BufferInitDescriptor, DeviceExt};
use winit::dpi::PhysicalSize;
use winit::event_loop::ActiveEventLoop;
use winit::window::{Window, WindowAttributes};
use zerocopy::IntoBytes;
#[derive(Debug)]
pub struct GraphicsContext {
surface_config: SurfaceConfiguration,
vertex_count: u32,
vertex_buf: Buffer,
pipeline: RenderPipeline,
queue: Queue,
device: Device,
surface: Surface<'static>,
window: Pin<Box<Window>>,
}
impl GraphicsContext {
#[must_use]
pub fn new(event_loop: &ActiveEventLoop) -> Self {
eprintln!("creating graphics context");
let size = PhysicalSize {
width: 0x200,
height: 0x180,
};
eprintln!("opening window");
let window = {
let title = format!("Bedrock {}", Version::CURRENT);
let attrs = WindowAttributes::default()
.with_inner_size(size)
.with_min_inner_size(size)
.with_title(&title);
match event_loop.create_window(attrs) {
Ok(window) => Pin::new(Box::new(window)),
Err(e) => panic!("unable to open window: {e}"),
}
};
eprintln!("creating wgpu instance");
let instance = Instance::new(&Default::default());
eprintln!("creating surface");
let surface = unsafe {
let target = match SurfaceTargetUnsafe::from_window(&*window) {
Ok(target) => target,
Err(e) => panic!("unable to create surface target: {e}"),
};
match instance.create_surface_unsafe(target) {
Ok(surface) => surface,
Err(e) => panic!("unable to create surface: {e}"),
}
};
eprintln!("creating adapter");
let adapter = {
let options = RequestAdapterOptions {
power_preference: PowerPreference::LowPower,
compatible_surface: Some(&surface),
..Default::default()
};
block_on(instance.request_adapter(&options))
.expect("no adapter available")
};
let surface_capabilities = surface.get_capabilities(&adapter);
let surface_format = surface_capabilities
.formats
.iter()
.find(|f| f.is_srgb())
.copied()
.expect("unable to find srgb surface format");
eprintln!("creating device and queue");
let (device, queue) = {
match block_on(adapter.request_device(&Default::default(), None)) {
Ok((device, queue)) => (device, queue),
Err(e) => panic!("unable to find device: {e}"),
}
};
eprintln!("configuring surface");
let surface_config = SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width,
height: size.height,
present_mode: PresentMode::Fifo,
desired_maximum_frame_latency: 0x2,
alpha_mode: surface_capabilities.alpha_modes[0x0],
view_formats: Default::default(),
};
surface.configure(&device, &surface_config);
eprintln!("creating shader module");
let shader = device.create_shader_module(MAIN_SHADER);
eprintln!("creating render pipeline");
let pipeline = {
let layout = device.create_pipeline_layout(&Default::default());
let vertex = VertexState {
module: &shader,
entry_point: Some("vertex_main"),
buffers: &[Vertex::LAYOUT],
compilation_options: Default::default(),
};
let fragment = FragmentState {
module: &shader,
entry_point: Some("fragment_main"),
targets: &[
Some(ColorTargetState {
format: surface_config.format,
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})
],
compilation_options: Default::default(),
};
let primitive = PrimitiveState {
topology: PrimitiveTopology::TriangleList,
front_face: FrontFace::Ccw,
cull_mode: Some(Face::Back),
polygon_mode: PolygonMode::Fill,
..Default::default()
};
let descriptor = RenderPipelineDescriptor {
label: Some("main"),
layout: Some(&layout),
vertex,
fragment: Some(fragment),
primitive,
depth_stencil: Default::default(),
multisample: Default::default(),
multiview: Default::default(),
cache: Default::default(),
};
device.create_render_pipeline(&descriptor)
};
eprintln!("creating vertex buffer");
let (vertex_count, vertex_buf) = {
let vertices = [
Vertex {
position: Vec4::new( 0.0, 1.0, 1.0, 1.0),
colour: Vec4::new( 1.0, 0.0, 0.0, 1.0),
},
Vertex {
position: Vec4::new(-0.5, 0.0, 0.0, 1.0),
colour: Vec4::new( 0.0, 1.0, 0.0, 1.0),
},
Vertex {
position: Vec4::new( 0.5, 0.0, 0.0, 1.0),
colour: Vec4::new( 0.0, 0.0, 1.0, 1.0),
},
Vertex {
position: Vec4::new(-0.5, 0.0, 0.0, 1.0),
colour: Vec4::new( 0.0, 1.0, 1.0, 1.0),
},
Vertex {
position: Vec4::new(-1.0, -1.0, 0.0, 1.0),
colour: Vec4::new( 1.0, 0.0, 1.0, 1.0),
},
Vertex {
position: Vec4::new( 0.0, -1.0, 0.0, 1.0),
colour: Vec4::new( 1.0, 1.0, 0.0, 1.0),
},
Vertex {
position: Vec4::new( 0.5, 0.0, 0.0, 1.0),
colour: Vec4::new( 1.0, 1.0, 1.0, 1.0),
},
Vertex {
position: Vec4::new( 0.0, -1.0, 0.0, 1.0),
colour: Vec4::new( 0.0, 0.0, 0.0, 1.0),
},
Vertex {
position: Vec4::new( 1.0, -1.0, 0.0, 1.0),
colour: Vec4::new( 0.0, 0.0, 0.0, 1.0),
},
];
let descriptor = BufferInitDescriptor {
label: Some("main"),
contents: vertices.as_bytes(),
usage: BufferUsages::VERTEX,
};
let count = vertices.len() as u32;
let buf = device.create_buffer_init(&descriptor);
(count, buf)
};
Self {
surface_config,
vertex_count,
vertex_buf,
pipeline,
queue,
device,
surface,
window,
}
}
#[inline]
pub fn resize(&mut self, width: u32, height: u32) {
self.surface_config.width = width;
self.surface_config.height = height;
eprintln!("resizing graphics context to `{width}*{height}`");
self.surface.configure(&self.device, &self.surface_config);
}
#[inline(always)]
pub fn request_redraw(&mut self) {
self.window.request_redraw();
}
}

View file

@ -1,30 +1,47 @@
// Copyright 2025 Gabriel Bjørnager Jensen.
use crate::app::GraphicsContext;
use crate::graphics::GraphicsContext;
use crate::level::Map;
use std::f64;
use std::iter;
use std::time::{SystemTime, UNIX_EPOCH};
use wgpu::{
Color,
CommandEncoderDescriptor,
LoadOp,
Operations,
RenderPassColorAttachment,
RenderPassDescriptor,
StoreOp,
TextureViewDescriptor,
};
impl GraphicsContext {
pub fn render(&mut self) {
pub fn render(&mut self, _map: &Map) {
let output = match self.surface.get_current_texture() {
Ok(output) => output,
Err(e) => panic!("unable to get current texture: {e}"),
};
let view = output.texture.create_view(&Default::default());
let view = {
let descriptor = TextureViewDescriptor {
label: Some("main"),
let mut encoder = self.device.create_command_encoder(&Default::default());
..Default::default()
};
output.texture.create_view(&descriptor)
};
let mut encoder = {
let descriptor = CommandEncoderDescriptor {
label: Some("main"),
};
self.device.create_command_encoder(&descriptor)
};
let colour = {
let time = SystemTime::now()
@ -32,27 +49,38 @@ impl GraphicsContext {
.unwrap()
.as_secs_f64();
let hue = time / 4.0;
let hue = time / 8.0;
hsva(hue, 1.0, 1.0, 1.0)
};
let _ = encoder.begin_render_pass(&RenderPassDescriptor {
color_attachments: &[
Some(RenderPassColorAttachment {
view: &view,
resolve_target: None,
{
let descriptor = RenderPassDescriptor {
label: Some("main"),
ops: Operations {
load: LoadOp::Clear(colour),
color_attachments: &[
Some(RenderPassColorAttachment {
view: &view,
resolve_target: None,
store: StoreOp::Store,
},
}),
],
ops: Operations {
load: LoadOp::Clear(colour),
..Default::default()
});
store: StoreOp::Store,
},
}),
],
..Default::default()
};
let mut pass = encoder.begin_render_pass(&descriptor);
pass.set_pipeline(&self.pipeline);
pass.set_vertex_buffer(0x0, self.vertex_buf.slice(..));
pass.draw(0x0..self.vertex_count, 0x0..0x1);
}
self.queue.submit(iter::once(encoder.finish()));

32
src/graphics/main.wgsl Normal file
View file

@ -0,0 +1,32 @@
// Copyright 2025 Gabriel Bjørnager Jensen.
struct VertexInput {
@location(0x0) position: vec4<f32>,
@location(0x1) colour: vec4<f32>,
};
struct VertexOutput {
@builtin(position) clip: vec4<f32>,
@location(0x0) colour: vec4<f32>,
};
@vertex
//@must_use
fn vertex_main(
input: VertexInput,
) -> VertexOutput {
var out: VertexOutput;
out.clip = input.position;
out.colour = input.colour;
return out;
}
@fragment
//@must_use
fn fragment_main(
input: VertexOutput,
) -> @location(0x0) vec4<f32> {
return input.colour;
}

14
src/graphics/mod.rs Normal file
View file

@ -0,0 +1,14 @@
// Copyright 2025 Gabriel Bjørnager Jensen.
mod graphics_context;
mod vec4;
mod vertex;
use vec4::Vec4;
use vertex::Vertex;
pub use graphics_context::GraphicsContext;
use wgpu::{include_wgsl, ShaderModuleDescriptor};
const MAIN_SHADER: ShaderModuleDescriptor = include_wgsl!("main.wgsl");

73
src/graphics/vec4/mod.rs Normal file
View file

@ -0,0 +1,73 @@
// Copyright 2025 Gabriel Bjørnager Jensen.
use std::cmp::Ordering;
use zerocopy::{
FromZeros,
Immutable,
IntoBytes,
KnownLayout,
transmute,
};
#[cfg(target_arch = "x86")]
type Buffer = std::arch::x86::__m128;
#[cfg(target_arch = "x86_64")]
type Buffer = std::arch::x86_64::__m128;
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
type Buffer = [f32; 0x4];
const _: () = assert!(size_of::<Buffer>() == size_of::<f32>() * 0x4);
#[repr(align(0x10), C)]
#[derive(Clone, Copy, Debug, FromZeros, Immutable, IntoBytes, KnownLayout)]
pub(super) struct Vec4(Buffer);
impl Vec4 {
#[inline(always)]
#[must_use]
pub const fn new(x: f32, y: f32, z: f32, w: f32) -> Self {
let buf = [x, y, z, w];
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
{
let buf = transmute!(buf);
Self(buf)
}
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
{
Self(buf)
}
}
#[inline(always)]
#[must_use]
pub const fn get(self) -> (f32, f32, f32, f32) {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
{
let [x, y, z, w] = transmute!(self.0);
(x, y, z, w)
}
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
{
self.0
}
}
}
impl PartialEq for Vec4 {
#[inline(always)]
fn eq(&self, other: &Self) -> bool {
self.get() == other.get()
}
}
impl PartialOrd for Vec4 {
#[inline(always)]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.get().partial_cmp(&other.get())
}
}

View file

@ -0,0 +1,44 @@
// Copyright 2025 Gabriel Bjørnager Jensen.
use crate::graphics::Vec4;
use std::mem::offset_of;
use wgpu::{
BufferAddress,
VertexAttribute,
VertexBufferLayout,
VertexFormat,
VertexStepMode,
};
use zerocopy::{FromZeros, Immutable, IntoBytes, KnownLayout};
#[repr(C)]
#[derive(Clone, Debug, FromZeros, Immutable, IntoBytes, KnownLayout, PartialEq)]
pub(super) struct Vertex {
pub position: Vec4,
pub colour: Vec4,
}
impl Vertex {
pub const LAYOUT: VertexBufferLayout<'_> = VertexBufferLayout {
array_stride: size_of::<Self>() as BufferAddress,
step_mode: VertexStepMode::Vertex,
attributes: &[
VertexAttribute {
offset: offset_of!(Self, position) as BufferAddress,
shader_location: 0x0,
format: VertexFormat::Float32x4,
},
VertexAttribute {
offset: offset_of!(Self, colour) as BufferAddress,
shader_location: 0x1,
format: VertexFormat::Float32x4,
},
],
};
}
const _: () = assert!(Vertex::LAYOUT.attributes[0x0].offset == 0x00);
const _: () = assert!(Vertex::LAYOUT.attributes[0x1].offset == 0x10);

View file

@ -1,21 +1,24 @@
// Copyright 2025 Gabriel Bjørnager Jensen.
#![feature(float_gamma)]
#![feature(thread_sleep_until)]
// Why is this needed?
#![allow(clippy::module_inception)]
const _: () = assert!(usize::BITS >= u32::BITS);
mod app;
mod config;
mod error;
mod graphics;
mod level;
mod version;
use crate::app::App;
use crate::error::Result;
use std::process::exit;
const _: () = assert!(usize::BITS >= u32::BITS);
fn main() -> ! {
let run = || -> Result<()> {
let app = App::new()?;

View file

@ -13,9 +13,9 @@ pub struct Version {
impl Version {
pub const CURRENT: Self = Self {
major: 0x0,
minor: 0x3,
patch: 0x1,
pre: None,
minor: 0x4,
patch: 0x0,
pre: Some(0x0),
};
}