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:
+6
-4
@@ -236,8 +236,6 @@ fn run_ascii_mode() {
|
|||||||
game.init_world();
|
game.init_world();
|
||||||
|
|
||||||
let mut input = WindowInput::new();
|
let mut input = WindowInput::new();
|
||||||
let vw = renderer.grid_w();
|
|
||||||
let vh = renderer.grid_h();
|
|
||||||
|
|
||||||
let fixed_dt = Duration::from_millis(16);
|
let fixed_dt = Duration::from_millis(16);
|
||||||
let mut last_time = Instant::now();
|
let mut last_time = Instant::now();
|
||||||
@@ -266,6 +264,9 @@ fn run_ascii_mode() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let vw = renderer.grid_w();
|
||||||
|
let vh = renderer.grid_h();
|
||||||
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let frame_time = now.duration_since(last_time);
|
let frame_time = now.duration_since(last_time);
|
||||||
last_time = now;
|
last_time = now;
|
||||||
@@ -378,8 +379,6 @@ fn run_graphics_mode() {
|
|||||||
game.init_world();
|
game.init_world();
|
||||||
|
|
||||||
let mut input = WindowInput::new();
|
let mut input = WindowInput::new();
|
||||||
let vw = renderer.grid_w();
|
|
||||||
let vh = renderer.grid_h();
|
|
||||||
|
|
||||||
let fixed_dt = Duration::from_millis(16);
|
let fixed_dt = Duration::from_millis(16);
|
||||||
let mut last_time = Instant::now();
|
let mut last_time = Instant::now();
|
||||||
@@ -408,6 +407,9 @@ fn run_graphics_mode() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let vw = renderer.grid_w();
|
||||||
|
let vh = renderer.grid_h();
|
||||||
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let frame_time = now.duration_since(last_time);
|
let frame_time = now.duration_since(last_time);
|
||||||
last_time = now;
|
last_time = now;
|
||||||
|
|||||||
+139
-1
@@ -33,6 +33,7 @@ pub struct GraphicsRenderer {
|
|||||||
entry: ash::Entry,
|
entry: ash::Entry,
|
||||||
instance: ash::Instance,
|
instance: ash::Instance,
|
||||||
surface: vk::SurfaceKHR,
|
surface: vk::SurfaceKHR,
|
||||||
|
physical_device: vk::PhysicalDevice,
|
||||||
device: ash::Device,
|
device: ash::Device,
|
||||||
graphics_queue: vk::Queue,
|
graphics_queue: vk::Queue,
|
||||||
|
|
||||||
@@ -292,7 +293,7 @@ impl GraphicsRenderer {
|
|||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
grid_w, grid_h,
|
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,
|
swapchain_loader, swapchain, swapchain_image_views, swapchain_extent: extent,
|
||||||
render_pass, pipeline, pipeline_layout, framebuffers,
|
render_pass, pipeline, pipeline_layout, framebuffers,
|
||||||
command_pool, command_buffers,
|
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) {
|
pub fn render(&mut self, grid: &Grid, entities: &EntityManager, cam_x: i32, cam_y: i32) {
|
||||||
|
self.check_resize();
|
||||||
let reg = MaterialRegistry::instance();
|
let reg = MaterialRegistry::instance();
|
||||||
|
|
||||||
let mut entity_map: std::collections::HashMap<(i32, i32), [u8; 4]> = std::collections::HashMap::new();
|
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_w(&self) -> usize { self.grid_w }
|
||||||
pub fn grid_h(&self) -> usize { self.grid_h }
|
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 {
|
impl Drop for GraphicsRenderer {
|
||||||
|
|||||||
+132
-1
@@ -43,6 +43,7 @@ pub struct VulkanRenderer {
|
|||||||
entry: ash::Entry,
|
entry: ash::Entry,
|
||||||
instance: ash::Instance,
|
instance: ash::Instance,
|
||||||
surface: vk::SurfaceKHR,
|
surface: vk::SurfaceKHR,
|
||||||
|
physical_device: vk::PhysicalDevice,
|
||||||
device: ash::Device,
|
device: ash::Device,
|
||||||
graphics_queue: vk::Queue,
|
graphics_queue: vk::Queue,
|
||||||
|
|
||||||
@@ -144,7 +145,7 @@ impl VulkanRenderer {
|
|||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
grid_w, grid_h,
|
grid_w, grid_h,
|
||||||
entry, instance, surface, device,
|
entry, instance, surface, physical_device, device,
|
||||||
graphics_queue,
|
graphics_queue,
|
||||||
swapchain_loader, swapchain, swapchain_image_views,
|
swapchain_loader, swapchain, swapchain_image_views,
|
||||||
swapchain_extent,
|
swapchain_extent,
|
||||||
@@ -160,6 +161,7 @@ impl VulkanRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&mut self, grid: &Grid, entities: &EntityManager, cam_x: i32, cam_y: i32) {
|
pub fn render(&mut self, grid: &Grid, entities: &EntityManager, cam_x: i32, cam_y: i32) {
|
||||||
|
self.check_resize();
|
||||||
let reg = MaterialRegistry::instance();
|
let reg = MaterialRegistry::instance();
|
||||||
|
|
||||||
let mut entity_map: std::collections::HashMap<(i32, i32), (char, [u8; 4])> = std::collections::HashMap::new();
|
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_w(&self) -> usize { self.grid_w }
|
||||||
pub fn grid_h(&self) -> usize { self.grid_h }
|
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 {
|
impl Drop for VulkanRenderer {
|
||||||
|
|||||||
Reference in New Issue
Block a user