From 6e9e90fb34b9c66398d0147fca6c54111b851187 Mon Sep 17 00:00:00 2001 From: Emil Date: Sun, 21 Jun 2026 00:34:01 +0300 Subject: [PATCH] feat: Vulkan renderer with instanced rendering (Phase 4) VulkanRenderer using ash 0.38 + winit: - Instance with KHR_surface + xlib + wayland extensions - Physical device selection (first GPU with graphics+present) - Swapchain (FIFO present mode, B8G8R8A8_UNORM) - Render pass with color attachment - Graphics pipeline with instanced rendering: - Vertex buffer: unit quad (4 verts, 6 indices) - Instance buffer: 8000 CellInstance structs (32 bytes each) - 2 vertex bindings: per-vertex (quad pos) + per-instance (grid pos, atlas UV, fg/bg colors) - Push constants: screen size + cell size - Glyph atlas: R8_UNORM texture, uploaded via staging buffer - fontdue rasterizes DejaVu Sans Mono at startup - Linear filtering sampler - Descriptor set: combined image sampler for atlas - 2 frames in flight with semaphores + fences - Persistent mapped instance buffer (zero-copy per frame) Shaders (pre-compiled SPIR-V): - cell.vert: positions quad from instance data, maps to clip space - cell.frag: samples atlas alpha, blends fg/bg --mode vulkan: tries Vulkan, falls back to softbuffer window mode 109 tests, 0 failures --- Cargo.toml | 4 + assets/shaders/cell.frag | 14 + assets/shaders/cell.vert | 28 ++ assets/shaders/cell_frag.spv | Bin 0 -> 896 bytes assets/shaders/cell_vert.spv | Bin 0 -> 2260 bytes headless_dump.txt | 366 +++++++++--------- src/main.rs | 150 ++++++++ src/render/mod.rs | 1 + src/render/vulkan.rs | 715 +++++++++++++++++++++++++++++++++++ 9 files changed, 1095 insertions(+), 183 deletions(-) create mode 100644 assets/shaders/cell.frag create mode 100644 assets/shaders/cell.vert create mode 100644 assets/shaders/cell_frag.spv create mode 100644 assets/shaders/cell_vert.spv create mode 100644 src/render/vulkan.rs diff --git a/Cargo.toml b/Cargo.toml index 20377ce..9e0370a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,10 @@ fontdue = "0.9" winit = "0.30" softbuffer = "0.4" log = "0.4" +ash = "0.38" +ash-window = "0.13" +raw-window-handle = "0.6" +bytemuck = "1" [profile.release] opt-level = 3 diff --git a/assets/shaders/cell.frag b/assets/shaders/cell.frag new file mode 100644 index 0000000..b6871a1 --- /dev/null +++ b/assets/shaders/cell.frag @@ -0,0 +1,14 @@ +#version 450 + +layout(location = 0) in vec2 in_uv; +layout(location = 1) in vec4 in_fg; +layout(location = 2) in vec4 in_bg; + +layout(binding = 0) uniform sampler2D atlas; + +layout(location = 0) out vec4 out_color; + +void main() { + float alpha = texture(atlas, in_uv).r; + out_color = mix(in_bg, in_fg, alpha); +} diff --git a/assets/shaders/cell.vert b/assets/shaders/cell.vert new file mode 100644 index 0000000..846d180 --- /dev/null +++ b/assets/shaders/cell.vert @@ -0,0 +1,28 @@ +#version 450 + +layout(push_constant) uniform PC { + vec2 screen_size; + vec2 cell_size; +} pc; + +layout(location = 0) in vec2 in_pos; +layout(location = 1) in vec2 in_grid; +layout(location = 2) in vec4 in_atlas; +layout(location = 3) in vec4 in_fg; +layout(location = 4) in vec4 in_bg; + +layout(location = 0) out vec2 out_uv; +layout(location = 1) out vec4 out_fg; +layout(location = 2) out vec4 out_bg; + +void main() { + vec2 pixel = in_grid * pc.cell_size + in_pos * pc.cell_size; + vec2 clip = vec2( + 2.0 * pixel.x / pc.screen_size.x - 1.0, + 1.0 - 2.0 * pixel.y / pc.screen_size.y + ); + gl_Position = vec4(clip, 0.0, 1.0); + out_uv = in_atlas.xy + in_pos * in_atlas.zw; + out_fg = in_fg; + out_bg = in_bg; +} diff --git a/assets/shaders/cell_frag.spv b/assets/shaders/cell_frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..9d72c46f1dda80ededc99f7ec03a40d6c9c1ef72 GIT binary patch literal 896 zcmYk4%}xSA5QJM65ClO)K*XO#k0!)}8WW>eE*vx-Jd#zTlB{54@d14*FDBMEJDcnz zMNidKcTdj}6i(L7)~sd~8(O+rR*+)S$}Wy450lX{c^REwoZ)C#;EZ^hR<)uc_=sk6 z4z`3{VNck1JaFe~q|89o&3=?zlA6zhS9BJ|pYM?(ybe#|Xz4k%XY=Xm+wqd?vsfk5 z=S95u@*eui^YrF9cMyMtkA2Od_N@>%t1c&;k-v%D*Dh(ntHn0 zm!*XcMdKX+T3_#1^?UO3E8_#@Z|DGDsD3qXLc8{ycVhL#8;aEC9{8Jz)I!G(XGS#q z+lu)479IWWD24*-)bmbCUA><6`~%3N9-KEqqhDW<+SxgrH$%e@=gr`k`pVps-YjbT zN+~Jg<(>FX5O2hpc~f9sI5V{b=6O^WJ=+2v^m}Q{))mlMx9UPoG-{AD5V&K;547iw naPs-Sa7Xo}n%WXQ9AsBZ0cq!cSk`DHQ(XxDx&W6%8o8 literal 0 HcmV?d00001 diff --git a/assets/shaders/cell_vert.spv b/assets/shaders/cell_vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..25b916c888e69531f1e597f68185b7a8e78d1629 GIT binary patch literal 2260 zcmZ9NX-`v85QeXm&0SPn5v#bPxPrJ_Q9(heG4U%wtmG!O5L%4!lg60%yZlvtG4XkB z&*+WEj5F^$%b7c8s+FFxfh^mSZO;a?S6TNAWj!z%Tz{cw7hW$+v^(!6ZcJV=Vpmov z0*%=X^7JAfmh+bOcH{sujnt4CWFA>SULuP~g+D#$pA_jMyPPlHif=vt)M%o`*34T= zs~h<{a3$Lbu2Z<5KiZ!5%0{EnT59K?8-ICwTxm3$UDjF^dZzYDx2}CscYUqh#UsWy z^K}%l{n-?HwYgMpY`ke~bQ+)Jy;I(!Z>^no^0gN1K*4)w!RM{cYn$PO&u_*z!xfM7 zc4xV@Vz1pir>bwW+5GFXU+3s?4$gCJv$M4MQQTL|mj0JJ&1Jrz8{@2vdB57l&Yc)x zQNuUT|2Fx35B$PxHqCkz+wYi7v$pR!vSZd`U{7$y$_*mcaIV(ySi^ZmjhLNp>am8L zHRRO6nIq?Hg433BXS}P1{&!bzF%z-5`sb71x?<;{-#TLFQI2;WV(agtigiv9ZM^mY ztbTMAIY`t1TCA@Mb_Q~acpKWg#di{p?;Hg?({lVSu=_7>yx5(U8wT$|R}pKDCp?s} z_wB_twtS!O5%KnV4B;=WTQ|7mRR3%~k&;s5d=>WzcF z-+A3Y8{>RO_$ywT48OZ%PWauWvfo|OKgEu_AAYmPm|rk`Xmj0}@|%hMx5!@a z;aBfK+?yI%zVSZ9z3}ZLW`E+p6JyRWVxD}&9K?1XqRt`g|LYuvlaD$_u+5YInra`# zw!Xgme3AXjS+|NlfylW_-tGIGMC7e29z)zk`5KJ7bOx~>cdVRyHu-*}+;d>_<-aBF z0=B&S7kymBwh#9!`nZHGAAL+<+lPGgaTz=MDChd0puXtiDz^FZ(Z@Axd(c;>H{aqq zVt@Lam+vbV?{W)U?p6AhJJ@ob;O=6}xl?tjx~umPWA(ZJ@$K&G1H|8mv-D1}@!o0f zV`K(dq?SECNu0dLd!oOm*zQf_&SIBy=fHA`yIw(Jf1Y8RZ%pjZb8PR^XKyvMT-1Mo Yy^wtV?!@7%V=pG3zd7-5x}8S;0H*AYDgXcg literal 0 HcmV?d00001 diff --git a/headless_dump.txt b/headless_dump.txt index 6f08e00..481b659 100644 --- a/headless_dump.txt +++ b/headless_dump.txt @@ -1,4 +1,4 @@ -=== Verbatim Headless Run: 20 ticks === +=== Verbatim Headless Run: 60 ticks === World: 250x250 Player start: (125.0, 242.0) @@ -33,66 +33,6 @@ Camera: (85, 230) Player: "Player hp=100.0/100.0 pos=(125.0,242.0) bodies=27/27 on_fire=false" Entities: 1 -=== Tick 2 === -30 -31 -32 -33 -34 -35 -36 -37 TTTTT -38 T T -39 T T . -40 T T @@@@@ ... -41 T T ## @@@@@@ ..... -42 T T ## @@@@@@ ....... -43 T T ## @@@@@ """"""""""""" ......... -44""" T T ## @@@@@ "":::::::::::::""" .......... -45:::""" ## """::::::::::::::::::"" ........... -46::::::"" ## "":::::::::::::::::::::::"""......... -47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" -48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -49################################################################################ -50???????????????????????????????????????????????????????????????????????????????? -51???????????????????????????????????????????????????????????????????????????????? -52???????????????????????????????????????????????????????????????????????????????? -53???????????????????????????????????????????????????????????????????????????????? -54???????????????????????????????????????????????????????????????????????????????? -Player: "Player hp=100.0/100.0 pos=(125.0,242.1) bodies=27/27 on_fire=false" -Entities: 1 -Alive entities: Player(hp=100, pos=(125.0, 242.11879)) - -=== Tick 4 === -30 -31 -32 -33 -34 -35 -36 -37 TTTTT -38 T T -39 T T . -40 T T @@@@@ ... -41 T T ## @@@@@@ ..... -42 T T ## @@@@@@ ....... -43 T T ## @@@@@ """"""""""""" ......... -44""" T T ## @@@@@ "":::::::::::::""" .......... -45:::""" ## """::::::::::::::::::"" ........... -46::::::"" ## "":::::::::::::::::::::::"""......... -47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" -48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -49################################################################################ -50???????????????????????????????????????????????????????????????????????????????? -51???????????????????????????????????????????????????????????????????????????????? -52???????????????????????????????????????????????????????????????????????????????? -53???????????????????????????????????????????????????????????????????????????????? -54???????????????????????????????????????????????????????????????????????????????? -Player: "Player hp=100.0/100.0 pos=(125.0,242.4) bodies=27/27 on_fire=false" -Entities: 1 -Alive entities: Player(hp=100, pos=(125.0, 242.38817)) - === Tick 6 === 30 31 @@ -121,67 +61,7 @@ Alive entities: Player(hp=100, pos=(125.0, 242.38817)) 54???????????????????????????????????????????????????????????????????????????????? Player: "Player hp=100.0/100.0 pos=(125.0,242.8) bodies=27/27 on_fire=false" Entities: 1 -Alive entities: Player(hp=100, pos=(125.0, 242.79922)) - -=== Tick 8 === -31 -32 -33 -34 -35 -36 -37 TTTTT -38 T T -39 T T . -40 T T ... -41 T T ## @@@@@ ..... -42 T T ## @@@@@@ ....... -43 T T ## @@@@@@ """"""""""""" ......... -44""" T T ## @@@@@ "":::::::::::::""" .......... -45:::""" ## @@@@@ """::::::::::::::::::"" ........... -46::::::"" ## "":::::::::::::::::::::::"""......... -47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" -48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -49################################################################################ -50???????????????????????????????????????????????????????????????????????????????? -51???????????????????????????????????????????????????????????????????????????????? -52???????????????????????????????????????????????????????????????????????????????? -53???????????????????????????????????????????????????????????????????????????????? -54???????????????????????????????????????????????????????????????????????????????? -55???????????????????????????????????????????????????????????????????????????????? -Player: "Player hp=100.0/100.0 pos=(125.0,243.3) bodies=27/27 on_fire=false" -Entities: 1 -Alive entities: Player(hp=100, pos=(125.0, 243.3436)) - -=== Tick 10 === -31 -32 -33 -34 -35 -36 -37 TTTTT -38 T T -39 T T . -40 T T ... -41 T T ## @@@@@ ..... -42 T T ## @@@@@@ ....... -43 T T ## @@@@@@ """"""""""""" ......... -44""" T T ## @@@@@ "":::::::::::::""" .......... -45:::""" ## @@@@@ """::::::::::::::::::"" ........... -46::::::"" ## "":::::::::::::::::::::::"""......... -47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" -48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -49################################################################################ -50???????????????????????????????????????????????????????????????????????????????? -51???????????????????????????????????????????????????????????????????????????????? -52???????????????????????????????????????????????????????????????????????????????? -53???????????????????????????????????????????????????????????????????????????????? -54???????????????????????????????????????????????????????????????????????????????? -55???????????????????????????????????????????????????????????????????????????????? -Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false" -Entities: 1 -Alive entities: Player(hp=100, pos=(125.0, 243.5)) +Alive entities: Player(hp=100, pos=(125.0, 242.82616)) === Tick 12 === 31 @@ -213,66 +93,6 @@ Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false" Entities: 1 Alive entities: Player(hp=100, pos=(125.0, 243.5)) -=== Tick 14 === -31 -32 -33 -34 -35 -36 -37 TTTTT -38 T T -39 T T . -40 T T ... -41 T T ## @@@@@ ..... -42 T T ## @@@@@@ ....... -43 T T ## @@@@@@ """"""""""""" ......... -44""" T T ## @@@@@ "":::::::::::::""" .......... -45:::""" ## @@@@@ """::::::::::::::::::"" ........... -46::::::"" ## "":::::::::::::::::::::::"""......... -47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" -48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -49################################################################################ -50???????????????????????????????????????????????????????????????????????????????? -51???????????????????????????????????????????????????????????????????????????????? -52???????????????????????????????????????????????????????????????????????????????? -53???????????????????????????????????????????????????????????????????????????????? -54???????????????????????????????????????????????????????????????????????????????? -55???????????????????????????????????????????????????????????????????????????????? -Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false" -Entities: 1 -Alive entities: Player(hp=100, pos=(125.0, 243.5)) - -=== Tick 16 === -31 -32 -33 -34 -35 -36 -37 TTTTT -38 T T -39 T T . -40 T T ... -41 T T ## @@@@@ ..... -42 T T ## @@@@@@ ....... -43 T T ## @@@@@@ """"""""""""" ......... -44""" T T ## @@@@@ "":::::::::::::""" .......... -45:::""" ## @@@@@ """::::::::::::::::::"" ........... -46::::::"" ## "":::::::::::::::::::::::"""......... -47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" -48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -49################################################################################ -50???????????????????????????????????????????????????????????????????????????????? -51???????????????????????????????????????????????????????????????????????????????? -52???????????????????????????????????????????????????????????????????????????????? -53???????????????????????????????????????????????????????????????????????????????? -54???????????????????????????????????????????????????????????????????????????????? -55???????????????????????????????????????????????????????????????????????????????? -Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false" -Entities: 1 -Alive entities: Player(hp=100, pos=(125.0, 243.5)) - === Tick 18 === 31 32 @@ -303,7 +123,7 @@ Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false" Entities: 1 Alive entities: Player(hp=100, pos=(125.0, 243.5)) -=== Tick 20 === +=== Tick 24 === 31 32 33 @@ -333,3 +153,183 @@ Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false" Entities: 1 Alive entities: Player(hp=100, pos=(125.0, 243.5)) +=== Tick 30 === +31 +32 +33 +34 +35 +36 +37 TTTTT +38 T T +39 T T . +40 T T ggggg ... +41 T T gggggg @@@@@ ..... +42 T T gggggg @@@@@@ ....... +43 T T ggggg @@@@@@ """"""""""""" ......... +44""" T T ggggg @@@@@ "":::::::::::::""" .......... +45:::""" ## @@@@@ """::::::::::::::::::"" ........... +46::::::"" ## "":::::::::::::::::::::::"""......... +47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" +48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +49################################################################################ +50???????????????????????????????????????????????????????????????????????????????? +51???????????????????????????????????????????????????????????????????????????????? +52???????????????????????????????????????????????????????????????????????????????? +53???????????????????????????????????????????????????????????????????????????????? +54???????????????????????????????????????????????????????????????????????????????? +55???????????????????????????????????????????????????????????????????????????????? +Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false" +Entities: 2 +Alive entities: Player(hp=100, pos=(125.0, 243.5)), Goblin(hp=40, pos=(110.0, 242.0)) + +=== Tick 36 === +31 +32 +33 +34 +35 +36 ggggg +37 TTTTT gggggg +38 T T gggggg +39 T T ggggg . +40 T T ggggg ... +41 T T ## @@@@@ ..... +42 T T ## @@@@@@ ....... +43 T T ## @@@@@@ """"""""""""" ......... +44""" T T ## @@@@@ "":::::::::::::""" .......... +45:::""" ## @@@@@ """::::::::::::::::::"" ........... +46::::::"" ## "":::::::::::::::::::::::"""......... +47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" +48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +49################################################################################ +50???????????????????????????????????????????????????????????????????????????????? +51???????????????????????????????????????????????????????????????????????????????? +52???????????????????????????????????????????????????????????????????????????????? +53???????????????????????????????????????????????????????????????????????????????? +54???????????????????????????????????????????????????????????????????????????????? +55???????????????????????????????????????????????????????????????????????????????? +Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false" +Entities: 2 +Alive entities: Player(hp=100, pos=(125.0, 243.5)), Goblin(hp=40, pos=(110.0, 238.5)) + +=== Tick 42 === +31 +32 +33 +34 +35 +36 ggggg +37 TTTTT gggggg +38 T T gggggg +39 T T ggggg . +40 T T ggggg ... +41 T T ## @@@@@ ..... +42 T T ## @@@@@@ ....... +43 T T ## @@@@@@ """"""""""""" ......... +44""" T T ## @@@@@ "":::::::::::::""" .......... +45:::""" ## @@@@@ """::::::::::::::::::"" ........... +46::::::"" ## "":::::::::::::::::::::::"""......... +47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" +48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +49################################################################################ +50???????????????????????????????????????????????????????????????????????????????? +51???????????????????????????????????????????????????????????????????????????????? +52???????????????????????????????????????????????????????????????????????????????? +53???????????????????????????????????????????????????????????????????????????????? +54???????????????????????????????????????????????????????????????????????????????? +55???????????????????????????????????????????????????????????????????????????????? +Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false" +Entities: 2 +Alive entities: Player(hp=100, pos=(125.0, 243.5)), Goblin(hp=40, pos=(110.0, 238.5)) + +=== Tick 48 === +31 +32 +33 +34 +35 +36 ggggg +37 TTTTT gggggg +38 T T gggggg +39 T T ggggg . +40 T T ggggg ... +41 T T ## @@@@@ ..... +42 T T ## @@@@@@ ....... +43 T T ## @@@@@@ """"""""""""" ......... +44""" T T ## @@@@@ "":::::::::::::""" .......... +45:::""" ## @@@@@ """::::::::::::::::::"" ........... +46::::::"" ## "":::::::::::::::::::::::"""......... +47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" +48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +49################################################################################ +50???????????????????????????????????????????????????????????????????????????????? +51???????????????????????????????????????????????????????????????????????????????? +52???????????????????????????????????????????????????????????????????????????????? +53???????????????????????????????????????????????????????????????????????????????? +54???????????????????????????????????????????????????????????????????????????????? +55???????????????????????????????????????????????????????????????????????????????? +Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false" +Entities: 2 +Alive entities: Player(hp=100, pos=(125.0, 243.5)), Goblin(hp=40, pos=(110.0, 238.5)) + +=== Tick 54 === +31 +32 +33 +34 +35 +36 ggggg +37 TTTTT gggggg +38 T T gggggg +39 T T ggggg . +40 T T ggggg ... +41 T T ## @@@@@ ..... +42 T T ## @@@@@@ ....... +43 T T ## @@@@@@ """"""""""""" ......... +44""" T T ## @@@@@ "":::::::::::::""" .......... +45:::""" ## @@@@@ """::::::::::::::::::"" ........... +46::::::"" ## "":::::::::::::::::::::::"""......... +47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" +48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +49################################################################################ +50???????????????????????????????????????????????????????????????????????????????? +51???????????????????????????????????????????????????????????????????????????????? +52???????????????????????????????????????????????????????????????????????????????? +53???????????????????????????????????????????????????????????????????????????????? +54???????????????????????????????????????????????????????????????????????????????? +55???????????????????????????????????????????????????????????????????????????????? +Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false" +Entities: 2 +Alive entities: Player(hp=100, pos=(125.0, 243.5)), Goblin(hp=40, pos=(110.0, 238.5)) + +=== Tick 60 === +31 +32 +33 +34 +35 +36 ggggg +37 TTTTT gggggg +38 T T gggggg +39 T T ggggg . +40 T T ggggg ... +41 T T gggggg @@@@@ ..... +42 T T gggggg @@@@@@ ....... +43 T T ggggg @@@@@@ """"""""""""" ......... +44""" T T ggggg @@@@@ "":::::::::::::""" .......... +45:::""" ## @@@@@ """::::::::::::::::::"" ........... +46::::::"" ## "":::::::::::::::::::::::"""......... +47::::::::"""""""""""""""""""""""""""""""""""::::::::::::::::::::::::::::""""""""" +48:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +49################################################################################ +50???????????????????????????????????????????????????????????????????????????????? +51???????????????????????????????????????????????????????????????????????????????? +52???????????????????????????????????????????????????????????????????????????????? +53???????????????????????????????????????????????????????????????????????????????? +54???????????????????????????????????????????????????????????????????????????????? +55???????????????????????????????????????????????????????????????????????????????? +Player: "Player hp=100.0/100.0 pos=(125.0,243.5) bodies=27/27 on_fire=false" +Entities: 3 +Alive entities: Player(hp=100, pos=(125.0, 243.5)), Goblin(hp=40, pos=(110.0, 238.5)), Goblin(hp=40, pos=(110.0, 242.0)) + diff --git a/src/main.rs b/src/main.rs index 271d624..453049b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,6 +33,9 @@ fn main() { "window" => { run_window_mode(); } + "vulkan" => { + run_vulkan_mode(); + } "terminal" => { std::panic::set_hook(Box::new(|info| { let _ = crossterm::terminal::disable_raw_mode(); @@ -209,6 +212,7 @@ fn run_window_mode() { use std::time::{Duration, Instant}; use std::sync::Arc; + use std::num::NonZeroU32; let event_loop = EventLoop::new().expect("Failed to create event loop"); let window = event_loop.create_window( @@ -224,6 +228,11 @@ fn run_window_mode() { let mut surface = softbuffer::Surface::new(&context, Arc::clone(&window)) .expect("Failed to create surface"); + let pw = 160 * 8; + let ph = 50 * 16; + surface.resize(NonZeroU32::new(pw).unwrap(), NonZeroU32::new(ph).unwrap()) + .expect("Failed to resize surface"); + let mut renderer = WindowRenderer::new(); let mut game = Game::new(); game.init_world(); @@ -345,3 +354,144 @@ fn run_window_mode() { } }).expect("event loop error"); } + +fn run_vulkan_mode() { + use verbatim::render::vulkan::VulkanRenderer; + use winit::event::{Event, WindowEvent}; + use winit::event_loop::{EventLoop, ControlFlow}; + use winit::window::Window; + use std::time::{Duration, Instant}; + use std::sync::Arc; + + let event_loop = EventLoop::new().expect("Failed to create event loop"); + let window = event_loop.create_window( + Window::default_attributes() + .with_title("Verbatim (Vulkan)") + .with_inner_size(winit::dpi::LogicalSize::new(160 * 8, 50 * 16)) + ).expect("Failed to create window"); + let window = Arc::new(window); + + let mut renderer = match VulkanRenderer::new(Arc::clone(&window)) { + Ok(r) => r, + Err(e) => { + eprintln!("Vulkan init failed: {e}"); + eprintln!("Vulkan init failed: {e}"); + eprintln!("Falling back to softbuffer window mode..."); + run_window_mode(); + return; + } + }; + + let mut game = Game::new(); + game.init_world(); + + let mut input = WindowInput::new(); + let vw = renderer.grid_w(); + let vh = renderer.grid_h(); + + let fixed_dt = Duration::from_millis(16); + let mut last_time = Instant::now(); + let mut accumulator = Duration::ZERO; + let mut running = true; + + event_loop.run(|event, ctrl| { + ctrl.set_control_flow(ControlFlow::Poll); + + match event { + Event::WindowEvent { event, .. } => { + match event { + WindowEvent::CloseRequested => { + running = false; + ctrl.exit(); + } + WindowEvent::KeyboardInput { event: key_event, .. } => { + input.on_key_event(key_event.physical_key, key_event.state); + } + _ => {} + } + } + Event::AboutToWait => { + if !running { + ctrl.exit(); + return; + } + + let now = Instant::now(); + let frame_time = now.duration_since(last_time); + last_time = now; + accumulator += frame_time; + + let mut steps = 0; + while accumulator >= fixed_dt && steps < 5 { + game.fixed_update(); + accumulator -= fixed_dt; + steps += 1; + } + + input.update(); + + if input.quit { + running = false; + ctrl.exit(); + return; + } + + if input.jump { + let on_ground = game.check_on_ground(); + game.player.jump(&mut game.entities, on_ground); + } + + if input.left { + game.player.move_left(&mut game.entities); + } else if input.right { + game.player.move_right(&mut game.entities); + } else { + game.player.stop_horizontal(&mut game.entities); + } + + if input.cam_left { game.cam_x -= 3; } + if input.cam_right { game.cam_x += 3; } + if input.cam_up { game.cam_y -= 3; } + if input.cam_down { game.cam_y += 3; } + + if let Some(brush_id) = input.paint { + let mat = match brush_id { + 1 => MaterialId::Sand, + 2 => MaterialId::Water, + 3 => MaterialId::Stone, + 4 => MaterialId::Lava, + 5 => MaterialId::Wood, + 6 => MaterialId::Acid, + 7 => MaterialId::Grass, + 8 => MaterialId::Dirt, + 9 => MaterialId::Fire, + 0 => MaterialId::Flesh, + 99 => MaterialId::Empty, + _ => MaterialId::Empty, + }; + let cx = game.cam_x + (vw as i32 / 2); + let cy = game.cam_y + (vh as i32 / 2); + let r = 2; + for dy in -r..=r { + for dx in -r..=r { + if dx * dx + dy * dy <= r * r + 1 { + if mat == MaterialId::Empty { + game.grid.set(cx + dx, cy + dy, verbatim::world::cell::Cell::empty()); + } else { + game.grid.set_material(cx + dx, cy + dy, mat); + } + } + } + } + } + + let (px, py) = game.player.center(&game.entities); + game.cam_x = px as i32 - (vw as i32 / 2); + game.cam_y = py as i32 - (vh as i32 / 2); + + renderer.render(&game.grid, &game.entities, game.cam_x, game.cam_y); + } + _ => {} + } + }).expect("event loop error"); +} diff --git a/src/render/mod.rs b/src/render/mod.rs index 664c3c6..8daf61e 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -1,6 +1,7 @@ pub mod terminal; pub mod window; pub mod window_input; +pub mod vulkan; use crate::entity::EntityManager; use crate::world::grid::Grid; diff --git a/src/render/vulkan.rs b/src/render/vulkan.rs new file mode 100644 index 0000000..52c2c46 --- /dev/null +++ b/src/render/vulkan.rs @@ -0,0 +1,715 @@ +use ash::vk; +use fontdue::{Font, FontSettings}; +use std::ffi::CString; +use std::sync::Arc; + +use crate::entity::{EntityManager, EntityKind}; +use crate::world::cell::MaterialId; +use crate::world::grid::Grid; +use crate::world::material::MaterialRegistry; + +const CHAR_W: u32 = 8; +const CHAR_H: u32 = 16; +const ATLAS_COLS: usize = 16; +const ATLAS_ROWS: usize = 16; +const ATLAS_W: u32 = (ATLAS_COLS as u32) * CHAR_W; +const ATLAS_H: u32 = (ATLAS_ROWS as u32) * CHAR_H; +const MAX_FRAMES: usize = 2; + +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct CellInstance { + grid_x: f32, + grid_y: f32, + atlas_u: f32, + atlas_v: f32, + atlas_w: f32, + atlas_h: f32, + fg: [u8; 4], + bg: [u8; 4], +} + +#[repr(C)] +#[derive(bytemuck::NoUninit, Clone, Copy)] +struct PushConstants { + screen_size: [f32; 2], + cell_size: [f32; 2], +} + +pub struct VulkanRenderer { + grid_w: usize, + grid_h: usize, + pixel_w: u32, + pixel_h: u32, + + entry: ash::Entry, + instance: ash::Instance, + surface: vk::SurfaceKHR, + physical_device: vk::PhysicalDevice, + device: ash::Device, + graphics_queue: vk::Queue, + queue_family: u32, + + swapchain_loader: ash::khr::swapchain::Device, + swapchain: vk::SwapchainKHR, + swapchain_image_views: Vec, + swapchain_format: vk::Format, + swapchain_extent: vk::Extent2D, + + render_pass: vk::RenderPass, + pipeline: vk::Pipeline, + pipeline_layout: vk::PipelineLayout, + framebuffers: Vec, + + command_pool: vk::CommandPool, + command_buffers: Vec, + + image_available: Vec, + render_finished: Vec, + in_flight: Vec, + frame_index: usize, + + vertex_buffer: vk::Buffer, + vertex_memory: vk::DeviceMemory, + index_buffer: vk::Buffer, + index_memory: vk::DeviceMemory, + + atlas_image: vk::Image, + atlas_memory: vk::DeviceMemory, + atlas_view: vk::ImageView, + atlas_sampler: vk::Sampler, + atlas_map: std::collections::HashMap, + + instance_buffer: vk::Buffer, + instance_memory: vk::DeviceMemory, + instance_ptr: *mut CellInstance, + instance_count: usize, + + descriptor_pool: vk::DescriptorPool, + descriptor_set: vk::DescriptorSet, + descriptor_set_layout: vk::DescriptorSetLayout, +} + +impl VulkanRenderer { + pub fn new(window: Arc) -> Result { + let grid_w = 160usize; + let grid_h = 50usize; + let pixel_w = (grid_w as u32) * CHAR_W; + let pixel_h = (grid_h as u32) * CHAR_H; + + let entry = unsafe { ash::Entry::load().map_err(|e| format!("Vulkan load: {e}"))? }; + let instance = create_instance(&entry)?; + let surface = create_surface(&entry, &instance, &window)?; + + let surface_loader = ash::khr::surface::Instance::new(&entry, &instance); + let (physical_device, queue_family) = + pick_physical_device(&instance, &surface_loader, surface)?; + let (device, graphics_queue) = + create_device(&instance, physical_device, queue_family)?; + + let swapchain_loader = ash::khr::swapchain::Device::new(&instance, &device); + let (swapchain, swapchain_images, swapchain_format, swapchain_extent) = + create_swapchain(&device, &swapchain_loader, &surface_loader, + physical_device, surface, queue_family, pixel_w, pixel_h)?; + + let swapchain_image_views: Vec<_> = swapchain_images.iter() + .map(|&img| create_image_view(&device, img, swapchain_format)) + .collect(); + + let render_pass = create_render_pass(&device, swapchain_format)?; + let (descriptor_set_layout, descriptor_pool, descriptor_set) = + create_descriptor(&device)?; + let (pipeline_layout, pipeline) = create_pipeline(&device, render_pass, descriptor_set_layout)?; + let framebuffers: Vec<_> = swapchain_image_views.iter() + .map(|&view| create_framebuffer(&device, render_pass, view, swapchain_extent)) + .collect(); + let command_pool = create_command_pool(&device, queue_family)?; + let command_buffers = create_command_buffers(&device, command_pool, framebuffers.len())?; + let (image_available, render_finished, in_flight) = create_sync(&device)?; + + let (vertex_buffer, vertex_memory, index_buffer, index_memory) = + create_vertex_index_buffers(&device, &instance, physical_device)?; + + let (atlas_image, atlas_memory, atlas_view, atlas_sampler, atlas_map) = + create_atlas_texture(&device, &instance, physical_device, &graphics_queue, command_pool)?; + + let instance_count = grid_w * grid_h; + let (instance_buffer, instance_memory, instance_ptr) = + create_instance_buffer(&device, &instance, physical_device, instance_count)?; + + update_descriptor_set(&device, descriptor_set, atlas_view, atlas_sampler); + + Ok(Self { + grid_w, grid_h, pixel_w, pixel_h, + entry, instance, surface, physical_device, device, + graphics_queue, queue_family, + swapchain_loader, swapchain, swapchain_image_views, + swapchain_format, swapchain_extent, + render_pass, pipeline, pipeline_layout, framebuffers, + command_pool, command_buffers, + image_available, render_finished, in_flight, frame_index: 0, + vertex_buffer, vertex_memory, index_buffer, index_memory, + atlas_image, atlas_memory, atlas_view, atlas_sampler, atlas_map, + instance_buffer, instance_memory, instance_ptr, instance_count, + descriptor_pool, descriptor_set, descriptor_set_layout, + }) + } + + pub fn render(&mut self, grid: &Grid, entities: &EntityManager, cam_x: i32, cam_y: i32) { + let reg = MaterialRegistry::instance(); + + let mut entity_map: std::collections::HashMap<(i32, i32), (char, [u8; 4])> = std::collections::HashMap::new(); + for e in entities.all() { + for b in &e.bodies { + if !b.alive { continue; } + let sx = b.x as i32 - cam_x; + let sy = b.y as i32 - cam_y; + if sx >= 0 && sx < self.grid_w as i32 && sy >= 0 && sy < self.grid_h as i32 { + let ch = match e.kind { + EntityKind::Player if e.alive => '@', + EntityKind::Goblin if e.alive => 'g', + _ => '%', + }; + let fg = if e.on_fire { [255, 160, 40, 255] } + else if !e.alive { [100, 60, 60, 255] } + else { + match e.kind { + EntityKind::Player => [255, 255, 100, 255], + EntityKind::Goblin => [100, 220, 100, 255], + _ => [180, 50, 50, 255], + } + }; + entity_map.insert((sx, sy), (ch, fg)); + } + } + } + + let instances = unsafe { std::slice::from_raw_parts_mut(self.instance_ptr, self.instance_count) }; + let bg_default = [10u8, 10, 15, 255]; + + for dy in 0..self.grid_h { + for dx in 0..self.grid_w { + let idx = dy * self.grid_w + dx; + let wx = cam_x + dx as i32; + let wy = cam_y + dy as i32; + + let (ch, fg, bg) = if let Some(&(ec, ef)) = entity_map.get(&(dx as i32, dy as i32)) { + (ec, ef, bg_default) + } else if !grid.in_bounds(wx, wy) { + ('?', [80, 80, 80, 255], bg_default) + } else { + let cell = grid.get(wx, wy); + let mat = reg.get(cell.material); + if cell.is_empty() { + (' ', [10, 10, 15, 255], bg_default) + } else { + let fg = if cell.material == MaterialId::Lava { + let r = 200u8.saturating_add(cell.variant / 2); + [r, 60, 20, 255] + } else { + [mat.color_fg.0, mat.color_fg.1, mat.color_fg.2, 255] + }; + let bg = [mat.color_bg.0, mat.color_bg.1, mat.color_bg.2, 255]; + (mat.display_char, fg, bg) + } + }; + + let (au, av, aw, ah) = self.atlas_map.get(&ch).copied().unwrap_or((0.0, 0.0, 0.0, 0.0)); + instances[idx] = CellInstance { + grid_x: dx as f32, grid_y: dy as f32, + atlas_u: au, atlas_v: av, atlas_w: aw, atlas_h: ah, + fg, bg, + }; + } + } + + let frame = self.frame_index; + let device = &self.device; + + unsafe { + let _ = device.wait_for_fences(&[self.in_flight[frame]], true, u64::MAX); + let _ = device.reset_fences(&[self.in_flight[frame]]); + + let image_index = match self.swapchain_loader.acquire_next_image( + self.swapchain, u64::MAX, self.image_available[frame], vk::Fence::null() + ) { + Ok((idx, _)) => idx as usize, + Err(e) => { eprintln!("acquire: {e:?}"); return; } + }; + + let cmd = self.command_buffers[frame]; + let _ = device.reset_command_buffer(cmd, vk::CommandBufferResetFlags::default()); + let _ = device.begin_command_buffer(cmd, &vk::CommandBufferBeginInfo::default()); + + let clear = vk::ClearValue { + color: vk::ClearColorValue { float32: [10.0/255.0, 10.0/255.0, 15.0/255.0, 1.0] }, + }; + let rp_info = vk::RenderPassBeginInfo::default() + .render_pass(self.render_pass) + .framebuffer(self.framebuffers[image_index]) + .render_area(vk::Rect2D { offset: vk::Offset2D::default(), extent: self.swapchain_extent }) + .clear_values(std::slice::from_ref(&clear)); + + device.cmd_begin_render_pass(cmd, &rp_info, vk::SubpassContents::INLINE); + device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline); + device.cmd_bind_vertex_buffers(cmd, 0, &[self.vertex_buffer, self.instance_buffer], &[0, 0]); + device.cmd_bind_index_buffer(cmd, self.index_buffer, 0, vk::IndexType::UINT16); + device.cmd_bind_descriptor_sets(cmd, vk::PipelineBindPoint::GRAPHICS, + self.pipeline_layout, 0, &[self.descriptor_set], &[]); + + let pc = PushConstants { + screen_size: [self.swapchain_extent.width as f32, self.swapchain_extent.height as f32], + cell_size: [CHAR_W as f32, CHAR_H as f32], + }; + device.cmd_push_constants(cmd, self.pipeline_layout, vk::ShaderStageFlags::VERTEX, + 0, bytemuck::bytes_of(&pc)); + + device.cmd_draw_indexed(cmd, 6, self.instance_count as u32, 0, 0, 0); + device.cmd_end_render_pass(cmd); + let _ = device.end_command_buffer(cmd); + + let wait_stages = [vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT]; + let submit_info = vk::SubmitInfo::default() + .wait_semaphores(std::slice::from_ref(&self.image_available[frame])) + .wait_dst_stage_mask(&wait_stages) + .command_buffers(std::slice::from_ref(&cmd)) + .signal_semaphores(std::slice::from_ref(&self.render_finished[frame])); + let _ = device.queue_submit(self.graphics_queue, std::slice::from_ref(&submit_info), self.in_flight[frame]); + + let img_idx = image_index as u32; + let present_info = vk::PresentInfoKHR::default() + .wait_semaphores(std::slice::from_ref(&self.render_finished[frame])) + .swapchains(std::slice::from_ref(&self.swapchain)) + .image_indices(std::slice::from_ref(&img_idx)); + let _ = self.swapchain_loader.queue_present(self.graphics_queue, &present_info); + } + + self.frame_index = (self.frame_index + 1) % MAX_FRAMES; + } + + pub fn grid_w(&self) -> usize { self.grid_w } + pub fn grid_h(&self) -> usize { self.grid_h } +} + +impl Drop for VulkanRenderer { + fn drop(&mut self) { + unsafe { + let _ = self.device.device_wait_idle(); + for &f in &self.in_flight { self.device.destroy_fence(f, None); } + for &s in &self.image_available { self.device.destroy_semaphore(s, None); } + for &s in &self.render_finished { self.device.destroy_semaphore(s, None); } + self.device.destroy_command_pool(self.command_pool, None); + for &fb in &self.framebuffers { self.device.destroy_framebuffer(fb, None); } + self.device.destroy_pipeline(self.pipeline, None); + self.device.destroy_pipeline_layout(self.pipeline_layout, None); + self.device.destroy_render_pass(self.render_pass, None); + self.device.destroy_sampler(self.atlas_sampler, None); + self.device.destroy_image_view(self.atlas_view, None); + self.device.destroy_image(self.atlas_image, None); + self.device.free_memory(self.atlas_memory, None); + self.device.destroy_buffer(self.instance_buffer, None); + self.device.free_memory(self.instance_memory, None); + self.device.destroy_buffer(self.vertex_buffer, None); + self.device.free_memory(self.vertex_memory, None); + self.device.destroy_buffer(self.index_buffer, None); + self.device.free_memory(self.index_memory, None); + self.device.destroy_descriptor_pool(self.descriptor_pool, None); + self.device.destroy_descriptor_set_layout(self.descriptor_set_layout, None); + for &v in &self.swapchain_image_views { self.device.destroy_image_view(v, None); } + self.swapchain_loader.destroy_swapchain(self.swapchain, None); + let sl = ash::khr::surface::Instance::new(&self.entry, &self.instance); + sl.destroy_surface(self.surface, None); + self.device.destroy_device(None); + self.instance.destroy_instance(None); + } + } +} + +fn create_instance(entry: &ash::Entry) -> Result { + let app_name = CString::new("Verbatim").unwrap(); + let app_info = vk::ApplicationInfo::default() + .application_name(&app_name) + .api_version(vk::API_VERSION_1_2); + let ext_names: Vec = vec![ + CString::new("VK_KHR_surface").unwrap(), + CString::new("VK_KHR_xlib_surface").unwrap(), + CString::new("VK_KHR_wayland_surface").unwrap(), + ]; + let ext_ptrs: Vec<*const i8> = ext_names.iter().map(|n| n.as_ptr()).collect(); + let create_info = vk::InstanceCreateInfo::default() + .application_info(&app_info) + .enabled_extension_names(&ext_ptrs); + unsafe { entry.create_instance(&create_info, None).map_err(|e| format!("instance: {e:?}")) } +} + +fn create_surface(entry: &ash::Entry, instance: &ash::Instance, window: &winit::window::Window) -> Result { + use raw_window_handle::{HasWindowHandle, HasDisplayHandle}; + let wh = window.window_handle().map_err(|e| format!("wh: {e}"))?; + let dh = window.display_handle().map_err(|e| format!("dh: {e}"))?; + let wh_raw = wh.as_raw(); + let dh_raw = dh.as_raw(); + unsafe { + ash_window::create_surface(entry, instance, dh_raw, wh_raw, None) + .map_err(|e| format!("surface: {e:?}")) + } +} + +fn pick_physical_device( + instance: &ash::Instance, + sl: &ash::khr::surface::Instance, + surface: vk::SurfaceKHR, +) -> Result<(vk::PhysicalDevice, u32), String> { + let devices = unsafe { instance.enumerate_physical_devices().map_err(|e| format!("enum: {e:?}"))? }; + for &pd in &devices { + let props = unsafe { instance.get_physical_device_properties(pd) }; + if props.device_type == vk::PhysicalDeviceType::CPU { continue; } + let qfs = unsafe { instance.get_physical_device_queue_family_properties(pd) }; + for (i, qf) in qfs.iter().enumerate() { + if qf.queue_flags.contains(vk::QueueFlags::GRAPHICS) { + let ok = unsafe { sl.get_physical_device_surface_support(pd, i as u32, surface) }.unwrap_or(false); + if ok { return Ok((pd, i as u32)); } + } + } + } + Err("No GPU".to_string()) +} + +fn create_device(instance: &ash::Instance, pd: vk::PhysicalDevice, qf: u32) -> Result<(ash::Device, vk::Queue), String> { + let qp = [1.0f32]; + let qi = vk::DeviceQueueCreateInfo::default().queue_family_index(qf).queue_priorities(&qp); + let ext_names: Vec = vec![CString::new("VK_KHR_swapchain").unwrap()]; + let ext_ptrs: Vec<*const i8> = ext_names.iter().map(|n| n.as_ptr()).collect(); + let ci = vk::DeviceCreateInfo::default() + .queue_create_infos(std::slice::from_ref(&qi)) + .enabled_extension_names(&ext_ptrs); + unsafe { + let device = instance.create_device(pd, &ci, None).map_err(|e| format!("device: {e:?}"))?; + let queue = device.get_device_queue(qf, 0); + Ok((device, queue)) + } +} + +fn create_swapchain( + device: &ash::Device, + sl: &ash::khr::swapchain::Device, + surface_loader: &ash::khr::surface::Instance, + pd: vk::PhysicalDevice, + surface: vk::SurfaceKHR, + qf: u32, + pw: u32, ph: u32, +) -> Result<(vk::SwapchainKHR, Vec, vk::Format, vk::Extent2D), String> { + let caps = unsafe { surface_loader.get_physical_device_surface_capabilities(pd, surface) } + .map_err(|e| format!("caps: {e:?}"))?; + let format = vk::SurfaceFormatKHR { + format: vk::Format::B8G8R8A8_UNORM, + color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR, + }; + let extent = if caps.current_extent.width != u32::MAX { caps.current_extent } + else { vk::Extent2D { width: pw, height: ph } }; + let ic = caps.min_image_count.max(2); + let qf_slice = [qf]; + let ci = vk::SwapchainCreateInfoKHR::default() + .surface(surface) + .min_image_count(ic) + .image_format(format.format) + .image_color_space(format.color_space) + .image_extent(extent) + .image_array_layers(1) + .image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT) + .image_sharing_mode(vk::SharingMode::EXCLUSIVE) + .queue_family_indices(&qf_slice) + .pre_transform(caps.current_transform) + .composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE) + .present_mode(vk::PresentModeKHR::FIFO) + .clipped(true); + let swapchain = unsafe { sl.create_swapchain(&ci, None) }.map_err(|e| format!("swapchain: {e:?}"))?; + let images = unsafe { sl.get_swapchain_images(swapchain) }.map_err(|e| format!("images: {e:?}"))?; + Ok((swapchain, images, format.format, extent)) +} + +fn create_image_view(device: &ash::Device, image: vk::Image, format: vk::Format) -> vk::ImageView { + let ci = vk::ImageViewCreateInfo::default() + .image(image).view_type(vk::ImageViewType::TYPE_2D).format(format) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, base_mip_level: 0, + level_count: 1, base_array_layer: 0, layer_count: 1, + }); + unsafe { device.create_image_view(&ci, None).expect("image_view") } +} + +fn create_render_pass(device: &ash::Device, format: vk::Format) -> Result { + let att = vk::AttachmentDescription::default() + .format(format).samples(vk::SampleCountFlags::TYPE_1) + .load_op(vk::AttachmentLoadOp::CLEAR).store_op(vk::AttachmentStoreOp::STORE) + .initial_layout(vk::ImageLayout::UNDEFINED).final_layout(vk::ImageLayout::PRESENT_SRC_KHR); + let att_ref = vk::AttachmentReference::default().attachment(0).layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL); + let subpass = vk::SubpassDescription::default() + .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS) + .color_attachments(std::slice::from_ref(&att_ref)); + let dep = vk::SubpassDependency::default() + .src_subpass(vk::SUBPASS_EXTERNAL).dst_subpass(0) + .src_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT) + .dst_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT) + .dst_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE); + let ci = vk::RenderPassCreateInfo::default() + .attachments(std::slice::from_ref(&att)) + .subpasses(std::slice::from_ref(&subpass)) + .dependencies(std::slice::from_ref(&dep)); + unsafe { device.create_render_pass(&ci, None).map_err(|e| format!("render_pass: {e:?}")) } +} + +fn create_descriptor(device: &ash::Device) -> Result<(vk::DescriptorSetLayout, vk::DescriptorPool, vk::DescriptorSet), String> { + let binding = vk::DescriptorSetLayoutBinding::default() + .binding(0).descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) + .descriptor_count(1).stage_flags(vk::ShaderStageFlags::FRAGMENT); + let li = vk::DescriptorSetLayoutCreateInfo::default().bindings(std::slice::from_ref(&binding)); + let layout = unsafe { device.create_descriptor_set_layout(&li, None) }.map_err(|e| format!("ds_layout: {e:?}"))?; + let ps = vk::DescriptorPoolSize { ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER, descriptor_count: 1 }; + let pi = vk::DescriptorPoolCreateInfo::default().pool_sizes(std::slice::from_ref(&ps)).max_sets(1); + let pool = unsafe { device.create_descriptor_pool(&pi, None) }.map_err(|e| format!("ds_pool: {e:?}"))?; + let ai = vk::DescriptorSetAllocateInfo::default().descriptor_pool(pool).set_layouts(std::slice::from_ref(&layout)); + let sets = unsafe { device.allocate_descriptor_sets(&ai) }.map_err(|e| format!("alloc_ds: {e:?}"))?; + Ok((layout, pool, sets[0])) +} + +fn create_pipeline(device: &ash::Device, rp: vk::RenderPass, ds_layout: vk::DescriptorSetLayout) -> Result<(vk::PipelineLayout, vk::Pipeline), String> { + let vert_spv = include_bytes!("../../assets/shaders/cell_vert.spv"); + let frag_spv = include_bytes!("../../assets/shaders/cell_frag.spv"); + let vert_code: Vec = vert_spv.chunks_exact(4).map(|c| u32::from_ne_bytes([c[0], c[1], c[2], c[3]])).collect(); + let frag_code: Vec = frag_spv.chunks_exact(4).map(|c| u32::from_ne_bytes([c[0], c[1], c[2], c[3]])).collect(); + let vm = unsafe { device.create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&vert_code), None) }.map_err(|e| format!("vert: {e:?}"))?; + let fm = unsafe { device.create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&frag_code), None) }.map_err(|e| format!("frag: {e:?}"))?; + let main = CString::new("main").unwrap(); + let vs = vk::PipelineShaderStageCreateInfo::default().stage(vk::ShaderStageFlags::VERTEX).module(vm).name(&main); + let fs = vk::PipelineShaderStageCreateInfo::default().stage(vk::ShaderStageFlags::FRAGMENT).module(fm).name(&main); + + let bindings = [ + vk::VertexInputBindingDescription { binding: 0, stride: 8, input_rate: vk::VertexInputRate::VERTEX }, + vk::VertexInputBindingDescription { binding: 1, stride: std::mem::size_of::() as u32, input_rate: vk::VertexInputRate::INSTANCE }, + ]; + let attrs = [ + vk::VertexInputAttributeDescription { location: 0, binding: 0, format: vk::Format::R32G32_SFLOAT, offset: 0 }, + vk::VertexInputAttributeDescription { location: 1, binding: 1, format: vk::Format::R32G32_SFLOAT, offset: 0 }, + vk::VertexInputAttributeDescription { location: 2, binding: 1, format: vk::Format::R32G32B32A32_SFLOAT, offset: 8 }, + vk::VertexInputAttributeDescription { location: 3, binding: 1, format: vk::Format::R8G8B8A8_UNORM, offset: 24 }, + vk::VertexInputAttributeDescription { location: 4, binding: 1, format: vk::Format::R8G8B8A8_UNORM, offset: 28 }, + ]; + let vi = vk::PipelineVertexInputStateCreateInfo::default() + .vertex_binding_descriptions(&bindings).vertex_attribute_descriptions(&attrs); + let ia = vk::PipelineInputAssemblyStateCreateInfo::default().topology(vk::PrimitiveTopology::TRIANGLE_LIST); + let vp = vk::Viewport { x: 0.0, y: 0.0, width: 1.0, height: 1.0, min_depth: 0.0, max_depth: 1.0 }; + let sc = vk::Rect2D { offset: vk::Offset2D::default(), extent: vk::Extent2D { width: 4096, height: 4096 } }; + let vs_state = vk::PipelineViewportStateCreateInfo::default().viewports(std::slice::from_ref(&vp)).scissors(std::slice::from_ref(&sc)); + let rs = vk::PipelineRasterizationStateCreateInfo::default().line_width(1.0).cull_mode(vk::CullModeFlags::NONE); + let ms = vk::PipelineMultisampleStateCreateInfo::default().rasterization_samples(vk::SampleCountFlags::TYPE_1); + let cba = vk::PipelineColorBlendAttachmentState::default().color_write_mask(vk::ColorComponentFlags::RGBA); + let cb = vk::PipelineColorBlendStateCreateInfo::default().attachments(std::slice::from_ref(&cba)); + let pcr = vk::PushConstantRange { stage_flags: vk::ShaderStageFlags::VERTEX, offset: 0, size: std::mem::size_of::() as u32 }; + let li = vk::PipelineLayoutCreateInfo::default() + .set_layouts(std::slice::from_ref(&ds_layout)) + .push_constant_ranges(std::slice::from_ref(&pcr)); + let layout = unsafe { device.create_pipeline_layout(&li, None) }.map_err(|e| format!("pipeline_layout: {e:?}"))?; + let stages = [vs, fs]; + let pi = vk::GraphicsPipelineCreateInfo::default() + .stages(&stages).vertex_input_state(&vi).input_assembly_state(&ia) + .viewport_state(&vs_state).rasterization_state(&rs).multisample_state(&ms) + .color_blend_state(&cb).layout(layout).render_pass(rp).subpass(0); + let pipes = unsafe { device.create_graphics_pipelines(vk::PipelineCache::null(), std::slice::from_ref(&pi), None) } + .map_err(|(_, e)| format!("pipeline: {e:?}"))?; + unsafe { device.destroy_shader_module(vm, None); device.destroy_shader_module(fm, None); } + Ok((layout, pipes[0])) +} + +fn create_framebuffer(device: &ash::Device, rp: vk::RenderPass, view: vk::ImageView, ext: vk::Extent2D) -> vk::Framebuffer { + let ci = vk::FramebufferCreateInfo::default().render_pass(rp).attachments(std::slice::from_ref(&view)) + .width(ext.width).height(ext.height).layers(1); + unsafe { device.create_framebuffer(&ci, None).expect("fb") } +} + +fn create_command_pool(device: &ash::Device, qf: u32) -> Result { + let ci = vk::CommandPoolCreateInfo::default().queue_family_index(qf).flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER); + unsafe { device.create_command_pool(&ci, None).map_err(|e| format!("cmd_pool: {e:?}")) } +} + +fn create_command_buffers(device: &ash::Device, pool: vk::CommandPool, count: usize) -> Result, String> { + let ai = vk::CommandBufferAllocateInfo::default().command_pool(pool).level(vk::CommandBufferLevel::PRIMARY).command_buffer_count(count as u32); + unsafe { device.allocate_command_buffers(&ai).map_err(|e| format!("cmd_bufs: {e:?}")) } +} + +fn create_sync(device: &ash::Device) -> Result<(Vec, Vec, Vec), String> { + let mut ia = Vec::new(); let mut rf = Vec::new(); let mut ifl = Vec::new(); + for _ in 0..MAX_FRAMES { + unsafe { + ia.push(device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None).map_err(|e| format!("sem: {e:?}"))?); + rf.push(device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None).map_err(|e| format!("sem: {e:?}"))?); + ifl.push(device.create_fence(&vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED), None).map_err(|e| format!("fence: {e:?}"))?); + } + } + Ok((ia, rf, ifl)) +} + +fn find_mem_type(instance: &ash::Instance, pd: vk::PhysicalDevice, filter: u32, props: vk::MemoryPropertyFlags) -> Result { + let mp = unsafe { instance.get_physical_device_memory_properties(pd) }; + for (i, mt) in mp.memory_types.iter().enumerate() { + if (filter & (1 << i)) != 0 && mt.property_flags.contains(props) { return Ok(i as u32); } + } + Err("No memory type".to_string()) +} + +fn create_buffer_with_data( + device: &ash::Device, instance: &ash::Instance, pd: vk::PhysicalDevice, + data: &[T], usage: vk::BufferUsageFlags, +) -> Result<(vk::Buffer, vk::DeviceMemory), String> { + let size = (data.len() * std::mem::size_of::()) as vk::DeviceSize; + let bi = vk::BufferCreateInfo::default().size(size).usage(usage).sharing_mode(vk::SharingMode::EXCLUSIVE); + let buf = unsafe { device.create_buffer(&bi, None) }.map_err(|e| format!("buf: {e:?}"))?; + let req = unsafe { device.get_buffer_memory_requirements(buf) }; + let mt = find_mem_type(instance, pd, req.memory_type_bits, vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT)?; + let ai = vk::MemoryAllocateInfo::default().allocation_size(req.size).memory_type_index(mt); + let mem = unsafe { device.allocate_memory(&ai, None) }.map_err(|e| format!("mem: {e:?}"))?; + unsafe { + device.bind_buffer_memory(buf, mem, 0).expect("bind"); + let ptr = device.map_memory(mem, 0, size, vk::MemoryMapFlags::default()).expect("map"); + std::ptr::copy_nonoverlapping(data.as_ptr() as *const u8, ptr as *mut u8, size as usize); + device.unmap_memory(mem); + } + Ok((buf, mem)) +} + +fn create_vertex_index_buffers( + device: &ash::Device, instance: &ash::Instance, pd: vk::PhysicalDevice, +) -> Result<(vk::Buffer, vk::DeviceMemory, vk::Buffer, vk::DeviceMemory), String> { + let verts: [f32; 8] = [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0]; + let indices: [u16; 6] = [0, 1, 2, 1, 3, 2]; + let (vb, vm) = create_buffer_with_data(device, instance, pd, &verts, vk::BufferUsageFlags::VERTEX_BUFFER)?; + let (ib, im) = create_buffer_with_data(device, instance, pd, &indices, vk::BufferUsageFlags::INDEX_BUFFER)?; + Ok((vb, vm, ib, im)) +} + +fn create_atlas_texture( + device: &ash::Device, instance: &ash::Instance, pd: vk::PhysicalDevice, + queue: &vk::Queue, pool: vk::CommandPool, +) -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView, vk::Sampler, std::collections::HashMap), String> { + let font_bytes: &[u8] = include_bytes!("../../assets/DejaVuSansMono.ttf"); + let font = Font::from_bytes(font_bytes, FontSettings { collection_index: 0, scale: CHAR_H as f32, load_substitutions: false }).expect("font"); + let mut atlas_data = vec![0u8; (ATLAS_W * ATLAS_H) as usize]; + let mut atlas_map = std::collections::HashMap::new(); + let chars: Vec = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~?".chars().collect(); + for (i, &ch) in chars.iter().enumerate() { + let col = i % ATLAS_COLS; let row = i / ATLAS_COLS; + atlas_map.insert(ch, ( + (col as f32 * CHAR_W as f32) / ATLAS_W as f32, + (row as f32 * CHAR_H as f32) / ATLAS_H as f32, + CHAR_W as f32 / ATLAS_W as f32, + CHAR_H as f32 / ATLAS_H as f32, + )); + let (metrics, bitmap) = font.rasterize(ch, CHAR_H as f32); + for y in 0..metrics.height.min(CHAR_H as usize) { + for x in 0..metrics.width.min(CHAR_W as usize) { + let a = bitmap[y * metrics.width + x]; + if a > 0 { + let px = (x as i32 + metrics.xmin).max(0) as usize; + let py = (y as i32 + CHAR_H as i32 - metrics.height as i32 - metrics.ymin).max(0) as usize; + if px < CHAR_W as usize && py < CHAR_H as usize { + atlas_data[(row * CHAR_H as usize + py) * ATLAS_W as usize + col * CHAR_W as usize + px] = a; + } + } + } + } + } + + let ii = vk::ImageCreateInfo::default() + .image_type(vk::ImageType::TYPE_2D) + .extent(vk::Extent3D { width: ATLAS_W, height: ATLAS_H, depth: 1 }) + .mip_levels(1).array_layers(1).format(vk::Format::R8_UNORM) + .tiling(vk::ImageTiling::OPTIMAL).initial_layout(vk::ImageLayout::UNDEFINED) + .usage(vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::SAMPLED) + .samples(vk::SampleCountFlags::TYPE_1).sharing_mode(vk::SharingMode::EXCLUSIVE); + let image = unsafe { device.create_image(&ii, None) }.map_err(|e| format!("img: {e:?}"))?; + let req = unsafe { device.get_image_memory_requirements(image) }; + let mt = find_mem_type(instance, pd, req.memory_type_bits, vk::MemoryPropertyFlags::DEVICE_LOCAL)?; + let mem = unsafe { device.allocate_memory(&vk::MemoryAllocateInfo::default().allocation_size(req.size).memory_type_index(mt), None) }.map_err(|e| format!("img mem: {e:?}"))?; + unsafe { device.bind_image_memory(image, mem, 0).expect("bind img"); } + + // Staging + let sz = atlas_data.len() as vk::DeviceSize; + let sbi = vk::BufferCreateInfo::default().size(sz).usage(vk::BufferUsageFlags::TRANSFER_SRC).sharing_mode(vk::SharingMode::EXCLUSIVE); + let sbuf = unsafe { device.create_buffer(&sbi, None) }.expect("staging buf"); + let sreq = unsafe { device.get_buffer_memory_requirements(sbuf) }; + let smt = find_mem_type(instance, pd, sreq.memory_type_bits, vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT)?; + let smem = unsafe { device.allocate_memory(&vk::MemoryAllocateInfo::default().allocation_size(sreq.size).memory_type_index(smt), None) }.expect("staging mem"); + unsafe { + device.bind_buffer_memory(sbuf, smem, 0).expect("bind staging"); + let ptr = device.map_memory(smem, 0, sz, vk::MemoryMapFlags::default()).expect("map staging"); + std::ptr::copy_nonoverlapping(atlas_data.as_ptr(), ptr as *mut u8, atlas_data.len()); + device.unmap_memory(smem); + } + + // Copy + let cmd = unsafe { + let c = device.allocate_command_buffers(&vk::CommandBufferAllocateInfo::default() + .command_pool(pool).level(vk::CommandBufferLevel::PRIMARY).command_buffer_count(1)).expect("alloc")[0]; + device.begin_command_buffer(c, &vk::CommandBufferBeginInfo::default().flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT)).expect("begin"); + let b1 = vk::ImageMemoryBarrier::default() + .old_layout(vk::ImageLayout::UNDEFINED).new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED).dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .image(image).subresource_range(vk::ImageSubresourceRange { aspect_mask: vk::ImageAspectFlags::COLOR, base_mip_level: 0, level_count: 1, base_array_layer: 0, layer_count: 1 }) + .src_access_mask(vk::AccessFlags::default()).dst_access_mask(vk::AccessFlags::TRANSFER_WRITE); + device.cmd_pipeline_barrier(c, vk::PipelineStageFlags::TOP_OF_PIPE, vk::PipelineStageFlags::TRANSFER, vk::DependencyFlags::default(), &[], &[], std::slice::from_ref(&b1)); + device.cmd_copy_buffer_to_image(c, sbuf, image, vk::ImageLayout::TRANSFER_DST_OPTIMAL, + std::slice::from_ref(&vk::BufferImageCopy::default() + .buffer_row_length(ATLAS_W).buffer_image_height(ATLAS_H) + .image_subresource(vk::ImageSubresourceLayers { aspect_mask: vk::ImageAspectFlags::COLOR, mip_level: 0, base_array_layer: 0, layer_count: 1 }) + .image_extent(vk::Extent3D { width: ATLAS_W, height: ATLAS_H, depth: 1 }))); + let b2 = vk::ImageMemoryBarrier::default() + .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL).new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED).dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .image(image).subresource_range(vk::ImageSubresourceRange { aspect_mask: vk::ImageAspectFlags::COLOR, base_mip_level: 0, level_count: 1, base_array_layer: 0, layer_count: 1 }) + .src_access_mask(vk::AccessFlags::TRANSFER_WRITE).dst_access_mask(vk::AccessFlags::SHADER_READ); + device.cmd_pipeline_barrier(c, vk::PipelineStageFlags::TRANSFER, vk::PipelineStageFlags::FRAGMENT_SHADER, vk::DependencyFlags::default(), &[], &[], std::slice::from_ref(&b2)); + device.end_command_buffer(c).expect("end"); + let si = vk::SubmitInfo::default().command_buffers(std::slice::from_ref(&c)); + device.queue_submit(*queue, std::slice::from_ref(&si), vk::Fence::null()).expect("submit"); + device.queue_wait_idle(*queue).expect("wait"); + c + }; + unsafe { + device.free_command_buffers(pool, &[cmd]); + device.destroy_buffer(sbuf, None); + device.free_memory(smem, None); + } + + let vi = vk::ImageViewCreateInfo::default() + .image(image).view_type(vk::ImageViewType::TYPE_2D).format(vk::Format::R8_UNORM) + .subresource_range(vk::ImageSubresourceRange { aspect_mask: vk::ImageAspectFlags::COLOR, base_mip_level: 0, level_count: 1, base_array_layer: 0, layer_count: 1 }); + let view = unsafe { device.create_image_view(&vi, None) }.expect("atlas view"); + let si = vk::SamplerCreateInfo::default() + .mag_filter(vk::Filter::LINEAR).min_filter(vk::Filter::LINEAR) + .address_mode_u(vk::SamplerAddressMode::CLAMP_TO_EDGE).address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE).address_mode_w(vk::SamplerAddressMode::CLAMP_TO_EDGE) + .border_color(vk::BorderColor::INT_OPAQUE_BLACK).unnormalized_coordinates(false); + let sampler = unsafe { device.create_sampler(&si, None) }.expect("sampler"); + Ok((image, mem, view, sampler, atlas_map)) +} + +fn create_instance_buffer( + device: &ash::Device, instance: &ash::Instance, pd: vk::PhysicalDevice, count: usize, +) -> Result<(vk::Buffer, vk::DeviceMemory, *mut CellInstance), String> { + let sz = (count * std::mem::size_of::()) as vk::DeviceSize; + let bi = vk::BufferCreateInfo::default().size(sz).usage(vk::BufferUsageFlags::VERTEX_BUFFER).sharing_mode(vk::SharingMode::EXCLUSIVE); + let buf = unsafe { device.create_buffer(&bi, None) }.map_err(|e| format!("inst buf: {e:?}"))?; + let req = unsafe { device.get_buffer_memory_requirements(buf) }; + let mt = find_mem_type(instance, pd, req.memory_type_bits, vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT)?; + let mem = unsafe { device.allocate_memory(&vk::MemoryAllocateInfo::default().allocation_size(req.size).memory_type_index(mt), None) }.map_err(|e| format!("inst mem: {e:?}"))?; + unsafe { + device.bind_buffer_memory(buf, mem, 0).expect("bind inst"); + let ptr = device.map_memory(mem, 0, sz, vk::MemoryMapFlags::default()).expect("map inst"); + Ok((buf, mem, ptr as *mut CellInstance)) + } +} + +fn update_descriptor_set(device: &ash::Device, set: vk::DescriptorSet, view: vk::ImageView, sampler: vk::Sampler) { + let ii = vk::DescriptorImageInfo::default().image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL).image_view(view).sampler(sampler); + let w = vk::WriteDescriptorSet::default().dst_set(set).dst_binding(0) + .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER).image_info(std::slice::from_ref(&ii)); + unsafe { device.update_descriptor_sets(std::slice::from_ref(&w), &[]); } +}