// vim:fileencoding=utf-8:foldmethod=marker
//
// Prism texture viewer — draws a texture fullscreen with contain-fit
// (letterbox/pillarbox to preserve aspect ratio, never crops or stretches).
// Texture selection cycles with +/-.

#include "prism/rhi/pr_rhi_types.h"
#include "prism/rhi/pr_rhi.h"
#include <SDL3/SDL_timer.h>
#include <SDL3/SDL.h>
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_init.h>
#include <SDL3/SDL_keycode.h>
#include <SDL3/SDL_video.h>
#include <iostream>

// ============================================================================
// Exit codes
// ============================================================================

enum ExitCode {
	EXIT_CODE_SUCCESS,
	EXIT_CODE_SDL_INIT_FAILED,
	EXIT_CODE_WINDOW_CREATION_FAILED,
	EXIT_CODE_GET_WINDOW_SIZE_FAILED,
	EXIT_CODE_NO_PHYSICAL_DEVICES,
	EXIT_CODE_NO_SUITABLE_PHYSICAL_DEVICE,
	EXIT_CODE_ALLOCATION_FAILURE,
	EXIT_CODE_SHADER_LOAD_FAILED,
};

static inline void check(bool result, i32 code) {
	if (!result) {
		std::cerr << "Call returned an error\n";
		exit(code);
	}
}

// ============================================================================
// Types
// ============================================================================

struct TextureResources {
	PrRhiTexture *texture;
	PrRhiSampler *sampler;
};

// Push constant block — matches BlitData in assets/blit.slang (16-byte aligned,
// so the struct is padded to 32 bytes).
struct BlitData {
	f32 rect[4];   // NDC fit rect: x0, y0, x1, y1
	u32 selected;
	u32 mode;      // 0 = sample texture, 1 = solid background
	u32 pad[2];
};

// Typedefs for wapp arrays of our types
typedef TextureResources       *TextureResourcesArray;
typedef PrRhiSemaphore        **PrRhiSemaphoreArray;
typedef PrRhiFence            **PrRhiFenceArray;
typedef PrRhiCommandBuffer    **PrRhiCommandBufferArray;

// ============================================================================
// Global state
// ============================================================================

static const char *const TEXTURE_PATHS[] = {
	"assets/suzanne0.ktx",
	"assets/suzanne1.ktx",
	"assets/suzanne2.ktx",
	"assets/test_square.ktx",
	"assets/test_fill.ktx",
	"assets/test_wide.ktx",
	"assets/test_tall.ktx",
};

struct AppState {
	PrRhiInstance       *inst;
	PrRhiPhysicalDevice *pdev;
	PrRhiDevice         *device;
	PrRhiSurface        *surface;
	PrRhiSwapchain      *swapchain;

	PrRhiFormat swapchain_format;

	static constexpr u32 max_frames_in_flight = 2;
	static constexpr u32 texture_count        = (u32)(sizeof(TEXTURE_PATHS) / sizeof(TEXTURE_PATHS[0]));

	PrRhiFenceArray          fences;
	PrRhiSemaphoreArray      image_acquired_semaphores;
	PrRhiSemaphoreArray      render_completed_semaphores;
	u32                      render_semaphore_count;
	PrRhiCommandPool        *cmd_pool;
	PrRhiCommandBufferArray  cmd_buffers;

	TextureResourcesArray    textures;
	PrRhiDescriptorSetLayout *desc_set_layout;
	PrRhiDescriptorPool      *desc_pool;
	PrRhiDescriptorSet       *desc_set;

	PrRhiShader             *shader;
	PrRhiPipelineLayout     *pipeline_layout;
	PrRhiPipeline           *pipeline;

	u32                      selected;
	u32                      frame_index;
	i32                      window_width;
	i32                      window_height;
	bool                     update_swapchain;

	SDL_Window              *window;
};

// ============================================================================
// Helpers
// ============================================================================

