From 5e5f0c8e1ba4ed3fba1cf3890792c8b8932ca925 Mon Sep 17 00:00:00 2001 From: Josh Simmons Date: Sat, 11 May 2024 12:21:05 +0200 Subject: [PATCH] shark: Re-arrange main loop --- title/shark/src/helpers.rs | 20 - title/shark/src/main.rs | 1197 ++++++++++++++++------------ title/shark/src/pipelines/basic.rs | 18 +- title/shark/src/pipelines/text.rs | 32 +- 4 files changed, 697 insertions(+), 570 deletions(-) diff --git a/title/shark/src/helpers.rs b/title/shark/src/helpers.rs index f64afea..e45e0ba 100644 --- a/title/shark/src/helpers.rs +++ b/title/shark/src/helpers.rs @@ -1,7 +1,6 @@ use std::path::Path; use narcissus_core::{obj, Widen}; -use narcissus_image as image; use narcissus_maths::{vec2, vec3, vec4, Vec2, Vec3}; use crate::pipelines::Vertex; @@ -38,7 +37,6 @@ pub fn load_obj>(path: P) -> (Vec, Vec) { fn visit_smooth_group(&mut self, _group: i32) {} } - let start = std::time::Instant::now(); let path = path.as_ref(); let file = std::fs::File::open(path).expect("couldn't open file"); let mut visitor = ObjVisitor::default(); @@ -67,23 +65,5 @@ pub fn load_obj>(path: P) -> (Vec, Vec) { }) .unzip(); - println!( - "parsing obj {path:?} took {:?}", - std::time::Instant::now() - start - ); - (vertices, indices) } - -pub fn load_image>(path: P) -> image::Image { - let start = std::time::Instant::now(); - let path = path.as_ref(); - let texture = - image::Image::from_buffer(std::fs::read(path).expect("failed to read file").as_slice()) - .expect("failed to load image"); - println!( - "loading image {path:?} took {:?}", - std::time::Instant::now() - start - ); - texture -} diff --git a/title/shark/src/main.rs b/title/shark/src/main.rs index c9d0c85..fd87d79 100644 --- a/title/shark/src/main.rs +++ b/title/shark/src/main.rs @@ -1,28 +1,25 @@ -use std::fmt::Write; +use std::path::Path; +use std::time::{Duration, Instant}; use renderdoc_sys as rdoc; -use crate::{ - fonts::{FontFamily, Fonts}, - pipelines::{BasicPipeline, TextPipeline}, -}; -use helpers::{load_image, load_obj}; +use fonts::{FontFamily, Fonts}; +use helpers::load_obj; use narcissus_app::{create_app, Event, Key, PressedState, WindowDesc}; -use narcissus_core::{ - box_assume_init, default, rand::Pcg64, slice::array_windows, zeroed_box, BitIter, -}; +use narcissus_core::{box_assume_init, default, rand::Pcg64, zeroed_box, BitIter}; use narcissus_font::{FontCollection, GlyphCache, HorizontalMetrics}; use narcissus_gpu::{ - create_device, Access, Bind, BufferImageCopy, BufferUsageFlags, ClearValue, DeviceExt, - Extent2d, Extent3d, ImageAspectFlags, ImageBarrier, ImageDesc, ImageDimension, ImageFormat, - ImageLayout, ImageTiling, ImageUsageFlags, IndexType, LoadOp, MemoryLocation, Offset2d, - Offset3d, RenderingAttachment, RenderingDesc, Scissor, StoreOp, ThreadToken, TypedBind, - Viewport, + create_device, Access, Bind, BufferImageCopy, BufferUsageFlags, ClearValue, CmdEncoder, Device, + DeviceExt, Extent2d, Extent3d, Frame, Image, ImageAspectFlags, ImageBarrier, ImageDesc, + ImageDimension, ImageFormat, ImageLayout, ImageTiling, ImageUsageFlags, IndexType, LoadOp, + MemoryLocation, Offset2d, PersistentBuffer, RenderingAttachment, RenderingDesc, Scissor, + StoreOp, ThreadToken, TypedBind, Viewport, }; +use narcissus_image as image; use narcissus_maths::{ clamp, perlin_noise3, sin_pi_f32, vec3, Affine3, Deg, HalfTurn, Mat3, Mat4, Point3, Vec3, }; -use pipelines::{BasicUniforms, PrimitiveInstance, PrimitiveVertex, TextUniforms}; +use pipelines::{BasicPipeline, BasicUniforms, PrimitiveInstance, PrimitiveVertex, TextPipeline}; use spring::simple_spring_damper_exact; mod fonts; @@ -31,9 +28,7 @@ mod pipelines; mod spring; const SQRT_2: f32 = 0.70710677; - const GLYPH_CACHE_SIZE: usize = 1024; - const ARCHTYPE_PROJECTILE_MAX: usize = 65536; struct GameVariables { @@ -439,577 +434,548 @@ impl GameState { } } -pub fn main() { - #[cfg(debug_assertions)] - if std::env::var("RUST_BACKTRACE").is_err() { - std::env::set_var("RUST_BACKTRACE", "1") - } +struct UiState<'a> { + scale: f32, + fonts: &'a Fonts<'a>, + glyph_cache: GlyphCache<'a, Fonts<'a>>, + primitive_instances: Vec, + primitive_vertices: Vec, +} - let renderdoc = rdoc::RenderdocApi1_5_0::load(); +impl<'a> UiState<'a> { + fn new(fonts: &'a Fonts<'a>, scale: f32) -> Self { + let glyph_cache = GlyphCache::new(fonts, GLYPH_CACHE_SIZE, GLYPH_CACHE_SIZE, 1); - // Default to wayland because otherwise HiDPI is totally borked. - // Unless renderdoc is attached, in which case wayland would break capture. - if renderdoc.is_none() && std::env::var("SDL_VIDEODRIVER").is_err() { - std::env::set_var("SDL_VIDEODRIVER", "wayland") + Self { + scale, + fonts, + glyph_cache, + primitive_instances: vec![], + primitive_vertices: vec![], + } } - let app = create_app(); - let main_window = app.create_window(&WindowDesc { - title: "shark", - width: 800, - height: 600, - }); + fn text(&mut self, mut x: f32, y: f32, font_family: FontFamily, font_size_px: f32, text: &str) { + let font = self.fonts.font(font_family); + let font_size_px = font_size_px * self.scale; + let scale = font.scale_for_size_px(font_size_px); - let device = create_device(narcissus_gpu::DeviceBackend::Vulkan); + let mut prev_index = None; - let thread_token = ThreadToken::new(); - let thread_token = &thread_token; + for c in text.chars() { + let glyph_index = font + .glyph_index(c) + .unwrap_or_else(|| font.glyph_index('□').unwrap()); - let basic_pipeline = BasicPipeline::new(device.as_ref()); - let text_pipeline = TextPipeline::new(device.as_ref()); + let touched_glyph_index = + self.glyph_cache + .touch_glyph(font_family, glyph_index, font_size_px); - let fonts = Fonts::new(); - let mut glyph_cache = GlyphCache::new(&fonts, GLYPH_CACHE_SIZE, GLYPH_CACHE_SIZE, 1); - - let blåhaj_image_data = load_image("title/shark/data/blåhaj.png"); - let (blåhaj_vertices, blåhaj_indices) = load_obj("title/shark/data/blåhaj.obj"); - - let blåhaj_vertex_buffer = device.create_persistent_buffer_with_data( - MemoryLocation::Device, - BufferUsageFlags::STORAGE, - blåhaj_vertices.as_slice(), - ); - - let blåhaj_index_buffer = device.create_persistent_buffer_with_data( - MemoryLocation::Device, - BufferUsageFlags::INDEX, - blåhaj_indices.as_slice(), - ); - - let blåhaj_image = device.create_image(&ImageDesc { - memory_location: MemoryLocation::Device, - host_mapped: false, - usage: ImageUsageFlags::SAMPLED | ImageUsageFlags::TRANSFER, - dimension: ImageDimension::Type2d, - format: ImageFormat::RGBA8_SRGB, - tiling: ImageTiling::Optimal, - width: blåhaj_image_data.width() as u32, - height: blåhaj_image_data.height() as u32, - depth: 1, - layer_count: 1, - mip_levels: 1, - }); - - let glyph_atlas = device.create_image(&ImageDesc { - memory_location: MemoryLocation::Device, - usage: ImageUsageFlags::SAMPLED | ImageUsageFlags::TRANSFER, - host_mapped: false, - dimension: ImageDimension::Type2d, - format: ImageFormat::R8_UNORM, - tiling: ImageTiling::Optimal, - width: glyph_cache.width() as u32, - height: glyph_cache.height() as u32, - depth: 1, - layer_count: 1, - mip_levels: 1, - }); + let HorizontalMetrics { + advance_width, + left_side_bearing: _, + } = font.horizontal_metrics(glyph_index); - { - let frame = device.begin_frame(); + let advance = if let Some(prev_index) = prev_index { + font.kerning_advance(prev_index, glyph_index) + } else { + 0.0 + }; + prev_index = Some(glyph_index); + + x += advance * scale; + + let instance_index = self.primitive_instances.len() as u32; + self.primitive_instances.push(PrimitiveInstance { + x, + y, + touched_glyph_index, + color: 0xff000000, + }); + let glyph_vertices = &[ + PrimitiveVertex::glyph(0, instance_index), + PrimitiveVertex::glyph(1, instance_index), + PrimitiveVertex::glyph(2, instance_index), + PrimitiveVertex::glyph(2, instance_index), + PrimitiveVertex::glyph(1, instance_index), + PrimitiveVertex::glyph(3, instance_index), + ]; + self.primitive_vertices.extend_from_slice(glyph_vertices); + + x += advance_width * scale; + } + } +} - let blåhaj_buffer = device.request_transient_buffer_with_data( - &frame, - thread_token, - BufferUsageFlags::TRANSFER, - blåhaj_image_data.as_slice(), - ); +struct Model<'device> { + indices: u32, + vertex_buffer: PersistentBuffer<'device>, + index_buffer: PersistentBuffer<'device>, +} - let mut cmd_encoder = device.request_cmd_encoder(&frame, thread_token); - { - let cmd_encoder = &mut cmd_encoder; +enum ModelRes { + Shark, +} - device.cmd_barrier( - cmd_encoder, - None, - &[ - ImageBarrier::layout_optimal( - &[Access::None], - &[Access::ShaderSampledImageRead], - glyph_atlas, - ImageAspectFlags::COLOR, - ), - ImageBarrier::layout_optimal( - &[Access::None], - &[Access::TransferWrite], - blåhaj_image, - ImageAspectFlags::COLOR, - ), - ], - ); +impl ModelRes { + const MAX_MODELS: usize = 1; +} - device.cmd_copy_buffer_to_image( - cmd_encoder, - blåhaj_buffer.to_arg(), - blåhaj_image, - ImageLayout::Optimal, - &[BufferImageCopy { - buffer_offset: 0, - buffer_row_length: 0, - buffer_image_height: 0, - image_subresource: default(), - image_offset: Offset3d { x: 0, y: 0, z: 0 }, - image_extent: Extent3d { - width: blåhaj_image_data.width() as u32, - height: blåhaj_image_data.width() as u32, - depth: 1, - }, - }], - ); +struct Models<'device>([Model<'device>; ModelRes::MAX_MODELS]); - device.cmd_barrier( - cmd_encoder, - None, - &[ImageBarrier::layout_optimal( - &[Access::TransferWrite], - &[Access::FragmentShaderSampledImageRead], - blåhaj_image, - ImageAspectFlags::COLOR, - )], - ); - } +enum ImageRes { + Shark, +} - device.submit(&frame, cmd_encoder); - device.end_frame(frame); - } +impl ImageRes { + const MAX_IMAGES: usize = 1; +} - let mut depth_width = 0; - let mut depth_height = 0; - let mut depth_image = default(); +struct Images([Image; ImageRes::MAX_IMAGES]); - let mut font_size_str = String::new(); - let mut primitive_instances = Vec::new(); - let mut primitive_vertices = Vec::new(); - let mut line_glyph_indices = Vec::new(); - let mut line_kern_advances = Vec::new(); +type Gpu = dyn Device + 'static; - let mut action_queue = Vec::new(); - let mut game_state = GameState::new(); +struct DrawState<'device> { + gpu: &'device Gpu, - let mut basic_transforms = vec![]; + basic_pipeline: BasicPipeline, + text_pipeline: TextPipeline, - let mut ui_scale; + width: u32, + height: u32, - 'main: loop { - let frame = device.begin_frame(); - { - let frame = &frame; + depth_image: Image, + glyph_atlas_image: Image, - let (width, height, swapchain_image) = loop { - let (virtual_width, _virtual_height) = main_window.extent(); - let (width, height) = main_window.drawable_extent(); + models: Models<'device>, + images: Images, - ui_scale = width as f32 / virtual_width as f32; + transforms: Vec, +} - if let Ok(result) = device.acquire_swapchain( - frame, - main_window.upcast(), - width, - height, - ImageFormat::BGRA8_SRGB, - ) { - break result; - } - }; +fn load_models(gpu: &Gpu) -> Models { + fn load_model

(gpu: &Gpu, path: P) -> Model + where + P: AsRef, + { + let (vertices, indices) = load_obj(path); + let vertex_buffer = gpu.create_persistent_buffer_with_data( + MemoryLocation::Device, + BufferUsageFlags::STORAGE, + vertices.as_slice(), + ); + let index_buffer = gpu.create_persistent_buffer_with_data( + MemoryLocation::Device, + BufferUsageFlags::INDEX, + indices.as_slice(), + ); - 'poll_events: while let Some(event) = app.poll_event() { - use Event::*; - match event { - KeyPress { - window_id: _, - key, - repeat, - pressed, - modifiers: _, - } => { - if repeat { - continue 'poll_events; - } + Model { + indices: indices.len() as u32, + vertex_buffer, + index_buffer, + } + } - if key == Key::Escape { - break 'main; - } + Models([load_model(gpu, "title/shark/data/blåhaj.obj")]) +} - { - let value = match pressed { - PressedState::Released => 0.0, - PressedState::Pressed => 1.0, - }; - - if key == Key::Left || key == Key::A { - action_queue.push(ActionEvent { - action: Action::Left, - value, - }) - } - if key == Key::Right || key == Key::D { - action_queue.push(ActionEvent { - action: Action::Right, - value, - }) - } - if key == Key::Up || key == Key::W { - action_queue.push(ActionEvent { - action: Action::Up, - value, - }) - } - if key == Key::Down || key == Key::S { - action_queue.push(ActionEvent { - action: Action::Down, - value, - }) - } - if key == Key::Space { - action_queue.push(ActionEvent { - action: Action::Damage, - value, - }) - } - } - } - Quit => { - break 'main; - } - Close { window_id } => { - let window = app.window(window_id); - device.destroy_swapchain(window.upcast()); - } - _ => {} - } - } +fn load_images(gpu: &Gpu, thread_token: &ThreadToken) -> Images { + fn load_image

( + gpu: &Gpu, + frame: &Frame, + thread_token: &ThreadToken, + cmd_encoder: &mut CmdEncoder, + path: P, + ) -> Image + where + P: AsRef, + { + let image_data = + image::Image::from_buffer(std::fs::read(path.as_ref()).unwrap().as_slice()).unwrap(); + + let width = image_data.width() as u32; + let height = image_data.height() as u32; + + let image = gpu.create_image(&ImageDesc { + memory_location: MemoryLocation::Device, + host_mapped: false, + usage: ImageUsageFlags::SAMPLED | ImageUsageFlags::TRANSFER, + dimension: ImageDimension::Type2d, + format: ImageFormat::RGBA8_SRGB, + tiling: ImageTiling::Optimal, + width, + height, + depth: 1, + layer_count: 1, + mip_levels: 1, + }); + + gpu.cmd_barrier( + cmd_encoder, + None, + &[ImageBarrier::layout_optimal( + &[Access::None], + &[Access::TransferWrite], + image, + ImageAspectFlags::COLOR, + )], + ); - game_state.tick(1.0 / 120.0, &action_queue); - action_queue.clear(); + let buffer = gpu.request_transient_buffer_with_data( + frame, + thread_token, + BufferUsageFlags::TRANSFER, + image_data.as_slice(), + ); - let half_turn_y = Mat3::from_axis_rotation(Vec3::Y, HalfTurn::new(0.5)); - let scale = Mat3::from_scale(Vec3::splat(0.125)); + gpu.cmd_copy_buffer_to_image( + cmd_encoder, + buffer.to_arg(), + image, + ImageLayout::Optimal, + &[BufferImageCopy { + image_extent: Extent3d { + width, + height, + depth: 1, + }, + ..default() + }], + ); - fn rotate_dir(dir: Vec3, up: Vec3) -> Mat3 { - let f = dir.normalized(); - let r = Vec3::cross(f, up).normalized(); - let u = Vec3::cross(r, f); - Mat3::from_rows([[r.x, u.x, -f.x], [r.y, u.y, -f.y], [r.z, u.z, -f.z]]) - } + gpu.cmd_barrier( + cmd_encoder, + None, + &[ImageBarrier::layout_optimal( + &[Access::TransferWrite], + &[Access::FragmentShaderSampledImageRead], + image, + ImageAspectFlags::COLOR, + )], + ); - let matrix = rotate_dir(game_state.player.heading, Vec3::Y) * half_turn_y; - let translation = game_state.player.position.as_vec3(); - basic_transforms.push(Affine3::new(matrix, translation)); - - let half_turn_y_scale = half_turn_y * scale; - - // Render projectiles - for i in BitIter::new( - game_state - .archetype_projectile - .bitmap_non_empty - .iter() - .copied(), - ) { - let chunk = &game_state.archetype_projectile.chunks[i]; - for (&bitmap, block) in chunk.bitmap.iter().zip(chunk.blocks.iter()) { - if bitmap == 0 { - continue; - } + image + } - for j in 0..8 { - if bitmap & (1 << j) == 0 { - continue; - } + let images; + let frame = gpu.begin_frame(); + { + let frame = &frame; - let translation = vec3(block.position_x[j], 0.0, block.position_z[j]); - let velocity = vec3(block.velocity_x[j], 0.0, block.velocity_z[j]); - let matrix = rotate_dir(velocity, Vec3::Y) * half_turn_y_scale; - basic_transforms.push(Affine3::new(matrix, translation)); - } - } - } + let mut cmd_encoder = gpu.request_cmd_encoder(frame, thread_token); + { + let cmd_encoder = &mut cmd_encoder; - let camera_from_model = game_state.camera.camera_from_model(); - let clip_from_camera = Mat4::perspective_rev_inf_zo( - HalfTurn::new(1.0 / 3.0), - width as f32 / height as f32, - 0.01, - ); - let clip_from_model = clip_from_camera * camera_from_model; + images = Images([load_image( + gpu, + frame, + thread_token, + cmd_encoder, + "title/shark/data/blåhaj.png", + )]); + } - let mut cmd_encoder = device.request_cmd_encoder(frame, thread_token); - { - let cmd_encoder = &mut cmd_encoder; - - if width != depth_width || height != depth_height { - device.destroy_image(frame, depth_image); - depth_image = device.create_image(&ImageDesc { - memory_location: MemoryLocation::Device, - host_mapped: false, - usage: ImageUsageFlags::DEPTH_STENCIL_ATTACHMENT, - dimension: ImageDimension::Type2d, - format: ImageFormat::DEPTH_F32, - tiling: ImageTiling::Optimal, - width, - height, - depth: 1, - layer_count: 1, - mip_levels: 1, - }); - - device.cmd_barrier( - cmd_encoder, - None, - &[ImageBarrier::layout_optimal( - &[Access::None], - &[Access::DepthStencilAttachmentWrite], - depth_image, - ImageAspectFlags::DEPTH, - )], - ); + gpu.submit(frame, cmd_encoder); + } + gpu.end_frame(frame); - depth_width = width; - depth_height = height; - } + images +} - // Do some Font Shit.' - let line0 = "Snarfe, Blåhaj! And the Quick Brown Fox jumped Over the Lazy doge."; - let line1 = "加盟国は、国際連合と協力して"; +impl<'device> DrawState<'device> { + fn new(gpu: &'device Gpu, thread_token: &ThreadToken) -> Self { + let basic_pipeline = BasicPipeline::new(gpu); + let text_pipeline = TextPipeline::new(gpu); - let mut x; - let mut y = 0.0; + let models = load_models(gpu); + let images = load_images(gpu, thread_token); - let mut rng = Pcg64::new(); + Self { + gpu, + basic_pipeline, + text_pipeline, + width: 0, + height: 0, + depth_image: default(), + glyph_atlas_image: default(), + models, + images, + transforms: vec![], + } + } - primitive_instances.clear(); - primitive_vertices.clear(); + fn draw( + &mut self, + thread_token: &ThreadToken, + frame: &Frame, + ui_state: &mut UiState, + game_state: &GameState, + width: u32, + height: u32, + swapchain_image: Image, + ) { + let gpu = self.gpu; + + let half_turn_y = Mat3::from_axis_rotation(Vec3::Y, HalfTurn::new(0.5)); + let scale = Mat3::from_scale(Vec3::splat(0.125)); + + fn rotate_dir(dir: Vec3, up: Vec3) -> Mat3 { + let f = dir.normalized(); + let r = Vec3::cross(f, up).normalized(); + let u = Vec3::cross(r, f); + Mat3::from_rows([[r.x, u.x, -f.x], [r.y, u.y, -f.y], [r.z, u.z, -f.z]]) + } - for line in 0..2 { - let (font_family, font_size_px, text) = if line & 1 == 0 { - (FontFamily::RobotoRegular, 22.0, line0) - } else { - (FontFamily::NotoSansJapanese, 22.0, line1) - }; + let matrix = rotate_dir(game_state.player.heading, Vec3::Y) * half_turn_y; + let translation = game_state.player.position.as_vec3(); + self.transforms.push(Affine3::new(matrix, translation)); + + let half_turn_y_scale = half_turn_y * scale; + + // Render projectiles + for i in BitIter::new( + game_state + .archetype_projectile + .bitmap_non_empty + .iter() + .copied(), + ) { + let chunk = &game_state.archetype_projectile.chunks[i]; + for (&bitmap, block) in chunk.bitmap.iter().zip(chunk.blocks.iter()) { + if bitmap == 0 { + continue; + } - let font_size_px = font_size_px * ui_scale; + for j in 0..8 { + if bitmap & (1 << j) == 0 { + continue; + } - let font = fonts.font(font_family); - let scale = font.scale_for_size_px(font_size_px); + let translation = vec3(block.position_x[j], 0.0, block.position_z[j]); + let velocity = vec3(block.velocity_x[j], 0.0, block.velocity_z[j]); + let matrix = rotate_dir(velocity, Vec3::Y) * half_turn_y_scale; + self.transforms.push(Affine3::new(matrix, translation)); + } + } + } - x = 0.0; - y += (font.ascent() - font.descent() + font.line_gap()) * scale; + let camera_from_model = game_state.camera.camera_from_model(); + let clip_from_camera = Mat4::perspective_rev_inf_zo( + HalfTurn::new(1.0 / 3.0), + width as f32 / height as f32, + 0.01, + ); + let clip_from_model = clip_from_camera * camera_from_model; - font_size_str.clear(); - write!(&mut font_size_str, "{font_size_px}: ").unwrap(); + let mut cmd_encoder = self.gpu.request_cmd_encoder(frame, thread_token); + { + let cmd_encoder = &mut cmd_encoder; - line_glyph_indices.clear(); - line_glyph_indices.extend(font_size_str.chars().chain(text.chars()).map(|c| { - font.glyph_index(c) - .unwrap_or_else(|| font.glyph_index('□').unwrap()) - })); + if width != self.width || height != self.height { + gpu.destroy_image(frame, self.depth_image); + self.depth_image = gpu.create_image(&ImageDesc { + memory_location: MemoryLocation::Device, + host_mapped: false, + usage: ImageUsageFlags::DEPTH_STENCIL_ATTACHMENT, + dimension: ImageDimension::Type2d, + format: ImageFormat::DEPTH_F32, + tiling: ImageTiling::Optimal, + width, + height, + depth: 1, + layer_count: 1, + mip_levels: 1, + }); - line_kern_advances.clear(); - line_kern_advances.push(0.0); - line_kern_advances.extend(array_windows(line_glyph_indices.as_slice()).map( - |&[prev_index, next_index]| font.kerning_advance(prev_index, next_index), - )); + gpu.cmd_barrier( + cmd_encoder, + None, + &[ImageBarrier::layout_optimal( + &[Access::None], + &[Access::DepthStencilAttachmentWrite], + self.depth_image, + ImageAspectFlags::DEPTH, + )], + ); - 'repeat_str: for _ in 0.. { - for (glyph_index, advance) in line_glyph_indices - .iter() - .copied() - .zip(line_kern_advances.iter().copied()) - { - if x >= width as f32 { - break 'repeat_str; - } + self.width = width; + self.height = height; + } - let touched_glyph_index = - glyph_cache.touch_glyph(font_family, glyph_index, font_size_px); - - let HorizontalMetrics { - advance_width, - left_side_bearing: _, - } = font.horizontal_metrics(glyph_index); - - x += advance * scale; - - let color = *rng - .array_select(&[0xfffac228, 0xfff57d15, 0xffd44842, 0xff9f2a63]); - - let instance_index = primitive_instances.len() as u32; - primitive_instances.push(PrimitiveInstance { - x, - y, - touched_glyph_index, - color, - }); - let glyph_vertices = &[ - PrimitiveVertex::glyph(0, instance_index), - PrimitiveVertex::glyph(1, instance_index), - PrimitiveVertex::glyph(2, instance_index), - PrimitiveVertex::glyph(2, instance_index), - PrimitiveVertex::glyph(1, instance_index), - PrimitiveVertex::glyph(3, instance_index), - ]; - primitive_vertices.extend_from_slice(glyph_vertices); - - x += advance_width * scale; - } - } - } + if self.glyph_atlas_image.is_null() { + let image = gpu.create_image(&ImageDesc { + memory_location: MemoryLocation::Device, + usage: ImageUsageFlags::SAMPLED | ImageUsageFlags::TRANSFER, + host_mapped: false, + dimension: ImageDimension::Type2d, + format: ImageFormat::R8_UNORM, + tiling: ImageTiling::Optimal, + width: ui_state.glyph_cache.width() as u32, + height: ui_state.glyph_cache.height() as u32, + depth: 1, + layer_count: 1, + mip_levels: 1, + }); + + gpu.cmd_barrier( + cmd_encoder, + None, + &[ImageBarrier::layout_optimal( + &[Access::None], + &[Access::ShaderSampledImageRead], + image, + ImageAspectFlags::COLOR, + )], + ); - let atlas_width = glyph_cache.width() as u32; - let atlas_height = glyph_cache.height() as u32; + self.glyph_atlas_image = image; + } - let (touched_glyphs, texture) = glyph_cache.update_atlas(); + let atlas_width = ui_state.glyph_cache.width() as u32; + let atlas_height = ui_state.glyph_cache.height() as u32; - // If the atlas has been updated, we need to upload it to the GPU. - if let Some(texture) = texture { - let width = atlas_width; - let height = atlas_height; - let image = glyph_atlas; + let (touched_glyphs, glyph_texture) = ui_state.glyph_cache.update_atlas(); - let buffer = device.request_transient_buffer_with_data( - frame, - thread_token, - BufferUsageFlags::TRANSFER, - texture, - ); + // If the atlas has been updated, we need to upload it to the GPU. + if let Some(texture) = glyph_texture { + let image = self.glyph_atlas_image; - device.cmd_barrier( - cmd_encoder, - None, - &[ImageBarrier::layout_optimal( - &[Access::ShaderSampledImageRead], - &[Access::TransferWrite], - image, - ImageAspectFlags::COLOR, - )], - ); + let buffer = gpu.request_transient_buffer_with_data( + frame, + thread_token, + BufferUsageFlags::TRANSFER, + texture, + ); - device.cmd_copy_buffer_to_image( - cmd_encoder, - buffer.to_arg(), + gpu.cmd_barrier( + cmd_encoder, + None, + &[ImageBarrier::layout_optimal( + &[Access::ShaderSampledImageRead], + &[Access::TransferWrite], image, - ImageLayout::Optimal, - &[BufferImageCopy { - buffer_offset: 0, - buffer_row_length: 0, - buffer_image_height: 0, - image_subresource: default(), - image_offset: Offset3d { x: 0, y: 0, z: 0 }, - image_extent: Extent3d { - width, - height, - depth: 1, - }, - }], - ); - - device.cmd_barrier( - cmd_encoder, - None, - &[ImageBarrier::layout_optimal( - &[Access::TransferWrite], - &[Access::FragmentShaderSampledImageRead], - image, - ImageAspectFlags::COLOR, - )], - ); - } + ImageAspectFlags::COLOR, + )], + ); - device.cmd_begin_rendering( + gpu.cmd_copy_buffer_to_image( cmd_encoder, - &RenderingDesc { - x: 0, - y: 0, - width, - height, - color_attachments: &[RenderingAttachment { - image: swapchain_image, - load_op: LoadOp::Clear(ClearValue::ColorF32([1.0, 1.0, 1.0, 1.0])), - store_op: StoreOp::Store, - }], - depth_attachment: Some(RenderingAttachment { - image: depth_image, - load_op: LoadOp::Clear(ClearValue::DepthStencil { - depth: 0.0, - stencil: 0, - }), - store_op: StoreOp::DontCare, - }), - stencil_attachment: None, - }, + buffer.to_arg(), + image, + ImageLayout::Optimal, + &[BufferImageCopy { + image_extent: Extent3d { + width: atlas_width, + height: atlas_height, + depth: 1, + }, + ..default() + }], ); - device.cmd_set_scissors( + gpu.cmd_barrier( cmd_encoder, - &[Scissor { - offset: Offset2d { x: 0, y: 0 }, - extent: Extent2d { width, height }, + None, + &[ImageBarrier::layout_optimal( + &[Access::TransferWrite], + &[Access::FragmentShaderSampledImageRead], + image, + ImageAspectFlags::COLOR, + )], + ); + } + + gpu.cmd_begin_rendering( + cmd_encoder, + &RenderingDesc { + x: 0, + y: 0, + width, + height, + color_attachments: &[RenderingAttachment { + image: swapchain_image, + load_op: LoadOp::Clear(ClearValue::ColorF32([1.0, 1.0, 1.0, 1.0])), + store_op: StoreOp::Store, }], + depth_attachment: Some(RenderingAttachment { + image: self.depth_image, + load_op: LoadOp::Clear(ClearValue::DepthStencil { + depth: 0.0, + stencil: 0, + }), + store_op: StoreOp::DontCare, + }), + stencil_attachment: None, + }, + ); + + gpu.cmd_set_scissors( + cmd_encoder, + &[Scissor { + offset: Offset2d { x: 0, y: 0 }, + extent: Extent2d { width, height }, + }], + ); + + gpu.cmd_set_viewports( + cmd_encoder, + &[Viewport { + x: 0.0, + y: 0.0, + width: width as f32, + height: height as f32, + min_depth: 0.0, + max_depth: 1.0, + }], + ); + + // Render basic stuff. + { + gpu.cmd_set_pipeline(cmd_encoder, self.basic_pipeline.pipeline); + + let basic_uniforms = BasicUniforms { clip_from_model }; + + let uniform_buffer = gpu.request_transient_buffer_with_data( + frame, + thread_token, + BufferUsageFlags::UNIFORM, + &basic_uniforms, ); - device.cmd_set_viewports( + gpu.cmd_set_bind_group( + frame, cmd_encoder, - &[Viewport { - x: 0.0, - y: 0.0, - width: width as f32, - height: height as f32, - min_depth: 0.0, - max_depth: 1.0, + self.basic_pipeline.uniforms_bind_group_layout, + 0, + &[Bind { + binding: 0, + array_element: 0, + typed: TypedBind::UniformBuffer(&[uniform_buffer.to_arg()]), }], ); - // Render basic stuff. { - device.cmd_set_pipeline(cmd_encoder, basic_pipeline.pipeline); - - let basic_uniforms = BasicUniforms { clip_from_model }; + let model = &self.models.0[ModelRes::Shark as usize]; + let image = self.images.0[ImageRes::Shark as usize]; - let uniform_buffer = device.request_transient_buffer_with_data( - frame, - thread_token, - BufferUsageFlags::UNIFORM, - &basic_uniforms, - ); - - let transform_buffer = device.request_transient_buffer_with_data( + let transform_buffer = gpu.request_transient_buffer_with_data( frame, thread_token, BufferUsageFlags::STORAGE, - basic_transforms.as_slice(), - ); - - device.cmd_set_bind_group( - frame, - cmd_encoder, - basic_pipeline.uniforms_bind_group_layout, - 0, - &[Bind { - binding: 0, - array_element: 0, - typed: TypedBind::UniformBuffer(&[uniform_buffer.to_arg()]), - }], + self.transforms.as_slice(), ); - device.cmd_set_bind_group( + gpu.cmd_set_bind_group( frame, cmd_encoder, - basic_pipeline.storage_bind_group_layout, + self.basic_pipeline.storage_bind_group_layout, 1, &[ Bind { binding: 0, array_element: 0, - typed: TypedBind::StorageBuffer(&[blåhaj_vertex_buffer.to_arg()]), + typed: TypedBind::StorageBuffer(&[model.vertex_buffer.to_arg()]), }, Bind { binding: 1, @@ -1019,60 +985,239 @@ pub fn main() { Bind { binding: 2, array_element: 0, - typed: TypedBind::Sampler(&[basic_pipeline.sampler]), + typed: TypedBind::Sampler(&[self.basic_pipeline.sampler]), }, Bind { binding: 3, array_element: 0, - typed: TypedBind::Image(&[(ImageLayout::Optimal, blåhaj_image)]), + typed: TypedBind::Image(&[(ImageLayout::Optimal, image)]), }, ], ); - device.cmd_set_index_buffer( + gpu.cmd_set_index_buffer( cmd_encoder, - blåhaj_index_buffer.to_arg(), + model.index_buffer.to_arg(), 0, IndexType::U16, ); - device.cmd_draw_indexed( + gpu.cmd_draw_indexed( cmd_encoder, - blåhaj_indices.len() as u32, - basic_transforms.len() as u32, + model.indices, + self.transforms.len() as u32, 0, 0, 0, ); + } - // We're done with you now! - basic_transforms.clear(); - }; + // We're done with you now! + self.transforms.clear(); // Render text stuff. - text_pipeline.bind( - device.as_ref(), + self.text_pipeline.bind( + gpu, frame, thread_token, cmd_encoder, - &TextUniforms { + &pipelines::TextUniforms { screen_width: width, screen_height: height, atlas_width, atlas_height, }, - primitive_vertices.as_slice(), + ui_state.primitive_vertices.as_slice(), touched_glyphs, - primitive_instances.as_slice(), - glyph_atlas, + ui_state.primitive_instances.as_slice(), + self.glyph_atlas_image, ); - device.cmd_draw(cmd_encoder, primitive_vertices.len() as u32, 1, 0, 0); + gpu.cmd_draw( + cmd_encoder, + ui_state.primitive_vertices.len() as u32, + 1, + 0, + 0, + ); - device.cmd_end_rendering(cmd_encoder); + ui_state.primitive_instances.clear(); + ui_state.primitive_vertices.clear(); + }; + + gpu.cmd_end_rendering(cmd_encoder); + } + gpu.submit(frame, cmd_encoder); + } +} + +pub fn main() { + #[cfg(debug_assertions)] + if std::env::var("RUST_BACKTRACE").is_err() { + std::env::set_var("RUST_BACKTRACE", "1") + } + + let renderdoc = rdoc::RenderdocApi1_5_0::load(); + + // Default to wayland because otherwise HiDPI is totally borked. + // Unless renderdoc is attached, in which case wayland would break capture. + if renderdoc.is_none() && std::env::var("SDL_VIDEODRIVER").is_err() { + std::env::set_var("SDL_VIDEODRIVER", "wayland") + } + + let app = create_app(); + let gpu = create_device(narcissus_gpu::DeviceBackend::Vulkan); + + let window = app.create_window(&WindowDesc { + title: "shark", + width: 800, + height: 600, + }); + + let scale = 2.0; + + let thread_token = ThreadToken::new(); + let thread_token = &thread_token; + + let mut action_queue = Vec::new(); + + let fonts = Fonts::new(); + let mut ui_state = UiState::new(&fonts, scale); + let mut game_state = GameState::new(); + let mut draw_state = DrawState::new(gpu.as_ref(), thread_token); + + let target_hz = 120.0; + let target_dt = Duration::from_secs_f64(1.0 / target_hz); + + let mut last_frame = Instant::now(); + let mut tick_accumulator = target_dt; + + 'main: loop { + let frame = gpu.begin_frame(); + { + let frame = &frame; + + let (width, height, swapchain_image) = loop { + let (width, height) = window.drawable_extent(); + + if let Ok(result) = gpu.acquire_swapchain( + frame, + window.upcast(), + width, + height, + ImageFormat::BGRA8_SRGB, + ) { + break result; + } + }; + + let tick_start = Instant::now(); + 'tick: loop { + 'poll_events: while let Some(event) = app.poll_event() { + use Event::*; + match event { + KeyPress { + window_id: _, + key, + repeat, + pressed, + modifiers: _, + } => { + if repeat { + continue 'poll_events; + } + + if key == Key::Escape { + break 'main; + } + + { + let value = match pressed { + PressedState::Released => 0.0, + PressedState::Pressed => 1.0, + }; + + if key == Key::Left || key == Key::A { + action_queue.push(ActionEvent { + action: Action::Left, + value, + }) + } + if key == Key::Right || key == Key::D { + action_queue.push(ActionEvent { + action: Action::Right, + value, + }) + } + if key == Key::Up || key == Key::W { + action_queue.push(ActionEvent { + action: Action::Up, + value, + }) + } + if key == Key::Down || key == Key::S { + action_queue.push(ActionEvent { + action: Action::Down, + value, + }) + } + if key == Key::Space { + action_queue.push(ActionEvent { + action: Action::Damage, + value, + }) + } + } + } + Quit => { + break 'main; + } + Close { window_id } => { + let window = app.window(window_id); + gpu.destroy_swapchain(window.upcast()); + } + _ => {} + } + } + + if tick_accumulator < target_dt { + break 'tick; + } + + game_state.tick(target_dt.as_secs_f32(), &action_queue); + + action_queue.clear(); + + tick_accumulator -= target_dt; } - device.submit(frame, cmd_encoder); + + let tick_duration = Instant::now() - tick_start; + + ui_state.text( + 5.0, + 30.0, + FontFamily::RobotoRegular, + 22.0, + format!("tick: {:?}", tick_duration).as_str(), + ); + + ui_state.text(5.0, 60.0, FontFamily::NotoSansJapanese, 22.0, "お握り"); + + draw_state.draw( + thread_token, + frame, + &mut ui_state, + &game_state, + width, + height, + swapchain_image, + ); } - device.end_frame(frame); + + gpu.end_frame(frame); + + let now = Instant::now(); + tick_accumulator += now - last_frame; + last_frame = now; } } diff --git a/title/shark/src/pipelines/basic.rs b/title/shark/src/pipelines/basic.rs index 2460777..2659f9a 100644 --- a/title/shark/src/pipelines/basic.rs +++ b/title/shark/src/pipelines/basic.rs @@ -1,12 +1,14 @@ use narcissus_core::default; use narcissus_gpu::{ BindGroupLayout, BindGroupLayoutDesc, BindGroupLayoutEntryDesc, BindingType, BlendMode, - CompareOp, CullingMode, Device, FrontFace, GraphicsPipelineDesc, GraphicsPipelineLayout, - ImageFormat, Pipeline, PolygonMode, Sampler, SamplerAddressMode, SamplerDesc, SamplerFilter, - ShaderDesc, ShaderStageFlags, Topology, + CompareOp, CullingMode, FrontFace, GraphicsPipelineDesc, GraphicsPipelineLayout, ImageFormat, + Pipeline, PolygonMode, Sampler, SamplerAddressMode, SamplerDesc, SamplerFilter, ShaderDesc, + ShaderStageFlags, Topology, }; use narcissus_maths::Mat4; +use crate::Gpu; + #[allow(unused)] #[repr(C)] pub struct BasicUniforms { @@ -29,8 +31,8 @@ pub struct BasicPipeline { } impl BasicPipeline { - pub fn new(device: &dyn Device) -> Self { - let uniforms_bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDesc { + pub fn new(gpu: &Gpu) -> Self { + let uniforms_bind_group_layout = gpu.create_bind_group_layout(&BindGroupLayoutDesc { entries: &[BindGroupLayoutEntryDesc { slot: 0, stages: ShaderStageFlags::ALL, @@ -39,7 +41,7 @@ impl BasicPipeline { }], }); - let storage_bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDesc { + let storage_bind_group_layout = gpu.create_bind_group_layout(&BindGroupLayoutDesc { entries: &[ BindGroupLayoutEntryDesc { slot: 0, @@ -68,7 +70,7 @@ impl BasicPipeline { ], }); - let sampler = device.create_sampler(&SamplerDesc { + let sampler = gpu.create_sampler(&SamplerDesc { filter: SamplerFilter::Point, address_mode: SamplerAddressMode::Clamp, compare_op: None, @@ -77,7 +79,7 @@ impl BasicPipeline { max_lod: 1000.0, }); - let pipeline = device.create_graphics_pipeline(&GraphicsPipelineDesc { + let pipeline = gpu.create_graphics_pipeline(&GraphicsPipelineDesc { vertex_shader: ShaderDesc { entry: c"main", code: shark_shaders::BASIC_VERT_SPV, diff --git a/title/shark/src/pipelines/text.rs b/title/shark/src/pipelines/text.rs index b996df4..bdfe8f8 100644 --- a/title/shark/src/pipelines/text.rs +++ b/title/shark/src/pipelines/text.rs @@ -2,12 +2,14 @@ use narcissus_core::default; use narcissus_font::{TouchedGlyph, TouchedGlyphIndex}; use narcissus_gpu::{ Bind, BindGroupLayout, BindGroupLayoutDesc, BindGroupLayoutEntryDesc, BindingType, BlendMode, - BufferUsageFlags, CmdEncoder, CompareOp, CullingMode, Device, DeviceExt, Frame, FrontFace, + BufferUsageFlags, CmdEncoder, CompareOp, CullingMode, DeviceExt, Frame, FrontFace, GraphicsPipelineDesc, GraphicsPipelineLayout, Image, ImageFormat, ImageLayout, Pipeline, PolygonMode, Sampler, SamplerAddressMode, SamplerDesc, SamplerFilter, ShaderDesc, ShaderStageFlags, ThreadToken, Topology, TypedBind, }; +use crate::Gpu; + #[allow(unused)] #[repr(C)] pub struct TextUniforms { @@ -50,8 +52,8 @@ pub struct TextPipeline { } impl TextPipeline { - pub fn new(device: &dyn Device) -> Self { - let bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDesc { + pub fn new(gpu: &Gpu) -> Self { + let bind_group_layout = gpu.create_bind_group_layout(&BindGroupLayoutDesc { entries: &[ BindGroupLayoutEntryDesc { slot: 0, @@ -92,7 +94,7 @@ impl TextPipeline { ], }); - let sampler = device.create_sampler(&SamplerDesc { + let sampler = gpu.create_sampler(&SamplerDesc { filter: SamplerFilter::Bilinear, address_mode: SamplerAddressMode::Clamp, compare_op: None, @@ -101,7 +103,7 @@ impl TextPipeline { max_lod: 0.0, }); - let pipeline = device.create_graphics_pipeline(&GraphicsPipelineDesc { + let pipeline = gpu.create_graphics_pipeline(&GraphicsPipelineDesc { vertex_shader: ShaderDesc { entry: c"main", code: shark_shaders::TEXT_VERT_SPV, @@ -140,7 +142,7 @@ impl TextPipeline { pub fn bind( &self, - device: &(dyn Device + 'static), + gpu: &Gpu, frame: &Frame, thread_token: &ThreadToken, cmd_encoder: &mut CmdEncoder, @@ -148,37 +150,35 @@ impl TextPipeline { primitive_vertices: &[PrimitiveVertex], touched_glyphs: &[TouchedGlyph], primitive_instances: &[PrimitiveInstance], - atlas: Image, + glyph_atlas_image: Image, ) { - let uniforms_buffer = device.request_transient_buffer_with_data( + let uniforms_buffer = gpu.request_transient_buffer_with_data( frame, thread_token, BufferUsageFlags::UNIFORM, text_uniforms, ); - - let primitive_vertex_buffer = device.request_transient_buffer_with_data( + let primitive_vertex_buffer = gpu.request_transient_buffer_with_data( frame, thread_token, BufferUsageFlags::STORAGE, primitive_vertices, ); - - let cached_glyphs_buffer = device.request_transient_buffer_with_data( + let cached_glyphs_buffer = gpu.request_transient_buffer_with_data( frame, thread_token, BufferUsageFlags::STORAGE, touched_glyphs, ); - let glyph_instance_buffer = device.request_transient_buffer_with_data( + let glyph_instance_buffer = gpu.request_transient_buffer_with_data( frame, thread_token, BufferUsageFlags::STORAGE, primitive_instances, ); - device.cmd_set_pipeline(cmd_encoder, self.pipeline); - device.cmd_set_bind_group( + gpu.cmd_set_pipeline(cmd_encoder, self.pipeline); + gpu.cmd_set_bind_group( frame, cmd_encoder, self.bind_group_layout, @@ -212,7 +212,7 @@ impl TextPipeline { Bind { binding: 5, array_element: 0, - typed: TypedBind::Image(&[(ImageLayout::Optimal, atlas)]), + typed: TypedBind::Image(&[(ImageLayout::Optimal, glyph_atlas_image)]), }, ], ); -- 2.49.0