diff --git a/runtime/shaders/present.frag b/runtime/shaders/present.frag new file mode 100644 index 00000000..9dba5cd8 --- /dev/null +++ b/runtime/shaders/present.frag @@ -0,0 +1,14 @@ +#version 450 +/* Swapchain present: sample the displayed VRAM rect and write it OPAQUE. + * VRAM alpha carries the PSX mask/STP bit (0 on most pixels); copying it into + * the swapchain with a transfer blit let AMD's compositor treat the window as + * partly transparent (desktop showing through as black/white blocks). */ +layout(location = 0) in vec2 v_uv; +layout(location = 0) out vec4 o_col; +layout(set = 0, binding = 0) uniform sampler2D u_src; +layout(push_constant) uniform PC { + vec4 u_src_rect; /* displayed region in normalized src coords: x0,y0,x1,y1 */ +} pc; +void main() { + o_col = vec4(texture(u_src, mix(pc.u_src_rect.xy, pc.u_src_rect.zw, v_uv)).rgb, 1.0); +} diff --git a/runtime/shaders/present.vert b/runtime/shaders/present.vert new file mode 100644 index 00000000..4fac1eda --- /dev/null +++ b/runtime/shaders/present.vert @@ -0,0 +1,9 @@ +#version 450 +/* Fullscreen triangle for the swapchain present pass. The viewport is set to + * the letterbox rect, so v_uv [0,1] spans exactly the displayed picture. */ +layout(location = 0) out vec2 v_uv; +void main() { + vec2 p = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2)); + v_uv = p; + gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0); +} diff --git a/runtime/src/gpu_vk_renderer.c b/runtime/src/gpu_vk_renderer.c index adc90521..c6b06190 100644 --- a/runtime/src/gpu_vk_renderer.c +++ b/runtime/src/gpu_vk_renderer.c @@ -889,7 +889,11 @@ static int create_swapchain(void) { return 1; } +static void pp_release_swapchain(void); +static void pp_destroy_all(void); + static void destroy_swapchain(void) { + pp_release_swapchain(); /* present-pass views/framebuffers/pipeline */ if (s_swapchain) { p_vkDestroySwapchainKHR(s_dev, s_swapchain, NULL); s_swapchain = VK_NULL_HANDLE; } } @@ -1421,6 +1425,7 @@ static void osd_staging_free(void); void vk_renderer_shutdown(void) { if (!s_dev) return; p_vkDeviceWaitIdle(s_dev); + pp_destroy_all(); vk_gpu_sync_internal(); /* reclaim deferred staging before tearing down */ cpres_cache_free(); /* FMV CPU-present cached image + staging */ osd_staging_free(); /* host toast OSD staging */ @@ -1679,6 +1684,219 @@ static void letterbox(int sw, int sh, int aw, int ah, VkOffset3D off[2]) { off[1].x = x + tw; off[1].y = y + th; off[1].z = 1; } +/* ---- opaque swapchain present pass --------------------------------------- */ +/* The plain present path blitted the VRAM image into the swapchain with + * vkCmdBlitImage, which copies alpha too. VRAM alpha is the PSX mask/STP bit + * (0 on most pixels), and AMD's compositor honours it despite + * VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR: parts of the window turned transparent + * (black/white blocks of whatever is behind it). This pass draws the displayed + * rect with a fullscreen triangle whose fragment shader writes alpha = 1. + * Everything is created lazily on first use; any failure sets s_pp_failed and + * the callers fall back to the original blit. The render pass ends in + * TRANSFER_DST_OPTIMAL so finish_present() (OSD blit + present barrier) runs + * unchanged. Format-dependent objects are released with the swapchain. */ +static VkRenderPass s_pp_rp; +static VkImageView s_pp_views[8]; +static VkFramebuffer s_pp_fbs[8]; +static VkPipeline s_pp_pipe; +static VkDescriptorSetLayout s_pp_dsl; +static VkPipelineLayout s_pp_pl; +static VkDescriptorPool s_pp_pool; +static VkDescriptorSet s_pp_ds[VK_FRAMES]; +static VkShaderModule s_pp_vs, s_pp_fs; +static VkSampler s_pp_near, s_pp_lin; +static int s_pp_failed; + +typedef struct { float src_rect[4]; } PresentPush; + +/* Swapchain-lifetime objects (the render pass depends on s_sc_format). */ +static void pp_release_swapchain(void) { + for (int i = 0; i < 8; i++) { + if (s_pp_fbs[i]) { p_vkDestroyFramebuffer(s_dev, s_pp_fbs[i], NULL); s_pp_fbs[i] = VK_NULL_HANDLE; } + if (s_pp_views[i]) { p_vkDestroyImageView(s_dev, s_pp_views[i], NULL); s_pp_views[i] = VK_NULL_HANDLE; } + } + if (s_pp_pipe) { p_vkDestroyPipeline(s_dev, s_pp_pipe, NULL); s_pp_pipe = VK_NULL_HANDLE; } + if (s_pp_rp) { p_vkDestroyRenderPass(s_dev, s_pp_rp, NULL); s_pp_rp = VK_NULL_HANDLE; } +} + +static void pp_destroy_all(void) { + pp_release_swapchain(); + if (s_pp_pool) { p_vkDestroyDescriptorPool(s_dev, s_pp_pool, NULL); s_pp_pool = VK_NULL_HANDLE; } + if (s_pp_pl) { p_vkDestroyPipelineLayout(s_dev, s_pp_pl, NULL); s_pp_pl = VK_NULL_HANDLE; } + if (s_pp_dsl) { p_vkDestroyDescriptorSetLayout(s_dev, s_pp_dsl, NULL); s_pp_dsl = VK_NULL_HANDLE; } + if (s_pp_vs) { p_vkDestroyShaderModule(s_dev, s_pp_vs, NULL); s_pp_vs = VK_NULL_HANDLE; } + if (s_pp_fs) { p_vkDestroyShaderModule(s_dev, s_pp_fs, NULL); s_pp_fs = VK_NULL_HANDLE; } + if (s_pp_near) { p_vkDestroySampler(s_dev, s_pp_near, NULL); s_pp_near = VK_NULL_HANDLE; } + if (s_pp_lin) { p_vkDestroySampler(s_dev, s_pp_lin, NULL); s_pp_lin = VK_NULL_HANDLE; } + s_pp_failed = 0; +} + +/* Device-lifetime objects: shaders, samplers, set layout, pool, sets, layout. */ +static int pp_init_device_objects(void) { + if (s_pp_pl) return 1; + s_pp_vs = make_module(spv_present_vert, spv_present_vert_size); + s_pp_fs = make_module(spv_present_frag, spv_present_frag_size); + if (!s_pp_vs || !s_pp_fs) return 0; + + VkSamplerCreateInfo sci = { VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; + sci.magFilter = sci.minFilter = VK_FILTER_NEAREST; + sci.addressModeU = sci.addressModeV = sci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sci.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + if (p_vkCreateSampler(s_dev, &sci, NULL, &s_pp_near) != VK_SUCCESS) return 0; + sci.magFilter = sci.minFilter = VK_FILTER_LINEAR; + if (p_vkCreateSampler(s_dev, &sci, NULL, &s_pp_lin) != VK_SUCCESS) return 0; + + VkDescriptorSetLayoutBinding b = {0}; + b.binding = 0; b.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + b.descriptorCount = 1; b.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + VkDescriptorSetLayoutCreateInfo dci = { VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; + dci.bindingCount = 1; dci.pBindings = &b; + if (p_vkCreateDescriptorSetLayout(s_dev, &dci, NULL, &s_pp_dsl) != VK_SUCCESS) return 0; + + /* One set per in-flight frame: acquire_present waits the frame fence, so + * the set is idle when it is rewritten for the next present. */ + VkDescriptorPoolSize ps = { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_FRAMES }; + VkDescriptorPoolCreateInfo pci = { VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; + pci.maxSets = VK_FRAMES; pci.poolSizeCount = 1; pci.pPoolSizes = &ps; + if (p_vkCreateDescriptorPool(s_dev, &pci, NULL, &s_pp_pool) != VK_SUCCESS) return 0; + for (int i = 0; i < VK_FRAMES; i++) { + VkDescriptorSetAllocateInfo ai = { VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; + ai.descriptorPool = s_pp_pool; ai.descriptorSetCount = 1; ai.pSetLayouts = &s_pp_dsl; + if (p_vkAllocateDescriptorSets(s_dev, &ai, &s_pp_ds[i]) != VK_SUCCESS) return 0; + } + + VkPushConstantRange pr = { VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(PresentPush) }; + VkPipelineLayoutCreateInfo li = { VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; + li.setLayoutCount = 1; li.pSetLayouts = &s_pp_dsl; + li.pushConstantRangeCount = 1; li.pPushConstantRanges = ≺ + return p_vkCreatePipelineLayout(s_dev, &li, NULL, &s_pp_pl) == VK_SUCCESS; +} + +static int pp_init_swapchain_objects(void) { + if (s_pp_pipe) return 1; + VkAttachmentDescription att = {0}; + att.format = s_sc_format; att.samples = VK_SAMPLE_COUNT_1_BIT; + att.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; /* black letterbox bars */ + att.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + att.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + att.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + att.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + att.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; /* finish_present */ + VkAttachmentReference cref = { 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; + VkSubpassDescription sub = {0}; + sub.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + sub.colorAttachmentCount = 1; sub.pColorAttachments = &cref; + VkSubpassDependency dep[2] = {0}; + dep[0].srcSubpass = VK_SUBPASS_EXTERNAL; dep[0].dstSubpass = 0; + dep[0].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + /* The OSD blit in finish_present writes the image with transfer ops. */ + dep[1].srcSubpass = 0; dep[1].dstSubpass = VK_SUBPASS_EXTERNAL; + dep[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + dep[1].dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT; + dep[1].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; + VkRenderPassCreateInfo rpi = { VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; + rpi.attachmentCount = 1; rpi.pAttachments = &att; + rpi.subpassCount = 1; rpi.pSubpasses = ⊂ + rpi.dependencyCount = 2; rpi.pDependencies = dep; + if (p_vkCreateRenderPass(s_dev, &rpi, NULL, &s_pp_rp) != VK_SUCCESS) return 0; + + VkPipelineShaderStageCreateInfo st[2] = { + { VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO }, + { VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO } }; + st[0].stage = VK_SHADER_STAGE_VERTEX_BIT; st[0].module = s_pp_vs; st[0].pName = "main"; + st[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; st[1].module = s_pp_fs; st[1].pName = "main"; + VkPipelineVertexInputStateCreateInfo vin = { VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; + VkPipelineInputAssemblyStateCreateInfo ia = { VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; + ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + VkPipelineViewportStateCreateInfo vp = { VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; + vp.viewportCount = 1; vp.scissorCount = 1; + VkPipelineRasterizationStateCreateInfo rs = { VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; + rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo ms = { VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; + ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineColorBlendAttachmentState ba = {0}; + ba.colorWriteMask = 0xF; + VkPipelineColorBlendStateCreateInfo cb = { VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; + cb.attachmentCount = 1; cb.pAttachments = &ba; + VkDynamicState dyn[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; + VkPipelineDynamicStateCreateInfo dy = { VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; + dy.dynamicStateCount = 2; dy.pDynamicStates = dyn; + VkGraphicsPipelineCreateInfo ci = { VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; + ci.stageCount = 2; ci.pStages = st; + ci.pVertexInputState = &vin; ci.pInputAssemblyState = &ia; + ci.pViewportState = &vp; ci.pRasterizationState = &rs; + ci.pMultisampleState = &ms; ci.pColorBlendState = &cb; ci.pDynamicState = &dy; + ci.layout = s_pp_pl; ci.renderPass = s_pp_rp; + return p_vkCreateGraphicsPipelines(s_dev, VK_NULL_HANDLE, 1, &ci, NULL, &s_pp_pipe) == VK_SUCCESS; +} + +/* Framebuffer for swapchain image idx, created on first use. */ +static VkFramebuffer pp_framebuffer(uint32_t idx) { + if (idx >= 8 || idx >= s_sc_count) return VK_NULL_HANDLE; + if (s_pp_fbs[idx]) return s_pp_fbs[idx]; + VkImageViewCreateInfo vi = { VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + vi.image = s_sc_images[idx]; vi.viewType = VK_IMAGE_VIEW_TYPE_2D; vi.format = s_sc_format; + vi.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + vi.subresourceRange.levelCount = 1; vi.subresourceRange.layerCount = 1; + if (p_vkCreateImageView(s_dev, &vi, NULL, &s_pp_views[idx]) != VK_SUCCESS) { + s_pp_views[idx] = VK_NULL_HANDLE; return VK_NULL_HANDLE; + } + VkFramebufferCreateInfo fi = { VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; + fi.renderPass = s_pp_rp; fi.attachmentCount = 1; fi.pAttachments = &s_pp_views[idx]; + fi.width = s_sc_extent.width; fi.height = s_sc_extent.height; fi.layers = 1; + if (p_vkCreateFramebuffer(s_dev, &fi, NULL, &s_pp_fbs[idx]) != VK_SUCCESS) + s_pp_fbs[idx] = VK_NULL_HANDLE; + return s_pp_fbs[idx]; +} + +/* Record the opaque present of src_view (already SHADER_READ_ONLY_OPTIMAL, + * normalized src rect u0..v1) into the letterbox rect dst of the acquired + * swapchain image, leaving it in TRANSFER_DST_OPTIMAL. Returns 0 (and records + * nothing) when the pass is unavailable: the caller then uses the blit. */ +static int pp_draw(VkCommandBuffer cb, uint32_t idx, uint32_t fr, VkImageView src_view, + int linear, const VkOffset3D dst[2], + float u0, float v0, float u1, float v1) { + if (s_pp_failed) return 0; + if (!pp_init_device_objects() || !pp_init_swapchain_objects()) { + s_pp_failed = 1; + fprintf(stdout, "psxrecomp: vulkan: present pass unavailable, using blit\n"); + return 0; + } + VkFramebuffer fb = pp_framebuffer(idx); + if (!fb) return 0; + + VkDescriptorImageInfo ii = { linear ? s_pp_lin : s_pp_near, src_view, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; + VkWriteDescriptorSet w = { VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET }; + w.dstSet = s_pp_ds[fr]; w.dstBinding = 0; w.descriptorCount = 1; + w.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; w.pImageInfo = ⅈ + p_vkUpdateDescriptorSets(s_dev, 1, &w, 0, NULL); + + VkClearValue clear = {0}; + clear.color.float32[3] = 1.0f; + VkRenderPassBeginInfo bi = { VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; + bi.renderPass = s_pp_rp; bi.framebuffer = fb; + bi.renderArea.extent = s_sc_extent; + bi.clearValueCount = 1; bi.pClearValues = &clear; + p_vkCmdBeginRenderPass(cb, &bi, VK_SUBPASS_CONTENTS_INLINE); + VkViewport view = { (float)dst[0].x, (float)dst[0].y, + (float)(dst[1].x - dst[0].x), (float)(dst[1].y - dst[0].y), 0.0f, 1.0f }; + VkRect2D sci = { { dst[0].x, dst[0].y }, + { (uint32_t)(dst[1].x - dst[0].x), (uint32_t)(dst[1].y - dst[0].y) } }; + p_vkCmdSetViewport(cb, 0, 1, &view); + p_vkCmdSetScissor(cb, 0, 1, &sci); + p_vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, s_pp_pipe); + p_vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, s_pp_pl, 0, 1, &s_pp_ds[fr], 0, NULL); + PresentPush pc = { { u0, v0, u1, v1 } }; + p_vkCmdPushConstants(cb, s_pp_pl, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof pc, &pc); + p_vkCmdDraw(cb, 3, 1, 0, 0); + p_vkCmdEndRenderPass(cb); + return 1; +} + int vk_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear, int force_4_3) { if (!s_ctx_ok) return 0; @@ -1688,6 +1906,22 @@ int vk_renderer_present_vram(int disp_x, int disp_y, int w, int h, if (!acquire_present(&sc, &cb, &idx, &fr)) return 1; /* frame skipped/recreated */ int S = s_scale; + VkOffset3D dst[2]; + letterbox((int)s_sc_extent.width, (int)s_sc_extent.height, + force_4_3 ? 4 : s_aspect_num, force_4_3 ? 3 : s_aspect_den, dst); + + /* Opaque shader present first (see pp_draw); blit only as the fallback. */ + vram_to(cb, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + int drawn = pp_draw(cb, idx, fr, s_vram_view, linear, dst, + disp_x / (float)VRAM_W, disp_y / (float)VRAM_H, + (disp_x + w) / (float)VRAM_W, (disp_y + h) / (float)VRAM_H); + vram_to(cb, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); /* re-park */ + if (drawn) { + finish_present(cb, sc, idx, fr); + perf_snapshot_present(); + return 1; + } + img_barrier(cb, sc, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); @@ -1696,10 +1930,6 @@ int vk_renderer_present_vram(int disp_x, int disp_y, int w, int h, VkImageSubresourceRange rng = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; p_vkCmdClearColorImage(cb, sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &rng); - VkOffset3D dst[2]; - letterbox((int)s_sc_extent.width, (int)s_sc_extent.height, - force_4_3 ? 4 : s_aspect_num, force_4_3 ? 3 : s_aspect_den, dst); - VkImageBlit blit = {0}; blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; blit.srcSubresource.layerCount = 1; @@ -1850,6 +2080,24 @@ int vk_renderer_present_wide(int disp_x, int disp_y, int disp_h, int linear) { if (!acquire_present(&sc, &cb, &idx, &fr)) return 1; /* frame skipped/recreated */ int S = s_scale; + int native_w = s_wide_w - 2 * s_wide_offset; + if (native_w <= 0) native_w = s_wide_w; + VkOffset3D dst[2]; + letterbox((int)s_sc_extent.width, (int)s_sc_extent.height, + 4 * s_wide_w, 3 * native_w, dst); + + /* Opaque shader present first (see pp_draw); blit only as the fallback. */ + img_to(cb, s_wide_img[i], &s_wide_layout[i], VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + int drawn = pp_draw(cb, idx, fr, s_wide_view[i], linear, dst, + 0.0f, disp_y / (float)VRAM_H, + 1.0f, (disp_y + disp_h) / (float)VRAM_H); + if (drawn) { + img_to(cb, s_wide_img[i], &s_wide_layout[i], VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + finish_present(cb, sc, idx, fr); + perf_snapshot_present(); + return 1; + } + img_barrier(cb, sc, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); @@ -1859,12 +2107,6 @@ int vk_renderer_present_wide(int disp_x, int disp_y, int disp_h, int linear) { img_to(cb, s_wide_img[i], &s_wide_layout[i], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); - int native_w = s_wide_w - 2 * s_wide_offset; - if (native_w <= 0) native_w = s_wide_w; - VkOffset3D dst[2]; - letterbox((int)s_sc_extent.width, (int)s_sc_extent.height, - 4 * s_wide_w, 3 * native_w, dst); - int sy0 = disp_y * S, sy1 = (disp_y + disp_h) * S; if (sy0 < 0) sy0 = 0; if (sy1 > VRAM_H * S) sy1 = VRAM_H * S; @@ -2565,7 +2807,7 @@ static int wide_surf_for(int base_x) { int S = s_scale, w = s_wide_w * S, h = VRAM_H * S; if (!make_image(VK_FORMAT_R8G8B8A8_UNORM, w, h, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | - VK_IMAGE_USAGE_TRANSFER_DST_BIT, + VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_COLOR_BIT, &s_wide_img[i], &s_wide_mem[i], &s_wide_view[i])) return -1; if (!make_image(s_ds_format, w, h,