mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
3600 lines
178 KiB
C
3600 lines
178 KiB
C
/* gpu_vk_renderer.c — hardware Vulkan renderer backend. See gpu_vk_renderer.h.
|
||
*
|
||
* GPU-authoritative VRAM (mirrors the GL backend's model): the canonical copy of
|
||
* VRAM is a Vulkan image (s_vram_img, RGBA8, 1024*S x 512*S). A CPU mirror is
|
||
* kept only for readback (screenshots / debug server / 24-bit FMV present) and
|
||
* is synced lazily. Present blits the displayed region straight from the VRAM
|
||
* image to the swapchain — no per-frame readback.
|
||
*
|
||
* Vulkan is loaded entirely at runtime (SDL_Vulkan_LoadLibrary +
|
||
* vkGetInstanceProcAddr), so the exe never link-depends on vulkan-1 and falls
|
||
* back to software when no ICD is present.
|
||
*
|
||
* BUILD STATUS: instance/device/swapchain + VRAM image + fills + CPU<->VRAM
|
||
* transfers + present are implemented; geometry pipelines (triangles/rects/
|
||
* lines), texturing/CLUT, mask-bit, semi-transparency, SSAA and the native-wide
|
||
* compositor are added in later phases — those vtable entries currently abort
|
||
* via psx_fatal_halt rather than silently no-op (no stubs).
|
||
*/
|
||
|
||
#include "gpu.h"
|
||
#include "gpu_render.h"
|
||
#include "gpu_vk_renderer.h"
|
||
#include "video_telemetry.h"
|
||
#include "gpu_sw_renderer.h"
|
||
#include "host_osd.h"
|
||
#include "psx_savestate_menu.h"
|
||
#include "psx_host_menu.h"
|
||
#include "psx_rewind.h"
|
||
#include "crash_trace.h"
|
||
#include "gpu_vk_upload.h"
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
#include <stdlib.h>
|
||
#include <math.h>
|
||
|
||
#if !defined(PSX_HAVE_VULKAN)
|
||
/* Vulkan SDK headers/glslc were not found at configure time: compile an inert
|
||
* backend so the runtime still builds and simply falls back to software. */
|
||
int vk_renderer_init_context(struct SDL_Window *win) { (void)win; return 0; }
|
||
void vk_renderer_shutdown(void) {}
|
||
int vk_renderer_present_vram(int a,int b,int c,int d,int e,int f){(void)a;(void)b;(void)c;(void)d;(void)e;(void)f;return 0;}
|
||
int vk_renderer_present_wide(int a,int b,int c,int d){(void)a;(void)b;(void)c;(void)d;return 0;}
|
||
void vk_renderer_present_cpu(const uint32_t*p,int w,int h,int l,int f){(void)p;(void)w;(void)h;(void)l;(void)f;}
|
||
void vk_renderer_present_blank(void){}
|
||
void vk_renderer_sync_cpu(void){}
|
||
void vk_renderer_restage_vram_after_savestate(void){}
|
||
void vk_renderer_set_present_mode(int m){(void)m;}
|
||
void vk_renderer_set_display_aspect(int n,int d){(void)n;(void)d;}
|
||
int vk_perf_json(char *out,int cap,int count){(void)count; return cap>2?snprintf(out,cap,"[]"):0;}
|
||
const GpuRenderBackend *vk_backend_get(void) { return 0; }
|
||
|
||
#else /* PSX_HAVE_VULKAN */
|
||
|
||
#include "psx_sdl.h"
|
||
#if defined(PSX_SDL3)
|
||
#include <SDL3/SDL_vulkan.h>
|
||
#else
|
||
#include <SDL_vulkan.h>
|
||
#endif
|
||
#define VK_NO_PROTOTYPES
|
||
#include <vulkan/vulkan.h>
|
||
#include "vk_shaders_spv.h" /* generated: spv_geo_vert/frag, spv_geo_tex_vert/frag,
|
||
* spv_pack_comp, spv_blit_vert/frag */
|
||
|
||
#define VRAM_W 1024
|
||
#define VRAM_H 512
|
||
|
||
static int s_aspect_num = 4, s_aspect_den = 3;
|
||
|
||
void vk_renderer_set_display_aspect(int num, int den) {
|
||
if (num <= 0 || den <= 0) { num = 4; den = 3; }
|
||
s_aspect_num = num; s_aspect_den = den;
|
||
}
|
||
|
||
/* ---- dynamic loader ---------------------------------------------------- */
|
||
/* vkGetInstanceProcAddr comes from SDL; everything else is loaded through it
|
||
* (global/instance scope) or vkGetDeviceProcAddr (device scope). */
|
||
static PFN_vkGetInstanceProcAddr p_vkGetInstanceProcAddr;
|
||
static PFN_vkGetDeviceProcAddr p_vkGetDeviceProcAddr;
|
||
|
||
#define VK_GLOBAL_FUNCS(X) \
|
||
X(vkCreateInstance) \
|
||
X(vkEnumerateInstanceExtensionProperties) \
|
||
X(vkEnumerateInstanceLayerProperties)
|
||
|
||
#define VK_INSTANCE_FUNCS(X) \
|
||
X(vkDestroyInstance) \
|
||
X(vkEnumeratePhysicalDevices) \
|
||
X(vkGetPhysicalDeviceProperties) \
|
||
X(vkGetPhysicalDeviceQueueFamilyProperties) \
|
||
X(vkGetPhysicalDeviceMemoryProperties) \
|
||
X(vkGetPhysicalDeviceFormatProperties) \
|
||
X(vkGetPhysicalDeviceSurfaceSupportKHR) \
|
||
X(vkGetPhysicalDeviceSurfaceCapabilitiesKHR) \
|
||
X(vkGetPhysicalDeviceSurfaceFormatsKHR) \
|
||
X(vkGetPhysicalDeviceSurfacePresentModesKHR) \
|
||
X(vkCreateDevice) \
|
||
X(vkDestroySurfaceKHR)
|
||
|
||
#define VK_DEVICE_FUNCS(X) \
|
||
X(vkDestroyDevice) \
|
||
X(vkGetDeviceQueue) \
|
||
X(vkDeviceWaitIdle) \
|
||
X(vkQueueWaitIdle) \
|
||
X(vkCreateSwapchainKHR) \
|
||
X(vkDestroySwapchainKHR) \
|
||
X(vkGetSwapchainImagesKHR) \
|
||
X(vkAcquireNextImageKHR) \
|
||
X(vkQueuePresentKHR) \
|
||
X(vkCreateImageView) \
|
||
X(vkDestroyImageView) \
|
||
X(vkCreateImage) \
|
||
X(vkDestroyImage) \
|
||
X(vkGetImageMemoryRequirements) \
|
||
X(vkBindImageMemory) \
|
||
X(vkCreateBuffer) \
|
||
X(vkDestroyBuffer) \
|
||
X(vkGetBufferMemoryRequirements) \
|
||
X(vkBindBufferMemory) \
|
||
X(vkAllocateMemory) \
|
||
X(vkFreeMemory) \
|
||
X(vkMapMemory) \
|
||
X(vkUnmapMemory) \
|
||
X(vkCreateCommandPool) \
|
||
X(vkDestroyCommandPool) \
|
||
X(vkResetCommandPool) \
|
||
X(vkAllocateCommandBuffers) \
|
||
X(vkFreeCommandBuffers) \
|
||
X(vkBeginCommandBuffer) \
|
||
X(vkEndCommandBuffer) \
|
||
X(vkResetCommandBuffer) \
|
||
X(vkQueueSubmit) \
|
||
X(vkCreateSemaphore) \
|
||
X(vkDestroySemaphore) \
|
||
X(vkCreateFence) \
|
||
X(vkDestroyFence) \
|
||
X(vkWaitForFences) \
|
||
X(vkResetFences) \
|
||
X(vkCmdPipelineBarrier) \
|
||
X(vkCmdClearColorImage) \
|
||
X(vkCmdClearDepthStencilImage) \
|
||
X(vkCmdCopyBufferToImage) \
|
||
X(vkCmdCopyImageToBuffer) \
|
||
X(vkCmdBlitImage) \
|
||
X(vkCmdCopyImage) \
|
||
X(vkCreateRenderPass) \
|
||
X(vkDestroyRenderPass) \
|
||
X(vkCreateFramebuffer) \
|
||
X(vkDestroyFramebuffer) \
|
||
X(vkCreateShaderModule) \
|
||
X(vkDestroyShaderModule) \
|
||
X(vkCreatePipelineLayout) \
|
||
X(vkDestroyPipelineLayout) \
|
||
X(vkCreateGraphicsPipelines) \
|
||
X(vkCreateComputePipelines) \
|
||
X(vkDestroyPipeline) \
|
||
X(vkCreateDescriptorSetLayout) \
|
||
X(vkDestroyDescriptorSetLayout) \
|
||
X(vkCreateDescriptorPool) \
|
||
X(vkDestroyDescriptorPool) \
|
||
X(vkAllocateDescriptorSets) \
|
||
X(vkUpdateDescriptorSets) \
|
||
X(vkCreateSampler) \
|
||
X(vkDestroySampler) \
|
||
X(vkCmdBeginRenderPass) \
|
||
X(vkCmdEndRenderPass) \
|
||
X(vkCmdBindPipeline) \
|
||
X(vkCmdBindVertexBuffers) \
|
||
X(vkCmdBindDescriptorSets) \
|
||
X(vkCmdPushConstants) \
|
||
X(vkCmdSetViewport) \
|
||
X(vkCmdSetScissor) \
|
||
X(vkCmdSetStencilReference) \
|
||
X(vkCmdDispatch) \
|
||
X(vkCmdDraw) \
|
||
X(vkCmdClearAttachments)
|
||
|
||
#define DECL_FP(n) static PFN_##n p_##n;
|
||
VK_GLOBAL_FUNCS(DECL_FP)
|
||
VK_INSTANCE_FUNCS(DECL_FP)
|
||
VK_DEVICE_FUNCS(DECL_FP)
|
||
#undef DECL_FP
|
||
|
||
/* ---- state ------------------------------------------------------------- */
|
||
static SDL_Window *s_win;
|
||
static VkInstance s_instance;
|
||
static VkSurfaceKHR s_surface;
|
||
static VkPhysicalDevice s_phys;
|
||
static VkDevice s_dev;
|
||
static uint32_t s_qfam = 0; /* graphics+present queue family */
|
||
static VkQueue s_queue;
|
||
static VkCommandPool s_cmd_pool; /* present command buffers */
|
||
static VkCommandPool s_work_pool; /* one-shot work CBs (reset at gpu_sync) */
|
||
static int s_work_pending; /* submitted work not yet waited */
|
||
static VkPhysicalDeviceMemoryProperties s_memprops;
|
||
|
||
/* Staging buffers whose GPU consumption is still in flight; freed at gpu_sync
|
||
* after the queue goes idle (work is submitted without per-op waits now). */
|
||
#define PENDING_STAGING_MAX 1024
|
||
static VkBuffer s_pending_buf[PENDING_STAGING_MAX];
|
||
static VkDeviceMemory s_pending_mem[PENDING_STAGING_MAX];
|
||
static int s_pending_n;
|
||
|
||
#define STAGING_CACHE_MAX 16
|
||
typedef struct {
|
||
VkBuffer buf;
|
||
VkDeviceMemory mem;
|
||
VkDeviceSize size;
|
||
void *map;
|
||
int busy;
|
||
} StagingCacheEntry;
|
||
static StagingCacheEntry s_staging_cache[STAGING_CACHE_MAX];
|
||
|
||
static void staging_release(VkBuffer buf, VkDeviceMemory mem) {
|
||
for (int i = 0; i < STAGING_CACHE_MAX; ++i) {
|
||
if (s_staging_cache[i].buf == buf && s_staging_cache[i].mem == mem) {
|
||
s_staging_cache[i].busy = 0;
|
||
return;
|
||
}
|
||
}
|
||
p_vkUnmapMemory(s_dev, mem);
|
||
p_vkDestroyBuffer(s_dev, buf, NULL);
|
||
p_vkFreeMemory(s_dev, mem, NULL);
|
||
}
|
||
|
||
static void staging_destroy(VkBuffer buf, VkDeviceMemory mem) {
|
||
for (int i = 0; i < STAGING_CACHE_MAX; ++i) {
|
||
StagingCacheEntry *entry = &s_staging_cache[i];
|
||
if (entry->buf == buf && entry->mem == mem) {
|
||
if (entry->map) p_vkUnmapMemory(s_dev, mem);
|
||
p_vkDestroyBuffer(s_dev, buf, NULL);
|
||
p_vkFreeMemory(s_dev, mem, NULL);
|
||
memset(entry, 0, sizeof *entry);
|
||
return;
|
||
}
|
||
}
|
||
p_vkUnmapMemory(s_dev, mem);
|
||
p_vkDestroyBuffer(s_dev, buf, NULL);
|
||
p_vkFreeMemory(s_dev, mem, NULL);
|
||
}
|
||
|
||
static VkSwapchainKHR s_swapchain;
|
||
static VkFormat s_sc_format;
|
||
static VkExtent2D s_sc_extent;
|
||
static uint32_t s_sc_count;
|
||
static VkImage s_sc_images[8];
|
||
static int s_present_mode_req = 1; /* 1 tear-free, 0 IMMEDIATE, -1 MAILBOX */
|
||
|
||
/* Per-frame sync (double-buffered command recording). */
|
||
#define VK_FRAMES 2
|
||
static VkCommandBuffer s_cmd[VK_FRAMES];
|
||
static VkSemaphore s_sem_acquire[VK_FRAMES];
|
||
static VkSemaphore s_sem_render[VK_FRAMES];
|
||
static VkFence s_fence[VK_FRAMES];
|
||
static uint32_t s_frame_idx;
|
||
|
||
/* GPU-authoritative VRAM image + CPU mirror. */
|
||
static int s_scale = 1;
|
||
static VkImage s_vram_img;
|
||
static VkDeviceMemory s_vram_mem;
|
||
static VkImageLayout s_vram_layout = VK_IMAGE_LAYOUT_UNDEFINED; /* tracked */
|
||
static uint16_t *s_vram; /* gpu.c's CPU VRAM array (1024x512) */
|
||
static int s_cpu_dirty; /* CPU mirror stale vs GPU image */
|
||
|
||
static int s_ready; /* full pipeline ready (Phase 2+) */
|
||
static int s_ctx_ok; /* instance/device/swapchain up */
|
||
|
||
static int s_texfilter;
|
||
|
||
/* ---- geometry / render targets ----------------------------------------- */
|
||
static VkImageView s_vram_view; /* RGBA8 hr render target view */
|
||
static VkRenderPass s_rpass; /* color (hr) + depth/stencil */
|
||
static VkFramebuffer s_fbo;
|
||
|
||
/* Native R16_UINT raw VRAM mirror: the canonical 1555 bits, sampled in the
|
||
* textured fragment shader (CLUT decode needs the exact index bits, which the
|
||
* lossy RGBA8 image cannot provide) and read back to the CPU mirror. Kept
|
||
* coherent with the hr render target by the PACK compute pass. */
|
||
static VkImage s_raw_img;
|
||
static VkDeviceMemory s_raw_mem;
|
||
static VkImageView s_raw_view;
|
||
static VkImageLayout s_raw_layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||
|
||
/* Depth/stencil attachment: stencil bit0 mirrors VRAM bit15 (mask). */
|
||
static VkImage s_ds_img;
|
||
static VkDeviceMemory s_ds_mem;
|
||
static VkImageView s_ds_view;
|
||
static VkFormat s_ds_format;
|
||
/* Both candidate formats (D24S8 / D32S8) are COMBINED depth+stencil: views,
|
||
* barriers and image clears must name BOTH aspects unless
|
||
* separateDepthStencilLayouts is enabled (it is not). Stencil-only leaves the
|
||
* depth half in UNDEFINED; on AMD (no D24S8 -> D32S8) depth and stencil share
|
||
* compression metadata. */
|
||
#define DS_ASPECT (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)
|
||
|
||
/* Scratch hr image for VRAM->VRAM copies (resolves overlap). */
|
||
static VkImage s_scratch_img;
|
||
static VkDeviceMemory s_scratch_mem;
|
||
static VkImageView s_scratch_view;
|
||
static VkImageLayout s_scratch_layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||
|
||
/* Native-res RGBA8 upload staging image (CPU->VRAM blit source). */
|
||
static VkImage s_up_img;
|
||
static VkDeviceMemory s_up_mem;
|
||
static VkImageView s_up_view;
|
||
static VkImageLayout s_up_layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||
|
||
/* Samplers + descriptor sets. One nearest sampler serves every fetch. */
|
||
/* ---- native-wide compositor surfaces (mirrors the GL backend) -----------
|
||
* Canonical VRAM (the hr image) stays 100% faithful; framebuffer-targeting
|
||
* batches are ALSO drawn into a per-base_x wide surface, x-translated in the
|
||
* vertex shader via the existing u_xoff/u_xhalf push constants. Each surface
|
||
* carries its OWN depth-stencil image (PSX mask-bit mirror; Part-A lesson:
|
||
* never render stencil-enabled passes into a stencil-less target) and its own
|
||
* VkFramebuffer on the SHARED render pass, so every existing pipeline works
|
||
* unchanged. The mirror is appended to the SAME one-shot command buffer as the
|
||
* canonical pass — one extra render pass per flushed batch, no extra submits. */
|
||
#define VK_WIDE_MAX_SURF 4
|
||
static VkImage s_wide_img[VK_WIDE_MAX_SURF];
|
||
static VkDeviceMemory s_wide_mem[VK_WIDE_MAX_SURF];
|
||
static VkImageView s_wide_view[VK_WIDE_MAX_SURF];
|
||
static VkImage s_wide_ds_img[VK_WIDE_MAX_SURF];
|
||
static VkDeviceMemory s_wide_ds_mem[VK_WIDE_MAX_SURF];
|
||
static VkImageView s_wide_ds_view[VK_WIDE_MAX_SURF];
|
||
static VkFramebuffer s_wide_fb[VK_WIDE_MAX_SURF];
|
||
static VkImageLayout s_wide_layout[VK_WIDE_MAX_SURF];
|
||
static int s_wide_base[VK_WIDE_MAX_SURF] = { -1, -1, -1, -1 };
|
||
static int s_wide_w = 0; /* wide width (native px); 0 = disabled */
|
||
static int s_wide_offset = 0; /* centering OFFSET (native px) */
|
||
static int s_wide_cur = -1; /* active mirror surface index, -1 = none */
|
||
static int s_wide_cur_base = 0;
|
||
static void wide_free_all(void); /* defined with the wide helpers below */
|
||
|
||
static VkSampler s_samp;
|
||
static VkDescriptorPool s_dpool;
|
||
static VkDescriptorSetLayout s_dsl_tex; /* binding0: raw usampler2D (frag) */
|
||
static VkDescriptorSetLayout s_dsl_pack; /* binding0: hr sampler, binding1: raw storage (compute) */
|
||
static VkDescriptorSetLayout s_dsl_blit; /* binding0: src sampler (frag) */
|
||
static VkDescriptorSet s_ds_tex;
|
||
static VkDescriptorSet s_ds_pack;
|
||
/* Blit src image is rebound per blit; a ring of sets avoids updating a set still
|
||
* in flight (work is no longer waited per-op). Cycled within a frame; the index
|
||
* resets at gpu_sync (which waits the queue idle). */
|
||
#define BLIT_DESC_RING 64
|
||
static VkDescriptorSet s_ds_blit_ring[BLIT_DESC_RING];
|
||
static int s_ds_blit_idx;
|
||
|
||
/* ---- always-on per-frame perf ring (transition-stall diagnosis) ----------
|
||
* Per-op counters accumulate into s_perf_cur; each present() snapshots them into
|
||
* the ring keyed by present index, then resets. Query via the debug-server
|
||
* "vk_perf" command (vk_perf_json). Op COUNTS alone reveal the dominant
|
||
* per-frame cost (e.g. make_staging vkAllocateMemory churn during transitions:
|
||
* a frame that jumps from ~10 to ~2000 allocs is the smoking gun). Always on. */
|
||
typedef struct {
|
||
uint32_t present_idx;
|
||
uint64_t present_qpc;
|
||
uint32_t wait_us, acquire_us, present_us;
|
||
uint32_t allocs, alloc_kb, oneshots, submits, syncs, pack_flushes,
|
||
blits, upload_blocks, copy_rects, geo_flushes, tex_flushes,
|
||
wide_passes, wide_clears;
|
||
} VkPerf;
|
||
#define VK_PERF_RING 256
|
||
static VkPerf s_perf_ring[VK_PERF_RING];
|
||
static uint32_t s_perf_head; /* number of frames recorded (monotonic) */
|
||
static VkPerf s_perf_cur; /* current (in-progress) frame */
|
||
static uint32_t s_present_idx;
|
||
|
||
static uint32_t perf_elapsed_us(uint64_t start) {
|
||
uint64_t frequency = SDL_GetPerformanceFrequency();
|
||
uint64_t ticks = SDL_GetPerformanceCounter() - start;
|
||
return frequency ? (uint32_t)(ticks * 1000000u / frequency) : 0;
|
||
}
|
||
|
||
static void perf_snapshot_present(void) {
|
||
s_perf_cur.present_idx = s_present_idx++;
|
||
s_perf_cur.present_qpc = SDL_GetPerformanceCounter();
|
||
s_perf_ring[s_perf_head % VK_PERF_RING] = s_perf_cur;
|
||
s_perf_head++;
|
||
memset(&s_perf_cur, 0, sizeof s_perf_cur);
|
||
}
|
||
|
||
/* Pipeline layouts (one per program family). */
|
||
static VkPipelineLayout s_pl_geo;
|
||
static VkPipelineLayout s_pl_tex;
|
||
static VkPipelineLayout s_pl_blit;
|
||
static VkPipelineLayout s_pl_pack;
|
||
static VkPipeline s_pipe_pack; /* compute */
|
||
|
||
/* Lazy graphics-pipeline cache keyed by (program, topology, blend, stencil,
|
||
* cmask). program: 0 GEO, 1 TEX, 2 BLIT. blend: 0 off, 1..4 = PS1 modes 0..3
|
||
* (+1). stencil: 0 SET (always+replace, dyn ref), 1 CHECK keep, 2 CHECK
|
||
* invert. cmask: 0 writes colour, 1 disables ALL colour writes (stencil-only
|
||
* fixup pass of an opaque textured batch — mirrors GL's glColorMask off). */
|
||
#define PIPE_PROGS 3
|
||
#define PIPE_TOPOS 2 /* 0 TRIANGLE_LIST, 1 LINE_LIST */
|
||
#define PIPE_BLENDS 5
|
||
#define PIPE_STENCILS 3
|
||
#define PIPE_CMASKS 2
|
||
#define PIPE_CACHE_N (PIPE_PROGS * PIPE_TOPOS * PIPE_BLENDS * PIPE_STENCILS * PIPE_CMASKS)
|
||
static VkPipeline s_pipe_cache[PIPE_CACHE_N];
|
||
static VkShaderModule s_mod_geo_v, s_mod_geo_f, s_mod_tex_v, s_mod_tex_f, s_mod_blit_v, s_mod_blit_f;
|
||
|
||
/* Untextured batch (flat/gouraud triangles, lines-as-quads, flat rects). */
|
||
typedef struct { float x, y, r, g, b, a; } Vert; /* GEO vertex, stride 24 */
|
||
#define VK_VBUF_VERTS 65536
|
||
static VkBuffer s_vbuf;
|
||
static VkDeviceMemory s_vbuf_mem;
|
||
static Vert *s_vmap; /* persistently mapped host-visible */
|
||
static uint32_t s_vcount; /* verts accumulated this batch */
|
||
/* Draws are SUBMITTED without waiting (deferred-submit model), so a flushed
|
||
* batch's vertices may still be read by the GPU when the next batch begins.
|
||
* Rewriting the buffer from offset 0 per batch raced that read and shredded
|
||
* whole batches on screen (terrain/mesh chunks missing or drawn with another
|
||
* batch's vertices). Batches therefore SUB-ALLOCATE: each flush draws
|
||
* [base, base+count) and bumps base; gpu_sync (queue idle) recycles to 0. */
|
||
static uint32_t s_vbase; /* first vertex of the open batch */
|
||
static int s_geo_semi = -2; /* open untextured batch keys */
|
||
static int s_geo_mask = 0, s_geo_check = 0;
|
||
static int s_geo_mirror_suppress = 0; /* open batch: skip the wide mirror
|
||
* (full-screen overlay rect draws its
|
||
* own full-width wide pass instead) */
|
||
|
||
/* Sub-pixel vertex override + perspective UV weights for the next triangle
|
||
* ([video] geometry_correction / perspective_texturing; see gpu_render.h).
|
||
* gpu.c sets these immediately before the matching gr_draw_*_triangle and they
|
||
* are consumed by it. All-zero == the faithful integer/affine path. */
|
||
static int s_pc_valid = 0; /* sub-pixel positions present */
|
||
static float s_pc_x[3], s_pc_y[3]; /* native VRAM px, fractional */
|
||
static int s_pq_valid = 0; /* perspective weights present */
|
||
static float s_pq[3];
|
||
|
||
/* Textured batch: 19 floats/vert (pos,uv,col,tpage,clut,depth,raw,limits,q).
|
||
* q is the perspective weight ([video] perspective_texturing); 0 = affine,
|
||
* which the vertex shader turns into w == 1.0 — the faithful default. */
|
||
#define TEXV 19
|
||
typedef struct { float v[TEXV]; } TexVert;
|
||
#define VK_TBUF_VERTS 24576 /* multiple of 3 */
|
||
static VkBuffer s_tbuf;
|
||
static VkDeviceMemory s_tbuf_mem;
|
||
static TexVert *s_tmap;
|
||
static uint32_t s_tcount;
|
||
static uint32_t s_tbase; /* first vertex of the open batch */
|
||
static int s_tb_semi = -2, s_tb_mask = 0, s_tb_check = 0, s_tb_filter = 0;
|
||
static int s_tb_twin[4] = {0,0,0,0};
|
||
|
||
static void flush_geometry(void); /* commit pending untextured batch */
|
||
static void flush_tex_batch(void); /* commit pending textured batch */
|
||
static void flush_cpu_upload(void); /* pending CPU writes -> GPU images */
|
||
static void pack_flush(void); /* hr -> raw mirror (dirty rect) */
|
||
static void flush_pack_if_sampling(int tpx, int tpy, int depth, int clx, int cly);
|
||
static void vram_upload_block(int x, int y, int w, int h, const uint16_t *data);
|
||
static void gpu_copy_rect(int sx, int sy, int dx, int dy, int w, int h);
|
||
static void ensure_cpu(void); /* GPU -> CPU mirror readback */
|
||
static int make_staging(VkDeviceSize bytes, VkBuffer *buf, VkDeviceMemory *mem, void **map);
|
||
static void free_staging(VkBuffer buf, VkDeviceMemory mem);
|
||
|
||
/* draw area / offset / per-prim render state (set via the vtable setters) */
|
||
static int s_da_x1, s_da_y1, s_da_x2 = VRAM_W - 1, s_da_y2 = VRAM_H - 1;
|
||
static int s_off_x, s_off_y;
|
||
static int s_semi_en = 0, s_semi_mode = 0;
|
||
static int s_mask_set = 0, s_mask_check = 0;
|
||
static int s_tw_mask_x = 0, s_tw_mask_y = 0, s_tw_off_x = 0, s_tw_off_y = 0;
|
||
static int s_mod_r = 128, s_mod_g = 128, s_mod_b = 128, s_mod_raw = 0;
|
||
|
||
/* Coherency: regions rendered to the hr image but not yet packed to the raw
|
||
* mirror, and whether the hr image diverges from the CPU mirror. */
|
||
typedef struct { int set, x0, y0, x1, y1; } DirtyRect;
|
||
static DirtyRect s_pack_dirty;
|
||
static int s_gpu_dirty;
|
||
/* CPU-side VRAM writes (GP0 A0 transfers, DMA, fills, single pokes) land in
|
||
* the CPU array immediately and accumulate here; ONE batched upload flushes
|
||
* them before the next GPU op that could observe them (mirrors the GL
|
||
* backend). Eager per-op uploads are the transition-stall:
|
||
* each PIXEL cost 2 vkAllocateMemory + 2 vkQueueSubmit + a 2-draw blit pass,
|
||
* so a single GP0 A0 texture upload wedged the main thread for minutes inside
|
||
* the driver (freeze dump: 8/8 samples in D3DKMTCreateAllocation churn).
|
||
*
|
||
* EXACT rect list, NOT a single union (ported from the GL fix for the MMX6
|
||
* black-frame flicker, ISSUES #7): a union of DISJOINT uploads covers the
|
||
* framebuffers in between, and flushing paints that whole box from the CPU
|
||
* VRAM array — STALE under VK once the hr image is authoritative (ensure_cpu
|
||
* syncs on demand only) — stomping freshly rendered frames. Only pixels the
|
||
* CPU actually wrote may ever be uploaded. See up_add()/up_add_transfer(). */
|
||
/* 64 (vs GL's 16): VK pays TWO queue submits per flush (copies + blit pass),
|
||
* so overflow-triggered flushes must stay rare. MDEC/FMV macroblock streams
|
||
* coalesce into one rect per block row via the row-band merge, so a whole
|
||
* streamed frame fits comfortably. */
|
||
#define UP_RECTS_MAX 64
|
||
static DirtyRect s_up_rects[UP_RECTS_MAX];
|
||
static int s_up_nrects = 0;
|
||
|
||
static void rect_clear(DirtyRect *r) { r->set = 0; }
|
||
static void rect_add(DirtyRect *r, int x0, int y0, int x1, int y1) {
|
||
if (x0 > x1 || y0 > y1) return;
|
||
if (!r->set) { r->x0=x0; r->y0=y0; r->x1=x1; r->y1=y1; r->set=1; return; }
|
||
if (x0 < r->x0) r->x0 = x0; if (y0 < r->y0) r->y0 = y0;
|
||
if (x1 > r->x1) r->x1 = x1; if (y1 > r->y1) r->y1 = y1;
|
||
}
|
||
static int rect_intersects(const DirtyRect *r, int x0, int y0, int x1, int y1) {
|
||
if (!r->set) return 0;
|
||
return !(x1 < r->x0 || x0 > r->x1 || y1 < r->y0 || y0 > r->y1);
|
||
}
|
||
|
||
/* Add an uploaded rect (see s_up_rects). Merges ONLY when the merge adds no
|
||
* uncovered pixels: containment either way, or same row-band / column-band
|
||
* extension. On overflow: flush the pending set (order-preserving) when the
|
||
* device is up; pre-ready the CPU array is fully authoritative, so a union is
|
||
* harmless there. Mirrors the GL backend's up_add (20k-randomized-rect
|
||
* host-test-proven merge rule). */
|
||
static void up_add(int x0, int y0, int x1, int y1) {
|
||
if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0;
|
||
if (x1 > VRAM_W - 1) x1 = VRAM_W - 1;
|
||
if (y1 > VRAM_H - 1) y1 = VRAM_H - 1;
|
||
if (x0 > x1 || y0 > y1) return;
|
||
for (int i = 0; i < s_up_nrects; i++) {
|
||
DirtyRect *r = &s_up_rects[i];
|
||
if (x0 >= r->x0 && x1 <= r->x1 && y0 >= r->y0 && y1 <= r->y1)
|
||
return; /* contained */
|
||
if (x0 <= r->x0 && x1 >= r->x1 && y0 <= r->y0 && y1 >= r->y1) {
|
||
r->x0 = x0; r->y0 = y0; r->x1 = x1; r->y1 = y1; /* contains */
|
||
return;
|
||
}
|
||
if (y0 == r->y0 && y1 == r->y1 &&
|
||
x0 <= r->x1 + 1 && x1 >= r->x0 - 1) { /* same row-band extension */
|
||
if (x0 < r->x0) r->x0 = x0;
|
||
if (x1 > r->x1) r->x1 = x1;
|
||
return;
|
||
}
|
||
if (x0 == r->x0 && x1 == r->x1 &&
|
||
y0 <= r->y1 + 1 && y1 >= r->y0 - 1) { /* same column-band extension */
|
||
if (y0 < r->y0) r->y0 = y0;
|
||
if (y1 > r->y1) r->y1 = y1;
|
||
return;
|
||
}
|
||
}
|
||
if (s_up_nrects >= UP_RECTS_MAX) {
|
||
if (s_ready) {
|
||
flush_cpu_upload(); /* order-preserving: land the old ones */
|
||
} else {
|
||
DirtyRect *r = &s_up_rects[0];
|
||
for (int i = 1; i < s_up_nrects; i++) {
|
||
if (s_up_rects[i].x0 < r->x0) r->x0 = s_up_rects[i].x0;
|
||
if (s_up_rects[i].y0 < r->y0) r->y0 = s_up_rects[i].y0;
|
||
if (s_up_rects[i].x1 > r->x1) r->x1 = s_up_rects[i].x1;
|
||
if (s_up_rects[i].y1 > r->y1) r->y1 = s_up_rects[i].y1;
|
||
}
|
||
if (x0 < r->x0) r->x0 = x0;
|
||
if (y0 < r->y0) r->y0 = y0;
|
||
if (x1 > r->x1) r->x1 = x1;
|
||
if (y1 > r->y1) r->y1 = y1;
|
||
s_up_nrects = 1;
|
||
return;
|
||
}
|
||
}
|
||
DirtyRect *r = &s_up_rects[s_up_nrects++];
|
||
r->x0 = x0; r->y0 = y0; r->x1 = x1; r->y1 = y1; r->set = 1;
|
||
}
|
||
|
||
/* GP0(A0) transfer: per-pixel wrap means a wrapping transfer touches up to
|
||
* FOUR exact rects — never all of VRAM (the "wrapped: take all" union was the
|
||
* same stale-stomp class). Mirrors the GL backend's up_add_transfer. */
|
||
static void up_add_transfer(int x, int y, int w, int h) {
|
||
x &= VRAM_W - 1; y &= VRAM_H - 1;
|
||
if (w > VRAM_W) w = VRAM_W;
|
||
if (h > VRAM_H) h = VRAM_H;
|
||
if (w <= 0 || h <= 0) return;
|
||
int w1 = w, w2 = 0, h1 = h, h2 = 0;
|
||
if (x + w > VRAM_W) { w1 = VRAM_W - x; w2 = w - w1; }
|
||
if (y + h > VRAM_H) { h1 = VRAM_H - y; h2 = h - h1; }
|
||
up_add(x, y, x + w1 - 1, y + h1 - 1);
|
||
if (w2) up_add(0, y, w2 - 1, y + h1 - 1);
|
||
if (h2) up_add(x, 0, x + w1 - 1, h2 - 1);
|
||
if (w2 && h2) up_add(0, 0, w2 - 1, h2 - 1);
|
||
}
|
||
|
||
/* ---- helpers ----------------------------------------------------------- */
|
||
static void vk_log(const char *msg) { fprintf(stdout, "psxrecomp: vulkan: %s\n", msg); }
|
||
|
||
static int vk_die(const char *why) { fprintf(stdout, "psxrecomp: vulkan: %s\n", why); return 0; }
|
||
|
||
static uint32_t find_mem_type(uint32_t type_bits, VkMemoryPropertyFlags want) {
|
||
for (uint32_t i = 0; i < s_memprops.memoryTypeCount; i++) {
|
||
if ((type_bits & (1u << i)) &&
|
||
(s_memprops.memoryTypes[i].propertyFlags & want) == want)
|
||
return i;
|
||
}
|
||
return UINT32_MAX;
|
||
}
|
||
|
||
static void vk_gpu_sync_internal(void); /* drain queue + reclaim work pool/staging */
|
||
|
||
/* Coalesce work operations into one command buffer per sync interval. This
|
||
* preserves recording order while removing a queue submission per small GPU
|
||
* operation. */
|
||
static VkCommandBuffer s_work_cb;
|
||
|
||
static VkCommandBuffer begin_oneshot(void) {
|
||
s_perf_cur.oneshots++;
|
||
if (s_work_cb) return s_work_cb;
|
||
VkCommandBufferAllocateInfo ai = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO };
|
||
ai.commandPool = s_work_pool;
|
||
ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||
ai.commandBufferCount = 1;
|
||
VkCommandBuffer cb;
|
||
if (p_vkAllocateCommandBuffers(s_dev, &ai, &cb) != VK_SUCCESS) return VK_NULL_HANDLE;
|
||
VkCommandBufferBeginInfo bi = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
|
||
bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||
p_vkBeginCommandBuffer(cb, &bi);
|
||
s_work_cb = cb;
|
||
return cb;
|
||
}
|
||
|
||
/* Finish a logical operation but keep the shared command buffer open. */
|
||
static void end_oneshot(VkCommandBuffer cb) {
|
||
(void)cb;
|
||
s_work_pending = 1;
|
||
}
|
||
|
||
static void flush_work(void) {
|
||
if (!s_work_cb) return;
|
||
p_vkEndCommandBuffer(s_work_cb);
|
||
VkSubmitInfo si = { VK_STRUCTURE_TYPE_SUBMIT_INFO };
|
||
si.commandBufferCount = 1;
|
||
si.pCommandBuffers = &s_work_cb;
|
||
p_vkQueueSubmit(s_queue, 1, &si, VK_NULL_HANDLE);
|
||
s_perf_cur.submits++;
|
||
s_work_cb = VK_NULL_HANDLE;
|
||
}
|
||
|
||
/* Defer a staging buffer's destruction until the queue is next idle (its
|
||
* copy may still be in flight). */
|
||
static void defer_staging(VkBuffer buf, VkDeviceMemory mem) {
|
||
if (s_pending_n >= PENDING_STAGING_MAX)
|
||
vk_gpu_sync_internal(); /* ring full mid-frame: drain so we never leak */
|
||
s_pending_buf[s_pending_n] = buf; s_pending_mem[s_pending_n] = mem; s_pending_n++;
|
||
}
|
||
|
||
/* Drain all submitted work, then reclaim the work-CB pool and deferred staging.
|
||
* The single sync point for the deferred-submit model. */
|
||
static void vk_gpu_sync_internal(void) {
|
||
s_perf_cur.syncs++;
|
||
flush_work();
|
||
if (s_work_pending) {
|
||
uint64_t wait_start = SDL_GetPerformanceCounter();
|
||
p_vkQueueWaitIdle(s_queue);
|
||
s_perf_cur.wait_us += perf_elapsed_us(wait_start);
|
||
/* RELEASE_RESOURCES so the per-op command buffers allocated since the
|
||
* last sync are actually freed (a plain reset only recycles them, and
|
||
* begin_oneshot always allocates fresh -> unbounded growth). */
|
||
p_vkResetCommandPool(s_dev, s_work_pool, VK_COMMAND_POOL_RESET_RELEASE_RESOURCES_BIT);
|
||
s_work_pending = 0;
|
||
}
|
||
for (int i = 0; i < s_pending_n; i++) {
|
||
staging_release(s_pending_buf[i], s_pending_mem[i]);
|
||
}
|
||
s_pending_n = 0;
|
||
s_ds_blit_idx = 0;
|
||
/* Queue idle: every submitted draw has consumed its vertex-buffer region,
|
||
* so recycle the sub-allocation cursors. An OPEN (unflushed) batch's
|
||
* vertices move to offset 0 so its eventual draw stays contiguous. */
|
||
if (s_vmap) {
|
||
if (s_vcount && s_vbase)
|
||
memmove(s_vmap, s_vmap + s_vbase, (size_t)s_vcount * sizeof(Vert));
|
||
s_vbase = 0;
|
||
}
|
||
if (s_tmap) {
|
||
if (s_tcount && s_tbase)
|
||
memmove(s_tmap, s_tmap + s_tbase, (size_t)s_tcount * sizeof(TexVert));
|
||
s_tbase = 0;
|
||
}
|
||
}
|
||
#define gpu_sync() vk_gpu_sync_internal()
|
||
|
||
/* Image layout transition (full subresource, color aspect). */
|
||
static void img_barrier(VkCommandBuffer cb, VkImage img,
|
||
VkImageLayout from, VkImageLayout to,
|
||
VkAccessFlags src_access, VkAccessFlags dst_access,
|
||
VkPipelineStageFlags src_stage, VkPipelineStageFlags dst_stage) {
|
||
VkImageMemoryBarrier b = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
|
||
b.oldLayout = from; b.newLayout = to;
|
||
b.srcQueueFamilyIndex = b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||
b.image = img;
|
||
b.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||
b.subresourceRange.levelCount = 1;
|
||
b.subresourceRange.layerCount = 1;
|
||
b.srcAccessMask = src_access; b.dstAccessMask = dst_access;
|
||
p_vkCmdPipelineBarrier(cb, src_stage, dst_stage, 0, 0, NULL, 0, NULL, 1, &b);
|
||
}
|
||
|
||
/* Clear a swapchain image (TRANSFER_DST_OPTIMAL) to opaque black, then order
|
||
* that clear before the next transfer write to the same image. Validation
|
||
* reported SYNC-HAZARD-WRITE-AFTER-WRITE for clear -> blit without a barrier;
|
||
* on RDNA3 the clear overlapped the blit and tiles of the frame (24-bit FMV
|
||
* via vk_renderer_present_cpu) came out black. */
|
||
static void sc_clear_black(VkCommandBuffer cb, VkImage sc) {
|
||
VkClearColorValue black = {{0, 0, 0, 1}};
|
||
VkImageSubresourceRange rng = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
|
||
p_vkCmdClearColorImage(cb, sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &rng);
|
||
img_barrier(cb, sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
||
}
|
||
|
||
/* Map a layout to its (access mask, pipeline stage) for barriers. */
|
||
static void layout_access(VkImageLayout l, VkAccessFlags *acc, VkPipelineStageFlags *stage) {
|
||
switch (l) {
|
||
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
|
||
*acc = VK_ACCESS_TRANSFER_READ_BIT; *stage = VK_PIPELINE_STAGE_TRANSFER_BIT; break;
|
||
case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:
|
||
*acc = VK_ACCESS_TRANSFER_WRITE_BIT; *stage = VK_PIPELINE_STAGE_TRANSFER_BIT; break;
|
||
case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:
|
||
*acc = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||
*stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; break;
|
||
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
|
||
/* sampled in both the fragment (textured draw) and compute (pack)
|
||
* stages, so cover all commands rather than guess a single stage. */
|
||
*acc = VK_ACCESS_SHADER_READ_BIT; *stage = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT; break;
|
||
case VK_IMAGE_LAYOUT_GENERAL:
|
||
*acc = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
|
||
*stage = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT; break;
|
||
default:
|
||
*acc = 0; *stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; break;
|
||
}
|
||
}
|
||
|
||
/* Transition an arbitrary color-aspect image to `want` on cb, tracking layout
|
||
* through *cur. */
|
||
static void img_to(VkCommandBuffer cb, VkImage img, VkImageLayout *cur, VkImageLayout want) {
|
||
if (*cur == want) return;
|
||
VkAccessFlags sa, da; VkPipelineStageFlags ss, ds;
|
||
layout_access(*cur, &sa, &ss);
|
||
layout_access(want, &da, &ds);
|
||
img_barrier(cb, img, *cur, want, sa, da, ss, ds);
|
||
*cur = want;
|
||
}
|
||
|
||
/* Transition the VRAM image to `want` on cb, tracking its current layout. */
|
||
static void vram_to(VkCommandBuffer cb, VkImageLayout want) {
|
||
img_to(cb, s_vram_img, &s_vram_layout, want);
|
||
}
|
||
|
||
/* ---- loader bring-up --------------------------------------------------- */
|
||
static int load_global_funcs(void) {
|
||
#define LOAD(n) p_##n = (PFN_##n)p_vkGetInstanceProcAddr(NULL, #n); if (!p_##n) return vk_die("missing " #n);
|
||
VK_GLOBAL_FUNCS(LOAD)
|
||
#undef LOAD
|
||
return 1;
|
||
}
|
||
static int load_instance_funcs(void) {
|
||
#define LOAD(n) p_##n = (PFN_##n)p_vkGetInstanceProcAddr(s_instance, #n); if (!p_##n) return vk_die("missing " #n);
|
||
VK_INSTANCE_FUNCS(LOAD)
|
||
#undef LOAD
|
||
p_vkGetDeviceProcAddr = (PFN_vkGetDeviceProcAddr)
|
||
p_vkGetInstanceProcAddr(s_instance, "vkGetDeviceProcAddr");
|
||
return p_vkGetDeviceProcAddr ? 1 : vk_die("missing vkGetDeviceProcAddr");
|
||
}
|
||
static int load_device_funcs(void) {
|
||
#define LOAD(n) p_##n = (PFN_##n)p_vkGetDeviceProcAddr(s_dev, #n); if (!p_##n) return vk_die("missing " #n);
|
||
VK_DEVICE_FUNCS(LOAD)
|
||
#undef LOAD
|
||
return 1;
|
||
}
|
||
|
||
/* ---- instance / device / swapchain ------------------------------------- */
|
||
static int create_instance(void) {
|
||
unsigned ext_count = 0;
|
||
#if defined(PSX_SDL3)
|
||
const char *const *sdl_exts = SDL_Vulkan_GetInstanceExtensions(&ext_count);
|
||
if (!sdl_exts)
|
||
return vk_die("SDL_Vulkan_GetInstanceExtensions failed");
|
||
const char *exts[16];
|
||
if (ext_count > 16) ext_count = 16;
|
||
for (unsigned i = 0; i < ext_count; ++i) exts[i] = sdl_exts[i];
|
||
#else
|
||
if (!SDL_Vulkan_GetInstanceExtensions(s_win, &ext_count, NULL))
|
||
return vk_die("SDL_Vulkan_GetInstanceExtensions(count) failed");
|
||
const char *exts[16];
|
||
if (ext_count > 16) ext_count = 16;
|
||
if (!SDL_Vulkan_GetInstanceExtensions(s_win, &ext_count, exts))
|
||
return vk_die("SDL_Vulkan_GetInstanceExtensions(list) failed");
|
||
#endif
|
||
|
||
VkApplicationInfo app = { VK_STRUCTURE_TYPE_APPLICATION_INFO };
|
||
app.pApplicationName = "psxrecomp";
|
||
app.apiVersion = VK_API_VERSION_1_1;
|
||
VkInstanceCreateInfo ci = { VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO };
|
||
ci.pApplicationInfo = &app;
|
||
ci.enabledExtensionCount = ext_count;
|
||
ci.ppEnabledExtensionNames = exts;
|
||
if (p_vkCreateInstance(&ci, NULL, &s_instance) != VK_SUCCESS)
|
||
return vk_die("vkCreateInstance failed");
|
||
return 1;
|
||
}
|
||
|
||
static int pick_device(void) {
|
||
uint32_t n = 0;
|
||
p_vkEnumeratePhysicalDevices(s_instance, &n, NULL);
|
||
if (!n) return vk_die("no Vulkan physical devices");
|
||
if (n > 8) n = 8;
|
||
VkPhysicalDevice devs[8];
|
||
p_vkEnumeratePhysicalDevices(s_instance, &n, devs);
|
||
|
||
/* Prefer a discrete GPU with a graphics+present queue family. */
|
||
VkPhysicalDevice best = VK_NULL_HANDLE; uint32_t best_fam = 0; int best_score = -1;
|
||
for (uint32_t i = 0; i < n; i++) {
|
||
VkPhysicalDeviceProperties pr; p_vkGetPhysicalDeviceProperties(devs[i], &pr);
|
||
uint32_t qn = 0; p_vkGetPhysicalDeviceQueueFamilyProperties(devs[i], &qn, NULL);
|
||
if (qn > 16) qn = 16;
|
||
VkQueueFamilyProperties qf[16];
|
||
p_vkGetPhysicalDeviceQueueFamilyProperties(devs[i], &qn, qf);
|
||
for (uint32_t f = 0; f < qn; f++) {
|
||
VkBool32 present = VK_FALSE;
|
||
p_vkGetPhysicalDeviceSurfaceSupportKHR(devs[i], f, s_surface, &present);
|
||
if ((qf[f].queueFlags & VK_QUEUE_GRAPHICS_BIT) && present) {
|
||
int score = (pr.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) ? 2 : 1;
|
||
if (score > best_score) { best_score = score; best = devs[i]; best_fam = f; }
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (best == VK_NULL_HANDLE) return vk_die("no graphics+present queue family");
|
||
s_phys = best; s_qfam = best_fam;
|
||
p_vkGetPhysicalDeviceMemoryProperties(s_phys, &s_memprops);
|
||
VkPhysicalDeviceProperties pr; p_vkGetPhysicalDeviceProperties(s_phys, &pr);
|
||
fprintf(stdout, "psxrecomp: vulkan: device = %s\n", pr.deviceName);
|
||
return 1;
|
||
}
|
||
|
||
static int create_device(void) {
|
||
float prio = 1.0f;
|
||
VkDeviceQueueCreateInfo q = { VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO };
|
||
q.queueFamilyIndex = s_qfam; q.queueCount = 1; q.pQueuePriorities = &prio;
|
||
const char *dev_exts[] = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
|
||
VkDeviceCreateInfo ci = { VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO };
|
||
ci.queueCreateInfoCount = 1; ci.pQueueCreateInfos = &q;
|
||
ci.enabledExtensionCount = 1; ci.ppEnabledExtensionNames = dev_exts;
|
||
if (p_vkCreateDevice(s_phys, &ci, NULL, &s_dev) != VK_SUCCESS)
|
||
return vk_die("vkCreateDevice failed");
|
||
if (!load_device_funcs()) return 0;
|
||
p_vkGetDeviceQueue(s_dev, s_qfam, 0, &s_queue);
|
||
return 1;
|
||
}
|
||
|
||
static VkPresentModeKHR choose_present_mode(void) {
|
||
uint32_t n = 0;
|
||
p_vkGetPhysicalDeviceSurfacePresentModesKHR(s_phys, s_surface, &n, NULL);
|
||
if (n > 8) n = 8;
|
||
VkPresentModeKHR modes[8];
|
||
p_vkGetPhysicalDeviceSurfacePresentModesKHR(s_phys, s_surface, &n, modes);
|
||
/* The frontend already owns frame cadence. Prefer MAILBOX for tear-free
|
||
* delivery so FIFO does not introduce a second blocking pacer. */
|
||
VkPresentModeKHR want = (s_present_mode_req == 0)
|
||
? VK_PRESENT_MODE_IMMEDIATE_KHR
|
||
: VK_PRESENT_MODE_MAILBOX_KHR;
|
||
for (uint32_t i = 0; i < n; i++) if (modes[i] == want) return want;
|
||
return VK_PRESENT_MODE_FIFO_KHR; /* always supported */
|
||
}
|
||
|
||
static int create_swapchain(void) {
|
||
VkSurfaceCapabilitiesKHR caps;
|
||
p_vkGetPhysicalDeviceSurfaceCapabilitiesKHR(s_phys, s_surface, &caps);
|
||
|
||
uint32_t fn = 0;
|
||
p_vkGetPhysicalDeviceSurfaceFormatsKHR(s_phys, s_surface, &fn, NULL);
|
||
if (fn > 16) fn = 16;
|
||
VkSurfaceFormatKHR fmts[16];
|
||
p_vkGetPhysicalDeviceSurfaceFormatsKHR(s_phys, s_surface, &fn, fmts);
|
||
VkSurfaceFormatKHR chosen = fmts[0];
|
||
for (uint32_t i = 0; i < fn; i++) {
|
||
if (fmts[i].format == VK_FORMAT_B8G8R8A8_UNORM &&
|
||
fmts[i].colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { chosen = fmts[i]; break; }
|
||
}
|
||
s_sc_format = chosen.format;
|
||
|
||
VkExtent2D ext = caps.currentExtent;
|
||
if (ext.width == 0xFFFFFFFFu) {
|
||
int w = 0, h = 0;
|
||
#if defined(PSX_SDL3)
|
||
SDL_GetWindowSizeInPixels(s_win, &w, &h);
|
||
#else
|
||
SDL_Vulkan_GetDrawableSize(s_win, &w, &h);
|
||
#endif
|
||
ext.width = (uint32_t)w; ext.height = (uint32_t)h;
|
||
}
|
||
s_sc_extent = ext;
|
||
|
||
uint32_t img_count = caps.minImageCount + 1;
|
||
if (caps.maxImageCount && img_count > caps.maxImageCount) img_count = caps.maxImageCount;
|
||
|
||
VkSwapchainCreateInfoKHR ci = { VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR };
|
||
ci.surface = s_surface;
|
||
ci.minImageCount = img_count;
|
||
ci.imageFormat = chosen.format;
|
||
ci.imageColorSpace = chosen.colorSpace;
|
||
ci.imageExtent = ext;
|
||
ci.imageArrayLayers = 1;
|
||
ci.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||
ci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||
ci.preTransform = caps.currentTransform;
|
||
ci.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
|
||
ci.presentMode = choose_present_mode();
|
||
ci.clipped = VK_TRUE;
|
||
if (p_vkCreateSwapchainKHR(s_dev, &ci, NULL, &s_swapchain) != VK_SUCCESS)
|
||
return vk_die("vkCreateSwapchainKHR failed");
|
||
|
||
s_sc_count = 0;
|
||
p_vkGetSwapchainImagesKHR(s_dev, s_swapchain, &s_sc_count, NULL);
|
||
if (s_sc_count > 8) s_sc_count = 8;
|
||
p_vkGetSwapchainImagesKHR(s_dev, s_swapchain, &s_sc_count, s_sc_images);
|
||
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; }
|
||
}
|
||
|
||
static int create_sync(void) {
|
||
VkCommandBufferAllocateInfo ai = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO };
|
||
ai.commandPool = s_cmd_pool;
|
||
ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||
ai.commandBufferCount = VK_FRAMES;
|
||
if (p_vkAllocateCommandBuffers(s_dev, &ai, s_cmd) != VK_SUCCESS)
|
||
return vk_die("vkAllocateCommandBuffers failed");
|
||
VkSemaphoreCreateInfo si = { VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
|
||
VkFenceCreateInfo fi = { VK_STRUCTURE_TYPE_FENCE_CREATE_INFO };
|
||
fi.flags = VK_FENCE_CREATE_SIGNALED_BIT;
|
||
for (int i = 0; i < VK_FRAMES; i++) {
|
||
if (p_vkCreateSemaphore(s_dev, &si, NULL, &s_sem_acquire[i]) != VK_SUCCESS ||
|
||
p_vkCreateSemaphore(s_dev, &si, NULL, &s_sem_render[i]) != VK_SUCCESS ||
|
||
p_vkCreateFence(s_dev, &fi, NULL, &s_fence[i]) != VK_SUCCESS)
|
||
return vk_die("sync object creation failed");
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
/* The VRAM image: RGBA8, transfer src+dst (fills via clear, copies, present
|
||
* blit) + color attachment (geometry, Phase 2). Created in UNDEFINED, cleared
|
||
* to black, left in TRANSFER_SRC_OPTIMAL between frames. */
|
||
static int create_vram_image(void) {
|
||
VkImageCreateInfo ci = { VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
|
||
ci.imageType = VK_IMAGE_TYPE_2D;
|
||
ci.format = VK_FORMAT_R8G8B8A8_UNORM;
|
||
ci.extent.width = VRAM_W * s_scale;
|
||
ci.extent.height = VRAM_H * s_scale;
|
||
ci.extent.depth = 1;
|
||
ci.mipLevels = 1; ci.arrayLayers = 1;
|
||
ci.samples = VK_SAMPLE_COUNT_1_BIT;
|
||
ci.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||
ci.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
|
||
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||
ci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||
ci.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||
if (p_vkCreateImage(s_dev, &ci, NULL, &s_vram_img) != VK_SUCCESS)
|
||
return vk_die("vram image create failed");
|
||
|
||
VkMemoryRequirements req; p_vkGetImageMemoryRequirements(s_dev, s_vram_img, &req);
|
||
VkMemoryAllocateInfo mi = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
|
||
mi.allocationSize = req.size;
|
||
mi.memoryTypeIndex = find_mem_type(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||
if (mi.memoryTypeIndex == UINT32_MAX) return vk_die("no device-local memory");
|
||
if (p_vkAllocateMemory(s_dev, &mi, NULL, &s_vram_mem) != VK_SUCCESS)
|
||
return vk_die("vram memory alloc failed");
|
||
p_vkBindImageMemory(s_dev, s_vram_img, s_vram_mem, 0);
|
||
|
||
/* Clear to black and park in TRANSFER_SRC_OPTIMAL. */
|
||
VkCommandBuffer cb = begin_oneshot();
|
||
vram_to(cb, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||
VkClearColorValue black = {{0,0,0,0}};
|
||
VkImageSubresourceRange rng = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
|
||
p_vkCmdClearColorImage(cb, s_vram_img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &rng);
|
||
vram_to(cb, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||
end_oneshot(cb);
|
||
return 1;
|
||
}
|
||
|
||
/* ---- geometry pipeline (Phase 2) --------------------------------------- */
|
||
static VkShaderModule make_module(const uint32_t *code, uint32_t size) {
|
||
VkShaderModuleCreateInfo ci = { VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO };
|
||
ci.codeSize = size; ci.pCode = code;
|
||
VkShaderModule m = VK_NULL_HANDLE;
|
||
p_vkCreateShaderModule(s_dev, &ci, NULL, &m);
|
||
return m;
|
||
}
|
||
|
||
/* ---- push-constant blocks (must match the shaders' layouts) ------------- */
|
||
typedef struct { float shift, xoff, xhalf; } GeoPush; /* 12 bytes */
|
||
typedef struct {
|
||
float shift, xoff, xhalf; /* 0,4,8 */
|
||
int semipass, maskset, filter; /* 12,16,20 */
|
||
int _pad[2]; /* 24,28 -> ivec4 16-byte align */
|
||
int twin[4]; /* 32..47 */
|
||
} TexPush; /* 48 bytes */
|
||
typedef struct {
|
||
float shift; /* 0 */
|
||
int stp_pass; /* 4 */
|
||
int maskset; /* 8 */
|
||
int src_div; /* 12 */
|
||
int src_off[2]; /* 16,20 */
|
||
int _pad[2]; /* 24,28 -> ivec4 16-byte align */
|
||
int rect[4]; /* 32..47: x0,y0,x1,y1 native px */
|
||
} BlitPush; /* 48 bytes */
|
||
typedef struct { int scale, off_x, off_y; } PackPush; /* 12 bytes */
|
||
|
||
/* Create a device-local image + view (color aspect by default). */
|
||
static int make_image(VkFormat fmt, int w, int h, VkImageUsageFlags usage,
|
||
VkImageAspectFlags aspect,
|
||
VkImage *img, VkDeviceMemory *mem, VkImageView *view) {
|
||
VkImageCreateInfo ci = { VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
|
||
ci.imageType = VK_IMAGE_TYPE_2D; ci.format = fmt;
|
||
ci.extent.width = w; ci.extent.height = h; ci.extent.depth = 1;
|
||
ci.mipLevels = 1; ci.arrayLayers = 1; ci.samples = VK_SAMPLE_COUNT_1_BIT;
|
||
ci.tiling = VK_IMAGE_TILING_OPTIMAL; ci.usage = usage;
|
||
ci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; ci.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||
if (p_vkCreateImage(s_dev, &ci, NULL, img) != VK_SUCCESS) return vk_die("image create failed");
|
||
VkMemoryRequirements req; p_vkGetImageMemoryRequirements(s_dev, *img, &req);
|
||
VkMemoryAllocateInfo mi = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
|
||
mi.allocationSize = req.size;
|
||
mi.memoryTypeIndex = find_mem_type(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||
if (mi.memoryTypeIndex == UINT32_MAX || p_vkAllocateMemory(s_dev, &mi, NULL, mem) != VK_SUCCESS)
|
||
return vk_die("image memory alloc failed");
|
||
p_vkBindImageMemory(s_dev, *img, *mem, 0);
|
||
if (view) {
|
||
VkImageViewCreateInfo vi = { VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
|
||
vi.image = *img; vi.viewType = VK_IMAGE_VIEW_TYPE_2D; vi.format = fmt;
|
||
vi.subresourceRange.aspectMask = aspect;
|
||
vi.subresourceRange.levelCount = 1; vi.subresourceRange.layerCount = 1;
|
||
if (p_vkCreateImageView(s_dev, &vi, NULL, view) != VK_SUCCESS)
|
||
return vk_die("image view failed");
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
static VkFormat choose_ds_format(void) {
|
||
VkFormat cand[2] = { VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D32_SFLOAT_S8_UINT };
|
||
for (int i = 0; i < 2; i++) {
|
||
VkFormatProperties fp; p_vkGetPhysicalDeviceFormatProperties(s_phys, cand[i], &fp);
|
||
if (fp.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)
|
||
return cand[i];
|
||
}
|
||
return VK_FORMAT_D24_UNORM_S8_UINT;
|
||
}
|
||
|
||
static int create_buffer_mapped(VkDeviceSize size, VkBuffer *buf, VkDeviceMemory *mem, void **map) {
|
||
VkBufferCreateInfo bci = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
|
||
bci.size = size; bci.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
|
||
bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||
if (p_vkCreateBuffer(s_dev, &bci, NULL, buf) != VK_SUCCESS) return vk_die("vertex buffer failed");
|
||
VkMemoryRequirements req; p_vkGetBufferMemoryRequirements(s_dev, *buf, &req);
|
||
VkMemoryAllocateInfo mi = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
|
||
mi.allocationSize = req.size;
|
||
mi.memoryTypeIndex = find_mem_type(req.memoryTypeBits,
|
||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
||
if (mi.memoryTypeIndex == UINT32_MAX || p_vkAllocateMemory(s_dev, &mi, NULL, mem) != VK_SUCCESS)
|
||
return vk_die("vertex buffer memory failed");
|
||
p_vkBindBufferMemory(s_dev, *buf, *mem, 0);
|
||
p_vkMapMemory(s_dev, *mem, 0, size, 0, map);
|
||
return 1;
|
||
}
|
||
|
||
/* Build all render targets, samplers, descriptors, pipeline layouts and the
|
||
* pack compute pipeline. Graphics pipelines are built lazily by get_pipeline. */
|
||
static int create_render_targets(void) {
|
||
int S = s_scale;
|
||
|
||
/* RGBA8 hr render-target view */
|
||
VkImageViewCreateInfo vv = { VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
|
||
vv.image = s_vram_img; vv.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||
vv.format = VK_FORMAT_R8G8B8A8_UNORM;
|
||
vv.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||
vv.subresourceRange.levelCount = 1; vv.subresourceRange.layerCount = 1;
|
||
if (p_vkCreateImageView(s_dev, &vv, NULL, &s_vram_view) != VK_SUCCESS)
|
||
return vk_die("vram image view failed");
|
||
|
||
/* Native raw mirror (sampled + storage), depth/stencil, scratch, upload. */
|
||
if (!make_image(VK_FORMAT_R16_UINT, VRAM_W, VRAM_H,
|
||
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT |
|
||
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
|
||
VK_IMAGE_ASPECT_COLOR_BIT, &s_raw_img, &s_raw_mem, &s_raw_view)) return 0;
|
||
s_ds_format = choose_ds_format();
|
||
if (!make_image(s_ds_format, VRAM_W * S, VRAM_H * S,
|
||
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, DS_ASPECT,
|
||
&s_ds_img, &s_ds_mem, &s_ds_view)) return 0;
|
||
if (!make_image(VK_FORMAT_R8G8B8A8_UNORM, VRAM_W * S, VRAM_H * S,
|
||
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
|
||
VK_IMAGE_ASPECT_COLOR_BIT, &s_scratch_img, &s_scratch_mem, &s_scratch_view)) return 0;
|
||
if (!make_image(VK_FORMAT_R8G8B8A8_UNORM, VRAM_W, VRAM_H,
|
||
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
|
||
VK_IMAGE_ASPECT_COLOR_BIT, &s_up_img, &s_up_mem, &s_up_view)) return 0;
|
||
|
||
/* Clear raw mirror to 0 (park sampled) and stencil to 0 (park attachment). */
|
||
{
|
||
VkCommandBuffer cb = begin_oneshot();
|
||
img_to(cb, s_raw_img, &s_raw_layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||
VkClearColorValue zero = {{0,0,0,0}};
|
||
VkImageSubresourceRange crng = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
|
||
p_vkCmdClearColorImage(cb, s_raw_img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &zero, 1, &crng);
|
||
img_to(cb, s_raw_img, &s_raw_layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||
|
||
VkImageMemoryBarrier db = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
|
||
db.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; db.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||
db.srcQueueFamilyIndex = db.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||
db.image = s_ds_img; db.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||
db.subresourceRange.aspectMask = DS_ASPECT;
|
||
db.subresourceRange.levelCount = 1; db.subresourceRange.layerCount = 1;
|
||
p_vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||
0, 0, NULL, 0, NULL, 1, &db);
|
||
VkClearDepthStencilValue dsv = { 0.0f, 0 };
|
||
VkImageSubresourceRange srng = { DS_ASPECT, 0, 1, 0, 1 };
|
||
p_vkCmdClearDepthStencilImage(cb, s_ds_img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &dsv, 1, &srng);
|
||
db.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||
db.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||
db.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||
db.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
||
p_vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT, 0, 0, NULL, 0, NULL, 1, &db);
|
||
end_oneshot(cb);
|
||
}
|
||
|
||
/* Render pass: color (hr, load/store) + depth-stencil (stencil load/store). */
|
||
VkAttachmentDescription att[2] = {0};
|
||
att[0].format = VK_FORMAT_R8G8B8A8_UNORM; att[0].samples = VK_SAMPLE_COUNT_1_BIT;
|
||
att[0].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; att[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||
att[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; att[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
||
att[0].initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||
att[0].finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||
att[1].format = s_ds_format; att[1].samples = VK_SAMPLE_COUNT_1_BIT;
|
||
att[1].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; att[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
||
att[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; att[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||
att[1].initialLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||
att[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||
VkAttachmentReference cref = { 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
|
||
VkAttachmentReference dref = { 1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL };
|
||
VkSubpassDescription sub = {0};
|
||
sub.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||
sub.colorAttachmentCount = 1; sub.pColorAttachments = &cref;
|
||
sub.pDepthStencilAttachment = &dref;
|
||
VkRenderPassCreateInfo rpi = { VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO };
|
||
rpi.attachmentCount = 2; rpi.pAttachments = att;
|
||
rpi.subpassCount = 1; rpi.pSubpasses = ⊂
|
||
if (p_vkCreateRenderPass(s_dev, &rpi, NULL, &s_rpass) != VK_SUCCESS)
|
||
return vk_die("render pass failed");
|
||
|
||
VkImageView fbviews[2] = { s_vram_view, s_ds_view };
|
||
VkFramebufferCreateInfo fi = { VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO };
|
||
fi.renderPass = s_rpass; fi.attachmentCount = 2; fi.pAttachments = fbviews;
|
||
fi.width = VRAM_W * S; fi.height = VRAM_H * S; fi.layers = 1;
|
||
if (p_vkCreateFramebuffer(s_dev, &fi, NULL, &s_fbo) != VK_SUCCESS)
|
||
return vk_die("framebuffer failed");
|
||
|
||
/* Nearest sampler (texelFetch ignores filtering/addressing, but a combined
|
||
* image sampler still needs one). */
|
||
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_samp) != VK_SUCCESS) return vk_die("sampler failed");
|
||
|
||
/* Descriptor set layouts. */
|
||
{
|
||
VkDescriptorSetLayoutBinding b = { 0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1,
|
||
VK_SHADER_STAGE_FRAGMENT_BIT, NULL };
|
||
VkDescriptorSetLayoutCreateInfo ci = { VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO };
|
||
ci.bindingCount = 1; ci.pBindings = &b;
|
||
if (p_vkCreateDescriptorSetLayout(s_dev, &ci, NULL, &s_dsl_tex) != VK_SUCCESS) return vk_die("dsl tex");
|
||
if (p_vkCreateDescriptorSetLayout(s_dev, &ci, NULL, &s_dsl_blit) != VK_SUCCESS) return vk_die("dsl blit");
|
||
VkDescriptorSetLayoutBinding pb[2] = {
|
||
{ 0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, NULL },
|
||
{ 1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, NULL } };
|
||
ci.bindingCount = 2; ci.pBindings = pb;
|
||
if (p_vkCreateDescriptorSetLayout(s_dev, &ci, NULL, &s_dsl_pack) != VK_SUCCESS) return vk_die("dsl pack");
|
||
}
|
||
|
||
/* Descriptor pool + sets: ds_tex (1) + ds_pack (1) + blit ring (BLIT_DESC_RING). */
|
||
{
|
||
VkDescriptorPoolSize sizes[2] = {
|
||
{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, BLIT_DESC_RING + 2 },
|
||
{ VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1 } };
|
||
VkDescriptorPoolCreateInfo pci = { VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO };
|
||
pci.maxSets = BLIT_DESC_RING + 2; pci.poolSizeCount = 2; pci.pPoolSizes = sizes;
|
||
if (p_vkCreateDescriptorPool(s_dev, &pci, NULL, &s_dpool) != VK_SUCCESS) return vk_die("desc pool");
|
||
VkDescriptorSetLayout layouts[2] = { s_dsl_tex, s_dsl_pack };
|
||
VkDescriptorSet *sets[2] = { &s_ds_tex, &s_ds_pack };
|
||
for (int i = 0; i < 2; i++) {
|
||
VkDescriptorSetAllocateInfo ai = { VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO };
|
||
ai.descriptorPool = s_dpool; ai.descriptorSetCount = 1; ai.pSetLayouts = &layouts[i];
|
||
if (p_vkAllocateDescriptorSets(s_dev, &ai, sets[i]) != VK_SUCCESS) return vk_die("desc set alloc");
|
||
}
|
||
for (int i = 0; i < BLIT_DESC_RING; i++) {
|
||
VkDescriptorSetAllocateInfo ai = { VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO };
|
||
ai.descriptorPool = s_dpool; ai.descriptorSetCount = 1; ai.pSetLayouts = &s_dsl_blit;
|
||
if (p_vkAllocateDescriptorSets(s_dev, &ai, &s_ds_blit_ring[i]) != VK_SUCCESS) return vk_die("blit set alloc");
|
||
}
|
||
/* ds_tex: raw mirror sampled. ds_pack: hr sampled + raw storage. */
|
||
VkDescriptorImageInfo raw_smp = { s_samp, s_raw_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL };
|
||
VkDescriptorImageInfo hr_smp = { s_samp, s_vram_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL };
|
||
VkDescriptorImageInfo raw_st = { VK_NULL_HANDLE, s_raw_view, VK_IMAGE_LAYOUT_GENERAL };
|
||
VkWriteDescriptorSet w[3] = {0};
|
||
w[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; w[0].dstSet = s_ds_tex; w[0].dstBinding = 0;
|
||
w[0].descriptorCount = 1; w[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; w[0].pImageInfo = &raw_smp;
|
||
w[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; w[1].dstSet = s_ds_pack; w[1].dstBinding = 0;
|
||
w[1].descriptorCount = 1; w[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; w[1].pImageInfo = &hr_smp;
|
||
w[2].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; w[2].dstSet = s_ds_pack; w[2].dstBinding = 1;
|
||
w[2].descriptorCount = 1; w[2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; w[2].pImageInfo = &raw_st;
|
||
p_vkUpdateDescriptorSets(s_dev, 3, w, 0, NULL);
|
||
}
|
||
|
||
/* Pipeline layouts. */
|
||
{
|
||
VkPushConstantRange pr;
|
||
VkPipelineLayoutCreateInfo li = { VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO };
|
||
pr.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; pr.offset = 0; pr.size = sizeof(GeoPush);
|
||
li.pushConstantRangeCount = 1; li.pPushConstantRanges = ≺
|
||
if (p_vkCreatePipelineLayout(s_dev, &li, NULL, &s_pl_geo) != VK_SUCCESS) return vk_die("pl geo");
|
||
pr.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; pr.size = sizeof(TexPush);
|
||
li.setLayoutCount = 1; li.pSetLayouts = &s_dsl_tex;
|
||
if (p_vkCreatePipelineLayout(s_dev, &li, NULL, &s_pl_tex) != VK_SUCCESS) return vk_die("pl tex");
|
||
pr.size = sizeof(BlitPush); li.pSetLayouts = &s_dsl_blit;
|
||
if (p_vkCreatePipelineLayout(s_dev, &li, NULL, &s_pl_blit) != VK_SUCCESS) return vk_die("pl blit");
|
||
pr.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; pr.size = sizeof(PackPush); li.pSetLayouts = &s_dsl_pack;
|
||
if (p_vkCreatePipelineLayout(s_dev, &li, NULL, &s_pl_pack) != VK_SUCCESS) return vk_die("pl pack");
|
||
}
|
||
|
||
/* Shader modules (kept for lazy graphics-pipeline creation). */
|
||
s_mod_geo_v = make_module(spv_geo_vert, spv_geo_vert_size);
|
||
s_mod_geo_f = make_module(spv_geo_frag, spv_geo_frag_size);
|
||
s_mod_tex_v = make_module(spv_geo_tex_vert, spv_geo_tex_vert_size);
|
||
s_mod_tex_f = make_module(spv_geo_tex_frag, spv_geo_tex_frag_size);
|
||
s_mod_blit_v = make_module(spv_blit_vert, spv_blit_vert_size);
|
||
s_mod_blit_f = make_module(spv_blit_frag, spv_blit_frag_size);
|
||
if (!s_mod_geo_v || !s_mod_geo_f || !s_mod_tex_v || !s_mod_tex_f || !s_mod_blit_v || !s_mod_blit_f)
|
||
return vk_die("shader module failed");
|
||
|
||
/* Pack compute pipeline. */
|
||
{
|
||
VkShaderModule cm = make_module(spv_pack_comp, spv_pack_comp_size);
|
||
if (!cm) return vk_die("pack module failed");
|
||
VkComputePipelineCreateInfo ci = { VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO };
|
||
ci.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||
ci.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; ci.stage.module = cm; ci.stage.pName = "main";
|
||
ci.layout = s_pl_pack;
|
||
VkResult r = p_vkCreateComputePipelines(s_dev, VK_NULL_HANDLE, 1, &ci, NULL, &s_pipe_pack);
|
||
p_vkDestroyShaderModule(s_dev, cm, NULL);
|
||
if (r != VK_SUCCESS) return vk_die("pack pipeline failed");
|
||
}
|
||
|
||
/* Vertex buffers (untextured + textured). */
|
||
if (!create_buffer_mapped((VkDeviceSize)VK_VBUF_VERTS * sizeof(Vert),
|
||
&s_vbuf, &s_vbuf_mem, (void**)&s_vmap)) return 0;
|
||
if (!create_buffer_mapped((VkDeviceSize)VK_TBUF_VERTS * sizeof(TexVert),
|
||
&s_tbuf, &s_tbuf_mem, (void**)&s_tmap)) return 0;
|
||
gpu_sync(); /* complete the init clears (VRAM/raw/stencil) before any frame */
|
||
s_ready = 1;
|
||
/* CPU VRAM writes made before the context existed (facade routed to us
|
||
* since gr_init, s_ready was 0) accumulated only in the CPU array — seed
|
||
* a full-frame upload so the GPU images start coherent with it. */
|
||
up_add(0, 0, VRAM_W - 1, VRAM_H - 1);
|
||
flush_cpu_upload();
|
||
return 1;
|
||
}
|
||
|
||
/* Lazily build + cache a graphics pipeline for (program, topology, blend,
|
||
* stencil). prog 0 GEO / 1 TEX / 2 BLIT; topo 0 tri / 1 line; blend 0 off else
|
||
* PS1 mode+1; stencil 0 SET / 1 CHECK-keep / 2 CHECK-invert. */
|
||
static VkPipeline get_pipeline(int prog, int topo, int blend, int stencil, int cmask) {
|
||
int key = (((prog * PIPE_TOPOS + topo) * PIPE_BLENDS + blend) * PIPE_STENCILS + stencil)
|
||
* PIPE_CMASKS + cmask;
|
||
if (s_pipe_cache[key]) return s_pipe_cache[key];
|
||
|
||
VkShaderModule vs, fs; VkPipelineLayout layout;
|
||
VkVertexInputBindingDescription bind = {0};
|
||
VkVertexInputAttributeDescription attrs[9]; uint32_t nattr;
|
||
if (prog == 0) { /* GEO */
|
||
vs = s_mod_geo_v; fs = s_mod_geo_f; layout = s_pl_geo;
|
||
bind = (VkVertexInputBindingDescription){ 0, sizeof(Vert), VK_VERTEX_INPUT_RATE_VERTEX };
|
||
attrs[0] = (VkVertexInputAttributeDescription){ 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 };
|
||
attrs[1] = (VkVertexInputAttributeDescription){ 1, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 8 };
|
||
nattr = 2;
|
||
} else if (prog == 1) { /* TEX */
|
||
vs = s_mod_tex_v; fs = s_mod_tex_f; layout = s_pl_tex;
|
||
bind = (VkVertexInputBindingDescription){ 0, sizeof(TexVert), VK_VERTEX_INPUT_RATE_VERTEX };
|
||
attrs[0] = (VkVertexInputAttributeDescription){ 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 };
|
||
attrs[1] = (VkVertexInputAttributeDescription){ 1, 0, VK_FORMAT_R32G32_SFLOAT, 8 };
|
||
attrs[2] = (VkVertexInputAttributeDescription){ 2, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 16 };
|
||
attrs[3] = (VkVertexInputAttributeDescription){ 3, 0, VK_FORMAT_R32G32_SFLOAT, 32 };
|
||
attrs[4] = (VkVertexInputAttributeDescription){ 4, 0, VK_FORMAT_R32G32_SFLOAT, 40 };
|
||
attrs[5] = (VkVertexInputAttributeDescription){ 5, 0, VK_FORMAT_R32_SFLOAT, 48 };
|
||
attrs[6] = (VkVertexInputAttributeDescription){ 6, 0, VK_FORMAT_R32_SFLOAT, 52 };
|
||
attrs[7] = (VkVertexInputAttributeDescription){ 7, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 56 };
|
||
attrs[8] = (VkVertexInputAttributeDescription){ 8, 0, VK_FORMAT_R32_SFLOAT, 72 }; /* a_q */
|
||
nattr = 9;
|
||
} else { /* BLIT (vertex-less: rect from push constant) */
|
||
vs = s_mod_blit_v; fs = s_mod_blit_f; layout = s_pl_blit;
|
||
nattr = 0;
|
||
}
|
||
|
||
VkPipelineShaderStageCreateInfo stages[2] = {
|
||
{ VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO },
|
||
{ VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO } };
|
||
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vs; stages[0].pName = "main";
|
||
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fs; stages[1].pName = "main";
|
||
|
||
VkPipelineVertexInputStateCreateInfo vin = { VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO };
|
||
vin.vertexBindingDescriptionCount = nattr ? 1 : 0; vin.pVertexBindingDescriptions = nattr ? &bind : NULL;
|
||
vin.vertexAttributeDescriptionCount = nattr; vin.pVertexAttributeDescriptions = attrs;
|
||
|
||
VkPipelineInputAssemblyStateCreateInfo ia = { VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO };
|
||
ia.topology = topo == 1 ? VK_PRIMITIVE_TOPOLOGY_LINE_LIST : 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.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f;
|
||
|
||
VkPipelineMultisampleStateCreateInfo ms = { VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO };
|
||
ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||
|
||
/* Blend (PS1 modes). Alpha always replaced so the colour-alpha mask mirror
|
||
* is exact (src ONE / dst ZERO). */
|
||
VkPipelineColorBlendAttachmentState cba = {0};
|
||
cba.colorWriteMask = cmask ? 0 : 0xF;
|
||
float bc[4] = {1,1,1,1};
|
||
if (blend == 0) {
|
||
cba.blendEnable = VK_FALSE;
|
||
} else {
|
||
cba.blendEnable = VK_TRUE;
|
||
cba.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; cba.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
|
||
cba.alphaBlendOp = VK_BLEND_OP_ADD;
|
||
switch (blend - 1) {
|
||
case 0: /* B/2 + F/2 */
|
||
cba.srcColorBlendFactor = VK_BLEND_FACTOR_CONSTANT_ALPHA;
|
||
cba.dstColorBlendFactor = VK_BLEND_FACTOR_CONSTANT_ALPHA;
|
||
cba.colorBlendOp = VK_BLEND_OP_ADD; bc[0]=bc[1]=bc[2]=bc[3]=0.5f; break;
|
||
case 1: /* B + F */
|
||
cba.srcColorBlendFactor = VK_BLEND_FACTOR_ONE;
|
||
cba.dstColorBlendFactor = VK_BLEND_FACTOR_ONE; cba.colorBlendOp = VK_BLEND_OP_ADD; break;
|
||
case 2: /* B - F */
|
||
cba.srcColorBlendFactor = VK_BLEND_FACTOR_ONE;
|
||
cba.dstColorBlendFactor = VK_BLEND_FACTOR_ONE;
|
||
cba.colorBlendOp = VK_BLEND_OP_REVERSE_SUBTRACT; break;
|
||
case 3: /* B + F/4 */
|
||
cba.srcColorBlendFactor = VK_BLEND_FACTOR_CONSTANT_ALPHA;
|
||
cba.dstColorBlendFactor = VK_BLEND_FACTOR_ONE;
|
||
cba.colorBlendOp = VK_BLEND_OP_ADD; bc[0]=bc[1]=bc[2]=bc[3]=0.25f; break;
|
||
}
|
||
}
|
||
VkPipelineColorBlendStateCreateInfo cb = { VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO };
|
||
cb.attachmentCount = 1; cb.pAttachments = &cba;
|
||
cb.blendConstants[0]=bc[0]; cb.blendConstants[1]=bc[1]; cb.blendConstants[2]=bc[2]; cb.blendConstants[3]=bc[3];
|
||
|
||
/* Stencil = bit15 mirror (bit0). SET: always+replace, dyn ref. CHECK: test
|
||
* stored==0; write 1 via INVERT (stencil 2) or leave (stencil 1). */
|
||
VkStencilOpState st = {0};
|
||
st.compareMask = 0x01; st.writeMask = 0x01; st.reference = 0;
|
||
if (stencil == 0) { st.compareOp = VK_COMPARE_OP_ALWAYS; st.failOp = VK_STENCIL_OP_KEEP; st.passOp = VK_STENCIL_OP_REPLACE; st.depthFailOp = VK_STENCIL_OP_KEEP; }
|
||
else if (stencil == 1) { st.compareOp = VK_COMPARE_OP_EQUAL; st.failOp = VK_STENCIL_OP_KEEP; st.passOp = VK_STENCIL_OP_KEEP; st.depthFailOp = VK_STENCIL_OP_KEEP; }
|
||
else { st.compareOp = VK_COMPARE_OP_EQUAL; st.failOp = VK_STENCIL_OP_KEEP; st.passOp = VK_STENCIL_OP_INVERT; st.depthFailOp = VK_STENCIL_OP_KEEP; }
|
||
VkPipelineDepthStencilStateCreateInfo dss = { VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO };
|
||
dss.depthTestEnable = VK_FALSE; dss.depthWriteEnable = VK_FALSE;
|
||
dss.stencilTestEnable = VK_TRUE; dss.front = st; dss.back = st;
|
||
|
||
VkDynamicState dyn[3] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR, VK_DYNAMIC_STATE_STENCIL_REFERENCE };
|
||
VkPipelineDynamicStateCreateInfo dy = { VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO };
|
||
dy.dynamicStateCount = 3; dy.pDynamicStates = dyn;
|
||
|
||
VkGraphicsPipelineCreateInfo pi = { VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO };
|
||
pi.stageCount = 2; pi.pStages = stages;
|
||
pi.pVertexInputState = &vin; pi.pInputAssemblyState = &ia;
|
||
pi.pViewportState = &vp; pi.pRasterizationState = &rs;
|
||
pi.pMultisampleState = &ms; pi.pDepthStencilState = &dss; pi.pColorBlendState = &cb;
|
||
pi.pDynamicState = &dy; pi.layout = layout; pi.renderPass = s_rpass;
|
||
VkPipeline p = VK_NULL_HANDLE;
|
||
if (p_vkCreateGraphicsPipelines(s_dev, VK_NULL_HANDLE, 1, &pi, NULL, &p) != VK_SUCCESS) {
|
||
vk_die("graphics pipeline create failed"); return VK_NULL_HANDLE;
|
||
}
|
||
s_pipe_cache[key] = p;
|
||
return p;
|
||
}
|
||
|
||
/* PSX 1555 -> RGBA8 (bit15 mask -> alpha). */
|
||
static inline void rgb555_to_rgba8(uint16_t p, uint8_t out[4]) {
|
||
out[0] = (uint8_t)(((p) & 0x1F) << 3);
|
||
out[1] = (uint8_t)(((p >> 5) & 0x1F) << 3);
|
||
out[2] = (uint8_t)(((p >> 10) & 0x1F) << 3);
|
||
out[3] = (uint8_t)((p & 0x8000) ? 0xFF : 0x00);
|
||
}
|
||
static inline uint16_t rgba8_to_rgb555(const uint8_t in[4]) {
|
||
return (uint16_t)(((in[0] >> 3) & 0x1F) |
|
||
(((in[1] >> 3) & 0x1F) << 5) |
|
||
(((in[2] >> 3) & 0x1F) << 10) |
|
||
(in[3] >= 0x80 ? 0x8000 : 0));
|
||
}
|
||
|
||
int vk_renderer_init_context(SDL_Window *win) {
|
||
s_win = win;
|
||
#if defined(PSX_SDL3)
|
||
if (!SDL_Vulkan_LoadLibrary(NULL))
|
||
#else
|
||
if (SDL_Vulkan_LoadLibrary(NULL) != 0)
|
||
#endif
|
||
return vk_die("SDL_Vulkan_LoadLibrary failed (no Vulkan ICD?)");
|
||
p_vkGetInstanceProcAddr =
|
||
(PFN_vkGetInstanceProcAddr)SDL_Vulkan_GetVkGetInstanceProcAddr();
|
||
if (!p_vkGetInstanceProcAddr) return vk_die("no vkGetInstanceProcAddr from SDL");
|
||
|
||
if (!load_global_funcs()) return 0;
|
||
if (!create_instance()) return 0;
|
||
if (!load_instance_funcs()) return 0;
|
||
#if defined(PSX_SDL3)
|
||
if (!SDL_Vulkan_CreateSurface(s_win, s_instance, NULL, &s_surface))
|
||
#else
|
||
if (!SDL_Vulkan_CreateSurface(s_win, s_instance, &s_surface))
|
||
#endif
|
||
return vk_die("SDL_Vulkan_CreateSurface failed");
|
||
if (!pick_device()) return 0;
|
||
if (!create_device()) return 0;
|
||
|
||
VkCommandPoolCreateInfo pi = { VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO };
|
||
pi.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||
pi.queueFamilyIndex = s_qfam;
|
||
if (p_vkCreateCommandPool(s_dev, &pi, NULL, &s_cmd_pool) != VK_SUCCESS)
|
||
return vk_die("vkCreateCommandPool failed");
|
||
/* Work pool for one-shot ops, bulk-reset at gpu_sync (TRANSIENT: short-lived
|
||
* CBs). */
|
||
pi.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
|
||
if (p_vkCreateCommandPool(s_dev, &pi, NULL, &s_work_pool) != VK_SUCCESS)
|
||
return vk_die("vkCreateCommandPool(work) failed");
|
||
|
||
if (!create_swapchain()) return 0;
|
||
if (!create_sync()) return 0;
|
||
if (!create_vram_image()) return 0;
|
||
if (!create_render_targets()) return 0;
|
||
|
||
s_ctx_ok = 1;
|
||
vk_log("instance/device/swapchain/VRAM + raw mirror/pipelines ready");
|
||
return 1;
|
||
}
|
||
|
||
static void cpres_cache_free(void); /* fwd: CPU-present resource cache */
|
||
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 */
|
||
for (int i = 0; i < STAGING_CACHE_MAX; ++i) {
|
||
if (s_staging_cache[i].buf)
|
||
staging_destroy(s_staging_cache[i].buf, s_staging_cache[i].mem);
|
||
}
|
||
wide_free_all(); /* native-wide surfaces (color + DS + framebuffers) */
|
||
for (int i = 0; i < PIPE_CACHE_N; i++)
|
||
if (s_pipe_cache[i]) p_vkDestroyPipeline(s_dev, s_pipe_cache[i], NULL);
|
||
if (s_pipe_pack) p_vkDestroyPipeline(s_dev, s_pipe_pack, NULL);
|
||
if (s_mod_geo_v) p_vkDestroyShaderModule(s_dev, s_mod_geo_v, NULL);
|
||
if (s_mod_geo_f) p_vkDestroyShaderModule(s_dev, s_mod_geo_f, NULL);
|
||
if (s_mod_tex_v) p_vkDestroyShaderModule(s_dev, s_mod_tex_v, NULL);
|
||
if (s_mod_tex_f) p_vkDestroyShaderModule(s_dev, s_mod_tex_f, NULL);
|
||
if (s_mod_blit_v) p_vkDestroyShaderModule(s_dev, s_mod_blit_v, NULL);
|
||
if (s_mod_blit_f) p_vkDestroyShaderModule(s_dev, s_mod_blit_f, NULL);
|
||
if (s_pl_geo) p_vkDestroyPipelineLayout(s_dev, s_pl_geo, NULL);
|
||
if (s_pl_tex) p_vkDestroyPipelineLayout(s_dev, s_pl_tex, NULL);
|
||
if (s_pl_blit) p_vkDestroyPipelineLayout(s_dev, s_pl_blit, NULL);
|
||
if (s_pl_pack) p_vkDestroyPipelineLayout(s_dev, s_pl_pack, NULL);
|
||
if (s_dpool) p_vkDestroyDescriptorPool(s_dev, s_dpool, NULL);
|
||
if (s_dsl_tex) p_vkDestroyDescriptorSetLayout(s_dev, s_dsl_tex, NULL);
|
||
if (s_dsl_pack) p_vkDestroyDescriptorSetLayout(s_dev, s_dsl_pack, NULL);
|
||
if (s_dsl_blit) p_vkDestroyDescriptorSetLayout(s_dev, s_dsl_blit, NULL);
|
||
if (s_samp) p_vkDestroySampler(s_dev, s_samp, NULL);
|
||
if (s_fbo) p_vkDestroyFramebuffer(s_dev, s_fbo, NULL);
|
||
if (s_rpass) p_vkDestroyRenderPass(s_dev, s_rpass, NULL);
|
||
if (s_vram_view) p_vkDestroyImageView(s_dev, s_vram_view, NULL);
|
||
if (s_raw_view) p_vkDestroyImageView(s_dev, s_raw_view, NULL);
|
||
if (s_ds_view) p_vkDestroyImageView(s_dev, s_ds_view, NULL);
|
||
if (s_scratch_view)p_vkDestroyImageView(s_dev, s_scratch_view, NULL);
|
||
if (s_up_view) p_vkDestroyImageView(s_dev, s_up_view, NULL);
|
||
if (s_vbuf) p_vkDestroyBuffer(s_dev, s_vbuf, NULL);
|
||
if (s_vbuf_mem) p_vkFreeMemory(s_dev, s_vbuf_mem, NULL);
|
||
if (s_tbuf) p_vkDestroyBuffer(s_dev, s_tbuf, NULL);
|
||
if (s_tbuf_mem) p_vkFreeMemory(s_dev, s_tbuf_mem, NULL);
|
||
if (s_vram_img) { p_vkDestroyImage(s_dev, s_vram_img, NULL); s_vram_img = VK_NULL_HANDLE; }
|
||
if (s_vram_mem) { p_vkFreeMemory(s_dev, s_vram_mem, NULL); s_vram_mem = VK_NULL_HANDLE; }
|
||
if (s_raw_img) p_vkDestroyImage(s_dev, s_raw_img, NULL);
|
||
if (s_raw_mem) p_vkFreeMemory(s_dev, s_raw_mem, NULL);
|
||
if (s_ds_img) p_vkDestroyImage(s_dev, s_ds_img, NULL);
|
||
if (s_ds_mem) p_vkFreeMemory(s_dev, s_ds_mem, NULL);
|
||
if (s_scratch_img) p_vkDestroyImage(s_dev, s_scratch_img, NULL);
|
||
if (s_scratch_mem) p_vkFreeMemory(s_dev, s_scratch_mem, NULL);
|
||
if (s_up_img) p_vkDestroyImage(s_dev, s_up_img, NULL);
|
||
if (s_up_mem) p_vkFreeMemory(s_dev, s_up_mem, NULL);
|
||
for (int i = 0; i < VK_FRAMES; i++) {
|
||
if (s_sem_acquire[i]) p_vkDestroySemaphore(s_dev, s_sem_acquire[i], NULL);
|
||
if (s_sem_render[i]) p_vkDestroySemaphore(s_dev, s_sem_render[i], NULL);
|
||
if (s_fence[i]) p_vkDestroyFence(s_dev, s_fence[i], NULL);
|
||
}
|
||
destroy_swapchain();
|
||
if (s_work_pool) p_vkDestroyCommandPool(s_dev, s_work_pool, NULL);
|
||
if (s_cmd_pool) p_vkDestroyCommandPool(s_dev, s_cmd_pool, NULL);
|
||
p_vkDestroyDevice(s_dev, NULL); s_dev = VK_NULL_HANDLE;
|
||
if (s_surface) p_vkDestroySurfaceKHR(s_instance, s_surface, NULL);
|
||
if (s_instance) p_vkDestroyInstance(s_instance, NULL);
|
||
s_ctx_ok = 0;
|
||
}
|
||
|
||
void vk_renderer_set_present_mode(int mode) { s_present_mode_req = mode; }
|
||
|
||
/* ---- present ----------------------------------------------------------- */
|
||
/* Acquire a swapchain image, run `record` (which leaves it in
|
||
* PRESENT_SRC_KHR-ready state), submit, present. record may be NULL for a
|
||
* blank (the image is cleared to black). Returns 1 on success. */
|
||
static int acquire_present(VkImage *out_sc, VkCommandBuffer *out_cb,
|
||
uint32_t *out_idx, uint32_t *out_frame) {
|
||
uint32_t fr = s_frame_idx % VK_FRAMES;
|
||
p_vkWaitForFences(s_dev, 1, &s_fence[fr], VK_TRUE, UINT64_MAX);
|
||
uint32_t img_idx = 0;
|
||
uint64_t acquire_start = SDL_GetPerformanceCounter();
|
||
VkResult r = p_vkAcquireNextImageKHR(s_dev, s_swapchain, UINT64_MAX,
|
||
s_sem_acquire[fr], VK_NULL_HANDLE, &img_idx);
|
||
s_perf_cur.acquire_us += perf_elapsed_us(acquire_start);
|
||
if (r == VK_ERROR_OUT_OF_DATE_KHR) {
|
||
p_vkDeviceWaitIdle(s_dev);
|
||
destroy_swapchain();
|
||
if (!create_swapchain()) return 0;
|
||
return 0; /* skip this frame; next one uses the new swapchain */
|
||
}
|
||
if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) return 0;
|
||
p_vkResetFences(s_dev, 1, &s_fence[fr]);
|
||
p_vkResetCommandBuffer(s_cmd[fr], 0);
|
||
VkCommandBufferBeginInfo bi = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
|
||
bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||
p_vkBeginCommandBuffer(s_cmd[fr], &bi);
|
||
*out_sc = s_sc_images[img_idx];
|
||
*out_cb = s_cmd[fr];
|
||
*out_idx = img_idx;
|
||
*out_frame = fr;
|
||
return 1;
|
||
}
|
||
|
||
/* Copy host toast into the swapchain (must still be TRANSFER_DST). Opaque
|
||
* overwrite — host_osd bakes an opaque panel so no blend pass is needed. */
|
||
static VkBuffer s_osd_buf = VK_NULL_HANDLE;
|
||
static VkDeviceMemory s_osd_mem = VK_NULL_HANDLE;
|
||
static void *s_osd_map = NULL;
|
||
static VkDeviceSize s_osd_cap = 0;
|
||
|
||
static void osd_staging_free(void) {
|
||
if (s_osd_buf) {
|
||
staging_destroy(s_osd_buf, s_osd_mem);
|
||
s_osd_buf = VK_NULL_HANDLE;
|
||
s_osd_mem = VK_NULL_HANDLE;
|
||
}
|
||
s_osd_map = NULL;
|
||
s_osd_cap = 0;
|
||
}
|
||
|
||
static void vk_osd_copy_rect(VkCommandBuffer cb, VkImage sc,
|
||
const uint32_t *px, int w, int h,
|
||
int x, int y, VkDeviceSize buf_off) {
|
||
if (!px || w <= 0 || h <= 0) return;
|
||
int dw = w, dh = h;
|
||
if (x + dw > (int)s_sc_extent.width) dw = (int)s_sc_extent.width - x;
|
||
if (y + dh > (int)s_sc_extent.height) dh = (int)s_sc_extent.height - y;
|
||
if (dw < 1 || dh < 1) return;
|
||
memcpy((uint8_t *)s_osd_map + (size_t)buf_off, px,
|
||
(size_t)w * (size_t)h * 4u);
|
||
VkBufferImageCopy bc = {0};
|
||
bc.bufferOffset = buf_off;
|
||
bc.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||
bc.imageSubresource.layerCount = 1;
|
||
bc.imageOffset.x = x;
|
||
bc.imageOffset.y = y;
|
||
bc.imageExtent.width = (uint32_t)dw;
|
||
bc.imageExtent.height = (uint32_t)dh;
|
||
bc.imageExtent.depth = 1;
|
||
bc.bufferRowLength = (uint32_t)w;
|
||
bc.bufferImageHeight = (uint32_t)h;
|
||
p_vkCmdCopyBufferToImage(cb, s_osd_buf, sc,
|
||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &bc);
|
||
}
|
||
|
||
static void vk_osd_blit(VkCommandBuffer cb, VkImage sc) {
|
||
const uint32_t *text_px = NULL, *vol_px = NULL, *rw_px = NULL, *ssm_px = NULL;
|
||
int tw = 0, th = 0, vw = 0, vh = 0, rw = 0, rh = 0, sw = 0, sh = 0;
|
||
const int have_text = host_osd_image(&text_px, &tw, &th) && text_px;
|
||
const int have_vol = host_osd_volume_image(&vol_px, &vw, &vh) && vol_px;
|
||
const int have_rw = psx_rewind_overlay_image(&rw_px, &rw, &rh) && rw_px;
|
||
const int have_ssm =
|
||
psx_host_menu_overlay_image(&ssm_px, &sw, &sh) && ssm_px;
|
||
if (!have_text && !have_vol && !have_rw && !have_ssm) {
|
||
host_osd_present_done();
|
||
return;
|
||
}
|
||
/* Only BGRA swapchains match ARGB8888 LE packing used by host_osd. */
|
||
if (s_sc_format != VK_FORMAT_B8G8R8A8_UNORM &&
|
||
s_sc_format != VK_FORMAT_B8G8R8A8_SRGB) {
|
||
host_osd_present_done();
|
||
return;
|
||
}
|
||
VkDeviceSize text_bytes =
|
||
have_text ? (VkDeviceSize)tw * (VkDeviceSize)th * 4u : 0;
|
||
VkDeviceSize vol_bytes =
|
||
have_vol ? (VkDeviceSize)vw * (VkDeviceSize)vh * 4u : 0;
|
||
VkDeviceSize rw_bytes =
|
||
have_rw ? (VkDeviceSize)rw * (VkDeviceSize)rh * 4u : 0;
|
||
VkDeviceSize ssm_bytes =
|
||
have_ssm ? (VkDeviceSize)sw * (VkDeviceSize)sh * 4u : 0;
|
||
VkDeviceSize bytes = text_bytes + vol_bytes + rw_bytes + ssm_bytes;
|
||
if (bytes > s_osd_cap) {
|
||
p_vkQueueWaitIdle(s_queue);
|
||
osd_staging_free();
|
||
if (!make_staging(bytes, &s_osd_buf, &s_osd_mem, &s_osd_map)) {
|
||
host_osd_present_done();
|
||
return;
|
||
}
|
||
s_osd_cap = bytes;
|
||
}
|
||
const int margin = 8;
|
||
VkDeviceSize off = 0;
|
||
if (have_text) {
|
||
vk_osd_copy_rect(cb, sc, text_px, tw, th, margin, margin, off);
|
||
off += text_bytes;
|
||
}
|
||
if (have_vol) {
|
||
int x = ((int)s_sc_extent.width > vw + margin)
|
||
? ((int)s_sc_extent.width - vw - margin)
|
||
: margin;
|
||
int y = ((int)s_sc_extent.height > vh)
|
||
? (((int)s_sc_extent.height - vh) / 2)
|
||
: margin;
|
||
vk_osd_copy_rect(cb, sc, vol_px, vw, vh, x, y, off);
|
||
off += vol_bytes;
|
||
}
|
||
if (have_rw) {
|
||
float slide = psx_rewind_slide();
|
||
int x = ((int)s_sc_extent.width > rw)
|
||
? (((int)s_sc_extent.width - rw) / 2)
|
||
: 0;
|
||
int y = (int)s_sc_extent.height - (int)((float)rh * slide + 0.5f);
|
||
vk_osd_copy_rect(cb, sc, rw_px, rw, rh, x, y, off);
|
||
off += rw_bytes;
|
||
}
|
||
if (have_ssm) {
|
||
int x = ((int)s_sc_extent.width > sw)
|
||
? (((int)s_sc_extent.width - sw) / 2)
|
||
: 0;
|
||
int y = ((int)s_sc_extent.height > sh)
|
||
? (((int)s_sc_extent.height - sh) / 2)
|
||
: 0;
|
||
vk_osd_copy_rect(cb, sc, ssm_px, sw, sh, x, y, off);
|
||
}
|
||
host_osd_present_done();
|
||
}
|
||
|
||
static void submit_present(VkCommandBuffer cb, uint32_t img_idx, uint32_t fr);
|
||
|
||
static void finish_present(VkCommandBuffer cb, VkImage sc,
|
||
uint32_t img_idx, uint32_t fr) {
|
||
vk_osd_blit(cb, sc);
|
||
img_barrier(cb, sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
|
||
VK_ACCESS_TRANSFER_WRITE_BIT, 0,
|
||
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
|
||
submit_present(cb, img_idx, fr);
|
||
}
|
||
|
||
static void submit_present(VkCommandBuffer cb, uint32_t img_idx, uint32_t fr) {
|
||
p_vkEndCommandBuffer(cb);
|
||
/* Present command buffers write the acquired swapchain image with
|
||
* transfer barriers, clears, and blits. Waiting only at color attachment
|
||
* output does not order those operations after presentation-engine
|
||
* release on stricter drivers. */
|
||
VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_TRANSFER_BIT |
|
||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
||
VkSubmitInfo si = { VK_STRUCTURE_TYPE_SUBMIT_INFO };
|
||
si.waitSemaphoreCount = 1; si.pWaitSemaphores = &s_sem_acquire[fr];
|
||
si.pWaitDstStageMask = &wait_stage;
|
||
si.commandBufferCount = 1; si.pCommandBuffers = &cb;
|
||
si.signalSemaphoreCount = 1; si.pSignalSemaphores = &s_sem_render[fr];
|
||
p_vkQueueSubmit(s_queue, 1, &si, s_fence[fr]);
|
||
|
||
VkPresentInfoKHR pi = { VK_STRUCTURE_TYPE_PRESENT_INFO_KHR };
|
||
pi.waitSemaphoreCount = 1; pi.pWaitSemaphores = &s_sem_render[fr];
|
||
pi.swapchainCount = 1; pi.pSwapchains = &s_swapchain;
|
||
pi.pImageIndices = &img_idx;
|
||
uint64_t present_start = SDL_GetPerformanceCounter();
|
||
psx_video_telemetry_compose(PSX_VIDEO_VULKAN, s_scale, 0, 0,
|
||
(int)s_sc_extent.width, (int)s_sc_extent.height, 0, 0);
|
||
const VkResult present_result = p_vkQueuePresentKHR(s_queue, &pi);
|
||
psx_video_telemetry_present(present_result == VK_SUCCESS || present_result == VK_SUBOPTIMAL_KHR);
|
||
s_perf_cur.present_us += perf_elapsed_us(present_start);
|
||
s_frame_idx++;
|
||
}
|
||
|
||
/* Compute a letterbox rect for the swapchain given a source aspect. */
|
||
static void letterbox(int sw, int sh, int aw, int ah, VkOffset3D off[2]) {
|
||
/* center-fit aw:ah inside sw x sh */
|
||
int tw = sw, th = sw * ah / aw;
|
||
if (th > sh) { th = sh; tw = sh * aw / ah; }
|
||
int x = (sw - tw) / 2, y = (sh - th) / 2;
|
||
off[0].x = x; off[0].y = y; off[0].z = 0;
|
||
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;
|
||
flush_cpu_upload(); /* displayed VRAM may include pending CPU writes */
|
||
flush_tex_batch(); flush_geometry(); gpu_sync(); /* drain all draws; VRAM in TRANSFER_SRC */
|
||
VkImage sc; VkCommandBuffer cb; uint32_t idx, fr;
|
||
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_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
||
/* clear the swapchain (letterbox bars) to black */
|
||
sc_clear_black(cb, sc);
|
||
|
||
VkImageBlit blit = {0};
|
||
blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||
blit.srcSubresource.layerCount = 1;
|
||
blit.srcOffsets[0].x = disp_x * S; blit.srcOffsets[0].y = disp_y * S;
|
||
blit.srcOffsets[1].x = (disp_x + w) * S; blit.srcOffsets[1].y = (disp_y + h) * S;
|
||
blit.srcOffsets[1].z = 1;
|
||
blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||
blit.dstSubresource.layerCount = 1;
|
||
blit.dstOffsets[0] = dst[0]; blit.dstOffsets[1] = dst[1];
|
||
|
||
/* VRAM image is parked in TRANSFER_SRC_OPTIMAL. */
|
||
p_vkCmdBlitImage(cb, s_vram_img, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||
sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||
1, &blit, linear ? VK_FILTER_LINEAR : VK_FILTER_NEAREST);
|
||
|
||
finish_present(cb, sc, idx, fr);
|
||
perf_snapshot_present();
|
||
return 1;
|
||
}
|
||
|
||
void vk_renderer_present_blank(void) {
|
||
if (!s_ctx_ok) return;
|
||
flush_tex_batch(); flush_geometry(); gpu_sync();
|
||
VkImage sc; VkCommandBuffer cb; uint32_t idx, fr;
|
||
if (!acquire_present(&sc, &cb, &idx, &fr)) return;
|
||
img_barrier(cb, sc, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||
0, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
||
sc_clear_black(cb, sc);
|
||
finish_present(cb, sc, idx, fr);
|
||
perf_snapshot_present();
|
||
}
|
||
|
||
/* Present an ARGB8888 image (24-bit FMV / display-disabled clear) letterboxed.
|
||
* The pixels are 0xAARRGGBB; little-endian byte order [B,G,R,A] matches a
|
||
* B8G8R8A8_UNORM staging image, blitted straight to the swapchain. */
|
||
/* CPU-present resource cache (FMV path): the depth24 compose presents a new
|
||
* frame 15-24x/s for minutes; creating + destroying a VkImage and a staging
|
||
* buffer per frame is the same driver-allocation churn class the batched
|
||
* upload fix killed. Cache both keyed by (w,h) — zero allocations at steady
|
||
* state; the staging stays persistently mapped. Freed on resize + shutdown.
|
||
* The per-frame vkQueueWaitIdle below also makes reuse race-free (the copy
|
||
* from the staging has fully completed before the next frame rewrites it). */
|
||
static VkImage s_cpres_img = VK_NULL_HANDLE;
|
||
static VkDeviceMemory s_cpres_imem = VK_NULL_HANDLE;
|
||
static VkImageLayout s_cpres_layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||
static VkBuffer s_cpres_buf = VK_NULL_HANDLE;
|
||
static VkDeviceMemory s_cpres_mem = VK_NULL_HANDLE;
|
||
static void *s_cpres_map = NULL;
|
||
static int s_cpres_w = 0, s_cpres_h = 0;
|
||
|
||
static void cpres_cache_free(void) {
|
||
if (s_cpres_img) { p_vkDestroyImage(s_dev, s_cpres_img, NULL); s_cpres_img = VK_NULL_HANDLE; }
|
||
if (s_cpres_imem) { p_vkFreeMemory(s_dev, s_cpres_imem, NULL); s_cpres_imem = VK_NULL_HANDLE; }
|
||
if (s_cpres_buf) {
|
||
staging_destroy(s_cpres_buf, s_cpres_mem);
|
||
s_cpres_buf = VK_NULL_HANDLE;
|
||
s_cpres_mem = VK_NULL_HANDLE;
|
||
}
|
||
s_cpres_map = NULL; s_cpres_w = s_cpres_h = 0;
|
||
s_cpres_layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||
}
|
||
|
||
void vk_renderer_present_cpu(const uint32_t *pixels, int src_w, int src_h,
|
||
int linear, int force_4_3) {
|
||
(void)force_4_3;
|
||
if (!s_ctx_ok || !pixels || src_w <= 0 || src_h <= 0) { vk_renderer_present_blank(); return; }
|
||
flush_tex_batch(); flush_geometry(); gpu_sync();
|
||
|
||
if (s_cpres_w != src_w || s_cpres_h != src_h) {
|
||
p_vkQueueWaitIdle(s_queue); /* old resources may be in flight */
|
||
cpres_cache_free();
|
||
if (!make_staging((VkDeviceSize)src_w * src_h * 4,
|
||
&s_cpres_buf, &s_cpres_mem, &s_cpres_map)) {
|
||
vk_renderer_present_blank(); return;
|
||
}
|
||
if (!make_image(VK_FORMAT_B8G8R8A8_UNORM, src_w, src_h,
|
||
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
|
||
VK_IMAGE_ASPECT_COLOR_BIT, &s_cpres_img, &s_cpres_imem, NULL)) {
|
||
cpres_cache_free(); vk_renderer_present_blank(); return;
|
||
}
|
||
s_cpres_w = src_w; s_cpres_h = src_h;
|
||
s_cpres_layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||
}
|
||
memcpy(s_cpres_map, pixels, (size_t)src_w * src_h * 4);
|
||
VkBuffer buf = s_cpres_buf;
|
||
VkImage img = s_cpres_img;
|
||
|
||
VkImage sc; VkCommandBuffer cb; uint32_t idx, fr;
|
||
if (!acquire_present(&sc, &cb, &idx, &fr)) return;
|
||
img_to(cb, img, &s_cpres_layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||
VkBufferImageCopy bc = {0};
|
||
bc.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; bc.imageSubresource.layerCount = 1;
|
||
bc.imageExtent.width = src_w; bc.imageExtent.height = src_h; bc.imageExtent.depth = 1;
|
||
p_vkCmdCopyBufferToImage(cb, buf, img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &bc);
|
||
img_to(cb, img, &s_cpres_layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||
|
||
img_barrier(cb, sc, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||
0, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
||
sc_clear_black(cb, sc);
|
||
|
||
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);
|
||
/* Short GP1(07h) bands: letterbox inside the 4:3 rect (see GL present). */
|
||
if (src_h > 0 && src_h < 240) {
|
||
int box_h = dst[1].y - dst[0].y;
|
||
int content_h = (box_h * src_h) / 240;
|
||
if (content_h < 1) content_h = 1;
|
||
int y0 = dst[0].y + (box_h - content_h) / 2;
|
||
dst[0].y = y0;
|
||
dst[1].y = y0 + content_h;
|
||
}
|
||
VkImageBlit blit = {0};
|
||
blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; blit.srcSubresource.layerCount = 1;
|
||
blit.srcOffsets[1].x = src_w; blit.srcOffsets[1].y = src_h; blit.srcOffsets[1].z = 1;
|
||
blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; blit.dstSubresource.layerCount = 1;
|
||
blit.dstOffsets[0] = dst[0]; blit.dstOffsets[1] = dst[1];
|
||
p_vkCmdBlitImage(cb, img, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||
1, &blit, linear ? VK_FILTER_LINEAR : VK_FILTER_NEAREST);
|
||
finish_present(cb, sc, idx, fr);
|
||
|
||
/* Cached resources are rewritten next frame: wait for this frame's copy +
|
||
* blit to complete first (FMV cadence is 15-24 fps; one idle-wait per
|
||
* movie frame is cheap, and it is what makes the persistent staging safe). */
|
||
p_vkQueueWaitIdle(s_queue);
|
||
perf_snapshot_present();
|
||
}
|
||
|
||
/* GPU-direct native-wide present: blit the displayed buffer's wide surface
|
||
* band straight to the swapchain (no readback), mirroring
|
||
* gl_renderer_present_wide_fbo. Letterbox aspect generalises past 16:9:
|
||
* the 4:3 native frame widened by EXTRA presents at (4*wide_w : 3*native_w).
|
||
* Returns 0 when no wide surface exists for disp_x (caller falls back). */
|
||
int vk_renderer_present_wide(int disp_x, int disp_y, int disp_h, int linear) {
|
||
if (!s_ctx_ok || !s_ready || s_wide_w <= 0) return 0;
|
||
int i = -1;
|
||
for (int k = 0; k < VK_WIDE_MAX_SURF; k++)
|
||
if (s_wide_img[k] && s_wide_base[k] == disp_x) { i = k; break; }
|
||
if (i < 0) return 0;
|
||
flush_cpu_upload();
|
||
flush_tex_batch(); flush_geometry(); gpu_sync();
|
||
VkImage sc; VkCommandBuffer cb; uint32_t idx, fr;
|
||
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_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
||
sc_clear_black(cb, sc);
|
||
|
||
img_to(cb, s_wide_img[i], &s_wide_layout[i], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||
|
||
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;
|
||
VkImageBlit blit = {0};
|
||
blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||
blit.srcSubresource.layerCount = 1;
|
||
blit.srcOffsets[0].x = 0; blit.srcOffsets[0].y = sy0;
|
||
blit.srcOffsets[1].x = s_wide_w * S; blit.srcOffsets[1].y = sy1;
|
||
blit.srcOffsets[1].z = 1;
|
||
blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||
blit.dstSubresource.layerCount = 1;
|
||
blit.dstOffsets[0] = dst[0]; blit.dstOffsets[1] = dst[1];
|
||
p_vkCmdBlitImage(cb, s_wide_img[i], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||
sc, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||
1, &blit, linear ? VK_FILTER_LINEAR : VK_FILTER_NEAREST);
|
||
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;
|
||
}
|
||
|
||
void vk_renderer_sync_cpu(void) {
|
||
if (!s_ctx_ok) return;
|
||
ensure_cpu(); /* drain batches, pack hr->raw, copy raw->CPU mirror */
|
||
}
|
||
|
||
/* Dump the last `count` per-frame perf records as a JSON array (oldest..newest).
|
||
* Always-on; the debug-server "vk_perf" command reads this to find which op
|
||
* explodes during a transition stall (e.g. allocs jumping 10 -> thousands). */
|
||
int vk_perf_json(char *out, int cap, int count) {
|
||
uint32_t total = s_perf_head;
|
||
uint32_t n = ((uint32_t)count < total) ? (uint32_t)count : total;
|
||
if (n > VK_PERF_RING) n = VK_PERF_RING;
|
||
int o = snprintf(out, cap, "[");
|
||
for (uint32_t i = 0; i < n; i++) {
|
||
uint32_t k = total - n + i; /* frame index, oldest..newest */
|
||
VkPerf *p = &s_perf_ring[k % VK_PERF_RING];
|
||
o += snprintf(out + o, cap - o,
|
||
"%s{\"f\":%u,\"qpc\":%llu,\"wait_us\":%u,\"acquire_us\":%u,\"present_us\":%u,"
|
||
"\"alloc\":%u,\"alloc_kb\":%u,\"oneshot\":%u,\"submit\":%u,"
|
||
"\"sync\":%u,\"pack\":%u,\"blit\":%u,\"upload\":%u,\"copy\":%u,"
|
||
"\"geo\":%u,\"tex\":%u,\"wide\":%u,\"wclr\":%u}",
|
||
i ? "," : "",
|
||
p->present_idx, (unsigned long long)p->present_qpc,
|
||
p->wait_us, p->acquire_us, p->present_us,
|
||
p->allocs, p->alloc_kb, p->oneshots, p->submits,
|
||
p->syncs, p->pack_flushes, p->blits, p->upload_blocks, p->copy_rects,
|
||
p->geo_flushes, p->tex_flushes, p->wide_passes, p->wide_clears);
|
||
if (o >= cap - 256) break;
|
||
}
|
||
o += snprintf(out + o, cap - o, "]");
|
||
return o;
|
||
}
|
||
|
||
/* ---- render-pass + coherency helpers ----------------------------------- */
|
||
/* Half an HR pixel in native units, minus 1/64 to keep exact-edge samples off
|
||
* texel centres (see geo.vert / the GL backend's u_shift). */
|
||
static float px_shift(void) { return 0.5f / (float)s_scale - 1.0f / 64.0f; }
|
||
|
||
static void set_vp_full(VkCommandBuffer cb) {
|
||
VkViewport vp = { 0.f, 0.f, (float)(VRAM_W * s_scale), (float)(VRAM_H * s_scale), 0.f, 1.f };
|
||
p_vkCmdSetViewport(cb, 0, 1, &vp);
|
||
}
|
||
static void set_scissor_px(VkCommandBuffer cb, int x, int y, int w, int h) {
|
||
if (x < 0) { w += x; x = 0; } if (y < 0) { h += y; y = 0; }
|
||
if (x + w > VRAM_W) w = VRAM_W - x; if (y + h > VRAM_H) h = VRAM_H - y;
|
||
if (w < 0) w = 0; if (h < 0) h = 0;
|
||
VkRect2D sc = { { x * s_scale, y * s_scale }, { (uint32_t)(w * s_scale), (uint32_t)(h * s_scale) } };
|
||
p_vkCmdSetScissor(cb, 0, 1, &sc);
|
||
}
|
||
|
||
/* A same-layout transition is elided by img_to(), while the render pass has
|
||
* no external dependency. Explicitly order a preceding pass's attachment
|
||
* writes before a following LOAD/blend pass on the same color image. */
|
||
static void color_self_barrier(VkCommandBuffer cb, VkImage img) {
|
||
VkImageMemoryBarrier barrier = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
|
||
barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||
barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||
barrier.image = img;
|
||
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||
barrier.subresourceRange.levelCount = 1;
|
||
barrier.subresourceRange.layerCount = 1;
|
||
barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT |
|
||
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||
p_vkCmdPipelineBarrier(cb,
|
||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||
0, 0, NULL, 0, NULL, 1, &barrier);
|
||
}
|
||
|
||
static void begin_geo_pass(VkCommandBuffer cb) {
|
||
/* Explicit stencil ordering across one-shot submits: this pass both TESTS
|
||
* and WRITES the stencil (PSX mask bits) that a PRIOR submit's pass wrote.
|
||
* Cross-submit ordering previously rode on the COLOR image barriers'
|
||
* execution dependencies alone — no memory dependency covered the DS
|
||
* attachment, which is spec-fragile (worked on this NVIDIA driver, could
|
||
* produce mask-bit flicker elsewhere). A self-barrier on the stencil
|
||
* aspect (late-tests write -> early-tests read|write) makes prior stencil
|
||
* writes visible before this pass's tests, per submission order. */
|
||
VkImageMemoryBarrier db = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
|
||
db.oldLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||
db.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||
db.srcQueueFamilyIndex = db.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||
db.image = s_ds_img;
|
||
db.subresourceRange.aspectMask = DS_ASPECT;
|
||
db.subresourceRange.levelCount = 1;
|
||
db.subresourceRange.layerCount = 1;
|
||
db.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
||
db.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT |
|
||
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
||
p_vkCmdPipelineBarrier(cb,
|
||
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
|
||
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
|
||
0, 0, NULL, 0, NULL, 1, &db);
|
||
color_self_barrier(cb, s_vram_img);
|
||
VkRenderPassBeginInfo rp = { VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO };
|
||
rp.renderPass = s_rpass; rp.framebuffer = s_fbo;
|
||
rp.renderArea.extent.width = VRAM_W * s_scale;
|
||
rp.renderArea.extent.height = VRAM_H * s_scale;
|
||
p_vkCmdBeginRenderPass(cb, &rp, VK_SUBPASS_CONTENTS_INLINE);
|
||
}
|
||
/* Bind a graphics pipeline implementing GL's mask_stencil: check==0 -> SET
|
||
* (always+replace, ref=write_val); check!=0 -> test stored==0 and write 1 via
|
||
* INVERT (write_val!=0) or leave (write_val==0), ref=0. */
|
||
static void bind_masked(VkCommandBuffer cb, int prog, int topo, int blend, int check, int write_val) {
|
||
int stencil = !check ? 0 : (write_val ? 2 : 1);
|
||
VkPipeline p = get_pipeline(prog, topo, blend, stencil, 0);
|
||
p_vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, p);
|
||
p_vkCmdSetStencilReference(cb, VK_STENCIL_FACE_FRONT_AND_BACK, check ? 0u : (uint32_t)write_val);
|
||
}
|
||
/* Like bind_masked but with ALL colour writes disabled (stencil-only pass). */
|
||
static void bind_masked_stencil_only(VkCommandBuffer cb, int prog, int topo, int check, int write_val) {
|
||
int stencil = !check ? 0 : (write_val ? 2 : 1);
|
||
VkPipeline p = get_pipeline(prog, topo, 0, stencil, 1);
|
||
p_vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, p);
|
||
p_vkCmdSetStencilReference(cb, VK_STENCIL_FACE_FRONT_AND_BACK, check ? 0u : (uint32_t)write_val);
|
||
}
|
||
|
||
/* Host-visible staging buffer (TRANSFER_SRC). */
|
||
static int make_staging(VkDeviceSize bytes, VkBuffer *buf, VkDeviceMemory *mem, void **map) {
|
||
int best = -1;
|
||
for (int i = 0; i < STAGING_CACHE_MAX; ++i) {
|
||
StagingCacheEntry *entry = &s_staging_cache[i];
|
||
if (!entry->busy && entry->buf && entry->size >= bytes &&
|
||
(best < 0 || entry->size < s_staging_cache[best].size))
|
||
best = i;
|
||
}
|
||
if (best >= 0) {
|
||
StagingCacheEntry *entry = &s_staging_cache[best];
|
||
entry->busy = 1;
|
||
*buf = entry->buf;
|
||
*mem = entry->mem;
|
||
*map = entry->map;
|
||
return 1;
|
||
}
|
||
|
||
VkBufferCreateInfo bci = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
|
||
/* Staging buffers serve uploads (copy source) AND readbacks via
|
||
* vkCmdCopyImageToBuffer (copy destination). */
|
||
bci.size = bytes; bci.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||
bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||
if (p_vkCreateBuffer(s_dev, &bci, NULL, buf) != VK_SUCCESS) return 0;
|
||
VkMemoryRequirements req; p_vkGetBufferMemoryRequirements(s_dev, *buf, &req);
|
||
VkMemoryAllocateInfo mi = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
|
||
mi.allocationSize = req.size;
|
||
mi.memoryTypeIndex = find_mem_type(req.memoryTypeBits,
|
||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
||
if (mi.memoryTypeIndex == UINT32_MAX || p_vkAllocateMemory(s_dev, &mi, NULL, mem) != VK_SUCCESS) {
|
||
p_vkDestroyBuffer(s_dev, *buf, NULL); return 0;
|
||
}
|
||
p_vkBindBufferMemory(s_dev, *buf, *mem, 0);
|
||
if (p_vkMapMemory(s_dev, *mem, 0, bytes, 0, map) != VK_SUCCESS) {
|
||
p_vkDestroyBuffer(s_dev, *buf, NULL);
|
||
p_vkFreeMemory(s_dev, *mem, NULL);
|
||
return 0;
|
||
}
|
||
for (int i = 0; i < STAGING_CACHE_MAX; ++i) {
|
||
if (!s_staging_cache[i].buf) {
|
||
s_staging_cache[i].buf = *buf;
|
||
s_staging_cache[i].mem = *mem;
|
||
s_staging_cache[i].size = bytes;
|
||
s_staging_cache[i].map = *map;
|
||
s_staging_cache[i].busy = 1;
|
||
break;
|
||
}
|
||
}
|
||
s_perf_cur.allocs++;
|
||
s_perf_cur.alloc_kb += (uint32_t)(bytes / 1024);
|
||
return 1;
|
||
}
|
||
static void free_staging(VkBuffer buf, VkDeviceMemory mem) {
|
||
staging_release(buf, mem);
|
||
}
|
||
|
||
/* hr -> raw mirror: pack the dirty rect via the compute pass (top-left sample of
|
||
* each SxS block). Caller has already drained the batches that dirtied it. */
|
||
static void pack_flush(void) {
|
||
if (!s_ready || !s_pack_dirty.set) return;
|
||
s_perf_cur.pack_flushes++;
|
||
int x = s_pack_dirty.x0, y = s_pack_dirty.y0;
|
||
int w = s_pack_dirty.x1 - s_pack_dirty.x0 + 1;
|
||
int h = s_pack_dirty.y1 - s_pack_dirty.y0 + 1;
|
||
rect_clear(&s_pack_dirty);
|
||
VkCommandBuffer cb = begin_oneshot();
|
||
vram_to(cb, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); /* hr sampled */
|
||
img_to(cb, s_raw_img, &s_raw_layout, VK_IMAGE_LAYOUT_GENERAL); /* raw storage */
|
||
p_vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, s_pipe_pack);
|
||
p_vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_COMPUTE, s_pl_pack, 0, 1, &s_ds_pack, 0, NULL);
|
||
PackPush pp = { s_scale, x, y };
|
||
p_vkCmdPushConstants(cb, s_pl_pack, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof pp, &pp);
|
||
p_vkCmdDispatch(cb, (uint32_t)((w + 7) / 8), (uint32_t)((h + 7) / 8), 1);
|
||
img_to(cb, s_raw_img, &s_raw_layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||
vram_to(cb, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||
end_oneshot(cb);
|
||
}
|
||
|
||
/* Ensure the raw mirror is current for a textured draw sampling the given page
|
||
* / CLUT (mirrors the GL backend). */
|
||
static void flush_pack_if_sampling(int tpx, int tpy, int depth, int clx, int cly) {
|
||
if (!s_pack_dirty.set) return;
|
||
int page_w = depth == 0 ? 64 : depth == 1 ? 128 : 256;
|
||
if (rect_intersects(&s_pack_dirty, tpx, tpy, tpx + page_w - 1, tpy + 255)) {
|
||
flush_tex_batch(); pack_flush(); return;
|
||
}
|
||
if (depth <= 1) {
|
||
int n = depth == 0 ? 16 : 256;
|
||
if (rect_intersects(&s_pack_dirty, clx, cly, clx + n - 1, cly)) {
|
||
flush_tex_batch(); pack_flush();
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Blit a native [x,y,w,h] rect from src_img into the hr image (two STP passes,
|
||
* bit15 -> stencil mirror). plain=1 (CPU upload: SET stencil, src already final)
|
||
* or plain=0 (VRAM copy: honour mask set/check). src_div: 1 hr-res src, S native.
|
||
* Marks the dest pack-dirty when mark_pack (copies re-derive raw from hr). */
|
||
static void blit_region(int x, int y, int w, int h, VkImageView src_view,
|
||
VkImage src_img, VkImageLayout *src_layout,
|
||
int src_div, int sox, int soy, int plain, int mark_pack) {
|
||
if (w <= 0 || h <= 0) return;
|
||
s_perf_cur.blits++;
|
||
if (s_ds_blit_idx >= BLIT_DESC_RING) gpu_sync(); /* ring exhausted: drain, reset idx */
|
||
VkDescriptorSet ds = s_ds_blit_ring[s_ds_blit_idx++];
|
||
VkDescriptorImageInfo ii = { s_samp, src_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL };
|
||
VkWriteDescriptorSet wr = { VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET };
|
||
wr.dstSet = ds; wr.dstBinding = 0; wr.descriptorCount = 1;
|
||
wr.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; wr.pImageInfo = ⅈ
|
||
p_vkUpdateDescriptorSets(s_dev, 1, &wr, 0, NULL);
|
||
|
||
VkCommandBuffer cb = begin_oneshot();
|
||
img_to(cb, src_img, src_layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||
vram_to(cb, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||
begin_geo_pass(cb);
|
||
set_vp_full(cb); set_scissor_px(cb, x, y, w, h);
|
||
p_vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, s_pl_blit, 0, 1, &ds, 0, NULL);
|
||
BlitPush bp = {0};
|
||
bp.shift = px_shift(); bp.maskset = plain ? 0 : s_mask_set; bp.src_div = src_div;
|
||
bp.src_off[0] = sox; bp.src_off[1] = soy;
|
||
bp.rect[0] = x; bp.rect[1] = y; bp.rect[2] = x + w; bp.rect[3] = y + h;
|
||
int chk = plain ? 0 : s_mask_check;
|
||
/* pass 1: bit15=0 texels, write stencil 0 (or honour mask set) */
|
||
bind_masked(cb, 2, 0, 0, chk, plain ? 0 : s_mask_set);
|
||
bp.stp_pass = 1;
|
||
p_vkCmdPushConstants(cb, s_pl_blit, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof bp, &bp);
|
||
p_vkCmdDraw(cb, 6, 1, 0, 0);
|
||
/* pass 2: bit15=1 texels, write stencil 1 */
|
||
bind_masked(cb, 2, 0, 0, chk, 1);
|
||
bp.stp_pass = 2;
|
||
p_vkCmdPushConstants(cb, s_pl_blit, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof bp, &bp);
|
||
p_vkCmdDraw(cb, 6, 1, 0, 0);
|
||
p_vkCmdEndRenderPass(cb);
|
||
vram_to(cb, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||
end_oneshot(cb);
|
||
if (mark_pack) rect_add(&s_pack_dirty, x, y, x + w - 1, y + h - 1);
|
||
s_gpu_dirty = 1;
|
||
}
|
||
|
||
/* ---- VRAM transfers (CPU <-> GPU image) -------------------------------- */
|
||
/* Upload a w x h block of 1555 pixels at (x,y). The raw mirror takes the exact
|
||
* 1555 bits directly (CLUT indices must survive), and the hr render target is
|
||
* updated via a blit so the stencil bit15 mirror stays exact. */
|
||
static void vram_upload_block(int x, int y, int w, int h, const uint16_t *data) {
|
||
if (w <= 0 || h <= 0 || !s_ready) return;
|
||
s_perf_cur.upload_blocks++;
|
||
flush_tex_batch(); flush_geometry();
|
||
|
||
/* 1) raw mirror: native 1555, exact. */
|
||
VkBuffer rbuf; VkDeviceMemory rmem; void *rmap;
|
||
if (make_staging((VkDeviceSize)w * h * 2, &rbuf, &rmem, &rmap)) {
|
||
memcpy(rmap, data, (size_t)w * h * 2);
|
||
VkCommandBuffer cb = begin_oneshot();
|
||
img_to(cb, s_raw_img, &s_raw_layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||
VkBufferImageCopy rc = {0};
|
||
rc.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; rc.imageSubresource.layerCount = 1;
|
||
rc.imageOffset.x = x; rc.imageOffset.y = y;
|
||
rc.imageExtent.width = w; rc.imageExtent.height = h; rc.imageExtent.depth = 1;
|
||
p_vkCmdCopyBufferToImage(cb, rbuf, s_raw_img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &rc);
|
||
img_to(cb, s_raw_img, &s_raw_layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||
end_oneshot(cb);
|
||
defer_staging(rbuf, rmem); /* copy in flight; freed at next gpu_sync */
|
||
}
|
||
|
||
/* 2) native RGBA8 staging -> upload image. */
|
||
VkBuffer ubuf; VkDeviceMemory umem; void *umap;
|
||
if (make_staging((VkDeviceSize)w * h * 4, &ubuf, &umem, &umap)) {
|
||
uint8_t *m = (uint8_t*)umap;
|
||
for (int i = 0; i < w * h; i++) rgb555_to_rgba8(data[i], m + (size_t)i * 4);
|
||
VkCommandBuffer cb = begin_oneshot();
|
||
img_to(cb, s_up_img, &s_up_layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||
VkBufferImageCopy uc = {0};
|
||
uc.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; uc.imageSubresource.layerCount = 1;
|
||
uc.imageOffset.x = x; uc.imageOffset.y = y;
|
||
uc.imageExtent.width = w; uc.imageExtent.height = h; uc.imageExtent.depth = 1;
|
||
p_vkCmdCopyBufferToImage(cb, ubuf, s_up_img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &uc);
|
||
end_oneshot(cb);
|
||
defer_staging(ubuf, umem); /* copy in flight; freed at next gpu_sync */
|
||
/* 3) blit upload image -> hr (native src: src_div=S, off 0). */
|
||
blit_region(x, y, w, h, s_up_view, s_up_img, &s_up_layout, s_scale, 0, 0, 1, 0);
|
||
}
|
||
}
|
||
|
||
/* CPU -> GPU: flush accumulated CPU-side VRAM writes as ONE upload (see
|
||
* s_up_rects). vram_upload_block drains the draw batches first, so PS1
|
||
* submission order is preserved: every draw appended before these writes has
|
||
* landed by the time the upload writes VRAM, and every GPU-op entry point
|
||
* calls this before touching VRAM so the writes land before later ops. */
|
||
static void flush_cpu_upload(void) {
|
||
if (s_up_nrects == 0 || !s_ready || !s_vram) return;
|
||
/* Snapshot + clear first (re-entrancy safe: up_add overflow calls back). */
|
||
DirtyRect rects[UP_RECTS_MAX];
|
||
int nrects = s_up_nrects;
|
||
memcpy(rects, s_up_rects, (size_t)nrects * sizeof(DirtyRect));
|
||
s_up_nrects = 0;
|
||
|
||
flush_tex_batch(); flush_geometry(); /* PS1 order: queued draws land first */
|
||
s_perf_cur.upload_blocks++;
|
||
|
||
/* COALESCED upload: exactly TWO queue submits per flush regardless of rect
|
||
* count (one for the buffer->image copies, one render pass for the hr
|
||
* blits). The naive per-rect vram_upload_block/blit_region loop paid
|
||
* 2 staging allocs + 3 submits PER RECT — with the exact-rect list feeding
|
||
* MDEC macroblock streams that re-created the boot-wedge submit churn the
|
||
* batched-upload fix existed to kill (measured 0.7 fps at the Tomba2
|
||
* Whoopee logo). Rect data is packed back-to-back in ONE 1555 staging +
|
||
* ONE RGBA8 staging, addressed via VkBufferImageCopy bufferOffset. */
|
||
size_t total = 0;
|
||
for (int i = 0; i < nrects; i++)
|
||
total += vk_upload_aligned_texels(
|
||
(size_t)(rects[i].x1 - rects[i].x0 + 1) *
|
||
(size_t)(rects[i].y1 - rects[i].y0 + 1));
|
||
if (total == 0) return;
|
||
|
||
VkBuffer rbuf; VkDeviceMemory rmem; void *rmap;
|
||
if (!make_staging((VkDeviceSize)total * 2, &rbuf, &rmem, &rmap)) return;
|
||
VkBuffer ubuf; VkDeviceMemory umem; void *umap;
|
||
if (!make_staging((VkDeviceSize)total * 4, &ubuf, &umem, &umap)) {
|
||
defer_staging(rbuf, rmem);
|
||
return;
|
||
}
|
||
|
||
VkBufferImageCopy rcopies[UP_RECTS_MAX], ucopies[UP_RECTS_MAX];
|
||
memset(rcopies, 0, sizeof rcopies); memset(ucopies, 0, sizeof ucopies);
|
||
size_t texoff = 0;
|
||
uint16_t *rdst = (uint16_t *)rmap;
|
||
uint8_t *udst = (uint8_t *)umap;
|
||
for (int i = 0; i < nrects; i++) {
|
||
int x = rects[i].x0, y = rects[i].y0;
|
||
int w = rects[i].x1 - rects[i].x0 + 1;
|
||
int h = rects[i].y1 - rects[i].y0 + 1;
|
||
for (int row = 0; row < h; row++) {
|
||
const uint16_t *src = s_vram + (size_t)(y + row) * VRAM_W + x;
|
||
memcpy(rdst + texoff + (size_t)row * w, src, (size_t)w * 2);
|
||
uint8_t *m = udst + (texoff + (size_t)row * w) * 4;
|
||
for (int col = 0; col < w; col++)
|
||
rgb555_to_rgba8(src[col], m + (size_t)col * 4);
|
||
}
|
||
rcopies[i].bufferOffset = (VkDeviceSize)texoff * 2;
|
||
rcopies[i].imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||
rcopies[i].imageSubresource.layerCount = 1;
|
||
rcopies[i].imageOffset.x = x; rcopies[i].imageOffset.y = y;
|
||
rcopies[i].imageExtent.width = w; rcopies[i].imageExtent.height = h;
|
||
rcopies[i].imageExtent.depth = 1;
|
||
ucopies[i] = rcopies[i];
|
||
ucopies[i].bufferOffset = (VkDeviceSize)texoff * 4;
|
||
/* Padding prevents an odd-sized R16 rect from leaving all following
|
||
* VkBufferImageCopy offsets at two modulo four bytes. */
|
||
texoff += vk_upload_aligned_texels((size_t)w * h);
|
||
}
|
||
|
||
/* Submit 1: both image copy sets. */
|
||
VkCommandBuffer cb = begin_oneshot();
|
||
img_to(cb, s_raw_img, &s_raw_layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||
p_vkCmdCopyBufferToImage(cb, rbuf, s_raw_img,
|
||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||
(uint32_t)nrects, rcopies);
|
||
img_to(cb, s_raw_img, &s_raw_layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||
img_to(cb, s_up_img, &s_up_layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||
p_vkCmdCopyBufferToImage(cb, ubuf, s_up_img,
|
||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||
(uint32_t)nrects, ucopies);
|
||
end_oneshot(cb);
|
||
defer_staging(rbuf, rmem);
|
||
defer_staging(ubuf, umem);
|
||
|
||
/* Submit 2: one render pass, N scissored 2-draw blits upload-image -> hr
|
||
* (plain blit semantics, matching vram_upload_block's blit_region call:
|
||
* mask writes off, no pack mark — uploaded content IS the CPU data). */
|
||
if (s_ds_blit_idx >= BLIT_DESC_RING) gpu_sync();
|
||
VkDescriptorSet ds = s_ds_blit_ring[s_ds_blit_idx++];
|
||
VkDescriptorImageInfo ii = { s_samp, s_up_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL };
|
||
VkWriteDescriptorSet wr = { VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET };
|
||
wr.dstSet = ds; wr.dstBinding = 0; wr.descriptorCount = 1;
|
||
wr.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; wr.pImageInfo = ⅈ
|
||
p_vkUpdateDescriptorSets(s_dev, 1, &wr, 0, NULL);
|
||
cb = begin_oneshot();
|
||
img_to(cb, s_up_img, &s_up_layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||
vram_to(cb, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||
begin_geo_pass(cb);
|
||
set_vp_full(cb);
|
||
p_vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, s_pl_blit, 0, 1, &ds, 0, NULL);
|
||
for (int i = 0; i < nrects; i++) {
|
||
int x = rects[i].x0, y = rects[i].y0;
|
||
int w = rects[i].x1 - rects[i].x0 + 1;
|
||
int h = rects[i].y1 - rects[i].y0 + 1;
|
||
set_scissor_px(cb, x, y, w, h);
|
||
s_perf_cur.blits++;
|
||
BlitPush bp = {0};
|
||
bp.shift = px_shift(); bp.maskset = 0; bp.src_div = s_scale;
|
||
bp.src_off[0] = 0; bp.src_off[1] = 0;
|
||
bp.rect[0] = x; bp.rect[1] = y; bp.rect[2] = x + w; bp.rect[3] = y + h;
|
||
bind_masked(cb, 2, 0, 0, 0, 0);
|
||
bp.stp_pass = 1;
|
||
p_vkCmdPushConstants(cb, s_pl_blit, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof bp, &bp);
|
||
p_vkCmdDraw(cb, 6, 1, 0, 0);
|
||
bind_masked(cb, 2, 0, 0, 0, 1);
|
||
bp.stp_pass = 2;
|
||
p_vkCmdPushConstants(cb, s_pl_blit, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof bp, &bp);
|
||
p_vkCmdDraw(cb, 6, 1, 0, 0);
|
||
}
|
||
p_vkCmdEndRenderPass(cb);
|
||
vram_to(cb, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||
end_oneshot(cb);
|
||
s_gpu_dirty = 1;
|
||
}
|
||
|
||
/* GPU -> CPU mirror: drain batches, pack, copy the whole raw mirror down. */
|
||
static void ensure_cpu(void) {
|
||
extern int psx_netplay_active(void);
|
||
if (!s_ready || !s_gpu_dirty || !s_vram) return;
|
||
if (psx_netplay_active()) {
|
||
s_gpu_dirty = 0;
|
||
return;
|
||
}
|
||
flush_cpu_upload(); /* readback overwrites s_vram: pending writes land first */
|
||
flush_tex_batch(); flush_geometry();
|
||
/* Readback must reflect ALL current hr content, not just the incremental
|
||
* s_pack_dirty rect. pack_flush() packs only that rect and early-returns if
|
||
* it is unset — so draws that updated hr (and present) but never marked the
|
||
* pack rect (e.g. the BIOS shell after the logo) left the raw mirror stale,
|
||
* and screenshots/guest VRAM reads returned an old frame (the logo) while
|
||
* the live window showed the shell. Force a full-frame pack here; this path
|
||
* is readback-only (NOT the per-frame present path), so the cost is paid
|
||
* only on screenshots / guest VRAM reads, never every frame. */
|
||
rect_add(&s_pack_dirty, 0, 0, VRAM_W - 1, VRAM_H - 1);
|
||
pack_flush();
|
||
VkBuffer buf; VkDeviceMemory mem; void *map;
|
||
if (!make_staging((VkDeviceSize)VRAM_W * VRAM_H * 2, &buf, &mem, &map)) return;
|
||
VkCommandBuffer cb = begin_oneshot();
|
||
img_to(cb, s_raw_img, &s_raw_layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||
VkBufferImageCopy rc = {0};
|
||
rc.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; rc.imageSubresource.layerCount = 1;
|
||
rc.imageExtent.width = VRAM_W; rc.imageExtent.height = VRAM_H; rc.imageExtent.depth = 1;
|
||
p_vkCmdCopyImageToBuffer(cb, s_raw_img, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, buf, 1, &rc);
|
||
img_to(cb, s_raw_img, &s_raw_layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||
end_oneshot(cb);
|
||
gpu_sync(); /* the readback result must be complete before we read it */
|
||
memcpy(s_vram, map, (size_t)VRAM_W * VRAM_H * 2);
|
||
free_staging(buf, mem);
|
||
s_gpu_dirty = 0;
|
||
}
|
||
|
||
/* ---- backend vtable ---------------------------------------------------- */
|
||
/* Also initialize the software renderer on the SAME VRAM array so a failed
|
||
* Vulkan context creation can fall back to software cleanly (mirrors how the GL
|
||
* backend's init calls sw_renderer_init). When Vulkan is active the SW mirror
|
||
* is just the CPU readback target. */
|
||
static void vkb_init(uint16_t *vram) { s_vram = vram; s_cpu_dirty = 0; sw_renderer_init(vram); }
|
||
static void vkb_set_scale(int scale) { if (scale >= 1 && scale <= 4) s_scale = scale; }
|
||
static int vkb_scale(void) { return s_scale; }
|
||
static void vkb_set_texture_filter(int b) { s_texfilter = b ? 1 : 0; }
|
||
static int vkb_texture_filter(void) { return s_texfilter; }
|
||
|
||
static void vkb_set_draw_area(int x1, int y1, int x2, int y2) {
|
||
/* Scissor is per-batch (one render pass uses one scissor), so drain both
|
||
* independent geometry batches before the clip rect changes. Otherwise
|
||
* earlier textured GP0 primitives render with the next draw area's scissor. */
|
||
if (x1 != s_da_x1 || y1 != s_da_y1 || x2 != s_da_x2 || y2 != s_da_y2) {
|
||
flush_tex_batch();
|
||
flush_geometry();
|
||
}
|
||
s_da_x1 = x1; s_da_y1 = y1; s_da_x2 = x2; s_da_y2 = y2;
|
||
}
|
||
static void vkb_get_draw_area(int *x1, int *y1, int *x2, int *y2) {
|
||
if (x1) *x1 = s_da_x1; if (y1) *y1 = s_da_y1; if (x2) *x2 = s_da_x2; if (y2) *y2 = s_da_y2;
|
||
}
|
||
static void vkb_set_draw_offset(int x, int y) { s_off_x = x; s_off_y = y; }
|
||
|
||
/* GP0 02h fill: set a VRAM rect to a solid 1555 colour. The write lands in
|
||
* the CPU array immediately (like every CPU-side VRAM write) and rides the
|
||
* batched s_up_rects upload — the old per-fill staging upload was part of
|
||
* the per-op alloc/submit churn. */
|
||
static void vkb_fill_rect(int x, int y, int w, int h, uint16_t color) {
|
||
sw_fill_rect(x, y, w, h, color);
|
||
if (!s_ctx_ok) return;
|
||
if (x < 0) { w += x; x = 0; } if (y < 0) { h += y; y = 0; }
|
||
if (x + w > VRAM_W) w = VRAM_W - x; if (y + h > VRAM_H) h = VRAM_H - y;
|
||
if (w <= 0 || h <= 0) return;
|
||
up_add(x, y, x + w - 1, y + h - 1);
|
||
}
|
||
|
||
/* Same policy as GL: skip framebuffer-sized depth24 (RGB888) transfers only;
|
||
* still stage smaller texture A0s. On leave clear skipped FB union — never
|
||
* restage packed RGB888 as 1555. */
|
||
static int s_depth24_skip_up = 0;
|
||
static DirtyRect s_d24_skip_fb;
|
||
|
||
static int depth24_is_fb_transfer(int x, int y, int w, int h) {
|
||
if (!gpu_display_is_depth24() || w <= 0 || h <= 0) return 0;
|
||
GpuDisplayInfo di;
|
||
gpu_get_display_info(&di);
|
||
int fb_w = (int)((di.width * 3u + 1u) / 2u);
|
||
int fb_h = (int)di.height;
|
||
if (fb_w < 8) fb_w = 8;
|
||
if (fb_h < 1) fb_h = 1;
|
||
/* See GL: 256×256 pages must not match the half-FB area heuristic. */
|
||
if (w <= 256 && h <= 256) return 0;
|
||
{
|
||
int dx = (int)(di.display_x & 1023u);
|
||
int dy = (int)(di.display_y & 511u);
|
||
int x0 = x & (VRAM_W - 1), y0 = y & (VRAM_H - 1);
|
||
if (x0 + w <= dx || x0 >= dx + fb_w || y0 + h <= dy || y0 >= dy + fb_h)
|
||
return 0;
|
||
}
|
||
if (h >= fb_h - 8 && h <= fb_h + 16 && w >= (fb_w * 3) / 4) return 1;
|
||
if ((int64_t)w * (int64_t)h >= ((int64_t)fb_w * fb_h) / 2) return 1;
|
||
return 0;
|
||
}
|
||
|
||
static void depth24_mark_scanout_band(void) {
|
||
GpuDisplayInfo di;
|
||
int fb_w, fb_h, x0, y0, x1, y1;
|
||
if (!gpu_display_is_depth24()) return;
|
||
gpu_get_display_info(&di);
|
||
fb_w = (int)((di.width * 3u + 1u) / 2u);
|
||
fb_h = (int)di.height;
|
||
if (fb_w < 8) fb_w = 8;
|
||
if (fb_h < 1) fb_h = 1;
|
||
x0 = (int)(di.display_x & 1023u);
|
||
y0 = (int)(di.display_y & 511u);
|
||
x1 = x0 + fb_w - 1;
|
||
y1 = y0 + fb_h - 1;
|
||
if (x1 > VRAM_W - 1) x1 = VRAM_W - 1;
|
||
if (y1 > VRAM_H - 1) y1 = VRAM_H - 1;
|
||
rect_add(&s_d24_skip_fb, x0, y0, x1, y1);
|
||
}
|
||
|
||
static void depth24_clear_skipped_fb(void) {
|
||
/* GL scissor-clears the skipped FB union. VK keeps texture uploads that
|
||
* landed during depth24; the FB bands are overwritten by the next 15-bit
|
||
* draws. (A full-image ClearColorImage would wipe those textures.) */
|
||
rect_clear(&s_d24_skip_fb);
|
||
}
|
||
|
||
static void depth24_upload_policy(void) {
|
||
int d24 = gpu_display_is_depth24();
|
||
if (d24 && !s_depth24_skip_up) {
|
||
s_up_nrects = 0;
|
||
rect_clear(&s_d24_skip_fb);
|
||
} else if (!d24 && s_depth24_skip_up) {
|
||
s_up_nrects = 0;
|
||
depth24_clear_skipped_fb();
|
||
gpu_depth24_upload_span_reset();
|
||
}
|
||
s_depth24_skip_up = d24;
|
||
}
|
||
|
||
static void vkb_vram_transfer_in(int x, int y, int w, int h, const uint16_t *data) {
|
||
sw_vram_transfer_in(x, y, w, h, data);
|
||
if (!s_ctx_ok) return;
|
||
depth24_upload_policy();
|
||
if (s_depth24_skip_up && depth24_is_fb_transfer(x, y, w, h)) {
|
||
/* Full-VRAM savestate restore: stage FBO; mark scanout band only. */
|
||
if (w >= VRAM_W && h >= VRAM_H) {
|
||
up_add_transfer(x, y, w, h);
|
||
rect_clear(&s_d24_skip_fb);
|
||
depth24_mark_scanout_band();
|
||
return;
|
||
}
|
||
depth24_mark_scanout_band();
|
||
return;
|
||
}
|
||
up_add_transfer(x, y, w, h); /* exact touched rects, incl. per-pixel wrap */
|
||
}
|
||
|
||
void vk_renderer_restage_vram_after_savestate(void) {
|
||
if (!s_ctx_ok || !s_vram) return;
|
||
s_up_nrects = 0;
|
||
rect_clear(&s_d24_skip_fb);
|
||
s_depth24_skip_up = 0;
|
||
up_add_transfer(0, 0, VRAM_W, VRAM_H);
|
||
flush_cpu_upload();
|
||
if (gpu_display_is_depth24()) {
|
||
s_depth24_skip_up = 1;
|
||
depth24_mark_scanout_band();
|
||
}
|
||
}
|
||
static void vkb_vram_transfer_out(int x, int y, int w, int h, uint16_t *data) {
|
||
ensure_cpu(); /* sync GPU-rendered content down to the CPU mirror first */
|
||
for (int row = 0; row < h; row++)
|
||
for (int col = 0; col < w; col++)
|
||
data[row * w + col] = s_vram ? s_vram[(y + row) * VRAM_W + (x + col)] : 0;
|
||
}
|
||
static void vkb_vram_write(int x, int y, uint16_t pixel) {
|
||
sw_vram_write(x, y, pixel);
|
||
if (!s_ctx_ok) return;
|
||
depth24_upload_policy();
|
||
up_add(x & (VRAM_W - 1), y & (VRAM_H - 1),
|
||
x & (VRAM_W - 1), y & (VRAM_H - 1));
|
||
}
|
||
static uint16_t vkb_vram_read(int x, int y) {
|
||
ensure_cpu(); /* gated by s_gpu_dirty; cheap when nothing was drawn */
|
||
return s_vram ? s_vram[y * VRAM_W + x] : 0;
|
||
}
|
||
|
||
/* render_display: CPU readout path (screenshots / debug server / the 24-bit
|
||
* FMV present). Sync the GPU-authoritative VRAM down, then read out via the
|
||
* software renderer on the shared array — mirrors the GL backend exactly. */
|
||
static int vkb_render_display(uint32_t *o, int p, int dx, int dy, int dw, int dh) {
|
||
ensure_cpu();
|
||
return sw_render_display(o, p, dx, dy, dw, dh);
|
||
}
|
||
static int vkb_render_display_hires(uint32_t *o, int p, int dx, int dy, int dw, int dh) {
|
||
ensure_cpu();
|
||
return sw_render_display_hires(o, p, dx, dy, dw, dh);
|
||
}
|
||
|
||
/* ---- geometry batch (Phase 2) ------------------------------------------ */
|
||
/* Set the full-image viewport and the draw-area scissor (in framebuffer px). */
|
||
static void set_vp_scissor(VkCommandBuffer cb) {
|
||
int S = s_scale;
|
||
VkViewport vp = { 0.f, 0.f, (float)(VRAM_W * S), (float)(VRAM_H * S), 0.f, 1.f };
|
||
p_vkCmdSetViewport(cb, 0, 1, &vp);
|
||
int x1 = s_da_x1 < 0 ? 0 : s_da_x1, y1 = s_da_y1 < 0 ? 0 : s_da_y1;
|
||
int x2 = s_da_x2 >= VRAM_W ? VRAM_W - 1 : s_da_x2;
|
||
int y2 = s_da_y2 >= VRAM_H ? VRAM_H - 1 : s_da_y2;
|
||
int w = x2 - x1 + 1, h = y2 - y1 + 1;
|
||
if (w < 0) w = 0; if (h < 0) h = 0;
|
||
VkRect2D sc = { { x1 * S, y1 * S }, { (uint32_t)(w * S), (uint32_t)(h * S) } };
|
||
p_vkCmdSetScissor(cb, 0, 1, &sc);
|
||
}
|
||
|
||
/* ---- native-wide compositor helpers ------------------------------------- */
|
||
/* X-translation (native px) from canonical VRAM space into the active wide
|
||
* surface: local_x = vram_x - base_x + OFFSET. Same as GL/SW wide_dx(). */
|
||
static int view_enabled, view_shift, view_pad_left, view_pad_right;
|
||
static int wide_dx(void) { return s_wide_offset + view_shift - s_wide_cur_base; }
|
||
|
||
static void wide_free_all(void) {
|
||
int any = 0;
|
||
for (int i = 0; i < VK_WIDE_MAX_SURF; i++) any |= (s_wide_img[i] != VK_NULL_HANDLE);
|
||
if (any) p_vkQueueWaitIdle(s_queue); /* surfaces may be referenced in flight */
|
||
for (int i = 0; i < VK_WIDE_MAX_SURF; i++) {
|
||
if (s_wide_fb[i]) { p_vkDestroyFramebuffer(s_dev, s_wide_fb[i], NULL); s_wide_fb[i] = VK_NULL_HANDLE; }
|
||
if (s_wide_view[i]) { p_vkDestroyImageView(s_dev, s_wide_view[i], NULL); s_wide_view[i] = VK_NULL_HANDLE; }
|
||
if (s_wide_img[i]) { p_vkDestroyImage(s_dev, s_wide_img[i], NULL); s_wide_img[i] = VK_NULL_HANDLE; }
|
||
if (s_wide_mem[i]) { p_vkFreeMemory(s_dev, s_wide_mem[i], NULL); s_wide_mem[i] = VK_NULL_HANDLE; }
|
||
if (s_wide_ds_view[i]) { p_vkDestroyImageView(s_dev, s_wide_ds_view[i], NULL); s_wide_ds_view[i] = VK_NULL_HANDLE; }
|
||
if (s_wide_ds_img[i]) { p_vkDestroyImage(s_dev, s_wide_ds_img[i], NULL); s_wide_ds_img[i] = VK_NULL_HANDLE; }
|
||
if (s_wide_ds_mem[i]) { p_vkFreeMemory(s_dev, s_wide_ds_mem[i], NULL); s_wide_ds_mem[i] = VK_NULL_HANDLE; }
|
||
s_wide_layout[i] = VK_IMAGE_LAYOUT_UNDEFINED;
|
||
s_wide_base[i] = -1;
|
||
}
|
||
s_wide_cur = -1;
|
||
}
|
||
|
||
/* Find (or lazily create) the wide surface for base_x. Returns the surface
|
||
* index or -1. A new surface = RGBA8 color image (attachment + blit src/dst)
|
||
* + its own stencil image + a framebuffer on the SHARED render pass; color is
|
||
* cleared black, stencil to 0, then both are parked in the layouts the render
|
||
* pass expects (color/DS ATTACHMENT_OPTIMAL). */
|
||
static int wide_surf_for(int base_x) {
|
||
if (s_wide_w <= 0 || !s_ready) return -1;
|
||
for (int i = 0; i < VK_WIDE_MAX_SURF; i++)
|
||
if (s_wide_img[i] && s_wide_base[i] == base_x) return i;
|
||
for (int i = 0; i < VK_WIDE_MAX_SURF; i++) {
|
||
if (s_wide_img[i]) continue;
|
||
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_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,
|
||
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
|
||
DS_ASPECT, &s_wide_ds_img[i], &s_wide_ds_mem[i], &s_wide_ds_view[i]))
|
||
return -1;
|
||
VkImageView views[2] = { s_wide_view[i], s_wide_ds_view[i] };
|
||
VkFramebufferCreateInfo fi = { VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO };
|
||
fi.renderPass = s_rpass; fi.attachmentCount = 2; fi.pAttachments = views;
|
||
fi.width = (uint32_t)w; fi.height = (uint32_t)h; fi.layers = 1;
|
||
if (p_vkCreateFramebuffer(s_dev, &fi, NULL, &s_wide_fb[i]) != VK_SUCCESS)
|
||
return -1;
|
||
/* Clear + park in attachment layouts (mirrors the s_ds init chain). */
|
||
VkCommandBuffer cb = begin_oneshot();
|
||
s_wide_layout[i] = VK_IMAGE_LAYOUT_UNDEFINED;
|
||
img_to(cb, s_wide_img[i], &s_wide_layout[i], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||
VkClearColorValue black = {{0, 0, 0, 0}};
|
||
VkImageSubresourceRange crng = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
|
||
p_vkCmdClearColorImage(cb, s_wide_img[i], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &crng);
|
||
img_to(cb, s_wide_img[i], &s_wide_layout[i], VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||
VkImageMemoryBarrier db = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
|
||
db.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; db.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||
db.srcQueueFamilyIndex = db.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||
db.image = s_wide_ds_img[i]; db.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||
db.subresourceRange.aspectMask = DS_ASPECT;
|
||
db.subresourceRange.levelCount = 1; db.subresourceRange.layerCount = 1;
|
||
p_vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||
0, 0, NULL, 0, NULL, 1, &db);
|
||
VkClearDepthStencilValue dsv = { 0.0f, 0 };
|
||
VkImageSubresourceRange srng = { DS_ASPECT, 0, 1, 0, 1 };
|
||
p_vkCmdClearDepthStencilImage(cb, s_wide_ds_img[i], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &dsv, 1, &srng);
|
||
db.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||
db.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||
db.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||
db.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
||
p_vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT, 0, 0, NULL, 0, NULL, 1, &db);
|
||
end_oneshot(cb);
|
||
s_wide_base[i] = base_x;
|
||
return i;
|
||
}
|
||
return -1; /* more distinct buffers than VK_WIDE_MAX_SURF — shouldn't happen */
|
||
}
|
||
|
||
/* Begin the mirror render pass on the active wide surface inside an already-
|
||
* recording one-shot CB (canonical pass already ended). Sets the wide viewport
|
||
* and the mirror scissor: FULL width X (the margins are the point) but the
|
||
* DRAW-AREA Y band — native-wide only widens X; a full-height Y let draws bleed
|
||
* across a vertical double-buffer band boundary (the GL band-flicker lesson). */
|
||
static void wide_pass_begin(VkCommandBuffer cb) {
|
||
int i = s_wide_cur, S = s_scale;
|
||
s_perf_cur.wide_passes++;
|
||
img_to(cb, s_wide_img[i], &s_wide_layout[i], VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||
/* Stencil self-barrier for the WIDE DS image (same hazard class as
|
||
* begin_geo_pass covers for the canonical DS across one-shot submits). */
|
||
VkImageMemoryBarrier db = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
|
||
db.oldLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||
db.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||
db.srcQueueFamilyIndex = db.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||
db.image = s_wide_ds_img[i];
|
||
db.subresourceRange.aspectMask = DS_ASPECT;
|
||
db.subresourceRange.levelCount = 1; db.subresourceRange.layerCount = 1;
|
||
db.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
||
db.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT |
|
||
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
||
p_vkCmdPipelineBarrier(cb,
|
||
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
|
||
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
|
||
0, 0, NULL, 0, NULL, 1, &db);
|
||
color_self_barrier(cb, s_wide_img[i]);
|
||
VkRenderPassBeginInfo rp = { VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO };
|
||
rp.renderPass = s_rpass; rp.framebuffer = s_wide_fb[i];
|
||
rp.renderArea.extent.width = (uint32_t)(s_wide_w * S);
|
||
rp.renderArea.extent.height = (uint32_t)(VRAM_H * S);
|
||
p_vkCmdBeginRenderPass(cb, &rp, VK_SUBPASS_CONTENTS_INLINE);
|
||
VkViewport vp = { 0.f, 0.f, (float)(s_wide_w * S), (float)(VRAM_H * S), 0.f, 1.f };
|
||
p_vkCmdSetViewport(cb, 0, 1, &vp);
|
||
int y1 = s_da_y1 < 0 ? 0 : s_da_y1;
|
||
int y2 = s_da_y2 >= VRAM_H ? VRAM_H - 1 : s_da_y2;
|
||
int hh = y2 - y1 + 1; if (hh < 0) hh = 0;
|
||
VkRect2D sc = { { view_pad_left * S, y1 * S },
|
||
{ (uint32_t)((s_wide_w - view_pad_left - view_pad_right) * S), (uint32_t)(hh * S) } };
|
||
p_vkCmdSetScissor(cb, 0, 1, &sc);
|
||
}
|
||
|
||
/* Tight bbox of the open untextured batch (added to s_pack_dirty at flush). */
|
||
static DirtyRect s_geo_bbox;
|
||
|
||
/* Flush accumulated untextured geometry: one render pass draws the whole batch
|
||
* (blend/mask per the batch keys), then the image returns to TRANSFER_SRC. */
|
||
static void flush_geometry(void) {
|
||
if (s_vcount == 0 || !s_ready) return;
|
||
s_perf_cur.geo_flushes++;
|
||
uint32_t base = s_vbase, n = s_vcount;
|
||
s_vbase += n; s_vcount = 0; /* consumed region stays untouched until gpu_sync */
|
||
int semi = s_geo_semi, blend = (semi < 0) ? 0 : (semi + 1);
|
||
VkCommandBuffer cb = begin_oneshot();
|
||
vram_to(cb, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||
begin_geo_pass(cb);
|
||
set_vp_scissor(cb);
|
||
bind_masked(cb, 0, 0, blend, s_geo_check, s_geo_mask);
|
||
GeoPush gp = { px_shift(), 0.0f, 512.0f };
|
||
p_vkCmdPushConstants(cb, s_pl_geo, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof gp, &gp);
|
||
VkDeviceSize off = 0;
|
||
p_vkCmdBindVertexBuffers(cb, 0, 1, &s_vbuf, &off);
|
||
p_vkCmdDraw(cb, n, 1, base, 0);
|
||
p_vkCmdEndRenderPass(cb);
|
||
if (s_wide_cur >= 0 && !s_geo_mirror_suppress) { /* native-wide mirror pass (same CB) */
|
||
wide_pass_begin(cb);
|
||
bind_masked(cb, 0, 0, blend, s_geo_check, s_geo_mask);
|
||
GeoPush wgp = { px_shift(), (float)wide_dx(), (float)s_wide_w / 2.0f };
|
||
p_vkCmdPushConstants(cb, s_pl_geo, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof wgp, &wgp);
|
||
p_vkCmdDraw(cb, n, 1, base, 0);
|
||
p_vkCmdEndRenderPass(cb);
|
||
}
|
||
s_geo_mirror_suppress = 0;
|
||
vram_to(cb, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||
end_oneshot(cb);
|
||
if (s_geo_bbox.set) {
|
||
int x0 = s_geo_bbox.x0 < s_da_x1 ? s_da_x1 : s_geo_bbox.x0;
|
||
int y0 = s_geo_bbox.y0 < s_da_y1 ? s_da_y1 : s_geo_bbox.y0;
|
||
int x1 = s_geo_bbox.x1 > s_da_x2 ? s_da_x2 : s_geo_bbox.x1;
|
||
int y1 = s_geo_bbox.y1 > s_da_y2 ? s_da_y2 : s_geo_bbox.y1;
|
||
rect_add(&s_pack_dirty, x0, y0, x1, y1);
|
||
rect_clear(&s_geo_bbox);
|
||
}
|
||
s_gpu_dirty = 1;
|
||
}
|
||
|
||
static inline void col555(uint16_t c, float out[3]) {
|
||
out[0] = ((c) & 0x1F) / 31.0f;
|
||
out[1] = ((c >> 5) & 0x1F) / 31.0f;
|
||
out[2] = ((c >> 10) & 0x1F) / 31.0f;
|
||
}
|
||
static inline void col888(uint32_t c, float out[3]) {
|
||
out[0] = ( c & 0xFF) / 255.0f;
|
||
out[1] = ((c >> 8) & 0xFF) / 255.0f;
|
||
out[2] = ((c >> 16) & 0xFF) / 255.0f;
|
||
}
|
||
static inline void push_vert(float x, float y, const float rgb[3]) {
|
||
Vert *v = &s_vmap[s_vbase + s_vcount++];
|
||
v->x = x; v->y = y; v->r = rgb[0]; v->g = rgb[1]; v->b = rgb[2];
|
||
v->a = (float)s_geo_mask;
|
||
rect_add(&s_geo_bbox, (int)floorf(x), (int)floorf(y), (int)ceilf(x), (int)ceilf(y));
|
||
}
|
||
/* Ensure room for `need` verts (flush mid-batch only between primitives). */
|
||
static void ensure_room(uint32_t need) {
|
||
if (s_vbase + s_vcount + need > VK_VBUF_VERTS) {
|
||
flush_geometry();
|
||
if (s_vbase + need > VK_VBUF_VERTS)
|
||
gpu_sync(); /* buffer exhausted: wait idle, cursors recycle to 0 */
|
||
}
|
||
}
|
||
/* Open / continue an untextured batch with the current blend/mask state; drains
|
||
* the textured batch and any differently-keyed untextured batch first (PS1
|
||
* submission order). */
|
||
static void geo_prim_begin(void) {
|
||
flush_cpu_upload(); /* pending CPU writes precede this prim (PS1 order) */
|
||
flush_tex_batch();
|
||
int semi = s_semi_en ? s_semi_mode : -1;
|
||
if (s_vcount > 0 && (semi != s_geo_semi || s_mask_set != s_geo_mask || s_mask_check != s_geo_check))
|
||
flush_geometry();
|
||
if (s_vcount == 0) { s_geo_semi = semi; s_geo_mask = s_mask_set; s_geo_check = s_mask_check; }
|
||
}
|
||
|
||
static void tri3(float ax,float ay,const float ca[3],
|
||
float bx,float by,const float cb_[3],
|
||
float cx,float cy,const float cc[3]) {
|
||
if (!s_ready) return;
|
||
ensure_room(3);
|
||
push_vert(ax, ay, ca); push_vert(bx, by, cb_); push_vert(cx, cy, cc);
|
||
}
|
||
static void quad(float x0,float y0,float x1,float y1,float x2,float y2,float x3,float y3,
|
||
const float c[3]) {
|
||
/* (x0,y0)-(x1,y1)-(x2,y2)-(x3,y3) as two tris */
|
||
tri3(x0,y0,c, x1,y1,c, x2,y2,c);
|
||
tri3(x0,y0,c, x2,y2,c, x3,y3,c);
|
||
}
|
||
|
||
/* Per-prim render state. Batch keys are compared at draw-append time (the draw
|
||
* funcs flush when they change), so the setters only record state. */
|
||
static void vkb_set_semi_transparency(int e, int m) { s_semi_en = e ? 1 : 0; s_semi_mode = m & 3; }
|
||
static void vkb_set_mask_bits(int s, int c) { s_mask_set = s ? 1 : 0; s_mask_check = c ? 1 : 0; }
|
||
static void vkb_set_texture_window(uint32_t raw) {
|
||
s_tw_mask_x = (int)(raw & 0x1F); s_tw_mask_y = (int)((raw >> 5) & 0x1F);
|
||
s_tw_off_x = (int)((raw >> 10) & 0x1F); s_tw_off_y = (int)((raw >> 15) & 0x1F);
|
||
}
|
||
static void vkb_set_color_modulation(int r, int g, int b, int raw) {
|
||
s_mod_r = r; s_mod_g = g; s_mod_b = b; s_mod_raw = raw ? 1 : 0;
|
||
}
|
||
|
||
/* Sub-pixel / perspective overrides for the next triangle. */
|
||
static void vkb_set_precise_triangle(int enabled,
|
||
int32_t x0,int32_t y0, int32_t x1,int32_t y1,
|
||
int32_t x2,int32_t y2) {
|
||
s_pc_valid = enabled ? 1 : 0;
|
||
if (s_pc_valid) {
|
||
/* 16.16 native VRAM px -> float; the VK pipeline is float end-to-end. */
|
||
s_pc_x[0] = (float)x0 / 65536.0f; s_pc_y[0] = (float)y0 / 65536.0f;
|
||
s_pc_x[1] = (float)x1 / 65536.0f; s_pc_y[1] = (float)y1 / 65536.0f;
|
||
s_pc_x[2] = (float)x2 / 65536.0f; s_pc_y[2] = (float)y2 / 65536.0f;
|
||
}
|
||
}
|
||
static void vkb_set_perspective_triangle(int enabled, float q0, float q1, float q2) {
|
||
s_pq_valid = (enabled && q0 > 0.0f && q1 > 0.0f && q2 > 0.0f) ? 1 : 0;
|
||
s_pq[0] = q0; s_pq[1] = q1; s_pq[2] = q2;
|
||
/* The corrected-triangle tally lives in the software rasterizer and backs
|
||
* gpu_texture_correction_hits() — the "is this doing anything on this
|
||
* title" counter. Forward so it reads the same on every backend. */
|
||
sw_set_perspective_triangle(enabled, q0, q1, q2);
|
||
}
|
||
/* The override describes exactly one triangle; drop it once submitted so a
|
||
* later prim can never inherit it. */
|
||
static inline void precise_consumed(void) { s_pc_valid = 0; s_pq_valid = 0; }
|
||
|
||
/* Position a vertex: the sub-pixel value when geometry_correction supplied one
|
||
* for this triangle, else the integer GP0 position. */
|
||
#define PCX(i, v) (s_pc_valid ? s_pc_x[i] : (float)(v))
|
||
#define PCY(i, v) (s_pc_valid ? s_pc_y[i] : (float)(v))
|
||
|
||
static void vkb_draw_flat_triangle(int x0,int y0,int x1,int y1,int x2,int y2,uint16_t c){
|
||
geo_prim_begin();
|
||
float col[3]; col555(c, col);
|
||
tri3(PCX(0,x0),PCY(0,y0),col, PCX(1,x1),PCY(1,y1),col, PCX(2,x2),PCY(2,y2),col);
|
||
precise_consumed();
|
||
}
|
||
static void vkb_draw_gouraud_triangle(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int x2,int y2,uint16_t c2){
|
||
geo_prim_begin();
|
||
float a[3],b[3],cc[3]; col555(c0,a); col555(c1,b); col555(c2,cc);
|
||
tri3(PCX(0,x0),PCY(0,y0),a, PCX(1,x1),PCY(1,y1),b, PCX(2,x2),PCY(2,y2),cc);
|
||
precise_consumed();
|
||
}
|
||
/* Full-screen-overlay wide pass (pause gray-filter / load fade): draw a flat
|
||
* rect covering the FULL wide width [0, wide_w) x [y, y+h) directly into the
|
||
* active wide surface (positions already wide-space, u_xoff = 0). Mirrors GL's
|
||
* wide_flat_rect_direct: full wide width (the overlay must dim the revealed
|
||
* margins too) but only the draw-area rows, like SW rt_wide() — a full-height
|
||
* scissor let a rect that canonically clips at a vertical double-buffer band
|
||
* boundary paint the OTHER band's rows. The 6 verts are consumed privately (s_vbase bumped)
|
||
* so they never join a canonical batch. */
|
||
static void wide_overlay_rect(int y, int h, uint16_t c, int semi) {
|
||
if (s_wide_cur < 0 || !s_ready) return;
|
||
ensure_room(6);
|
||
float col[3]; col555(c, col);
|
||
uint32_t base = s_vbase + s_vcount; /* == s_vbase (batches drained) */
|
||
float x0 = 0.0f, x1 = (float)s_wide_w, fy0 = (float)y, fy1 = (float)(y + h);
|
||
push_vert(x0, fy0, col); push_vert(x1, fy0, col); push_vert(x1, fy1, col);
|
||
push_vert(x0, fy0, col); push_vert(x1, fy1, col); push_vert(x0, fy1, col);
|
||
s_vbase += s_vcount; s_vcount = 0; /* consume privately */
|
||
int blend = (semi < 0) ? 0 : (semi + 1);
|
||
VkCommandBuffer cb = begin_oneshot();
|
||
wide_pass_begin(cb); /* band scissor: full wide width, draw-area rows */
|
||
bind_masked(cb, 0, 0, blend, s_mask_check, s_mask_set);
|
||
GeoPush gp = { px_shift(), 0.0f, (float)s_wide_w / 2.0f };
|
||
p_vkCmdPushConstants(cb, s_pl_geo, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof gp, &gp);
|
||
VkDeviceSize off = 0;
|
||
p_vkCmdBindVertexBuffers(cb, 0, 1, &s_vbuf, &off);
|
||
p_vkCmdDraw(cb, 6, 1, base, 0);
|
||
p_vkCmdEndRenderPass(cb);
|
||
end_oneshot(cb);
|
||
}
|
||
|
||
static void vkb_draw_flat_rect(int x,int y,int w,int h,uint16_t c){
|
||
/* Full-screen 2D overlay under native-wide: the canonical rect must NOT
|
||
* mirror 1:1 (it would cover only the translated 4:3 span) — suppress the
|
||
* batch mirror and emit one full-wide-width rect instead, so the revealed
|
||
* margins are dimmed/faded too. Same detection as GL/SW. */
|
||
int overlay = 0;
|
||
if (s_wide_cur >= 0) {
|
||
int native_w = s_wide_w - 2 * s_wide_offset;
|
||
int lx = x - s_wide_cur_base, rx = x + w - s_wide_cur_base;
|
||
overlay = (native_w > 0 && lx <= 0 && rx >= native_w);
|
||
}
|
||
if (overlay) { flush_tex_batch(); flush_geometry(); }
|
||
geo_prim_begin();
|
||
float col[3]; col555(c, col);
|
||
if (overlay) s_geo_mirror_suppress = 1;
|
||
quad((float)x,(float)y,(float)(x+w),(float)y,(float)(x+w),(float)(y+h),(float)x,(float)(y+h),col);
|
||
if (overlay) {
|
||
int semi = s_semi_en ? s_semi_mode : -1;
|
||
flush_geometry(); /* canonical only (mirror suppressed) */
|
||
wide_overlay_rect(y, h, c, semi);
|
||
}
|
||
}
|
||
/* 1px line as a thin quad along the segment. */
|
||
static void line_quad(int x0,int y0,int x1,int y1,const float ca[3],const float cb_[3]){
|
||
float dx = (float)(x1 - x0), dy = (float)(y1 - y0);
|
||
float len = dx*dx + dy*dy;
|
||
float nx = 0.5f, ny = 0.5f;
|
||
if (len > 0.0001f) { float il = 0.5f / (float)sqrt(len); nx = -dy * il; ny = dx * il; }
|
||
ensure_room(6);
|
||
float ax0 = x0 + nx, ay0 = y0 + ny, bx0 = x0 - nx, by0 = y0 - ny;
|
||
float ax1 = x1 + nx, ay1 = y1 + ny, bx1 = x1 - nx, by1 = y1 - ny;
|
||
tri3(ax0,ay0,ca, ax1,ay1,cb_, bx1,by1,cb_);
|
||
tri3(ax0,ay0,ca, bx1,by1,cb_, bx0,by0,ca);
|
||
}
|
||
static void vkb_draw_line(int x0,int y0,int x1,int y1,uint16_t c){
|
||
geo_prim_begin();
|
||
float col[3]; col555(c, col); line_quad(x0,y0,x1,y1,col,col);
|
||
}
|
||
static void vkb_draw_shaded_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1){
|
||
geo_prim_begin();
|
||
float a[3],b[3]; col555(c0,a); col555(c1,b); line_quad(x0,y0,x1,y1,a,b);
|
||
}
|
||
|
||
/* ---- textured primitives + VRAM->VRAM copy (Phase 3) ------------------- */
|
||
/* Flush the textured batch: two STP-split passes (pass 1 opaque texels, pass 2
|
||
* semi-transparent texels with PS1 blending). Per-prim texture state rode in the
|
||
* vertex; the batch keys (semi/mask/twin/filter) drive the push constants and
|
||
* pipeline. The raw mirror was made coherent at append time (flush_pack_if_sampling). */
|
||
static void flush_tex_batch(void) {
|
||
if (s_tcount == 0 || !s_ready) return;
|
||
s_perf_cur.tex_flushes++;
|
||
uint32_t base = s_tbase, n = s_tcount;
|
||
s_tbase += n; s_tcount = 0; /* consumed region stays untouched until gpu_sync */
|
||
int semi = s_tb_semi, blend = (semi < 0) ? 0 : (semi + 1);
|
||
VkCommandBuffer cb = begin_oneshot();
|
||
img_to(cb, s_raw_img, &s_raw_layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||
vram_to(cb, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||
begin_geo_pass(cb);
|
||
set_vp_scissor(cb);
|
||
p_vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, s_pl_tex, 0, 1, &s_ds_tex, 0, NULL);
|
||
VkDeviceSize off = 0;
|
||
p_vkCmdBindVertexBuffers(cb, 0, 1, &s_tbuf, &off);
|
||
TexPush tp = {0};
|
||
tp.shift = px_shift(); tp.xoff = 0.0f; tp.xhalf = 512.0f;
|
||
tp.maskset = s_tb_mask; tp.filter = s_tb_filter;
|
||
tp.twin[0] = s_tb_twin[0]; tp.twin[1] = s_tb_twin[1]; tp.twin[2] = s_tb_twin[2]; tp.twin[3] = s_tb_twin[3];
|
||
if (semi < 0) {
|
||
/* OPAQUE batch: the STP bit does not gate COLOUR (every texel is
|
||
* opaque), so colour draws in ONE ordered pass over all texels —
|
||
* painter's order across the batch is exact — followed by a
|
||
* COLOUR-MASKED pass that only fixes the STENCIL for STP=1 texels.
|
||
* (The old whole-batch STP-split let a behind prim's STP=1 texels
|
||
* paint OVER a front prim's STP=0 colour — the water-sparkle field
|
||
* speckled every hill in the Tomba2 attract demo. Mirrors GL's
|
||
* tex_batch_draw_passes.) */
|
||
bind_masked(cb, 1, 0, 0, s_tb_check, s_tb_mask);
|
||
tp.semipass = 0;
|
||
p_vkCmdPushConstants(cb, s_pl_tex, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof tp, &tp);
|
||
p_vkCmdDraw(cb, n, 1, base, 0);
|
||
bind_masked_stencil_only(cb, 1, 0, s_tb_check, 1);
|
||
tp.semipass = 2;
|
||
p_vkCmdPushConstants(cb, s_pl_tex, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof tp, &tp);
|
||
p_vkCmdDraw(cb, n, 1, base, 0);
|
||
} else {
|
||
/* SEMI batch: genuine two-pass — STP=0 texels opaque, STP=1 texels
|
||
* blended per the PSX mode. Cross-prim order is kept by isolating
|
||
* semi prims to one per batch (gpu_textured_triangle), so the two
|
||
* passes never straddle overlapping prims. */
|
||
bind_masked(cb, 1, 0, 0, s_tb_check, s_tb_mask);
|
||
tp.semipass = 1;
|
||
p_vkCmdPushConstants(cb, s_pl_tex, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof tp, &tp);
|
||
p_vkCmdDraw(cb, n, 1, base, 0);
|
||
bind_masked(cb, 1, 0, blend, s_tb_check, 1);
|
||
tp.semipass = 2;
|
||
p_vkCmdPushConstants(cb, s_pl_tex, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof tp, &tp);
|
||
p_vkCmdDraw(cb, n, 1, base, 0);
|
||
}
|
||
p_vkCmdEndRenderPass(cb);
|
||
if (s_wide_cur >= 0) { /* native-wide mirror pass (same CB) */
|
||
wide_pass_begin(cb);
|
||
tp.xoff = (float)wide_dx(); tp.xhalf = (float)s_wide_w / 2.0f;
|
||
if (semi < 0) {
|
||
bind_masked(cb, 1, 0, 0, s_tb_check, s_tb_mask);
|
||
tp.semipass = 0;
|
||
p_vkCmdPushConstants(cb, s_pl_tex, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof tp, &tp);
|
||
p_vkCmdDraw(cb, n, 1, base, 0);
|
||
bind_masked_stencil_only(cb, 1, 0, s_tb_check, 1);
|
||
tp.semipass = 2;
|
||
p_vkCmdPushConstants(cb, s_pl_tex, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof tp, &tp);
|
||
p_vkCmdDraw(cb, n, 1, base, 0);
|
||
} else {
|
||
bind_masked(cb, 1, 0, 0, s_tb_check, s_tb_mask);
|
||
tp.semipass = 1;
|
||
p_vkCmdPushConstants(cb, s_pl_tex, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof tp, &tp);
|
||
p_vkCmdDraw(cb, n, 1, base, 0);
|
||
bind_masked(cb, 1, 0, blend, s_tb_check, 1);
|
||
tp.semipass = 2;
|
||
p_vkCmdPushConstants(cb, s_pl_tex, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof tp, &tp);
|
||
p_vkCmdDraw(cb, n, 1, base, 0);
|
||
}
|
||
p_vkCmdEndRenderPass(cb);
|
||
}
|
||
vram_to(cb, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||
end_oneshot(cb);
|
||
s_gpu_dirty = 1;
|
||
}
|
||
|
||
/* Shared PS1 uv-sampling model (limits + mirrored-2D compensation) — one
|
||
* implementation for GL/VK/SW, see gpu_uv.h. */
|
||
#include "gpu_uv.h"
|
||
|
||
/* Append a textured triangle to the batch (per-prim state in the vertex; flush
|
||
* first if the batch keys differ or the raw mirror needs packing). col = 3x RGB
|
||
* modulation per vertex; lim = uv bounds (NULL -> derive). */
|
||
static void gpu_textured_triangle(const int *xs, const int *ys, const int *us, const int *vs,
|
||
const float *col, uint16_t texpage,
|
||
uint16_t clut_x, uint16_t clut_y, int rawtex,
|
||
int semi, const int *lim) {
|
||
if (!s_ready) return;
|
||
int lim_buf[4];
|
||
int uv_buf[6];
|
||
if (!lim) {
|
||
/* Poly path: exact sampled bounds from the ORIGINAL uvs, then the
|
||
* center-sampling mirror compensation (rect prims arrive with their
|
||
* own precomputed lim and pre-bumped uvs). */
|
||
int *mu = uv_buf, *mv = uv_buf + 3;
|
||
for (int i = 0; i < 3; i++) { mu[i] = us[i]; mv[i] = vs[i]; }
|
||
psx_uv_tri_center_sample(xs, ys, mu, mv, s_pq_valid, lim_buf);
|
||
us = mu; vs = mv;
|
||
lim = lim_buf;
|
||
}
|
||
int base_x = (texpage & 0xF) * 64;
|
||
int base_y = ((texpage >> 4) & 1) * 256;
|
||
int depth = (texpage >> 7) & 3; if (depth > 2) depth = 2;
|
||
|
||
flush_geometry(); /* drain untextured (PS1 order) */
|
||
flush_cpu_upload(); /* uploaded texels/CLUTs must be sampleable + order */
|
||
flush_pack_if_sampling(base_x, base_y, depth, clut_x, clut_y);
|
||
|
||
int twx = s_tw_mask_x, twy = s_tw_mask_y, tox = s_tw_off_x, toy = s_tw_off_y;
|
||
/* A semi-transparent prim must NOT coalesce with its neighbours (its two
|
||
* STP passes would straddle overlapping prims — painter's-order violation;
|
||
* see flush_tex_batch). It drains the open batch and draws alone; the next
|
||
* append flushes it out again. Opaque prims keep batching. */
|
||
int isolate = (semi >= 0);
|
||
if (s_tcount > 0 &&
|
||
(isolate || semi != s_tb_semi || s_mask_set != s_tb_mask || s_mask_check != s_tb_check ||
|
||
s_texfilter != s_tb_filter ||
|
||
twx != s_tb_twin[0] || twy != s_tb_twin[1] || tox != s_tb_twin[2] || toy != s_tb_twin[3]))
|
||
flush_tex_batch();
|
||
if (s_tbase + s_tcount + 3 > VK_TBUF_VERTS) {
|
||
flush_tex_batch();
|
||
if (s_tbase + 3 > VK_TBUF_VERTS)
|
||
gpu_sync(); /* buffer exhausted: wait idle, cursors recycle to 0 */
|
||
}
|
||
if (s_tcount == 0) {
|
||
s_tb_semi = semi; s_tb_mask = s_mask_set; s_tb_check = s_mask_check; s_tb_filter = s_texfilter;
|
||
s_tb_twin[0] = twx; s_tb_twin[1] = twy; s_tb_twin[2] = tox; s_tb_twin[3] = toy;
|
||
}
|
||
int bx0 = xs[0], bx1 = xs[0], by0 = ys[0], by1 = ys[0];
|
||
for (int i = 0; i < 3; i++) {
|
||
float *vp = s_tmap[s_tbase + s_tcount + i].v;
|
||
vp[0] = s_pc_valid ? s_pc_x[i] : (float)xs[i];
|
||
vp[1] = s_pc_valid ? s_pc_y[i] : (float)ys[i];
|
||
vp[2] = (float)us[i]; vp[3] = (float)vs[i];
|
||
vp[4] = col[i*3+0]; vp[5] = col[i*3+1]; vp[6] = col[i*3+2]; vp[7] = 1.0f;
|
||
vp[8] = (float)base_x; vp[9] = (float)base_y;
|
||
vp[10] = (float)clut_x; vp[11] = (float)clut_y;
|
||
vp[12] = (float)depth; vp[13] = (float)rawtex;
|
||
vp[14] = (float)lim[0]; vp[15] = (float)lim[1];
|
||
vp[16] = (float)lim[2]; vp[17] = (float)lim[3];
|
||
vp[18] = s_pq_valid ? s_pq[i] : 0.0f; /* a_q; 0 = affine */
|
||
if (xs[i] < bx0) bx0 = xs[i]; if (xs[i] > bx1) bx1 = xs[i];
|
||
if (ys[i] < by0) by0 = ys[i]; if (ys[i] > by1) by1 = ys[i];
|
||
}
|
||
s_tcount += 3;
|
||
/* A sub-pixel-corrected vertex lies in [int, int+1) of the integer position
|
||
* it was rounded from, so widen the readback rect by one pixel. */
|
||
if (s_pc_valid) { bx1 += 1; by1 += 1; }
|
||
/* mark dirty for later readback/pack (clamped to draw area) */
|
||
if (bx0 < s_da_x1) bx0 = s_da_x1; if (by0 < s_da_y1) by0 = s_da_y1;
|
||
if (bx1 > s_da_x2) bx1 = s_da_x2; if (by1 > s_da_y2) by1 = s_da_y2;
|
||
rect_add(&s_pack_dirty, bx0, by0, bx1, by1);
|
||
s_gpu_dirty = 1;
|
||
}
|
||
|
||
static void gpu_textured_rect(int x,int y,int w,int h, int u0,int v0,int u1,int v1,
|
||
uint16_t clut_x,uint16_t clut_y,uint16_t tp,int semi) {
|
||
if (w <= 0 || h <= 0) return;
|
||
float mr = s_mod_r/255.0f, mg = s_mod_g/255.0f, mb = s_mod_b/255.0f;
|
||
float col[9] = { mr,mg,mb, mr,mg,mb, mr,mg,mb };
|
||
/* Mirrored quads arrive here as scaled rects with u0>u1 / v0>v1 (see
|
||
* the GL backend): exact bounds from the original corners, then the
|
||
* mirror bump (gpu_uv.h). */
|
||
int lim[4];
|
||
psx_uv_rect_limits(u0, v0, u1, v1, lim);
|
||
psx_uv_rect_mirror_offset(&u0, &v0, &u1, &v1);
|
||
int xs1[3]={x, x+w, x}, ys1[3]={y, y, y+h};
|
||
int us1[3]={u0,u1,u0}, vs1[3]={v0,v0,v1};
|
||
gpu_textured_triangle(xs1,ys1,us1,vs1,col,tp,clut_x,clut_y,s_mod_raw,semi,lim);
|
||
int xs2[3]={x+w, x, x+w}, ys2[3]={y, y+h, y+h};
|
||
int us2[3]={u1,u0,u1}, vs2[3]={v0,v1,v1};
|
||
gpu_textured_triangle(xs2,ys2,us2,vs2,col,tp,clut_x,clut_y,s_mod_raw,semi,lim);
|
||
}
|
||
|
||
static void vkb_draw_textured_triangle(int x0,int y0,int u0,int v0,int x1,int y1,int u1,int v1,
|
||
int x2,int y2,int u2,int v2,uint16_t cx,uint16_t cy,uint16_t tp){
|
||
int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2};
|
||
float mr=s_mod_r/255.0f, mg=s_mod_g/255.0f, mb=s_mod_b/255.0f;
|
||
float col[9]={mr,mg,mb, mr,mg,mb, mr,mg,mb};
|
||
gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,s_mod_raw, s_semi_en?s_semi_mode:-1, NULL);
|
||
precise_consumed();
|
||
}
|
||
static void vkb_draw_shaded_textured_triangle(int x0,int y0,int u0,int v0,uint32_t c0,
|
||
int x1,int y1,int u1,int v1,uint32_t c1,
|
||
int x2,int y2,int u2,int v2,uint32_t c2,
|
||
uint16_t cx,uint16_t cy,uint16_t tp,int raw){
|
||
int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2};
|
||
/* shaded-textured: per-vertex 24-bit colour modulates the texel (PS1 *2). */
|
||
float col[9];
|
||
col888(c0, &col[0]); col888(c1, &col[3]); col888(c2, &col[6]);
|
||
gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,raw, s_semi_en?s_semi_mode:-1, NULL);
|
||
precise_consumed();
|
||
}
|
||
static void vkb_draw_textured_rect(int x,int y,int w,int h,int u,int v,uint16_t cx,uint16_t cy,uint16_t tp){
|
||
gpu_textured_rect(x,y,w,h, u,v, u+w,v+h, cx,cy,tp, s_semi_en?s_semi_mode:-1);
|
||
}
|
||
static void vkb_draw_textured_rect_scaled(int x,int y,int w,int h,int u0,int v0,int u1,int v1,uint16_t cx,uint16_t cy,uint16_t tp){
|
||
gpu_textured_rect(x,y,w,h, u0,v0, u1,v1, cx,cy,tp, s_semi_en?s_semi_mode:-1);
|
||
}
|
||
|
||
/* VRAM->VRAM copy: blit the source hr region to scratch (resolves overlap),
|
||
* then blit scratch -> dest with STP/mask split. Mirrors the GL backend. */
|
||
static void vkb_copy_rect(int sx,int sy,int dx,int dy,int w,int h){
|
||
if (!s_ctx_ok || w <= 0 || h <= 0) return;
|
||
flush_cpu_upload(); /* the copy source may include pending CPU writes */
|
||
flush_tex_batch(); flush_geometry();
|
||
if (sx + w > VRAM_W) w = VRAM_W - sx;
|
||
if (dx + w > VRAM_W) w = VRAM_W - dx;
|
||
if (sy + h > VRAM_H) h = VRAM_H - sy;
|
||
if (dy + h > VRAM_H) h = VRAM_H - dy;
|
||
if (w <= 0 || h <= 0) return;
|
||
s_perf_cur.copy_rects++;
|
||
int S = s_scale;
|
||
|
||
/* stage 1: copy source hr region into scratch at the same coords */
|
||
VkCommandBuffer cb = begin_oneshot();
|
||
vram_to(cb, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||
img_to(cb, s_scratch_img, &s_scratch_layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||
VkImageCopy ic = {0};
|
||
ic.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; ic.srcSubresource.layerCount = 1;
|
||
ic.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; ic.dstSubresource.layerCount = 1;
|
||
ic.srcOffset.x = sx * S; ic.srcOffset.y = sy * S;
|
||
ic.dstOffset.x = sx * S; ic.dstOffset.y = sy * S;
|
||
ic.extent.width = w * S; ic.extent.height = h * S; ic.extent.depth = 1;
|
||
p_vkCmdCopyImage(cb, s_vram_img, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||
s_scratch_img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &ic);
|
||
end_oneshot(cb);
|
||
|
||
/* stage 2: blit scratch -> dest (hr-res source: src_div=1, offset (sx-dx)*S). */
|
||
blit_region(dx, dy, w, h, s_scratch_view, s_scratch_img, &s_scratch_layout,
|
||
1, (sx - dx) * S, (sy - dy) * S, 0, 1);
|
||
|
||
if (s_vram) { /* keep CPU mirror coherent for non-GPU readers */
|
||
for (int row = 0; row < h; row++)
|
||
for (int col = 0; col < w; col++)
|
||
s_vram[(dy + row) * VRAM_W + (dx + col)] = s_vram[(sy + row) * VRAM_W + (sx + col)];
|
||
}
|
||
}
|
||
|
||
/* ---- native-wide vtable entry points ------------------------------------ */
|
||
/* Enable native-wide with a wide width + centering offset (native px), or
|
||
* disable (wide_w <= 0). No-ops when nothing changed (gpu.c calls this on
|
||
* every draw-area set via ws_nw_sync_target — Part-A lesson: the wide entry
|
||
* points sit inside guest emulation, keep the common path free). */
|
||
static void vkb_wide_set_view(int enabled, int shift, int pad_left, int pad_right) {
|
||
if (!enabled) shift = pad_left = pad_right = 0;
|
||
if (view_enabled == enabled && view_shift == shift &&
|
||
view_pad_left == pad_left && view_pad_right == pad_right) return;
|
||
flush_tex_batch(); flush_geometry();
|
||
view_enabled = enabled;
|
||
view_shift = shift;
|
||
view_pad_left = pad_left;
|
||
view_pad_right = pad_right;
|
||
}
|
||
|
||
static void vkb_wide_configure(int wide_w, int offset) {
|
||
if (!s_ready) return;
|
||
if (wide_w == s_wide_w && offset == s_wide_offset) return;
|
||
flush_tex_batch(); flush_geometry();
|
||
if (wide_w <= 0) { wide_free_all(); s_wide_w = 0; s_wide_offset = 0; return; }
|
||
if (wide_w != s_wide_w) wide_free_all();
|
||
s_wide_w = wide_w;
|
||
s_wide_offset = offset;
|
||
}
|
||
|
||
/* Select the wide surface to mirror into for the back buffer at base_x. */
|
||
static void vkb_wide_set_target(int base_x) {
|
||
if (!s_ready) { s_wide_cur = -1; return; }
|
||
if (s_wide_cur >= 0 && s_wide_base[s_wide_cur] == base_x) { s_wide_cur_base = base_x; return; }
|
||
flush_tex_batch(); flush_geometry(); /* drain into the OLD target first */
|
||
s_wide_cur = wide_surf_for(base_x);
|
||
s_wide_cur_base = base_x;
|
||
}
|
||
|
||
/* Stop mirroring (offscreen draws that don't target a framebuffer). */
|
||
static void vkb_wide_disable_target(void) {
|
||
if (s_wide_cur < 0) return;
|
||
flush_tex_batch(); flush_geometry();
|
||
s_wide_cur = -1;
|
||
}
|
||
|
||
/* Mirror a framebuffer clear: fill the full wide width over [y, y+h) of the
|
||
* surface for base_x (revealed margins stay clean). Stencil clears to bit15,
|
||
* like the canonical fill path. */
|
||
static void vkb_wide_clear(int base_x, int y, int h, uint16_t color) {
|
||
if (!s_ready || s_wide_w <= 0) return;
|
||
flush_tex_batch(); flush_geometry();
|
||
int i = wide_surf_for(base_x);
|
||
if (i < 0) return;
|
||
s_perf_cur.wide_clears++;
|
||
int S = s_scale, H = VRAM_H * S;
|
||
int y0 = y * S, y1 = (y + h) * S;
|
||
if (y0 < 0) y0 = 0;
|
||
if (y1 > H) y1 = H;
|
||
if (y1 <= y0) return;
|
||
VkCommandBuffer cb = begin_oneshot();
|
||
img_to(cb, s_wide_img[i], &s_wide_layout[i], VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||
color_self_barrier(cb, s_wide_img[i]);
|
||
VkRenderPassBeginInfo rp = { VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO };
|
||
rp.renderPass = s_rpass; rp.framebuffer = s_wide_fb[i];
|
||
rp.renderArea.extent.width = (uint32_t)(s_wide_w * S);
|
||
rp.renderArea.extent.height = (uint32_t)H;
|
||
p_vkCmdBeginRenderPass(cb, &rp, VK_SUBPASS_CONTENTS_INLINE);
|
||
VkClearAttachment ca[2] = {0};
|
||
ca[0].aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||
ca[0].colorAttachment = 0;
|
||
ca[0].clearValue.color.float32[0] = (float)((color) & 0x1F) / 31.0f;
|
||
ca[0].clearValue.color.float32[1] = (float)((color >> 5) & 0x1F) / 31.0f;
|
||
ca[0].clearValue.color.float32[2] = (float)((color >> 10) & 0x1F) / 31.0f;
|
||
ca[0].clearValue.color.float32[3] = (color >> 15) & 1 ? 1.0f : 0.0f;
|
||
ca[1].aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
|
||
ca[1].clearValue.depthStencil.depth = 0.0f;
|
||
ca[1].clearValue.depthStencil.stencil = (color >> 15) & 1;
|
||
VkClearRect cr = { { { 0, y0 }, { (uint32_t)(s_wide_w * S), (uint32_t)(y1 - y0) } }, 0, 1 };
|
||
p_vkCmdClearAttachments(cb, 2, ca, 1, &cr);
|
||
p_vkCmdEndRenderPass(cb);
|
||
end_oneshot(cb);
|
||
}
|
||
|
||
/* Clear only the two synthetic reveal strips, leaving the guest-owned centre
|
||
* intact. Mirrors the software/OpenGL opt-in transition cleanup. */
|
||
static void vkb_wide_clear_margins(int base_x, int y, int h, uint16_t color, int sides) {
|
||
if (!s_ready || s_wide_w <= 0 || s_wide_offset <= 0) return;
|
||
flush_tex_batch(); flush_geometry();
|
||
int i = wide_surf_for(base_x);
|
||
if (i < 0) return;
|
||
s_perf_cur.wide_clears++;
|
||
int S = s_scale, H = VRAM_H * S;
|
||
int W = s_wide_w * S, margin = s_wide_offset * S;
|
||
int y0 = y * S, y1 = (y + h) * S;
|
||
if (y0 < 0) y0 = 0;
|
||
if (y1 > H) y1 = H;
|
||
if (y1 <= y0 || margin * 2 >= W) return;
|
||
VkCommandBuffer cb = begin_oneshot();
|
||
img_to(cb, s_wide_img[i], &s_wide_layout[i], VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||
color_self_barrier(cb, s_wide_img[i]);
|
||
VkRenderPassBeginInfo rp = { VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO };
|
||
rp.renderPass = s_rpass; rp.framebuffer = s_wide_fb[i];
|
||
rp.renderArea.extent.width = (uint32_t)W;
|
||
rp.renderArea.extent.height = (uint32_t)H;
|
||
p_vkCmdBeginRenderPass(cb, &rp, VK_SUBPASS_CONTENTS_INLINE);
|
||
VkClearAttachment ca[2] = {0};
|
||
ca[0].aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||
ca[0].colorAttachment = 0;
|
||
ca[0].clearValue.color.float32[0] = (float)((color) & 0x1F) / 31.0f;
|
||
ca[0].clearValue.color.float32[1] = (float)((color >> 5) & 0x1F) / 31.0f;
|
||
ca[0].clearValue.color.float32[2] = (float)((color >> 10) & 0x1F) / 31.0f;
|
||
ca[0].clearValue.color.float32[3] = (color >> 15) & 1 ? 1.0f : 0.0f;
|
||
ca[1].aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
|
||
ca[1].clearValue.depthStencil.depth = 0.0f;
|
||
ca[1].clearValue.depthStencil.stencil = (color >> 15) & 1;
|
||
VkClearRect cr[2];
|
||
uint32_t ncr = 0;
|
||
if (sides & 1)
|
||
cr[ncr++] = (VkClearRect){ { { 0, y0 }, { (uint32_t)margin, (uint32_t)(y1 - y0) } }, 0, 1 };
|
||
if (sides & 2)
|
||
cr[ncr++] = (VkClearRect){ { { W - margin, y0 }, { (uint32_t)margin, (uint32_t)(y1 - y0) } }, 0, 1 };
|
||
if (ncr) p_vkCmdClearAttachments(cb, 2, ca, ncr, cr);
|
||
p_vkCmdEndRenderPass(cb);
|
||
end_oneshot(cb);
|
||
}
|
||
|
||
/* Present source (CPU): read the wide surface band for the displayed buffer
|
||
* into an ARGB8888 buffer, byte-compatible with sw_render_wide_display — used
|
||
* by the shared CPU present fallback and the ws debug dump. The GPU-direct
|
||
* vk_renderer_present_wide is the per-frame path; this readback is cold. */
|
||
static int vkb_render_wide_display(uint32_t *out, int pitch, int base_x,
|
||
int disp_y, int disp_h) {
|
||
if (!s_ready || s_wide_w <= 0) return 0;
|
||
int i = -1;
|
||
for (int k = 0; k < VK_WIDE_MAX_SURF; k++)
|
||
if (s_wide_img[k] && s_wide_base[k] == base_x) { i = k; break; }
|
||
if (i < 0) return 0;
|
||
flush_cpu_upload(); flush_tex_batch(); flush_geometry();
|
||
int S = s_scale, W = s_wide_w * S, H = VRAM_H * S;
|
||
int out_h = disp_h * S, ry0 = disp_y * S;
|
||
if (ry0 < 0) ry0 = 0;
|
||
if (ry0 + out_h > H) out_h = H - ry0;
|
||
if (out_h <= 0) return 0;
|
||
VkBuffer buf; VkDeviceMemory mem; void *map;
|
||
if (!make_staging((VkDeviceSize)W * out_h * 4, &buf, &mem, &map)) return 0;
|
||
VkCommandBuffer cb = begin_oneshot();
|
||
img_to(cb, s_wide_img[i], &s_wide_layout[i], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||
VkBufferImageCopy rc = {0};
|
||
rc.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||
rc.imageSubresource.layerCount = 1;
|
||
rc.imageOffset.y = ry0;
|
||
rc.imageExtent.width = (uint32_t)W; rc.imageExtent.height = (uint32_t)out_h;
|
||
rc.imageExtent.depth = 1;
|
||
p_vkCmdCopyImageToBuffer(cb, s_wide_img[i], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, buf, 1, &rc);
|
||
img_to(cb, s_wide_img[i], &s_wide_layout[i], VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||
end_oneshot(cb);
|
||
gpu_sync(); /* readback must be complete before the CPU reads it */
|
||
const uint8_t *src = (const uint8_t *)map;
|
||
int count = 0;
|
||
for (int row = 0; row < out_h; row++) {
|
||
uint32_t *dst = (uint32_t *)((uint8_t *)out + (size_t)row * pitch);
|
||
const uint8_t *s8 = src + (size_t)row * W * 4;
|
||
for (int col = 0; col < W; col++) { /* RGBA8 -> 0xAARRGGBB, alpha forced */
|
||
dst[col] = 0xFF000000u | ((uint32_t)s8[col * 4 + 0] << 16)
|
||
| ((uint32_t)s8[col * 4 + 1] << 8) | s8[col * 4 + 2];
|
||
count++;
|
||
}
|
||
}
|
||
free_staging(buf, mem);
|
||
return count;
|
||
}
|
||
|
||
static const GpuRenderBackend VK_BACKEND = {
|
||
.name = "vulkan",
|
||
.init = vkb_init,
|
||
.set_scale = vkb_set_scale,
|
||
.scale = vkb_scale,
|
||
.set_texture_filter = vkb_set_texture_filter,
|
||
.texture_filter = vkb_texture_filter,
|
||
.set_semi_transparency = vkb_set_semi_transparency,
|
||
.set_mask_bits = vkb_set_mask_bits,
|
||
.set_texture_window = vkb_set_texture_window,
|
||
.set_color_modulation = vkb_set_color_modulation,
|
||
.set_precise_triangle = vkb_set_precise_triangle,
|
||
.set_perspective_triangle = vkb_set_perspective_triangle,
|
||
.fill_rect = vkb_fill_rect,
|
||
.copy_rect = vkb_copy_rect,
|
||
.draw_flat_triangle = vkb_draw_flat_triangle,
|
||
.draw_gouraud_triangle = vkb_draw_gouraud_triangle,
|
||
.draw_textured_triangle = vkb_draw_textured_triangle,
|
||
.draw_shaded_textured_triangle = vkb_draw_shaded_textured_triangle,
|
||
.draw_flat_rect = vkb_draw_flat_rect,
|
||
.draw_textured_rect = vkb_draw_textured_rect,
|
||
.draw_textured_rect_scaled = vkb_draw_textured_rect_scaled,
|
||
.draw_line = vkb_draw_line,
|
||
.draw_shaded_line = vkb_draw_shaded_line,
|
||
.render_display = vkb_render_display,
|
||
.render_display_hires = vkb_render_display_hires,
|
||
.vram_write = vkb_vram_write,
|
||
.vram_read = vkb_vram_read,
|
||
.vram_transfer_in = vkb_vram_transfer_in,
|
||
.vram_transfer_out = vkb_vram_transfer_out,
|
||
.set_draw_area = vkb_set_draw_area,
|
||
.get_draw_area = vkb_get_draw_area,
|
||
.set_draw_offset = vkb_set_draw_offset,
|
||
.wide_configure = vkb_wide_configure,
|
||
.wide_set_view = vkb_wide_set_view,
|
||
.wide_set_target = vkb_wide_set_target,
|
||
.wide_disable_target = vkb_wide_disable_target,
|
||
.wide_clear = vkb_wide_clear,
|
||
.wide_clear_margins = vkb_wide_clear_margins,
|
||
.render_wide_display = vkb_render_wide_display,
|
||
};
|
||
|
||
/* Returned unconditionally (like gl_backend_get): the table is selected before
|
||
* the context exists; vk_renderer_init_context creates the device, and on
|
||
* failure main.cpp switches the facade back to software. */
|
||
const GpuRenderBackend *vk_backend_get(void) { return &VK_BACKEND; }
|
||
|
||
#endif /* PSX_HAVE_VULKAN */
|