Files
Verbatim/src/render/graphics.rs
T
Emil 07c7b88521 feat: items render as multi-cell pictures in graphics mode
- Item.shape() returns Vec<(dx, dy, color)> per type:
  Sword: 4-tall blade + crossguard + handle (6 cells)
  Dagger: blade + 2-cell handle (3 cells)
  Bow: curved arc + bowstring (4 cells)
  Leather/Plate Armor: 2x2 body shape (4 cells)
  Shield: 2x2 shield shape (4 cells)
  Health/Mana Potion: bottle with cork + body (4 cells)
  Food: 3-cell round shape
  Scroll: 3-cell horizontal scroll
- Graphics renderer paints all shape cells per item
- ASCII mode still uses 2-char glyph encoding in UI
- Items in world have visual shape, not just single colored cell

All 171 tests + 14 scenarios pass.
2026-06-21 18:39:58 +03:00

1371 lines
52 KiB
Rust

use ash::vk;
use std::ffi::CString;
use std::sync::Arc;
use crate::entity::EntityManager;
use crate::render::lighting;
use crate::world::cell::MaterialId;
use crate::world::grid::{Grid, WORLD_H, WORLD_W};
const CHAR_W: u32 = 8;
const CHAR_H: u32 = 8;
const UI_CELL_SIZE: u32 = 2;
const MAX_FRAMES: usize = 2;
fn entity_priority(kind: crate::entity::EntityKind) -> u32 {
use crate::entity::EntityKind;
match kind {
EntityKind::Player => 3,
EntityKind::Goblin => 2,
EntityKind::Slime => 1,
EntityKind::Corpse => 0,
}
}
fn background_color(wx: i32, wy: i32, vy: i32, view_h: i32) -> [u8; 4] {
let t = (vy as f32 / view_h as f32).clamp(0.0, 1.0);
let base_r = (10.0 + t * 15.0) as u8;
let base_g = (10.0 + t * 25.0) as u8;
let base_b = (25.0 + t * 35.0) as u8;
let hash = ((wx.wrapping_mul(73856093)) ^ (wy.wrapping_mul(19349663))).wrapping_abs();
if hash % 80 == 0 {
let brightness = (60 + (hash % 120) as u8).min(255);
return [brightness, brightness, brightness + 20, 255];
}
[base_r, base_g, base_b, 255]
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct ColorInstance {
grid_x: f32,
grid_y: f32,
color: [u8; 4],
}
#[repr(C)]
#[derive(bytemuck::NoUninit, Clone, Copy, Default)]
struct GpuLightSource {
pos: [f32; 2],
radius: f32,
_pad0: f32,
color: [f32; 3],
_pad1: f32,
}
const MAX_LIGHT_SOURCES: usize = 64;
#[repr(C)]
#[derive(bytemuck::NoUninit, Clone, Copy)]
struct PushConstants {
screen_size: [f32; 2],
cell_size: [f32; 2],
world_size: [i32; 2],
cam_pos: [i32; 2],
ambient: [f32; 3],
is_ui: u32,
light_count: u32,
}
pub struct GraphicsRenderer {
grid_w: usize,
grid_h: usize,
entry: ash::Entry,
instance: ash::Instance,
surface: vk::SurfaceKHR,
physical_device: vk::PhysicalDevice,
device: ash::Device,
graphics_queue: vk::Queue,
swapchain_loader: ash::khr::swapchain::Device,
swapchain: vk::SwapchainKHR,
swapchain_image_views: Vec<vk::ImageView>,
swapchain_extent: vk::Extent2D,
present_mode: vk::PresentModeKHR,
render_pass: vk::RenderPass,
pipeline: vk::Pipeline,
pipeline_layout: vk::PipelineLayout,
framebuffers: Vec<vk::Framebuffer>,
command_pool: vk::CommandPool,
command_buffers: Vec<vk::CommandBuffer>,
image_available: Vec<vk::Semaphore>,
render_finished: Vec<vk::Semaphore>,
in_flight: Vec<vk::Fence>,
frame_index: usize,
vertex_buffer: vk::Buffer,
vertex_memory: vk::DeviceMemory,
index_buffer: vk::Buffer,
index_memory: vk::DeviceMemory,
instance_buffer: vk::Buffer,
instance_memory: vk::DeviceMemory,
instance_ptr: *mut ColorInstance,
instance_count: usize,
ui_instance_buffer: vk::Buffer,
ui_instance_memory: vk::DeviceMemory,
ui_instance_ptr: *mut ColorInstance,
ui_instance_capacity: usize,
grid_buffer: vk::Buffer,
grid_memory: vk::DeviceMemory,
grid_ptr: *mut u32,
light_buffer: vk::Buffer,
light_memory: vk::DeviceMemory,
light_ptr: *mut GpuLightSource,
ent_pri_buf: Vec<u8>,
entity_color_buf: Vec<[u8; 4]>,
item_color_buf: Vec<[u8; 4]>,
shadow_buf: Vec<bool>,
descriptor_set_layout: vk::DescriptorSetLayout,
descriptor_pool: vk::DescriptorPool,
descriptor_set: vk::DescriptorSet,
window: Arc<winit::window::Window>,
}
impl GraphicsRenderer {
pub fn new(window: Arc<winit::window::Window>) -> Result<Self, String> {
let grid_w = 160usize;
let grid_h = 50usize;
let entry = unsafe { ash::Entry::load().map_err(|e| format!("Vulkan load: {e}"))? };
use raw_window_handle::HasDisplayHandle;
let dh = window
.display_handle()
.map_err(|e| format!("display_handle: {e}"))?;
let required_exts = ash_window::enumerate_required_extensions(dh.as_raw())
.map_err(|e| format!("enumerate_required_extensions: {e:?}"))?;
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 mut ext_ptrs: Vec<*const i8> = required_exts.iter().map(|&p| p as *const i8).collect();
let avail_exts =
unsafe { entry.enumerate_instance_extension_properties(None) }.unwrap_or_default();
let has_debug = avail_exts.iter().any(|e| {
let name = unsafe { std::ffi::CStr::from_ptr(e.extension_name.as_ptr() as *const i8) };
name.to_str().unwrap_or("") == "VK_EXT_debug_utils"
});
if has_debug {
ext_ptrs.push(b"VK_EXT_debug_utils\0".as_ptr() as *const i8);
}
let ci = vk::InstanceCreateInfo::default()
.application_info(&app_info)
.enabled_extension_names(&ext_ptrs);
let instance =
unsafe { entry.create_instance(&ci, None) }.map_err(|e| format!("instance: {e:?}"))?;
let surface = {
use raw_window_handle::HasWindowHandle;
let wh = window.window_handle().map_err(|e| format!("wh: {e}"))?;
let dh = window.display_handle().map_err(|e| format!("dh: {e}"))?;
unsafe { ash_window::create_surface(&entry, &instance, dh.as_raw(), wh.as_raw(), None) }
.map_err(|e| format!("surface: {e:?}"))?
};
let sl = ash::khr::surface::Instance::new(&entry, &instance);
let devices =
unsafe { instance.enumerate_physical_devices() }.map_err(|e| format!("enum: {e:?}"))?;
let mut physical_device = vk::PhysicalDevice::null();
let mut qf = 0u32;
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, q) in qfs.iter().enumerate() {
if q.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 {
physical_device = pd;
qf = i as u32;
break;
}
}
}
if physical_device != vk::PhysicalDevice::null() {
break;
}
}
if physical_device == vk::PhysicalDevice::null() {
return Err("No GPU".to_string());
}
let qp = [1.0f32];
let qi = vk::DeviceQueueCreateInfo::default()
.queue_family_index(qf)
.queue_priorities(&qp);
let dev_ext_names: Vec<CString> = vec![CString::new("VK_KHR_swapchain").unwrap()];
let dev_ext_ptrs: Vec<*const i8> = dev_ext_names.iter().map(|n| n.as_ptr()).collect();
let dci = vk::DeviceCreateInfo::default()
.queue_create_infos(std::slice::from_ref(&qi))
.enabled_extension_names(&dev_ext_ptrs);
let device = unsafe { instance.create_device(physical_device, &dci, None) }
.map_err(|e| format!("device: {e:?}"))?;
let graphics_queue = unsafe { device.get_device_queue(qf, 0) };
let swapchain_loader = ash::khr::swapchain::Device::new(&instance, &device);
let caps = unsafe { sl.get_physical_device_surface_capabilities(physical_device, surface) }
.map_err(|e| format!("caps: {e:?}"))?;
let present_modes =
unsafe { sl.get_physical_device_surface_present_modes(physical_device, surface) }
.unwrap_or_default();
let present_mode = present_modes
.iter()
.copied()
.find(|&m| m == vk::PresentModeKHR::MAILBOX)
.unwrap_or(vk::PresentModeKHR::FIFO);
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: grid_w as u32 * CHAR_W,
height: grid_h as u32 * CHAR_H,
}
};
let ic = caps.min_image_count.max(2);
let qf_slice = [qf];
let sci = 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(present_mode)
.clipped(true);
let swapchain = unsafe { swapchain_loader.create_swapchain(&sci, None) }
.map_err(|e| format!("swapchain: {e:?}"))?;
let swapchain_images = unsafe { swapchain_loader.get_swapchain_images(swapchain) }
.map_err(|e| format!("images: {e:?}"))?;
let swapchain_image_views: Vec<_> = swapchain_images
.iter()
.map(|&img| {
let vi = vk::ImageViewCreateInfo::default()
.image(img)
.view_type(vk::ImageViewType::TYPE_2D)
.format(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(&vi, None).expect("image_view") }
})
.collect();
let att = vk::AttachmentDescription::default()
.format(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 rpci = vk::RenderPassCreateInfo::default()
.attachments(std::slice::from_ref(&att))
.subpasses(std::slice::from_ref(&subpass))
.dependencies(std::slice::from_ref(&dep));
let render_pass = unsafe { device.create_render_pass(&rpci, None) }
.map_err(|e| format!("render_pass: {e:?}"))?;
// Pipeline — no atlas, no descriptor set, just colored quads
let vert_spv = include_bytes!("../../assets/shaders/graphics_vert.spv");
let frag_spv = include_bytes!("../../assets/shaders/graphics_frag.spv");
let vert_code: Vec<u32> = vert_spv
.chunks_exact(4)
.map(|c| u32::from_ne_bytes([c[0], c[1], c[2], c[3]]))
.collect();
let frag_code: Vec<u32> = 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::<ColorInstance>() 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::R8G8B8A8_UNORM,
offset: 8,
},
];
let vi = vk::PipelineVertexInputStateCreateInfo::default()
.vertex_binding_descriptions(&bindings)
.vertex_attribute_descriptions(&attrs);
let ia = vk::PipelineInputAssemblyStateCreateInfo::default()
.topology(vk::PrimitiveTopology::TRIANGLE_LIST);
let dynamic_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR];
let dynamic_state =
vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states);
let vs_state = vk::PipelineViewportStateCreateInfo::default()
.viewport_count(1)
.scissor_count(1);
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()
.blend_enable(true)
.src_color_blend_factor(vk::BlendFactor::SRC_ALPHA)
.dst_color_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
.color_blend_op(vk::BlendOp::ADD)
.src_alpha_blend_factor(vk::BlendFactor::ONE)
.dst_alpha_blend_factor(vk::BlendFactor::ZERO)
.alpha_blend_op(vk::BlendOp::ADD)
.color_write_mask(vk::ColorComponentFlags::RGBA);
let cb = vk::PipelineColorBlendStateCreateInfo::default()
.attachments(std::slice::from_ref(&cba));
let grid_binding = vk::DescriptorSetLayoutBinding::default()
.binding(0)
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::VERTEX);
let light_binding = vk::DescriptorSetLayoutBinding::default()
.binding(1)
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::VERTEX);
let dsl_bindings = [grid_binding, light_binding];
let dsl_ci = vk::DescriptorSetLayoutCreateInfo::default().bindings(&dsl_bindings);
let descriptor_set_layout = unsafe { device.create_descriptor_set_layout(&dsl_ci, None) }
.map_err(|e| format!("dsl: {e:?}"))?;
let pcr = vk::PushConstantRange {
stage_flags: vk::ShaderStageFlags::VERTEX,
offset: 0,
size: std::mem::size_of::<PushConstants>() as u32,
};
let pli = vk::PipelineLayoutCreateInfo::default()
.set_layouts(std::slice::from_ref(&descriptor_set_layout))
.push_constant_ranges(std::slice::from_ref(&pcr));
let pipeline_layout = unsafe { device.create_pipeline_layout(&pli, None) }
.map_err(|e| format!("pipeline_layout: {e:?}"))?;
let stages = [vs, fs];
let gpci = 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)
.dynamic_state(&dynamic_state)
.layout(pipeline_layout)
.render_pass(render_pass)
.subpass(0);
let pipes = unsafe {
device.create_graphics_pipelines(
vk::PipelineCache::null(),
std::slice::from_ref(&gpci),
None,
)
}
.map_err(|(_, e)| format!("pipeline: {e:?}"))?;
unsafe {
device.destroy_shader_module(vm, None);
device.destroy_shader_module(fm, None);
}
let pipeline = pipes[0];
let framebuffers: Vec<_> = swapchain_image_views
.iter()
.map(|&view| {
let fci = vk::FramebufferCreateInfo::default()
.render_pass(render_pass)
.attachments(std::slice::from_ref(&view))
.width(extent.width)
.height(extent.height)
.layers(1);
unsafe { device.create_framebuffer(&fci, None).expect("fb") }
})
.collect();
let command_pool = {
let cpci = vk::CommandPoolCreateInfo::default()
.queue_family_index(qf)
.flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER);
unsafe { device.create_command_pool(&cpci, None) }
.map_err(|e| format!("cmd_pool: {e:?}"))?
};
let command_buffers = {
let cai = vk::CommandBufferAllocateInfo::default()
.command_pool(command_pool)
.level(vk::CommandBufferLevel::PRIMARY)
.command_buffer_count(framebuffers.len() as u32);
unsafe { device.allocate_command_buffers(&cai) }
.map_err(|e| format!("cmd_bufs: {e:?}"))?
};
let mut image_available = Vec::new();
let mut render_finished = Vec::new();
let mut in_flight = Vec::new();
for _ in 0..MAX_FRAMES {
unsafe {
image_available.push(
device
.create_semaphore(&vk::SemaphoreCreateInfo::default(), None)
.map_err(|e| format!("sem: {e:?}"))?,
);
render_finished.push(
device
.create_semaphore(&vk::SemaphoreCreateInfo::default(), None)
.map_err(|e| format!("sem: {e:?}"))?,
);
in_flight.push(
device
.create_fence(
&vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED),
None,
)
.map_err(|e| format!("fence: {e:?}"))?,
);
}
}
// Vertex + index buffers
let find_mem = |filter: u32, props: vk::MemoryPropertyFlags| -> Result<u32, String> {
let mp = unsafe { instance.get_physical_device_memory_properties(physical_device) };
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())
};
let make_buf = |data: &[u8],
usage: vk::BufferUsageFlags|
-> Result<(vk::Buffer, vk::DeviceMemory), String> {
let sz = data.len() as vk::DeviceSize;
let bi = vk::BufferCreateInfo::default()
.size(sz)
.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(
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!("mem: {e:?}"))?;
unsafe {
device.bind_buffer_memory(buf, mem, 0).expect("bind");
let ptr = device
.map_memory(mem, 0, sz, vk::MemoryMapFlags::default())
.expect("map");
std::ptr::copy_nonoverlapping(data.as_ptr(), ptr as *mut u8, data.len());
device.unmap_memory(mem);
}
Ok((buf, mem))
};
let grid_data = vec![0u32; WORLD_W * WORLD_H];
let (grid_buffer, grid_memory) = make_buf(
bytemuck::cast_slice(&grid_data),
vk::BufferUsageFlags::STORAGE_BUFFER,
)?;
let grid_ptr = unsafe {
let sz = (WORLD_W * WORLD_H * std::mem::size_of::<u32>()) as vk::DeviceSize;
let ptr = device
.map_memory(grid_memory, 0, sz, vk::MemoryMapFlags::default())
.map_err(|e| format!("map grid: {e:?}"))?;
ptr as *mut u32
};
let light_data = vec![GpuLightSource::default(); MAX_LIGHT_SOURCES];
let (light_buffer, light_memory) = make_buf(
bytemuck::cast_slice(&light_data),
vk::BufferUsageFlags::STORAGE_BUFFER,
)?;
let light_ptr = unsafe {
let sz = (MAX_LIGHT_SOURCES * std::mem::size_of::<GpuLightSource>()) as vk::DeviceSize;
let ptr = device
.map_memory(light_memory, 0, sz, vk::MemoryMapFlags::default())
.map_err(|e| format!("map light: {e:?}"))?;
ptr as *mut GpuLightSource
};
let pool_sizes = [vk::DescriptorPoolSize {
ty: vk::DescriptorType::STORAGE_BUFFER,
descriptor_count: 2,
}];
let descriptor_pool = unsafe {
device.create_descriptor_pool(
&vk::DescriptorPoolCreateInfo::default()
.pool_sizes(&pool_sizes)
.max_sets(1),
None,
)
}
.map_err(|e| format!("descriptor_pool: {e:?}"))?;
let descriptor_set = unsafe {
device.allocate_descriptor_sets(
&vk::DescriptorSetAllocateInfo::default()
.descriptor_pool(descriptor_pool)
.set_layouts(std::slice::from_ref(&descriptor_set_layout)),
)
}
.map_err(|e| format!("descriptor_set: {e:?}"))?[0];
let grid_info = vk::DescriptorBufferInfo::default()
.buffer(grid_buffer)
.offset(0)
.range((WORLD_W * WORLD_H * std::mem::size_of::<u32>()) as vk::DeviceSize);
let light_info = vk::DescriptorBufferInfo::default()
.buffer(light_buffer)
.offset(0)
.range((MAX_LIGHT_SOURCES * std::mem::size_of::<GpuLightSource>()) as vk::DeviceSize);
let descriptor_writes = [
vk::WriteDescriptorSet::default()
.dst_set(descriptor_set)
.dst_binding(0)
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
.buffer_info(std::slice::from_ref(&grid_info)),
vk::WriteDescriptorSet::default()
.dst_set(descriptor_set)
.dst_binding(1)
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
.buffer_info(std::slice::from_ref(&light_info)),
];
unsafe {
device.update_descriptor_sets(&descriptor_writes, &[]);
}
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 (vertex_buffer, vertex_memory) = make_buf(
bytemuck::cast_slice(&verts),
vk::BufferUsageFlags::VERTEX_BUFFER,
)?;
let (index_buffer, index_memory) = make_buf(
bytemuck::cast_slice(&indices),
vk::BufferUsageFlags::INDEX_BUFFER,
)?;
// Instance buffer
let instance_count = grid_w * grid_h;
let inst_sz = (instance_count * std::mem::size_of::<ColorInstance>()) as vk::DeviceSize;
let ibi = vk::BufferCreateInfo::default()
.size(inst_sz)
.usage(vk::BufferUsageFlags::VERTEX_BUFFER)
.sharing_mode(vk::SharingMode::EXCLUSIVE);
let instance_buffer =
unsafe { device.create_buffer(&ibi, None) }.map_err(|e| format!("inst buf: {e:?}"))?;
let ireq = unsafe { device.get_buffer_memory_requirements(instance_buffer) };
let imt = find_mem(
ireq.memory_type_bits,
vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT,
)?;
let instance_memory = unsafe {
device.allocate_memory(
&vk::MemoryAllocateInfo::default()
.allocation_size(ireq.size)
.memory_type_index(imt),
None,
)
}
.map_err(|e| format!("inst mem: {e:?}"))?;
let instance_ptr = unsafe {
device
.bind_buffer_memory(instance_buffer, instance_memory, 0)
.expect("bind inst");
let ptr = device
.map_memory(instance_memory, 0, inst_sz, vk::MemoryMapFlags::default())
.expect("map inst");
ptr as *mut ColorInstance
};
let ui_capacity = 65536usize;
let ui_inst_sz = (ui_capacity * std::mem::size_of::<ColorInstance>()) as vk::DeviceSize;
let uibi = vk::BufferCreateInfo::default()
.size(ui_inst_sz)
.usage(vk::BufferUsageFlags::VERTEX_BUFFER)
.sharing_mode(vk::SharingMode::EXCLUSIVE);
let ui_instance_buffer = unsafe { device.create_buffer(&uibi, None) }
.map_err(|e| format!("ui inst buf: {e:?}"))?;
let uireq = unsafe { device.get_buffer_memory_requirements(ui_instance_buffer) };
let uimt = find_mem(
uireq.memory_type_bits,
vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT,
)?;
let ui_instance_memory = unsafe {
device.allocate_memory(
&vk::MemoryAllocateInfo::default()
.allocation_size(uireq.size)
.memory_type_index(uimt),
None,
)
}
.map_err(|e| format!("ui inst mem: {e:?}"))?;
let ui_instance_ptr = unsafe {
device
.bind_buffer_memory(ui_instance_buffer, ui_instance_memory, 0)
.expect("bind ui inst");
let ptr = device
.map_memory(
ui_instance_memory,
0,
ui_inst_sz,
vk::MemoryMapFlags::default(),
)
.expect("map ui inst");
ptr as *mut ColorInstance
};
Ok(Self {
grid_w,
grid_h,
entry,
instance,
surface,
physical_device,
device,
graphics_queue,
swapchain_loader,
swapchain,
swapchain_image_views,
swapchain_extent: extent,
present_mode,
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,
instance_buffer,
instance_memory,
instance_ptr,
instance_count,
ui_instance_buffer,
ui_instance_memory,
ui_instance_ptr,
ui_instance_capacity: ui_capacity,
grid_buffer,
grid_memory,
grid_ptr,
light_buffer,
light_memory,
light_ptr,
ent_pri_buf: Vec::new(),
entity_color_buf: Vec::new(),
item_color_buf: Vec::new(),
shadow_buf: Vec::new(),
descriptor_set_layout,
descriptor_pool,
descriptor_set,
window,
})
}
pub fn render(
&mut self,
grid: &Grid,
entities: &EntityManager,
items: &crate::entity::item::ItemManager,
ui: &crate::ui::UiLayer,
cam_x: i32,
cam_y: i32,
_lighting: Option<&lighting::LightGrid>,
) {
self.check_resize();
let amb_u8 = lighting::ambient_light();
let ambient = [
amb_u8[0] as f32 / 255.0,
amb_u8[1] as f32 / 255.0,
amb_u8[2] as f32 / 255.0,
];
let vp_size = self.grid_w * self.grid_h;
if self.ent_pri_buf.len() != vp_size {
self.ent_pri_buf.resize(vp_size, 0);
self.entity_color_buf.resize(vp_size, [0, 0, 0, 0]);
self.item_color_buf.resize(vp_size, [0, 0, 0, 0]);
self.shadow_buf.resize(vp_size, false);
}
self.ent_pri_buf.fill(0);
self.entity_color_buf.fill([0, 0, 0, 0]);
self.item_color_buf.fill([0, 0, 0, 0]);
self.shadow_buf.fill(false);
let ent_pri = &mut self.ent_pri_buf;
let entity_color = &mut self.entity_color_buf;
let item_color = &mut self.item_color_buf;
for item in items.all() {
let sx = item.x - cam_x;
let sy = item.y - cam_y;
for (dx, dy, col) in item.shape() {
let px = sx + dx;
let py = sy + dy;
if px >= 0 && px < self.grid_w as i32 && py >= 0 && py < self.grid_h as i32 {
let idx = py as usize * self.grid_w + px as usize;
item_color[idx] = [col[0], col[1], col[2], 255];
}
}
}
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 idx = sy as usize * self.grid_w + sx as usize;
let color = if e.on_fire {
let flicker = b.fire_timer % 4;
[255, 120 + flicker as u8 * 20, 20 + flicker as u8 * 10, 255]
} else {
b.color
};
let pri = entity_priority(e.kind);
if pri as u8 > ent_pri[idx] {
ent_pri[idx] = pri as u8;
entity_color[idx] = color;
}
}
}
}
let shadow_buf = &mut self.shadow_buf;
for idx in 0..vp_size {
if ent_pri[idx] == 0 {
continue;
}
let ex = idx % self.grid_w;
let ey = idx / self.grid_w;
for dy in -1i32..=1 {
for dx in -1i32..=1 {
if dx == 0 && dy == 0 {
continue;
}
let sx = ex as i32 + dx;
let sy = ey as i32 + dy;
if sx < 0 || sx >= self.grid_w as i32 || sy < 0 || sy >= self.grid_h as i32 {
continue;
}
let sidx = sy as usize * self.grid_w + sx as usize;
if ent_pri[sidx] > 0 {
continue;
}
let wx = cam_x + sx;
let wy = cam_y + sy;
if !grid.in_bounds(wx, wy) || grid.get(wx, wy).is_empty() {
shadow_buf[sidx] = true;
}
}
}
}
let instances =
unsafe { std::slice::from_raw_parts_mut(self.instance_ptr, self.instance_count) };
let gh = self.grid_h as i32;
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 color = if ent_pri[idx] > 0 {
entity_color[idx]
} else if item_color[idx][3] > 0 {
item_color[idx]
} else if shadow_buf[idx] {
[0, 0, 0, 255]
} else if !grid.in_bounds(wx, wy) {
[40, 40, 40, 255]
} else {
let cell = grid.get(wx, wy);
if cell.is_empty() {
background_color(wx, wy, dy as i32, gh)
} else if cell.material == MaterialId::Lava {
let r = 200u8.saturating_add(cell.variant / 2);
[r, 60, 20, 255]
} else {
[cell.fg[0], cell.fg[1], cell.fg[2], 255]
}
};
instances[idx] = ColorInstance {
grid_x: dx as f32,
grid_y: dy as f32,
color,
};
}
}
let ui_instances = unsafe {
std::slice::from_raw_parts_mut(self.ui_instance_ptr, self.ui_instance_capacity)
};
let mut ui_count = 0usize;
for (x, y) in ui.keys() {
if ui_count >= self.ui_instance_capacity {
break;
}
ui_instances[ui_count] = ColorInstance {
grid_x: *x as f32,
grid_y: *y as f32,
color: {
let cell = ui.get(*x, *y).unwrap();
[cell.fg[0], cell.fg[1], cell.fg[2], cell.alpha]
},
};
ui_count += 1;
}
unsafe {
let margin = 30i32;
let x_min = (cam_x - margin).max(0) as usize;
let x_max = (cam_x + self.grid_w as i32 + margin).min(WORLD_W as i32) as usize;
let y_min = (cam_y - margin).max(0) as usize;
let y_max = (cam_y + self.grid_h as i32 + margin).min(WORLD_H as i32) as usize;
for y in y_min..y_max {
let row_offset = y * WORLD_W;
for x in x_min..x_max {
let i = row_offset + x;
*self.grid_ptr.add(i) = grid.cells[i].material as u32;
}
}
}
let sources =
lighting::gather_sources_in_range(grid, cam_x, cam_y, self.grid_w, self.grid_h, 30);
let light_count = sources.len().min(MAX_LIGHT_SOURCES) as u32;
unsafe {
let light_slice = std::slice::from_raw_parts_mut(self.light_ptr, MAX_LIGHT_SOURCES);
for (i, src) in sources.iter().take(MAX_LIGHT_SOURCES).enumerate() {
light_slice[i] = GpuLightSource {
pos: [src.x as f32, src.y as f32],
radius: src.radius as f32,
_pad0: 0.0,
color: [
src.color[0] as f32 / 255.0,
src.color[1] as f32 / 255.0,
src.color[2] as f32 / 255.0,
],
_pad1: 0.0,
};
}
}
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);
let viewport = vk::Viewport {
x: 0.0,
y: 0.0,
width: self.swapchain_extent.width as f32,
height: self.swapchain_extent.height as f32,
min_depth: 0.0,
max_depth: 1.0,
};
let scissor = vk::Rect2D {
offset: vk::Offset2D::default(),
extent: self.swapchain_extent,
};
device.cmd_set_viewport(cmd, 0, std::slice::from_ref(&viewport));
device.cmd_set_scissor(cmd, 0, std::slice::from_ref(&scissor));
device.cmd_bind_descriptor_sets(
cmd,
vk::PipelineBindPoint::GRAPHICS,
self.pipeline_layout,
0,
&[self.descriptor_set],
&[],
);
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);
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],
world_size: [WORLD_W as i32, WORLD_H as i32],
cam_pos: [cam_x, cam_y],
ambient,
is_ui: 0,
light_count,
};
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);
if ui_count > 0 {
device.cmd_bind_vertex_buffers(
cmd,
0,
&[self.vertex_buffer, self.ui_instance_buffer],
&[0, 0],
);
let ui_pc = PushConstants {
screen_size: [
self.swapchain_extent.width as f32,
self.swapchain_extent.height as f32,
],
cell_size: [UI_CELL_SIZE as f32, UI_CELL_SIZE as f32],
world_size: [WORLD_W as i32, WORLD_H as i32],
cam_pos: [0, 0],
ambient: [1.0, 1.0, 1.0],
is_ui: 1,
light_count: 0,
};
device.cmd_push_constants(
cmd,
self.pipeline_layout,
vk::ShaderStageFlags::VERTEX,
0,
bytemuck::bytes_of(&ui_pc),
);
device.cmd_draw_indexed(cmd, 6, ui_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
}
fn check_resize(&mut self) {
let sl = ash::khr::surface::Instance::new(&self.entry, &self.instance);
let caps = match unsafe {
sl.get_physical_device_surface_capabilities(self.physical_device, self.surface)
} {
Ok(c) => c,
Err(_) => return,
};
let new_extent = if caps.current_extent.width != u32::MAX {
caps.current_extent
} else {
// Wayland: surface extent is undefined, use window inner size
let inner = self.window.inner_size();
vk::Extent2D {
width: inner.width.max(1),
height: inner.height.max(1),
}
};
if new_extent.width == self.swapchain_extent.width
&& new_extent.height == self.swapchain_extent.height
{
return;
}
if new_extent.width == 0 || new_extent.height == 0 {
return;
}
unsafe {
let _ = self.device.device_wait_idle();
}
// Destroy old swapchain resources
for &fb in &self.framebuffers {
unsafe {
self.device.destroy_framebuffer(fb, None);
}
}
for &v in &self.swapchain_image_views {
unsafe {
self.device.destroy_image_view(v, None);
}
}
// Recreate swapchain
let sci = vk::SwapchainCreateInfoKHR::default()
.surface(self.surface)
.min_image_count(caps.min_image_count.max(2))
.image_format(vk::Format::B8G8R8A8_UNORM)
.image_color_space(vk::ColorSpaceKHR::SRGB_NONLINEAR)
.image_extent(new_extent)
.image_array_layers(1)
.image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT)
.image_sharing_mode(vk::SharingMode::EXCLUSIVE)
.pre_transform(caps.current_transform)
.composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE)
.present_mode(self.present_mode)
.clipped(true)
.old_swapchain(self.swapchain);
let new_swapchain = match unsafe { self.swapchain_loader.create_swapchain(&sci, None) } {
Ok(s) => s,
Err(_) => return,
};
let new_images = match unsafe { self.swapchain_loader.get_swapchain_images(new_swapchain) }
{
Ok(i) => i,
Err(_) => return,
};
let new_views: Vec<_> = new_images
.iter()
.map(|&img| {
let vi = vk::ImageViewCreateInfo::default()
.image(img)
.view_type(vk::ImageViewType::TYPE_2D)
.format(vk::Format::B8G8R8A8_UNORM)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
});
unsafe {
self.device
.create_image_view(&vi, None)
.expect("image_view")
}
})
.collect();
let new_framebuffers: Vec<_> = new_views
.iter()
.map(|&view| {
let fci = vk::FramebufferCreateInfo::default()
.render_pass(self.render_pass)
.attachments(std::slice::from_ref(&view))
.width(new_extent.width)
.height(new_extent.height)
.layers(1);
unsafe { self.device.create_framebuffer(&fci, None).expect("fb") }
})
.collect();
// Reallocate command buffers for new count
unsafe {
self.device
.free_command_buffers(self.command_pool, &self.command_buffers);
}
let cai = vk::CommandBufferAllocateInfo::default()
.command_pool(self.command_pool)
.level(vk::CommandBufferLevel::PRIMARY)
.command_buffer_count(new_framebuffers.len() as u32);
let new_cmd_bufs = unsafe {
self.device
.allocate_command_buffers(&cai)
.expect("cmd_bufs")
};
// Reallocate instance buffer if grid size changed
let new_grid_w = (new_extent.width / CHAR_W) as usize;
let new_grid_h = (new_extent.height / CHAR_H) as usize;
let new_count = new_grid_w * new_grid_h;
if new_count != self.instance_count {
unsafe {
self.device.unmap_memory(self.instance_memory);
self.device.destroy_buffer(self.instance_buffer, None);
self.device.free_memory(self.instance_memory, None);
}
let inst_sz = (new_count * std::mem::size_of::<ColorInstance>()) as vk::DeviceSize;
let ibi = vk::BufferCreateInfo::default()
.size(inst_sz)
.usage(vk::BufferUsageFlags::VERTEX_BUFFER)
.sharing_mode(vk::SharingMode::EXCLUSIVE);
self.instance_buffer =
unsafe { self.device.create_buffer(&ibi, None) }.expect("inst buf");
let ireq = unsafe {
self.device
.get_buffer_memory_requirements(self.instance_buffer)
};
let find_mem = |filter: u32, props: vk::MemoryPropertyFlags| -> u32 {
let mp = unsafe {
self.instance
.get_physical_device_memory_properties(self.physical_device)
};
for (i, mt) in mp.memory_types.iter().enumerate() {
if (filter & (1 << i)) != 0 && mt.property_flags.contains(props) {
return i as u32;
}
}
0
};
let imt = find_mem(
ireq.memory_type_bits,
vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT,
);
self.instance_memory = unsafe {
self.device.allocate_memory(
&vk::MemoryAllocateInfo::default()
.allocation_size(ireq.size)
.memory_type_index(imt),
None,
)
}
.expect("inst mem");
self.instance_ptr = unsafe {
self.device
.bind_buffer_memory(self.instance_buffer, self.instance_memory, 0)
.expect("bind");
let ptr = self
.device
.map_memory(
self.instance_memory,
0,
inst_sz,
vk::MemoryMapFlags::default(),
)
.expect("map");
ptr as *mut ColorInstance
};
self.instance_count = new_count;
}
// Destroy old swapchain
unsafe {
self.swapchain_loader
.destroy_swapchain(self.swapchain, None);
}
self.swapchain = new_swapchain;
self.swapchain_image_views = new_views;
self.framebuffers = new_framebuffers;
self.command_buffers = new_cmd_bufs;
self.swapchain_extent = new_extent;
self.grid_w = new_grid_w;
self.grid_h = new_grid_h;
}
}
impl Drop for GraphicsRenderer {
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_buffer(self.instance_buffer, None);
self.device.free_memory(self.instance_memory, None);
self.device.destroy_buffer(self.ui_instance_buffer, None);
self.device.free_memory(self.ui_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_buffer(self.grid_buffer, None);
self.device.free_memory(self.grid_memory, None);
self.device.destroy_buffer(self.light_buffer, None);
self.device.free_memory(self.light_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);
}
}
}