feat: adaptive viewport — window resize expands camera, no stretching

Both renderers (ascii + graphics) now support dynamic window resize:

- check_resize() called at start of every render() frame
- Compares surface capabilities with current swapchain extent
- If changed: recreates swapchain, image views, framebuffers, command buffers
- Recalculates grid_w/grid_h from new extent / cell_size (16x16)
- Reallocates instance buffer if grid cell count changed
- Old swapchain properly destroyed after new one created
- Handles window minimize (skip if extent = 0)
- physical_device field added back to both renderers (needed for surface caps query)

main.rs: vw/vh queried every frame from renderer.grid_w()/grid_h()
instead of cached once at startup. Camera center uses dynamic dimensions.

Result: resize window → more/fewer cells visible, cells stay 16x16 pixels.
109 tests, 0 failures
This commit is contained in:
Emil
2026-06-21 01:13:34 +03:00
parent 3235957a26
commit 57a88fd41e
3 changed files with 277 additions and 6 deletions
+6 -4
View File
@@ -236,8 +236,6 @@ fn run_ascii_mode() {
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();
@@ -266,6 +264,9 @@ fn run_ascii_mode() {
return;
}
let vw = renderer.grid_w();
let vh = renderer.grid_h();
let now = Instant::now();
let frame_time = now.duration_since(last_time);
last_time = now;
@@ -378,8 +379,6 @@ fn run_graphics_mode() {
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();
@@ -408,6 +407,9 @@ fn run_graphics_mode() {
return;
}
let vw = renderer.grid_w();
let vh = renderer.grid_h();
let now = Instant::now();
let frame_time = now.duration_since(last_time);
last_time = now;
+139 -1
View File
@@ -33,6 +33,7 @@ pub struct GraphicsRenderer {
entry: ash::Entry,
instance: ash::Instance,
surface: vk::SurfaceKHR,
physical_device: vk::PhysicalDevice,
device: ash::Device,
graphics_queue: vk::Queue,
@@ -292,7 +293,7 @@ impl GraphicsRenderer {
Ok(Self {
grid_w, grid_h,
entry, instance, surface, device, graphics_queue,
entry, instance, surface, physical_device, device, graphics_queue,
swapchain_loader, swapchain, swapchain_image_views, swapchain_extent: extent,
render_pass, pipeline, pipeline_layout, framebuffers,
command_pool, command_buffers,
@@ -304,6 +305,7 @@ impl GraphicsRenderer {
}
pub fn render(&mut self, grid: &Grid, entities: &EntityManager, cam_x: i32, cam_y: i32) {
self.check_resize();
let reg = MaterialRegistry::instance();
let mut entity_map: std::collections::HashMap<(i32, i32), [u8; 4]> = std::collections::HashMap::new();
@@ -438,6 +440,142 @@ impl GraphicsRenderer {
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 {
return;
};
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 qf_slice = [0u32]; // placeholder, not used with EXCLUSIVE
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(vk::PresentModeKHR::FIFO)
.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 {
+132 -1
View File
@@ -43,6 +43,7 @@ pub struct VulkanRenderer {
entry: ash::Entry,
instance: ash::Instance,
surface: vk::SurfaceKHR,
physical_device: vk::PhysicalDevice,
device: ash::Device,
graphics_queue: vk::Queue,
@@ -144,7 +145,7 @@ impl VulkanRenderer {
Ok(Self {
grid_w, grid_h,
entry, instance, surface, device,
entry, instance, surface, physical_device, device,
graphics_queue,
swapchain_loader, swapchain, swapchain_image_views,
swapchain_extent,
@@ -160,6 +161,7 @@ impl VulkanRenderer {
}
pub fn render(&mut self, grid: &Grid, entities: &EntityManager, cam_x: i32, cam_y: i32) {
self.check_resize();
let reg = MaterialRegistry::instance();
let mut entity_map: std::collections::HashMap<(i32, i32), (char, [u8; 4])> = std::collections::HashMap::new();
@@ -308,6 +310,135 @@ impl VulkanRenderer {
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 {
return;
};
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(); }
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); } }
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(vk::PresentModeKHR::FIFO)
.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();
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") };
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::<CellInstance>()) 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 CellInstance
};
self.instance_count = new_count;
}
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 VulkanRenderer {