// Fills rect with the NDC bounds (x0, y0, x1, y1) of the texture content when
// contained within the window, preserving aspect ratio and centering.
static void computeFitRect(f32 tex_w, f32 tex_h, f32 win_w, f32 win_h, f32 *rect) {
	if (win_w <= 0.0f || win_h <= 0.0f || tex_w <= 0.0f || tex_h <= 0.0f) {
		rect[0] = -1.0f;
		rect[1] = -1.0f;
		rect[2] =  1.0f;
		rect[3] =  1.0f;
		return;
	}

	f32 scale_x = win_w / tex_w;
	f32 scale_y = win_h / tex_h;
	f32 scale   = scale_x < scale_y ? scale_x : scale_y;

	f32 content_w = tex_w * scale;
	f32 content_h = tex_h * scale;

	rect[0] = -content_w / win_w;
	rect[1] = -content_h / win_h;
	rect[2] =  content_w / win_w;
	rect[3] =  content_h / win_h;
}

// ============================================================================
// Main
// ============================================================================

int main() {
	AppState app = {};
	WpAllocator arena = wpMemArenaAllocatorInitZero(MiB(128));

	// {{{ Initialisation
	prRhiInit();

	check(SDL_Init(SDL_INIT_VIDEO), EXIT_CODE_SDL_INIT_FAILED);

	f32 display_scale = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay());
	app.window        = SDL_CreateWindow("Prism — Texture Viewer", (i32)(display_scale * 1920),
	                                     (i32)(display_scale * 1080),
	                                     SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE);
	check(app.window != nullptr, EXIT_CODE_WINDOW_CREATION_FAILED);
	// }}}

	// {{{ Instance creation
	app.inst = prRhiCreateInstance(PrRhiInstanceDesc{});
	// }}}

	// {{{ Physical device selection
	PrRhiPhysicalDeviceArray pdevs = prRhiGetPhysicalDevices(app.inst);
	check(wpArrayCount(pdevs) > 0, EXIT_CODE_NO_PHYSICAL_DEVICES);

	i32 selected = -1;
	for (u32 i = 0; i < wpArrayCount(pdevs); ++i) {
		PrRhiPhysicalDeviceProperties props = prRhiGetPhysicalDeviceProperties(pdevs[i]);
		switch (props.device_type) {
			case PR_RHI_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU:
				selected = (i32)i;
				break;
			case PR_RHI_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU:
				if (selected == -1)
					selected = (i32)i;
				break;
			default: continue;
		}
	}
	check(selected != -1, EXIT_CODE_NO_SUITABLE_PHYSICAL_DEVICE);
	app.pdev = pdevs[selected];

	// Print device info
	WpStr8 dev_name    = wpStr8Buf(512);
	WpStr8 driver_info = wpStr8Buf(512);
	prRhiGetPhysicalDeviceName(app.pdev, &dev_name);
	prRhiGetPhysicalDeviceDriverInfo(app.pdev, &driver_info);
	std::cout << "Selected GPU:   " << std::string_view((const char *)dev_name.buf, dev_name.size) << '\n'
	          << "Driver version: " << std::string_view((const char *)driver_info.buf, driver_info.size) << '\n';
	// }}}

	// {{{ Surface creation
	check(SDL_GetWindowSizeInPixels(app.window, &app.window_width, &app.window_height),
	      EXIT_CODE_GET_WINDOW_SIZE_FAILED);
	app.surface = prRhiCreateSurfaceFromWindow(app.inst, app.window);
	// }}}

	// {{{ Device creation
	PrRhiDeviceDesc dev_desc = {};
	dev_desc.present_mode    = PR_RHI_PRESENT_MODE_FIFO;

	app.device = prRhiCreateDevice(app.pdev, app.surface, dev_desc);
	// }}}

	// {{{ Swapchain creation
	PrRhiSwapchainDesc swap_desc = {};
	swap_desc.surface            = app.surface;
	swap_desc.width              = (u32)app.window_width;
	swap_desc.height             = (u32)app.window_height;
	swap_desc.has_depth          = true;
	swap_desc.depth_format       = PR_RHI_FORMAT_D24_UNORM_S8_UINT;

	app.swapchain        = prRhiCreateSwapchain(app.device, swap_desc);
	app.swapchain_format = prRhiGetSwapchainFormat(app.swapchain);
	// }}}

	// {{{ Synchronisation objects
	app.fences = wpArrayAllocCapacity(PrRhiFence *, &arena, AppState::max_frames_in_flight,
	                                  WP_ARRAY_INIT_FILLED);
	for (u32 i = 0; i < AppState::max_frames_in_flight; ++i) {
		PrRhiFenceDesc fd = {};
		fd.signaled = true;
		app.fences[i] = prRhiCreateFence(app.device, fd);
	}

	app.image_acquired_semaphores = wpArrayAllocCapacity(PrRhiSemaphore *, &arena,
	                                                     AppState::max_frames_in_flight,
	                                                     WP_ARRAY_INIT_FILLED);
	for (u32 i = 0; i < AppState::max_frames_in_flight; ++i) {
		app.image_acquired_semaphores[i] = prRhiCreateSemaphore(app.device);
	}

	app.render_semaphore_count   = prRhiGetSwapchainImageCount(app.swapchain);
	app.render_completed_semaphores = wpArrayAllocCapacity(PrRhiSemaphore *, &arena,
	                                                       app.render_semaphore_count,
	                                                       WP_ARRAY_INIT_FILLED);
	for (u32 i = 0; i < app.render_semaphore_count; ++i) {
		app.render_completed_semaphores[i] = prRhiCreateSemaphore(app.device);
	}
	// }}}

	// {{{ Command pool and buffers
	app.cmd_pool    = prRhiCreateCommandPool(app.device);
	app.cmd_buffers = prRhiAllocateCommandBuffers(app.device, app.cmd_pool,
	                                              AppState::max_frames_in_flight);
	// }}}

	// {{{ Texture loading
	app.textures = wpArrayAllocCapacity(TextureResources, &arena, AppState::texture_count,
	                                    WP_ARRAY_INIT_FILLED);

	PrRhiCommandBufferArray upload_cbs = prRhiAllocateCommandBuffers(app.device, app.cmd_pool, 1);
	PrRhiCommandBuffer *upload_cb      = upload_cbs[0];

	for (u32 i = 0; i < AppState::texture_count; ++i) {
		PrRhiTexture *tex = prRhiCreateTextureFromKtx(app.device, TEXTURE_PATHS[i], app.cmd_pool, upload_cb);

		PrRhiSamplerDesc samp_desc = {};
		samp_desc.mag_filter       = PR_RHI_FILTER_LINEAR;
		samp_desc.min_filter       = PR_RHI_FILTER_LINEAR;
		samp_desc.mipmap_mode      = PR_RHI_MIPMAP_MODE_LINEAR;
		samp_desc.max_anisotropy   = 8.0f;
		samp_desc.max_lod          = PR_RHI_LOD_CLAMP_NONE;

		PrRhiSampler *sampler = prRhiCreateSampler(app.device, samp_desc);

		app.textures[i].texture = tex;
		app.textures[i].sampler = sampler;
	}

	prRhiFreeCommandBuffers(app.device, app.cmd_pool, upload_cbs);
	// }}}

	// {{{ Descriptor set layout, pool, set
	PrRhiDescriptorSetLayoutBindingArray ds_layouts = wpArray(
		PrRhiDescriptorSetLayoutBinding,
		PrRhiDescriptorSetLayoutBinding{
			PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
			AppState::texture_count,
			PR_RHI_SHADER_STAGE_FRAGMENT,
			PR_RHI_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT,
		}
	);

	PrRhiDescriptorSetLayoutDesc layout_desc = { ds_layouts };
	app.desc_set_layout                      = prRhiCreateDescriptorSetLayout(app.device, layout_desc);

	PrRhiDescriptorPoolSize pool_size = {};
	pool_size.type                    = PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
	pool_size.descriptor_count        = AppState::texture_count;

	PrRhiDescriptorPoolDesc ds_pool_desc = {};
	ds_pool_desc.max_sets                = 1;
	ds_pool_desc.pool_sizes              = wpArray(PrRhiDescriptorPoolSize, pool_size);

	app.desc_pool = prRhiCreateDescriptorPool(app.device, ds_pool_desc);

	WpU32Array var_counts = wpArray(u32, AppState::texture_count);
	app.desc_set          = prRhiAllocateDescriptorSet(app.device, app.desc_pool, app.desc_set_layout,
	                                                   var_counts);

	PrRhiDescriptorImageInfoArray img_infos = wpArrayAllocCapacity(PrRhiDescriptorImageInfo,
	                                                               &arena,
	                                                               AppState::texture_count,
	                                                               WP_ARRAY_INIT_NONE);
	for (u32 i = 0; i < AppState::texture_count; ++i) {
		PrRhiDescriptorImageInfo info = {};
		info.texture                  = app.textures[i].texture;
		info.sampler                  = app.textures[i].sampler;
		info.layout                   = PR_RHI_LAYOUT_READ_ONLY_OPTIMAL;
		wpArrayAppendCapped(PrRhiDescriptorImageInfo, img_infos, &info);
	}

	PrRhiWriteDescriptorSet write = {};
	write.dst_set                 = app.desc_set;
	write.dst_binding             = 0;
	write.type                    = PR_RHI_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
	write.image_info              = img_infos;

	PrRhiWriteDescriptorSetArray writes = wpArray(PrRhiWriteDescriptorSet, write);
	prRhiUpdateDescriptorSet(app.device, writes);
	// }}}

	// {{{ Load blit shader (pre-compiled SPIR-V)
	WpStr8RO spirv_path = wpStr8LitRo("build/shaders/blit.spv");
	WpFile *spirv_file  = wpFileOpen(&arena, &spirv_path, WP_ACCESS_READ);
	check(spirv_file != nullptr, EXIT_CODE_SHADER_LOAD_FAILED);

	i64 spirv_len = wpFileGetLength(spirv_file);
	void *spirv   = wpMemAllocatorAlloc(&arena, (u64)spirv_len);
	check(spirv != nullptr, EXIT_CODE_ALLOCATION_FAILURE);
	wpFileRead(spirv, spirv_file, (u64)spirv_len);
	wpFileClose(spirv_file);

	PrRhiShaderDesc shader_desc = {};
	shader_desc.spirv_code      = spirv;
	shader_desc.spirv_size      = (u64)spirv_len;

	app.shader = prRhiCreateShader(app.device, shader_desc);
	// }}}

	// {{{ Pipeline layout
	PrRhiPushConstantRange pc_range = {};
	pc_range.stage_flags            = (PrRhiShaderStage)(PR_RHI_SHADER_STAGE_VERTEX |
	                                                     PR_RHI_SHADER_STAGE_FRAGMENT);
	pc_range.size                   = sizeof(BlitData);

	PrRhiDescriptorSetLayoutArray pl_layouts = wpArray(PrRhiDescriptorSetLayout *, app.desc_set_layout);

	PrRhiPipelineLayoutDesc pl_desc = {};
	pl_desc.set_layouts             = pl_layouts;
	pl_desc.push_constant_ranges    = wpArray(PrRhiPushConstantRange, pc_range);

	app.pipeline_layout = prRhiCreatePipelineLayout(app.device, pl_desc);
	// }}}

	// {{{ Graphics pipeline
	PrRhiColorBlendAttachmentArray blend_attachments = wpArray(
		PrRhiColorBlendAttachment,
		PrRhiColorBlendAttachment{ 0xf }
	);

	PrRhiFormatArray color_fmt_array = wpArray(PrRhiFormat, app.swapchain_format);

	PrRhiGraphicsPipelineDesc pipe_desc = {};
	pipe_desc.vertex_shader               = app.shader;
	pipe_desc.vertex_shader_entry_point   = "main";
	pipe_desc.fragment_shader             = app.shader;
	pipe_desc.fragment_shader_entry_point = "main";
	pipe_desc.topology                    = PR_RHI_TOPOLOGY_TRIANGLE_STRIP;
	pipe_desc.color_attachment_formats    = color_fmt_array;
	pipe_desc.depth_attachment_format     = swap_desc.depth_format;
	pipe_desc.depth_test_enable           = false;
	pipe_desc.depth_write_enable          = false;
	pipe_desc.blend_attachments           = blend_attachments;
	pipe_desc.dynamic_viewport            = true;
	pipe_desc.dynamic_scissor             = true;
	pipe_desc.cull_mode                   = PR_RHI_CULL_MODE_NONE;
	pipe_desc.line_width                  = 1.0f;
	pipe_desc.multisample_count           = PR_RHI_SAMPLE_COUNT_1;
	pipe_desc.layout                      = app.pipeline_layout;

	app.pipeline = prRhiCreateGraphicsPipeline(app.device, pipe_desc);
	// }}}

	// {{{ Render loop
	app.frame_index = 0;
	app.selected    = 0;
	u32 image_index = 0;
	PrRhiTextureSize tex_size = prRhiGetTextureSize(app.textures[app.selected].texture);
	std::cout << "Texture " << app.selected << ": " << TEXTURE_PATHS[app.selected]
	          << " (" << tex_size.width << 'x' << tex_size.height << ")\n";

	while (true) {
		// {{{ Wait on fence
		PrRhiFence *wait_fence = app.fences[app.frame_index];
		prRhiWaitForFences(app.device, wpArray(PrRhiFence *, wait_fence), 1, true, UINT64_MAX);
		prRhiResetFences(app.device, wpArray(PrRhiFence *, wait_fence), 1);
		// }}}

		// {{{ Acquire next image
		PrRhiSwapchainResult acq = prRhiAcquireNextImage(app.device, app.swapchain,
		                                                 app.image_acquired_semaphores[app.frame_index],
		                                                 &image_index);
		if (acq == PR_RHI_SWAPCHAIN_OUT_OF_DATE) {
			app.update_swapchain = true;
		}
		// }}}

		if (app.update_swapchain) {
			// Skip this frame — will recreate below
		} else {
			// {{{ Compute contain-fit rect
			PrRhiTextureSize tex_size = prRhiGetTextureSize(app.textures[app.selected].texture);
			f32 fit_rect[4] = {};
			computeFitRect((f32)tex_size.width, (f32)tex_size.height,
			               (f32)app.window_width, (f32)app.window_height, fit_rect);
			// }}}

			// {{{ Record command buffer
			PrRhiCommandBuffer *cb = app.cmd_buffers[app.frame_index];
			prRhiResetCommandBuffer(cb);
			prRhiBeginCommandBuffer(cb);

			// Transition images to attachment optimal
			{
				PrRhiImageMemoryBarrierArray barriers_arr =
					wpArrayWithCapacity(PrRhiImageMemoryBarrier, 2, WP_ARRAY_INIT_FILLED);

				PrRhiTexture *color_tex          = prRhiGetSwapchainTexture(app.swapchain, image_index);
				barriers_arr[0].texture          = color_tex;
				barriers_arr[0].old_layout       = PR_RHI_LAYOUT_UNDEFINED;
				barriers_arr[0].new_layout       = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL;
				barriers_arr[0].src_stage_mask   = PR_RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT;
				barriers_arr[0].src_access_mask  = PR_RHI_ACCESS_NONE;
				barriers_arr[0].dst_stage_mask   = PR_RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT;
				barriers_arr[0].dst_access_mask  = (PrRhiAccess)(PR_RHI_ACCESS_COLOR_ATTACHMENT_READ | PR_RHI_ACCESS_COLOR_ATTACHMENT_WRITE);

				PrRhiTexture *depth_tex          = prRhiGetSwapchainDepthTexture(app.swapchain);
				barriers_arr[1].texture          = depth_tex;
				barriers_arr[1].old_layout       = PR_RHI_LAYOUT_UNDEFINED;
				barriers_arr[1].new_layout       = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL;
				barriers_arr[1].src_stage_mask   = PR_RHI_PIPELINE_STAGE_LATE_FRAGMENT_TESTS;
				barriers_arr[1].src_access_mask  = PR_RHI_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE;
				barriers_arr[1].dst_stage_mask   = PR_RHI_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS;
				barriers_arr[1].dst_access_mask  = PR_RHI_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE;

				prRhiCmdPipelineBarrier(cb, barriers_arr, NULL);
			}

			// Rendering
			{
				PrRhiColorAttachmentArray color_arr =
					wpArrayWithCapacity(PrRhiColorAttachment, 1, WP_ARRAY_INIT_FILLED);
				color_arr[0].texture        = prRhiGetSwapchainTexture(app.swapchain, image_index);
				color_arr[0].layout         = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL;
				color_arr[0].clear          = true;
				color_arr[0].clear_color[0] = 0.0f;
				color_arr[0].clear_color[1] = 0.0f;
				color_arr[0].clear_color[2] = 0.0f;
				color_arr[0].clear_color[3] = 0.0f;

				PrRhiDepthAttachment depth_att = {};
				depth_att.texture              = prRhiGetSwapchainDepthTexture(app.swapchain);
				depth_att.layout               = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL;
				depth_att.clear                = true;
				depth_att.clear_depth          = 1.0f;

				prRhiCmdBeginRendering(cb, color_arr, &depth_att);
			}

			prRhiCmdSetViewport(cb, 0.0f, 0.0f, (f32)app.window_width, (f32)app.window_height);
			prRhiCmdSetScissor(cb, 0, 0, (u32)app.window_width, (u32)app.window_height);

			prRhiCmdBindPipeline(cb, PR_RHI_PIPELINE_BIND_POINT_GRAPHICS, app.pipeline);

			PrRhiDescriptorSetArray sets = wpArray(PrRhiDescriptorSet *, app.desc_set);
			prRhiCmdBindDescriptorSets(cb, PR_RHI_PIPELINE_BIND_POINT_GRAPHICS,
			                           app.pipeline_layout, 0, sets);

			// Draw background (fullscreen grey quad)
			BlitData bg = {};
			bg.rect[0] = -1.0f;
			bg.rect[1] = -1.0f;
			bg.rect[2] =  1.0f;
			bg.rect[3] =  1.0f;
			bg.selected = 0;
			bg.mode     = 1;
			prRhiCmdPushConstants(cb, app.pipeline_layout,
			                      (PrRhiShaderStage)(PR_RHI_SHADER_STAGE_VERTEX |
			                                         PR_RHI_SHADER_STAGE_FRAGMENT),
			                      0, sizeof(BlitData), &bg);
			prRhiCmdDraw(cb, 4, 1, 0, 0);

			// Draw texture (contain-fit)
			BlitData blit = {};
			blit.rect[0] = fit_rect[0];
			blit.rect[1] = fit_rect[1];
			blit.rect[2] = fit_rect[2];
			blit.rect[3] = fit_rect[3];
			blit.selected = app.selected;
			blit.mode     = 0;
			prRhiCmdPushConstants(cb, app.pipeline_layout,
			                      (PrRhiShaderStage)(PR_RHI_SHADER_STAGE_VERTEX |
			                                         PR_RHI_SHADER_STAGE_FRAGMENT),
			                      0, sizeof(BlitData), &blit);

			prRhiCmdDraw(cb, 4, 1, 0, 0);
			prRhiCmdEndRendering(cb);

			// Transition to present
			{
				PrRhiImageMemoryBarrierArray present_barriers =
					wpArrayWithCapacity(PrRhiImageMemoryBarrier, 1, WP_ARRAY_INIT_FILLED);
				present_barriers[0].texture                 = prRhiGetSwapchainTexture(app.swapchain, image_index);
				present_barriers[0].old_layout              = PR_RHI_LAYOUT_ATTACHMENT_OPTIMAL;
				present_barriers[0].new_layout              = PR_RHI_LAYOUT_PRESENT_SRC;
				present_barriers[0].src_stage_mask          = PR_RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT;
				present_barriers[0].src_access_mask         = PR_RHI_ACCESS_COLOR_ATTACHMENT_WRITE;
				present_barriers[0].dst_stage_mask          = PR_RHI_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT;
				present_barriers[0].dst_access_mask         = PR_RHI_ACCESS_NONE;

				prRhiCmdPipelineBarrier(cb, present_barriers, NULL);
			}

			prRhiEndCommandBuffer(cb);
			// }}}

			// {{{ Submit
			prRhiQueueSubmit(app.device, cb, app.image_acquired_semaphores[app.frame_index],
			                 app.render_completed_semaphores[image_index], app.fences[app.frame_index]);
			// }}}

			// {{{ Present
			PrRhiSwapchainResult pres = prRhiPresent(app.device, app.swapchain,
			                                         app.render_completed_semaphores[image_index]);
			if (pres == PR_RHI_SWAPCHAIN_OUT_OF_DATE) {
				app.update_swapchain = true;
			}
			// }}}
		}

		// {{{ Poll events
		SDL_Event event = {};
		while (SDL_PollEvent(&event)) {
			switch (event.type) {
				case SDL_EVENT_QUIT:
					goto done;
				case SDL_EVENT_KEY_DOWN:
					if (event.key.key == SDLK_ESCAPE) goto done;
					if (event.key.key == SDLK_PLUS || event.key.key == SDLK_KP_PLUS || event.key.key == SDLK_EQUALS) {
						app.selected = (app.selected + 1) % AppState::texture_count;
						PrRhiTextureSize ts = prRhiGetTextureSize(app.textures[app.selected].texture);
						std::cout << "Texture " << app.selected << ": " << TEXTURE_PATHS[app.selected]
						          << " (" << ts.width << 'x' << ts.height << ")\n";
					}
					if (event.key.key == SDLK_MINUS || event.key.key == SDLK_KP_MINUS) {
						app.selected = (app.selected + AppState::texture_count - 1) % AppState::texture_count;
						PrRhiTextureSize ts = prRhiGetTextureSize(app.textures[app.selected].texture);
						std::cout << "Texture " << app.selected << ": " << TEXTURE_PATHS[app.selected]
						          << " (" << ts.width << 'x' << ts.height << ")\n";
					}
					break;
				case SDL_EVENT_WINDOW_RESIZED:
					check(SDL_GetWindowSizeInPixels(app.window, &app.window_width, &app.window_height),
					      EXIT_CODE_GET_WINDOW_SIZE_FAILED);
					app.update_swapchain = true;
					break;
			}
		}
		// }}}

		// {{{ Swapchain recreate
		if (app.update_swapchain) {
			prRhiDeviceWaitIdle(app.device);
			prRhiRecreateSwapchain(app.device, &app.swapchain,
			                        (u32)app.window_width, (u32)app.window_height);

			// Re-create render completed semaphores for the new image count
			u32 new_count = prRhiGetSwapchainImageCount(app.swapchain);
			for (u32 i = 0; i < app.render_semaphore_count; ++i) {
				prRhiDestroySemaphore(app.device, app.render_completed_semaphores[i]);
			}
			app.render_semaphore_count = new_count;
			app.render_completed_semaphores = wpArrayAllocCapacity(PrRhiSemaphore *, &arena,
			                                                        new_count, WP_ARRAY_INIT_FILLED);
			for (u32 i = 0; i < new_count; ++i) {
				app.render_completed_semaphores[i] = prRhiCreateSemaphore(app.device);
			}

			app.update_swapchain = false;
		}
		// }}}

		app.frame_index = (app.frame_index + 1) % AppState::max_frames_in_flight;
	}
	done:
	// }}}

	// {{{ Cleanup
	prRhiDeviceWaitIdle(app.device);

	prRhiDestroyPipeline(app.device, app.pipeline);
	prRhiDestroyPipelineLayout(app.device, app.pipeline_layout);
	prRhiDestroyShader(app.device, app.shader);
	prRhiDestroyDescriptorPool(app.device, app.desc_pool);
	prRhiDestroyDescriptorSetLayout(app.device, app.desc_set_layout);

	for (u32 i = 0; i < AppState::texture_count; ++i) {
		prRhiDestroySampler(app.device, app.textures[i].sampler);
		prRhiDestroyTexture(app.device, app.textures[i].texture);
	}

	prRhiFreeCommandBuffers(app.device, app.cmd_pool, app.cmd_buffers);
	prRhiDestroyCommandPool(app.device, app.cmd_pool);

	for (u32 i = 0; i < app.render_semaphore_count; ++i) {
		prRhiDestroySemaphore(app.device, app.render_completed_semaphores[i]);
	}
	for (u32 i = 0; i < AppState::max_frames_in_flight; ++i) {
		prRhiDestroySemaphore(app.device, app.image_acquired_semaphores[i]);
		prRhiDestroyFence(app.device, app.fences[i]);
	}

	prRhiDestroySwapchain(app.device, app.swapchain);
	prRhiDestroyDevice(app.device);
	prRhiDestroySurface(app.inst, app.surface);
	prRhiDestroyInstance(app.inst);

	SDL_DestroyWindow(app.window);
	SDL_Quit();

	prRhiDestroy();
	wpMemArenaAllocatorDestroy(&arena);
	// }}}

	return EXIT_CODE_SUCCESS;
}
