diff --git a/android/app/build.gradle b/android/app/build.gradle index 892cf001..6f8d9484 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -28,6 +28,13 @@ android { ndk { abiFilters "arm64-v8a", "x86_64" } + + externalNativeBuild { + cmake { + arguments "-DPython3_EXECUTABLE=C:/Python312/python.exe", + "-DPYTHON_EXECUTABLE=C:/Python312/python.exe" + } + } } signingConfigs { diff --git a/cmake/qt6.cmake b/cmake/qt6.cmake index 136fcc16..b84ab48b 100644 --- a/cmake/qt6.cmake +++ b/cmake/qt6.cmake @@ -2,6 +2,9 @@ # Provides the vita3k::qt6 INTERFACE target. add_library(vita3k_qt6 INTERFACE) +# // Nick +set(Qt6_ROOT "C:/Qt/6.11.1/msvc2022_64" CACHE PATH "Qt6 installation root" FORCE) + set(VITA3K_QT_MIN_VER 6.11.0) set(VITA3K_QT_COMPONENTS Core Gui Widgets Network Concurrent Svg Multimedia LinguistTools) diff --git a/external/VulkanMemoryAllocator-Hpp b/external/VulkanMemoryAllocator-Hpp --- a/external/VulkanMemoryAllocator-Hpp +++ b/external/VulkanMemoryAllocator-Hpp @@ -1 +1 @@ -Subproject commit 2059f0fdd73492b03d60a90c73e5038224b99093 +Subproject commit 2059f0fdd73492b03d60a90c73e5038224b99093-dirty diff --git a/vita3k/camera/src/camera.cpp b/vita3k/camera/src/camera.cpp index 53714cff..7fd0dba5 100644 --- a/vita3k/camera/src/camera.cpp +++ b/vita3k/camera/src/camera.cpp @@ -321,8 +321,8 @@ int Camera::read(SceCameraRead *read, void *pIBase, void *pUBase, void *pVBase, read->status = SCE_CAMERA_STATUS_IS_ACTIVE; read->timestamp = last_frame_timestamp_us; if (pImpl->frame) { + sizeIBase = std::min(sizeIBase, (SceSize)(pImpl->frame->pitch * pImpl->frame->h)); // here pitch is width in bytes if (this->info.format == SCE_CAMERA_FORMAT_YUV420_PLANE) { - sizeIBase = std::min(sizeIBase, (SceSize)(pImpl->frame->pitch * pImpl->frame->h)); sizeUBase = std::min(sizeUBase, (SceSize)(pImpl->frame->pitch * pImpl->frame->h / 4)); sizeVBase = std::min(sizeVBase, (SceSize)(pImpl->frame->pitch * pImpl->frame->h / 4)); memcpy(pIBase, (uint8_t *)pImpl->frame->pixels, sizeIBase); @@ -333,8 +333,12 @@ int Camera::read(SceCameraRead *read, void *pIBase, void *pUBase, void *pVBase, const int width = pImpl->frame->w; const int height = pImpl->frame->h; if (sizeIBase < (SceSize)(width * height) || sizeUBase < (SceSize)((width / 2) * height) || sizeVBase < (SceSize)((width / 2) * height)) { - LOG_ERROR_ONCE("Buffer sizes too small for YUV422 planar conversion: IBase {}, UBase {}, VBase {}, required I {}, U {}, V {}", sizeIBase, sizeUBase, sizeVBase, width * height, (width / 2) * height, (width / 2) * height); - return SCE_CAMERA_ERROR_PARAM; + LOG_ERROR_ONCE("Buffer sizes too small for YUV422 planar conversion: IBase {}, UBase {}, VBase {}, required I {}, U {}, V {}.", sizeIBase, sizeUBase, sizeVBase, width * height, (width / 2) * height, (width / 2) * height); + if (sizeIBase >= (SceSize)(width * height)) { + memcpy(pIBase, pImpl->frame->pixels, sizeIBase); // Copy Y plane only if buffer is large enough + } else { + return SCE_CAMERA_ERROR_PARAM; + } } if (!SDL_LockSurface(pImpl->frame.get())) { LOG_ERROR("Failed to lock camera frame surface: {}", SDL_GetError()); @@ -357,7 +361,6 @@ int Camera::read(SceCameraRead *read, void *pIBase, void *pUBase, void *pVBase, } SDL_UnlockSurface(pImpl->frame.get()); } else { - sizeIBase = std::min(sizeIBase, (SceSize)(pImpl->frame->pitch * pImpl->frame->h)); // here pitch is width in bytes memcpy(pIBase, pImpl->frame->pixels, sizeIBase); } } else { diff --git a/vita3k/display/src/display.cpp b/vita3k/display/src/display.cpp index 35383cd1..18845fa9 100644 --- a/vita3k/display/src/display.cpp +++ b/vita3k/display/src/display.cpp @@ -27,6 +27,13 @@ #include #include +#ifdef _WIN32 +#include + +#include +#pragma comment(lib, "winmm.lib") +#endif + // Code heavily influenced by PPSSSPP's SceDisplay.cpp static constexpr int TARGET_FPS = 60; @@ -38,6 +45,15 @@ static constexpr int max_expected_swapchain_size = 6; static void vblank_sync_thread(EmuEnvState &emuenv) { DisplayState &display = emuenv.display; +#ifdef _WIN32 + // raise the OS timer resolution: the default granularity (up to ~15ms) makes the + // vblank grid jitter enough that games intermittently observe 3-vblank frames at a + // solid 30fps and engage their performance-degradation paths (dynamic resolution, + // reduced shadow update rates, ...) + timeBeginPeriod(1); +#endif + auto next_tick = std::chrono::steady_clock::now(); + while (!display.abort.load()) { { const std::lock_guard guard(display.mutex); @@ -75,10 +91,23 @@ static void vblank_sync_thread(EmuEnvState &emuenv) { } } } - const auto time_ms = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); - const auto time_left = TARGET_MICRO_PER_FRAME - (time_ms % TARGET_MICRO_PER_FRAME); - std::this_thread::sleep_for(std::chrono::microseconds(time_left)); + // pace the vblank grid precisely: absolute scheduling (no drift accumulation), + // coarse sleep to ~2ms before the tick, then spin the remainder so ticks land + // within tens of microseconds instead of the sleep granularity + next_tick += std::chrono::microseconds(TARGET_MICRO_PER_FRAME); + auto now = std::chrono::steady_clock::now(); + if (next_tick < now - std::chrono::milliseconds(100)) + // catastrophic lag (debugger pause, ...): resynchronize instead of fast-forwarding + next_tick = now; + if (next_tick - now > std::chrono::milliseconds(2)) + std::this_thread::sleep_until(next_tick - std::chrono::milliseconds(2)); + while (std::chrono::steady_clock::now() < next_tick) + std::this_thread::yield(); } + +#ifdef _WIN32 + timeEndPeriod(1); +#endif } void start_sync_thread(EmuEnvState &emuenv) { diff --git a/vita3k/gxm/include/gxm/types.h b/vita3k/gxm/include/gxm/types.h index e0cb8359..246840d2 100644 --- a/vita3k/gxm/include/gxm/types.h +++ b/vita3k/gxm/include/gxm/types.h @@ -1225,6 +1225,13 @@ struct SceGxmTexture { uint32_t count = (mip_count + 1) & 15; return (count == 0) ? 1 : count; // 0 count is no mip chain, but there's still top level... } + + // the 4-bit min LOD clamp is split across control words 2 and 3: + // lod_min0 (CW2) holds the high 2 bits, lod_min1 (CW3) the low 2 bits + // (verified against Killzone Mercenary, which writes control words directly) + uint32_t true_lod_min() const { + return (lod_min0 << 2) | lod_min1; + } }; static_assert(sizeof(SceGxmTexture) == 16); diff --git a/vita3k/mem/include/mem/functions.h b/vita3k/mem/include/mem/functions.h index fdc3cd3b..44766011 100644 --- a/vita3k/mem/include/mem/functions.h +++ b/vita3k/mem/include/mem/functions.h @@ -53,13 +53,22 @@ Address alloc(MemState &state, uint32_t size, const char *name, Address start_ad Address alloc_aligned(MemState &state, uint32_t size, const char *name, unsigned int alignment, Address start_addr = user_main_memory_start); void protect_inner(MemState &state, Address addr, uint32_t size, const MemPerm perm); void unprotect_inner(MemState &state, Address addr, uint32_t size); -bool add_protect(MemState &state, Address addr, const uint32_t size, const MemPerm perm, const ProtectCallback &callback); +bool add_protect(MemState &state, Address addr, const uint32_t size, const MemPerm perm, const ProtectCallback &callback, const void *owner = nullptr, bool page_granular = false); +// remove every protect block registered with this owner (callbacks captured by those blocks +// must not outlive the owner object — a fault would otherwise invoke a dangling callback) +void remove_protect_owner(MemState &state, const void *owner); +// run writer() with protection traps overlapping [addr, addr + size) temporarily lifted and +// re-armed afterwards without firing or consuming them: for the emulator's own guest-RAM +// write-backs, which must stay invisible to write-tracking clients (a consumed trap would +// otherwise clobber shader-store mirrors and miss subsequent genuine game writes) +void write_through_protection(MemState &state, Address addr, uint32_t size, const std::function &writer); void open_access_parent_protect_segment(MemState &state, Address addr); void close_access_parent_protect_segment(MemState &state, Address addr); void add_external_mapping(MemState &mem, Address addr, uint32_t size, uint8_t *addr_ptr); void remove_external_mapping(MemState &mem, uint8_t *addr_ptr, uint32_t size); bool is_protecting(MemState &state, Address addr, MemPerm *perm = nullptr); bool is_valid_addr(const MemState &state, Address addr); +Address host_to_guest(const MemState &state, const void *host); bool is_valid_addr_range(const MemState &state, Address start, Address end); bool handle_access_violation(MemState &state, uint8_t *addr, bool write) noexcept; Block alloc_block(MemState &mem, uint32_t size, const char *name, Address start_addr = user_main_memory_start); diff --git a/vita3k/mem/include/mem/ptr.h b/vita3k/mem/include/mem/ptr.h index 8a26e99b..cbdc29e1 100644 --- a/vita3k/mem/include/mem/ptr.h +++ b/vita3k/mem/include/mem/ptr.h @@ -37,12 +37,7 @@ public: } Ptr(T *pointer, const MemState &mem) { - const uint8_t *const pointer_bytes = reinterpret_cast(pointer); - if (pointer_bytes == 0) { - addr = 0; - } else { - addr = static_cast
(pointer_bytes - &mem.memory[0]); - } + addr = host_to_guest(mem, pointer); } Address address() const { diff --git a/vita3k/mem/include/mem/state.h b/vita3k/mem/include/mem/state.h index 85ce17be..a0ee7547 100644 --- a/vita3k/mem/include/mem/state.h +++ b/vita3k/mem/include/mem/state.h @@ -41,6 +41,16 @@ typedef std::map PageNameMap; struct ProtectBlockInfo { uint32_t size = 0; ProtectCallback callback; + // identifies the registering client object: re-registrations by the same owner replace + // that owner's previous blocks intersecting the new range (stale callbacks would + // otherwise accumulate, since blocks not touching a faulting page are re-armed rather + // than consumed). Distinct owners sharing a range keep independent blocks. + const void *owner = nullptr; + // when set, a fault does not consume the whole block: the block is re-armed minus the + // faulting page, so every written page generates exactly one fault — used by clients + // that need to know WHICH pages were written (per-page dirty tracking for buffers + // mixing CPU-written data with GPU shader-store output) + bool page_granular = false; }; struct ProtectSegmentInfo { @@ -77,4 +87,5 @@ struct MemState { bool use_page_table = false; PageTable page_table; std::map> external_mapping; + mutable std::mutex external_mapping_mutex; }; diff --git a/vita3k/mem/src/mem.cpp b/vita3k/mem/src/mem.cpp index 28bc81bf..259e3da1 100644 --- a/vita3k/mem/src/mem.cpp +++ b/vita3k/mem/src/mem.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -268,6 +269,8 @@ void protect_inner(MemState &state, Address addr, uint32_t size, const MemPerm p #endif } +static bool add_protect_unlocked(MemState &state, Address addr, const uint32_t size, const MemPerm perm, const ProtectCallback &callback, const void *owner, bool page_granular); + bool handle_access_violation(MemState &state, uint8_t *addr, bool write) noexcept { const uintptr_t memory_addr = reinterpret_cast(state.memory.get()); const uintptr_t fault_addr = reinterpret_cast(addr); @@ -314,27 +317,85 @@ bool handle_access_violation(MemState &state, uint8_t *addr, bool write) noexcep return true; } - Address previous_beg = it->first; - for (auto &[block_addr, block] : info.blocks) { - block.callback(vaddr, write); - } - - unprotect_inner(state, it->first, info.size); + // a segment can hold blocks merged from independent protection clients (surface cache, + // GPU buffer trapping, texture cache) whose ranges share pages: only blocks touching the + // faulting page may be notified and consumed — firing them all corrupts the other + // clients' tracking (e.g. unrelated ring-buffer writes marking a surface guest-written), + // while silently dropping them disarms their traps and later writes go undetected. + // Untouched blocks are re-armed as fresh protections instead. + const Address seg_addr = it->first; + ProtectSegmentInfo seg = std::move(it->second); state.protect_tree.erase(it); + unprotect_inner(state, seg_addr, seg.size); + + const Address fault_page_start = align_down(vaddr, state.host_page_size); + const Address fault_page_end = fault_page_start + state.host_page_size; + const auto touches_fault_page = [&](Address block_addr, const ProtectBlockInfo &block) { + return block_addr < fault_page_end && fault_page_start < block_addr + block.size; + }; + + for (auto &[block_addr, block] : seg.blocks) { + if (touches_fault_page(block_addr, block)) + block.callback(vaddr, write); + } + // re-arm untouched blocks; their byte ranges page-align outside the faulting page, so + // re-protection cannot cover it and re-fault the current access. Page-granular blocks + // touching the fault page are re-armed minus that page (their trap keeps firing for + // every further written page), other touched blocks stay consumed (whole-block one-shot) + for (auto &[block_addr, block] : seg.blocks) { + const bool touched = touches_fault_page(block_addr, block); + if (!touched) { + add_protect_unlocked(state, block_addr, block.size, seg.perm, block.callback, block.owner, block.page_granular); + } else if (block.page_granular) { + const Address block_end = block_addr + block.size; + if (block_addr < fault_page_start) + add_protect_unlocked(state, block_addr, static_cast(fault_page_start - block_addr), seg.perm, block.callback, block.owner, true); + if (fault_page_end < block_end) + add_protect_unlocked(state, fault_page_end, static_cast(block_end - fault_page_end), seg.perm, block.callback, block.owner, true); + } + } return true; } -bool add_protect(MemState &state, Address addr, const uint32_t size, const MemPerm perm, const ProtectCallback &callback) { +Address host_to_guest(const MemState &state, const void *host) { + if (host == nullptr) + return 0; + + const uint64_t host_val = std::bit_cast(host); + const uint64_t memory_base = std::bit_cast(state.memory.get()); + + // Fast path: the pointer is inside the linear guest-RAM window. Covers most conversions and takes no lock + if (host_val >= memory_base && host_val < memory_base + TOTAL_MEM_SIZE) + return static_cast
(host_val - memory_base); + + // Otherwise the pointer lives inside an external GPU mapping whose pages were pointed away + if (state.use_page_table) { + const std::lock_guard ext_lock(state.external_mapping_mutex); + auto it = state.external_mapping.lower_bound(host_val); + if (it != state.external_mapping.end() && host_val < it->first + it->second.size) + return it->second.address + static_cast
(host_val - it->first); + } + + // Fallback: linear (likely a bogus pointer?) + return static_cast
(host_val - memory_base); +} + +bool add_protect(MemState &state, Address addr, const uint32_t size, const MemPerm perm, const ProtectCallback &callback, const void *owner, bool page_granular) { const std::lock_guard lock(state.protect_mutex); + return add_protect_unlocked(state, addr, size, perm, callback, owner, page_granular); +} + +static bool add_protect_unlocked(MemState &state, Address addr, const uint32_t size, const MemPerm perm, const ProtectCallback &callback, const void *owner, bool page_granular) { ProtectSegmentInfo protect(size, perm); align_to_page(state, addr, protect.size); ProtectBlockInfo block; block.size = size; block.callback = callback; - - protect.blocks.emplace(addr, std::move(block)); + block.owner = owner; + block.page_granular = page_granular; + const Address block_key = addr; auto it = state.protect_tree.lower_bound(addr); if (it == state.protect_tree.end() || it->first + it->second.size <= addr) { @@ -344,7 +405,11 @@ bool add_protect(MemState &state, Address addr, const uint32_t size, const MemPe --it; } - while (it != state.protect_tree.end() && it->first < addr + size) { + // merge every segment overlapping the union as it grows: comparing against the + // original request size instead would leave segments inside a merged-in larger + // segment unmerged, producing overlapping tree entries — a fault then tears down + // only one of them and silently disarms the other's trap over the shared pages + while (it != state.protect_tree.end() && it->first < addr + protect.size) { const Address start = std::min(it->first, addr); protect.size = std::max(it->first + it->second.size, addr + protect.size) - start; addr = start; @@ -360,12 +425,75 @@ bool add_protect(MemState &state, Address addr, const uint32_t size, const MemPe state.protect_tree.erase(it--); } + // drop this owner's older registrations intersecting the new range before adding the + // new block: clients (surface cache, buffer trapping, texture cache) re-protect their + // ranges every frame, and since blocks are re-armed on faults rather than consumed + // (including page-granular split pieces), stale/fragmented duplicates would otherwise + // accumulate in merged segments forever. The match is owner-scoped: distinct clients + // can legitimately trap the same range, and erasing another client's block would + // silently kill its write tracking. + if (owner) { + const uint64_t new_end = static_cast(block_key) + size; + for (auto dup_it = protect.blocks.begin(); dup_it != protect.blocks.end();) { + if (dup_it->second.owner == owner && dup_it->first < new_end && block_key < dup_it->first + dup_it->second.size) + dup_it = protect.blocks.erase(dup_it); + else + ++dup_it; + } + } + protect.blocks.emplace(block_key, std::move(block)); + protect_inner(state, addr, protect.size, protect.perm); state.protect_tree.emplace(addr, std::move(protect)); return true; } +void remove_protect_owner(MemState &state, const void *owner) { + if (!owner) + return; + const std::lock_guard lock(state.protect_mutex); + for (auto it = state.protect_tree.begin(); it != state.protect_tree.end();) { + auto &blocks = it->second.blocks; + for (auto b = blocks.begin(); b != blocks.end();) { + if (b->second.owner == owner) + b = blocks.erase(b); + else + ++b; + } + if (blocks.empty()) { + unprotect_inner(state, it->first, it->second.size); + it = state.protect_tree.erase(it); + } else { + ++it; + } + } +} + +void write_through_protection(MemState &state, Address addr, uint32_t size, const std::function &writer) { + const std::lock_guard lock(state.protect_mutex); + + // lift every segment overlapping [addr, addr + size); the tree is keyed descending, so + // walk from the first segment starting at or below the range's last byte until segments + // end before the range starts (segments are non-overlapping, so lower ones end earlier) + std::vector> lifted; + auto it = state.protect_tree.lower_bound(addr + size - 1); + while (it != state.protect_tree.end()) { + if (it->first + it->second.size <= addr) + break; + unprotect_inner(state, it->first, it->second.size); + lifted.emplace_back(it->first, std::move(it->second)); + it = state.protect_tree.erase(it); + } + + writer(); + + for (auto &[seg_addr, seg] : lifted) { + protect_inner(state, seg_addr, seg.size, seg.perm); + state.protect_tree.emplace(seg_addr, std::move(seg)); + } +} + bool is_protecting(MemState &state, Address addr, MemPerm *perm) { const std::lock_guard lock(state.protect_mutex); auto ite = state.protect_tree.lower_bound(addr); @@ -400,6 +528,7 @@ void add_external_mapping(MemState &mem, Address addr, uint32_t size, uint8_t *a mem.page_table[addr / KiB(4)] = page_table_entry; const std::unique_lock lock(mem.protect_mutex); + const std::lock_guard ext_lock(mem.external_mapping_mutex); mem.external_mapping[addr_value] = { addr, size }; } @@ -408,6 +537,7 @@ void remove_external_mapping(MemState &mem, uint8_t *addr_ptr, uint32_t size) { MemExternalMapping mapping; if (mem.use_page_table) { const std::unique_lock lock(mem.protect_mutex); + const std::lock_guard ext_lock(mem.external_mapping_mutex); auto it = mem.external_mapping.find(addr_value); assert(it != mem.external_mapping.end()); @@ -487,6 +617,11 @@ void free(MemState &state, Address address) { const uint32_t page_num = address / STANDARD_PAGE_SIZE; assert(page_num >= 0); + if (state.alloc_table == nullptr) { + LOG_CRITICAL("Freeing unallocated alloc_table"); + return; + } + AllocMemPage &page = state.alloc_table[page_num]; if (!page.allocated) { LOG_CRITICAL("Freeing unallocated page"); diff --git a/vita3k/modules/SceDisplay/SceDisplay.cpp b/vita3k/modules/SceDisplay/SceDisplay.cpp index e084a37a..115ac161 100644 --- a/vita3k/modules/SceDisplay/SceDisplay.cpp +++ b/vita3k/modules/SceDisplay/SceDisplay.cpp @@ -24,8 +24,12 @@ #include #include #include +#include #include +#include +#include + #include TRACY_MODULE_NAME(SceDisplay); @@ -155,6 +159,23 @@ EXPORT(SceInt32, _sceDisplaySetFrameBuf, const SceDisplayFrameBuf *pFrameBuf, Sc info.image_size.y = pFrameBuf->height; update_prediction(emuenv, info); + // TEMP-VSYNC-DEBUG: histogram of vblank deltas the game observes between frame swaps + { + static uint64_t prev_vc = 0; + static int counts[8] = {}; + static int n = 0; + const uint64_t vc = emuenv.display.vblank_count.load(); + const uint64_t d = std::min(vc - prev_vc, 7); + prev_vc = vc; + counts[d]++; + if (++n == 120) { + LOG_INFO("VSDBG swap deltas (last 120): 1vb x{} 2vb x{} 3vb x{} 4vb x{} 5+vb x{}", + counts[1], counts[2], counts[3], counts[4], counts[5] + counts[6] + counts[7]); + memset(counts, 0, sizeof(counts)); + n = 0; + } + } + emuenv.display.last_setframe_vblank_count = emuenv.display.vblank_count.load(); emuenv.frame_count++; diff --git a/vita3k/modules/SceGxm/SceGxm.cpp b/vita3k/modules/SceGxm/SceGxm.cpp index e9f76be6..08acc0eb 100644 --- a/vita3k/modules/SceGxm/SceGxm.cpp +++ b/vita3k/modules/SceGxm/SceGxm.cpp @@ -3609,6 +3609,29 @@ EXPORT(int, sceGxmRenderTargetGetDriverMemBlock, const SceGxmRenderTarget *rende return 0; } +// Replace an immediate context's default-uniform ring with a large private ring: on real +// hardware libgxm never wraps the ring over data the GPU hasn't fetched yet (it tracks the +// GPU fetch pointer), but the emulator has no such backpressure — with the game-provided +// (small) ring, mid-frame wraps hand out slots whose draws are still queued for the +// renderer/GPU, and the game's next uniform write corrupts those draws' uniforms (Killzone +// Mercenary's shadow-caster light matrices flicker erratically). A large ring pushes wraps +// dozens of frames apart, far outside the in-flight window. +static constexpr uint32_t GXM_PRIVATE_UNIFORM_RING_SIZE = MiB(8); + +static Ptr gxmAllocPrivateUniformRing(EmuEnvState &emuenv, const char *name) { + const Ptr ring = Ptr(alloc_aligned(emuenv.mem, GXM_PRIVATE_UNIFORM_RING_SIZE, name, KiB(4))); + if (!ring) + return ring; + + // register it as GPU-visible memory, exactly like sceGxmMapMemory does + const Address base = ring.address(); + emuenv.gxm.memory_mapped_regions.emplace(base, MemoryMapInfo{ base, GXM_PRIVATE_UNIFORM_RING_SIZE, 3 }); + if (emuenv.renderer->features.enable_memory_mapping) + renderer::send_single_command(*emuenv.renderer, nullptr, renderer::CommandOpcode::MemoryMap, true, base, GXM_PRIVATE_UNIFORM_RING_SIZE); + + return ring; +} + EXPORT(int, sceGxmReserveFragmentDefaultUniformBuffer, SceGxmContext *context, Ptr *uniformBuffer) { TRACY_FUNC(sceGxmReserveFragmentDefaultUniformBuffer, context, uniformBuffer); if (!context || !uniformBuffer) @@ -3617,6 +3640,15 @@ EXPORT(int, sceGxmReserveFragmentDefaultUniformBuffer, SceGxmContext *context, P const auto fragment_program = context->state.fragment_program.get(emuenv.mem); const auto program = fragment_program->program.get(emuenv.mem); + if (context->state.type == SCE_GXM_CONTEXT_TYPE_IMMEDIATE && context->state.fragment_ring_buffer_size < GXM_PRIVATE_UNIFORM_RING_SIZE) { + const Ptr ring = gxmAllocPrivateUniformRing(emuenv, "GxmFragmentUniformRing"); + if (ring) { + context->state.fragment_ring_buffer = ring; + context->state.fragment_ring_buffer_size = GXM_PRIVATE_UNIFORM_RING_SIZE; + context->state.fragment_ring_buffer_used = 0; + } + } + const size_t size = (size_t)program->default_uniform_buffer_count * 4; // data for the ring buffer must be 4 bytes aligned context->state.fragment_ring_buffer_used = align(context->state.fragment_ring_buffer_used, 4); @@ -3662,6 +3694,15 @@ EXPORT(int, sceGxmReserveVertexDefaultUniformBuffer, SceGxmContext *context, Ptr const auto vertex_program = context->state.vertex_program.get(emuenv.mem); const auto program = vertex_program->program.get(emuenv.mem); + if (context->state.type == SCE_GXM_CONTEXT_TYPE_IMMEDIATE && context->state.vertex_ring_buffer_size < GXM_PRIVATE_UNIFORM_RING_SIZE) { + const Ptr ring = gxmAllocPrivateUniformRing(emuenv, "GxmVertexUniformRing"); + if (ring) { + context->state.vertex_ring_buffer = ring; + context->state.vertex_ring_buffer_size = GXM_PRIVATE_UNIFORM_RING_SIZE; + context->state.vertex_ring_buffer_used = 0; + } + } + const size_t size = (size_t)program->default_uniform_buffer_count * 4; // data for the ring buffer must be 4 bytes aligned context->state.vertex_ring_buffer_used = align(context->state.vertex_ring_buffer_used, 4); @@ -4066,16 +4107,25 @@ EXPORT(void, sceGxmSetFrontVisibilityTestEnable, SceGxmContext *context, SceGxmV renderer::set_visibility_index(*emuenv.renderer, context->renderer.get(), context->state.visibility_enable, context->state.visibility_index, context->state.visibility_is_increment); } -EXPORT(void, sceGxmSetFrontVisibilityTestIndex, SceGxmContext *context, uint32_t index) { +EXPORT(int, sceGxmSetFrontVisibilityTestIndex, SceGxmContext *context, uint32_t index) { TRACY_FUNC(sceGxmSetFrontVisibilityTestIndex, context, index); if (!emuenv.renderer->features.enable_memory_mapping) { UNIMPLEMENTED(); - return; + return 0; + } + + // libgxm validates the index (the hardware register field is 14 bits) and rejects + // out-of-range values, keeping the previous index. KZM passes 0xDEADBEEF sentinels + // here — redirecting them into a real slot corrupts that slot's occlusion count. + if (index > 16383) { + LOG_WARN_ONCE("Visibility index out of range: 0x{:X}", index); + return RET_ERROR(SCE_GXM_ERROR_INVALID_VALUE); } context->state.visibility_index = index; renderer::set_visibility_index(*emuenv.renderer, context->renderer.get(), context->state.visibility_enable, context->state.visibility_index, context->state.visibility_is_increment); + return 0; } EXPORT(void, sceGxmSetFrontVisibilityTestOp, SceGxmContext *context, SceGxmVisibilityTestOp op) { @@ -4408,6 +4458,26 @@ EXPORT(int, sceGxmSetVisibilityBuffer, SceGxmContext *immediateContext, Ptrfeatures.enable_memory_mapping) { + // The Vita GPU has SCE_GXM_GPU_CORE_COUNT cores. Each core accumulates the + // occlusion count for the tiles it processed into its OWN region at + // base + core * stridePerCore, and the application sums the per-core counts to + // get the total sample count for a visibility index. + // + // We emulate the whole GPU with a single Vulkan occlusion query, whose result is + // already the TOTAL, and write it into core 0's region only (see + // VKContext::stop_recording -> copyQueryPoolResults). The remaining core regions + // are never written, so they keep whatever stale guest data happens to be there + // and the game's sum becomes total + garbage — an unstable value that changes + // between frames. Killzone Mercenary drives its light/shadow selection off these + // sums, so the instability makes it alternate between two valid caster sets every + // few frames: the erratic shadow flicker (it renders genuinely different draw + // batches on those frames, which is why no renderer-side fix affected it). + // Zeroing the cores we don't emulate makes the sum equal the true total. + if (uint8_t *vis_base = static_cast(bufferBase.get(emuenv.mem))) { + LOG_INFO_ONCE("Zeroing visibility buffer cores 1-{} (stride {}) so per-core sums are exact", + SCE_GXM_GPU_CORE_COUNT - 1, stridePerCore); + memset(vis_base + stridePerCore, 0, (SCE_GXM_GPU_CORE_COUNT - 1) * static_cast(stridePerCore)); + } renderer::set_visibility_buffer(*emuenv.renderer, immediateContext->renderer.get(), bufferBase.cast(), stridePerCore); } else { STUBBED("Set all visible"); @@ -4868,8 +4938,9 @@ EXPORT(int, sceGxmSyncObjectDestroy, Ptr syncObject) { std::lock_guard lock(emuenv.gxm.sync_objects_mutex); emuenv.gxm.sync_objects.erase(syncObject.get(emuenv.mem)); } - renderer::destroy(syncObject.get(emuenv.mem), *emuenv.renderer); - free(emuenv.mem, syncObject); + renderer::destroy(syncObject.get(emuenv.mem), *emuenv.renderer, [&mem = emuenv.mem, syncObject]() { + free(mem, syncObject); + }); return 0; } @@ -4930,7 +5001,7 @@ EXPORT(uint32_t, sceGxmTextureGetLodMin, const SceGxmTexture *texture) { return 0; } - return texture->lod_min0 | (texture->lod_min1 << 2); + return texture->true_lod_min(); } EXPORT(int, sceGxmTextureGetMagFilter, const SceGxmTexture *texture) { @@ -5246,8 +5317,8 @@ EXPORT(int, sceGxmTextureSetLodMin, SceGxmTexture *texture, uint32_t lodMin) { return RET_ERROR(SCE_GXM_ERROR_UNSUPPORTED); } - texture->lod_min0 = lodMin & 3; - texture->lod_min1 = lodMin >> 2; + texture->lod_min0 = lodMin >> 2; + texture->lod_min1 = lodMin & 3; return 0; } diff --git a/vita3k/modules/SceKernelThreadMgr/SceThreadmgr.cpp b/vita3k/modules/SceKernelThreadMgr/SceThreadmgr.cpp index b59fb56c..6b40fbbc 100644 --- a/vita3k/modules/SceKernelThreadMgr/SceThreadmgr.cpp +++ b/vita3k/modules/SceKernelThreadMgr/SceThreadmgr.cpp @@ -18,6 +18,7 @@ #include "SceThreadmgr.h" #include +#include #include #include #include @@ -27,6 +28,7 @@ #include #include +#include #include #include diff --git a/vita3k/modules/SceLibKernel/SceLibKernel.cpp b/vita3k/modules/SceLibKernel/SceLibKernel.cpp index cceae267..4c4b58e1 100644 --- a/vita3k/modules/SceLibKernel/SceLibKernel.cpp +++ b/vita3k/modules/SceLibKernel/SceLibKernel.cpp @@ -24,6 +24,7 @@ #include <../SceKernelThreadMgr/SceThreadmgr.h> #include +#include #include #include #include diff --git a/vita3k/renderer/include/renderer/functions.h b/vita3k/renderer/include/renderer/functions.h index f771528b..1632c96e 100644 --- a/vita3k/renderer/include/renderer/functions.h +++ b/vita3k/renderer/include/renderer/functions.h @@ -20,6 +20,7 @@ #include #include +#include #include struct MemState; @@ -40,7 +41,7 @@ struct YUVConversionCache; bool create(std::unique_ptr &fp, State &state, const SceGxmProgram &program, const SceGxmBlendInfo *blend, GXPPtrMap &gxp_ptr_map); bool create(std::unique_ptr &vp, State &state, const SceGxmProgram &program, GXPPtrMap &gxp_ptr_map, const std::vector &attributes); void create(SceGxmSyncObject *sync, State &state); -void destroy(SceGxmSyncObject *sync, State &state); +void destroy(SceGxmSyncObject *sync, State &state, std::function dealloc = nullptr); void finish(State &state, Context *context); enum class SyncWaitResult { diff --git a/vita3k/renderer/include/renderer/vulkan/gxm_to_vulkan.h b/vita3k/renderer/include/renderer/vulkan/gxm_to_vulkan.h index 578f6f89..3ba000ce 100644 --- a/vita3k/renderer/include/renderer/vulkan/gxm_to_vulkan.h +++ b/vita3k/renderer/include/renderer/vulkan/gxm_to_vulkan.h @@ -41,6 +41,7 @@ vk::StencilOp translate_stencil_op(SceGxmStencilOp stencil_op); namespace color { vk::Format translate_format(SceGxmColorBaseFormat base_format); +vk::Format translate_surface_format(SceGxmColorBaseFormat base_format); vk::ComponentMapping translate_swizzle(SceGxmColorFormat format); } // namespace color diff --git a/vita3k/renderer/include/renderer/vulkan/state.h b/vita3k/renderer/include/renderer/vulkan/state.h index d5fe8e84..15bde591 100644 --- a/vita3k/renderer/include/renderer/vulkan/state.h +++ b/vita3k/renderer/include/renderer/vulkan/state.h @@ -115,6 +115,10 @@ struct VKState : public renderer::State { // support for the VK_KHR_uniform_buffer_standard_layout extension, needed for memory mapping and texture viewport bool support_standard_layout = false; bool support_rasterized_order_access = false; + // VK_EXT_depth_bias_control: exact GXM depth-bias semantics + bool support_depth_bias_control = false; + // true: float representation available; false: FORCE_UNORM fallback representation + bool depth_bias_control_use_float = false; LinuxSurfaceType linux_surface_type = LinuxSurfaceType::Unknown; #ifdef __ANDROID__ diff --git a/vita3k/renderer/include/renderer/vulkan/surface_cache.h b/vita3k/renderer/include/renderer/vulkan/surface_cache.h index 9c958493..5f87459b 100644 --- a/vita3k/renderer/include/renderer/vulkan/surface_cache.h +++ b/vita3k/renderer/include/renderer/vulkan/surface_cache.h @@ -23,6 +23,9 @@ #include #include +#include +#include +#include #include struct SwsContext; @@ -64,14 +67,20 @@ struct Framebuffer { struct CastedTexture { vkutil::Image texture; - // only used if an image to image copy is not possible + // only used if an image to image copy is not possible (cropped/partial reads) vkutil::Buffer transition_buffer; + // storage view of the texture, used as the compute de-interleave output + vk::ImageView reinterpret_view = nullptr; uint64_t scene_timestamp = 0; uint32_t cropped_x = 0; uint32_t cropped_y = 0; uint32_t cropped_width = 0; uint32_t cropped_height = 0; SceGxmColorBaseFormat format; + // mip levels served by this cast (from the bind's declared mip chain) + uint16_t mip_count = 1; + // TEMP dual-phase experiment: frame parity this cast belongs to (mask-pool casts only) + uint8_t frame_parity = 0; }; struct ColorSurfaceCacheInfo : public SurfaceCacheInfo { @@ -92,6 +101,10 @@ struct ColorSurfaceCacheInfo : public SurfaceCacheInfo { // same image with a different view(swizzle) used for sampling vk::ImageView alternate_view = nullptr; + // R32G32_UINT view of this surface, used as the raw-word input to the + // typeless reinterpret compute pass. Only created if that path is taken. + vk::ImageView reinterpret_store_view = nullptr; + // only used when upscaling is enabled, to downscale the image first std::unique_ptr blit_image; @@ -110,12 +123,49 @@ struct ColorSurfaceCacheInfo : public SurfaceCacheInfo { // only for double buffer, do we need to sync the two views? bool need_buffer_sync = false; + // the surface is synced only for GPU raw buffer reads: keep the data in the mapped + // GPU buffer but skip the write-back to guest RAM (a CPU-side write-back would trip the + // surface's own dirty trap and force a full re-upload of the surface every frame) + bool gpu_read_sync_only = false; + + // union of the address ranges the CPU actually reads from this surface (via transfers). + // When set, guest RAM write-backs are restricted to this range: the surface's address + // range can be a memory pool that also holds CPU-written buffers (e.g. Killzone + // Mercenary's exposure meter chain) which a full-range write-back would clobber. + Address cpu_read_range_start = 0; + uint32_t cpu_read_range_size = 0; + std::shared_ptr dirty = std::make_shared(false); + // union of address ranges the GUEST CPU has written inside this surface's memory + // (excluding the emulator's own surface-sync write-backs). Texture reads intersecting + // this range must be served from guest RAM, not from the surface image: games copy + // data (e.g. Killzone Mercenary's per-light shadow-mask snapshots) into buffers that + // share a memory pool with render targets, and the surface image does NOT contain + // that CPU-written data. Persistent for the lifetime of the cache entry. + std::shared_ptr> guest_written_range = std::make_shared>(0, 0); + + // recent GXM TRANSFER destinations inside this surface's range, with the frame they + // were written. Unlike guest_written_range (a union poisoned by ordinary ring writes), + // this only holds explicit transfer copies — e.g. KZM's per-light shadow-mask + // snapshots, which consumers then bind as textures: those binds must be served from + // guest RAM (where the CPU transfer wrote), not from the live surface image, or the + // sampled mask alternates with the pool's round-robin re-use of the slot. + struct GuestTransferWrite { + Address start; + Address end; + uint64_t frame; + }; + std::shared_ptr> guest_transfer_writes = std::make_shared>(); + ColorSurfaceCacheInfo() = default; ~ColorSurfaceCacheInfo(); }; +// set while the emulator itself writes surface data back to guest memory, so the +// surface write traps can tell emulator write-backs apart from genuine guest writes +extern thread_local bool surface_sync_internal_write; + struct DepthSurfaceView { vkutil::Image depth_view; // only contains an image view with the stencil aspect @@ -156,12 +206,28 @@ struct SurfaceRetrieveResult { vkutil::Image *base_image; }; +// for use with the surface_cast_reinterpret shader +struct ReinterpretPushConstants { + uint32_t out_width; + uint32_t out_height; + uint32_t scaled_store_w; + uint32_t scaled_store_h; + uint32_t ratio; + uint32_t half_index; +}; + class VKSurfaceCache { private: VKState &state; - // only have 20 color surfaces and 20 depth surfaces allocated at most at a given time - static constexpr uint32_t max_surfaces_allowed = 20; + // maximum number of color / depth surfaces allocated at a given time. + // must exceed the number of distinct surfaces a game uses across two consecutive frames: + // a surface rendered in frame N and only consumed in frame N+1 (e.g. Killzone Mercenary's + // half-rate-updated 128x128 dynamic shadow masks) must survive a full frame of cache churn, + // otherwise it is evicted and recreated cleared, making the shadow flicker off every other + // frame. Larger pools keep stale entries at aliased pool addresses alive longer, which is + // why the range-based lookups must prefer the most recently rendered overlapping entry. + static constexpr uint32_t max_surfaces_allowed = 64; std::map color_address_lookup; @@ -189,6 +255,52 @@ private: void destroy_surface(ColorSurfaceCacheInfo &info); void destroy_surface(DepthStencilSurfaceCacheInfo &info); + // compute pipeline that re-groups a typeless byte-reinterpret so the wanted + // 32-bit half is written coherently at fully upscaled resolution, instead of + // being interleaved every column (which misaligns the sampling) + vk::ShaderModule reinterpret_shader = nullptr; + vk::DescriptorSetLayout reinterpret_desc_layout = nullptr; + vk::PipelineLayout reinterpret_pipeline_layout = nullptr; + vk::Pipeline reinterpret_pipeline = nullptr; + vk::DescriptorPool reinterpret_desc_pool = nullptr; + std::vector reinterpret_desc_sets; + uint32_t reinterpret_desc_idx = 0; + // point sampler used to texelFetch the store inside the reinterpret shader + vk::Sampler reinterpret_sampler = nullptr; + + // lazily build the reinterpret compute pipeline (no-op once built) + void ensure_reinterpret_pipeline(); + + // record and submit a one-off command buffer that copies the surface image into the + // mapped memory buffer at its guest address (shared by check_for_surface and + // sync_surface_for_gpu_read). If mem is non-null, the whole sync (fence wait, guest RAM + // write-back, post sync) completes synchronously before returning, and only + // [sync_addr, sync_addr + sync_size) is written to guest RAM: a surface's address range + // can be a memory pool that also holds CPU-written data (e.g. transfer destinations), + // which a full-range write-back would clobber with stale image content. + void submit_immediate_surface_sync(ColorSurfaceCacheInfo &surface, MemState *mem, Address sync_addr = 0, uint32_t sync_size = 0); + + // guest-RAM writes queued on the wait thread (transfer operations whose results the + // GPU-ordered request queue delivers later): texture uploads hash guest RAM on the + // renderer thread, so they must wait for intersecting queued writes to land first + struct PendingGuestWrite { + Address start; + Address end; + uint64_t id; + }; + std::mutex pending_guest_writes_mutex; + std::condition_variable pending_guest_writes_cv; + std::vector pending_guest_writes; + uint64_t next_pending_write_id = 0; + // lock-free fast path for wait_for_pending_guest_write when nothing is pending + std::atomic pending_guest_write_count{ 0 }; + + // queue callback on the wait thread; if the callback writes guest RAM at + // [target_address, target_address + target_size), register the range so texture + // uploads reading it wait for the write instead of hashing stale data + void push_guest_write_callback(CallbackRequestFunction &&callback, Address target_address, uint32_t target_size); + void complete_pending_guest_write(uint64_t id); + public: // when creating a mutable image, can we pass as an argument // the possible format used for an image view to improve performance ? @@ -202,12 +314,22 @@ public: explicit VKSurfaceCache(VKState &state); void cleanup(); + uint32_t color_surface_count() const { return static_cast(color_address_lookup.size()); } + SurfaceRetrieveResult retrieve_color_surface_for_framebuffer(MemState &mem, SceGxmColorSurface *color); std::optional retrieve_color_surface_as_texture(const SceGxmTexture &texture, const SceGxmColorBaseFormat base_format, TextureViewport *texture_viewport); SurfaceRetrieveResult retrieve_depth_stencil_for_framebuffer(SceGxmDepthStencilSurface *depth_stencil, const uint32_t width, const uint32_t height); std::optional retrieve_depth_stencil_as_texture(const SceGxmTexture &texture, TextureViewport *texture_viewport); + // block until no queued wait-thread guest write intersects [addr, addr + size): the + // request queue executes transfer results in GPU order after the producing scene's + // fence, while texture uploads hash guest RAM immediately — sampling before the queued + // write lands uploads the previous frame's data (Killzone Mercenary's per-light shadow + // masks visibly lag/flicker while the view changes). Safe to call at texture-bind time: + // the producing scene is already submitted, so the queue always drains + void wait_for_pending_guest_write(Address addr, uint32_t size); + Framebuffer &retrieve_framebuffer_handle(MemState &mem, SceGxmColorSurface *color, SceGxmDepthStencilSurface *depth_stencil, vk::RenderPass standard_render_pass, vk::RenderPass interlock_render_pass, vk::ImageView &color_view, vk::ImageView &ds_view); @@ -216,7 +338,15 @@ public: // synchronize the surface back to the RAM then only call the callback // if this call is used for a copy or similar operation set the changed address to the destination // so that subsequent calls to check_for_surface with the target destination also get delayed - bool check_for_surface(MemState &mem, Address source_address, CallbackRequestFunction &callback, Address target_address); + bool check_for_surface(MemState &mem, Address source_address, CallbackRequestFunction &callback, Address target_address, uint32_t source_size = 0, uint32_t target_size = 0); + + // Called when a guest address is about to be read by the GPU through raw buffer + // accesses (e.g. a uniform buffer aliasing a rendered surface, like Killzone Mercenary's + // bloom chain reading its HDR render target). If the address lies inside a + // recently-rendered color surface, make sure the surface content is synced into the + // mapped memory buffer so the shader reads up-to-date data. Returns true if the address + // belongs to such a surface. + bool sync_surface_for_gpu_read(Address address, uint32_t size); // If non-null, the return value must be sent as a PostSurfaceSyncRequest ColorSurfaceCacheInfo *perform_surface_sync(); diff --git a/vita3k/renderer/include/renderer/vulkan/types.h b/vita3k/renderer/include/renderer/vulkan/types.h index e85fb542..975d01c7 100644 --- a/vita3k/renderer/include/renderer/vulkan/types.h +++ b/vita3k/renderer/include/renderer/vulkan/types.h @@ -22,6 +22,10 @@ #include #include +#include +#include +#include + struct MemState; namespace renderer::vulkan { @@ -142,6 +146,16 @@ struct TrappedBuffer { uint32_t extra; // no need for it to be atomic bool dirty = false; + // dirty specifically from a genuine guest (game) write, not the emulator's own + // surface-sync write-backs to guest RAM: shader-store buffers must not have their + // mapped mirror (which can hold GPU-written data guest RAM never sees) refreshed + // from guest RAM unless the game actually wrote there + bool guest_dirty = false; + // pages (4K-aligned guest addresses) the game actually wrote since the last refresh: + // shader-store buffers mix CPU-written data with GPU-stored output (e.g. Killzone + // Mercenary's skinned shadow casters), so only genuinely written pages may be + // refreshed from guest RAM — a whole-range refresh would clobber the GPU's vertices + std::vector
guest_dirty_pages; uint8_t *mapped_location; TrappedBuffer() {} @@ -153,12 +167,15 @@ struct BufferTrapping { std::map trapped_buffers; // Used when no buffer trapping is applied TrappedBuffer temp_buffer; + // guards guest_dirty_pages: appended from the fault handler (game thread), consumed + // by access_buffer (renderer thread) + std::mutex dirty_pages_mutex; VKState &state; BufferTrapping(VKState &state); TrappedBuffer *access_buffer(Address addr, uint32_t size, MemState &mem, bool always_trap = false, bool cover_everything = false); - void remove_range(Address start, Address end); + void remove_range(MemState &mem, Address start, Address end); }; // Use vulkan queries to implement visibility buffer diff --git a/vita3k/renderer/src/creation.cpp b/vita3k/renderer/src/creation.cpp index ed8ad97d..90b7092a 100644 --- a/vita3k/renderer/src/creation.cpp +++ b/vita3k/renderer/src/creation.cpp @@ -249,8 +249,15 @@ void create(SceGxmSyncObject *sync, State &state) { sync->being_deleted = false; } -void destroy(SceGxmSyncObject *sync, State &state) { - // nothing to do right now +void destroy(SceGxmSyncObject *sync, State &state, std::function dealloc) { + if (dealloc && state.current_backend == Backend::Vulkan && state.features.enable_memory_mapping) { + auto *vk_state = static_cast(&state); + vk_state->request_queue.push(vulkan::CallbackRequest{ + new vulkan::CallbackRequestFunction(std::move(dealloc)) + }); + } else if (dealloc) { + dealloc(); + } } bool init(FrameHost &frame, std::unique_ptr &state, Backend backend, const Config &config, const Root &root_paths) { diff --git a/vita3k/renderer/src/gl/texture.cpp b/vita3k/renderer/src/gl/texture.cpp index 5b940d67..ea84da02 100644 --- a/vita3k/renderer/src/gl/texture.cpp +++ b/vita3k/renderer/src/gl/texture.cpp @@ -39,7 +39,7 @@ static void apply_sampler_state(const SceGxmTexture &gxm_texture, const GLenum t #ifndef __ANDROID__ glTexParameterf(texture_bind_type, GL_TEXTURE_LOD_BIAS, (static_cast(gxm_texture.lod_bias) - 31.f) / 8.f); #endif - glTexParameteri(texture_bind_type, GL_TEXTURE_MIN_LOD, gxm_texture.lod_min0 | (gxm_texture.lod_min1 << 2)); + glTexParameteri(texture_bind_type, GL_TEXTURE_MIN_LOD, gxm_texture.true_lod_min()); glTexParameteri(texture_bind_type, GL_TEXTURE_MIN_FILTER, min_filter); glTexParameteri(texture_bind_type, GL_TEXTURE_MAG_FILTER, mag_filter); diff --git a/vita3k/renderer/src/sync.cpp b/vita3k/renderer/src/sync.cpp index 684c12ca..9ac321b1 100644 --- a/vita3k/renderer/src/sync.cpp +++ b/vita3k/renderer/src/sync.cpp @@ -48,7 +48,9 @@ COMMAND(handle_signal_sync_object) { if (features.enable_memory_mapping && config.current_config.high_accuracy) { assert(renderer.current_backend == renderer::Backend::Vulkan); vulkan::signal_sync_object(dynamic_cast(renderer), sync, timestamp); - } else { + } + else // Nick + { renderer::subject_done(sync, timestamp); } } diff --git a/vita3k/renderer/src/texture/cache.cpp b/vita3k/renderer/src/texture/cache.cpp index 9f9d9271..a7de641c 100644 --- a/vita3k/renderer/src/texture/cache.cpp +++ b/vita3k/renderer/src/texture/cache.cpp @@ -21,13 +21,16 @@ #include #include +#include #include #include #include #include #include +#include #include +#include #if defined(__x86_64__) && !defined(__APPLE__) #include #else @@ -53,6 +56,13 @@ uint64_t hash_texture_data(const SceGxmTexture &texture, uint32_t texture_size, const Ptr data(texture.data_addr << 2); uint64_t data_hash = 0; + // the texture memory may have been freed by the game while this bind was queued + // (aggressive texture streaming) — don't read invalid guest memory + if (data.address() && !is_valid_addr_range(mem, data.address(), data.address() + texture_size)) { + LOG_WARN_ONCE("Texture data not in valid memory (addr=0x{:08X} size={}), not hashing", data.address(), texture_size); + return 0; + } + if (data.address()) { data_hash = hash_data(data.get(mem), texture_size); } @@ -436,6 +446,23 @@ void TextureCache::upload_texture(const SceGxmTexture &gxm_texture, MemState &me pixels_per_stride = align(pixels_per_stride, align_width); memory_height = align(memory_height, align_height); + // The texture data lives in guest memory that the game can free at any time: with + // aggressive texture streaming (e.g. Killzone Mercenary), a queued texture bind can + // be processed after the game already released the staging memory, and reading it + // would crash on decommitted pages. Validate this mip's source range and bail out + // gracefully instead (the texture content is undefined in that case anyway, which + // matches what the real hardware would sample). + { + const uint32_t src_nb_pixels = align(layout_width, align_width) * align(layout_height, align_height); + const uint32_t src_mip_size = (src_nb_pixels >> block_shift) * block_size; + const Address src_address = (gxm_texture.data_addr << 2) + total_source_so_far; + if (src_mip_size > 0 && !is_valid_addr_range(mem, src_address, src_address + src_mip_size)) { + LOG_WARN("Texture upload source is not in valid memory (addr=0x{:08X} size={}), skipping upload", + src_address, src_mip_size); + break; + } + } + // perform all needed conversions (formats not supported by modern GPUs) switch (base_format) { case SCE_GXM_TEXTURE_BASE_FORMAT_P4: @@ -539,6 +566,21 @@ void TextureCache::upload_texture(const SceGxmTexture &gxm_texture, MemState &me break; } + // ================= TEMP-LMPROBE (HARDCODED — remove after the run) ================= + // Killzone Mercenary black-section bug. RenderDoc proved: the black pixels sample the + // 1024x256 UBC2 lightmap (LM0) at texel (324,130) = BC2 block (81,32), whose UPLOADED + // data is c0=0x1b4f c1=0x0000 with index bits selecting c1 -> pure black. The UVs are + // correct (100% block coverage) and the decode is correct, so the only open question + // is whether the guest data we read actually contains that black block, or whether our + // unswizzle places the WRONG source block there. + // resolve_z_order_compressed_image is a pure block permutation, so this is decidable: + // recompute the source index for the dest block with the same math, then print the + // source bytes, the dest bytes, and the bytes the block SHOULD have come from. + const uint8_t *lm_src_before = static_cast(pixels); + const bool lm_probe = (base_format == SCE_GXM_TEXTURE_BASE_FORMAT_UBC2) + && (pixels_per_stride == 1024) && (memory_height == 256) && (mip_index == 0); + // ================================================================================== + if (texture_type != SCE_GXM_TEXTURE_LINEAR && texture_type != SCE_GXM_TEXTURE_LINEAR_STRIDED && !gxm::is_pvrt_format(base_format)) { // Convert data to linear layout texture_pixels_lineared.resize(pixels_per_stride * memory_height * bytes_per_pixel); @@ -556,6 +598,102 @@ void TextureCache::upload_texture(const SceGxmTexture &gxm_texture, MemState &me pixels = texture_pixels_lineared.data(); } + // ================= TEMP-LMPROBE output ================= + if (lm_probe) { + static int lm_hits = 0; + if (lm_hits < 24) { + lm_hits++; + const Address guest_base = (gxm_texture.data_addr << 2) + total_source_so_far; + const uint8_t *dst = static_cast(pixels); + + auto hex16 = [](const uint8_t *p) { + std::string s; + for (int i = 0; i < 16; i++) + s += fmt::format("{:02x} ", p[i]); + return s; + }; + auto blkdesc = [](const uint8_t *p) { + const uint16_t c0 = static_cast(p[8] | (p[9] << 8)); + const uint16_t c1 = static_cast(p[10] | (p[11] << 8)); + const uint32_t idx = static_cast(p[12] | (p[13] << 8) | (p[14] << 16) | (p[15] << 24)); + bool alpha_zero = true; + for (int i = 0; i < 8; i++) + if (p[i]) alpha_zero = false; + return fmt::format("c0=0x{:04x} c1=0x{:04x} idx=0x{:08x} alphaZero={}", c0, c1, idx, alpha_zero); + }; + + LOG_WARN("TEMP-LMPROBE ==== UBC2 {}x{} upload #{} ====", pixels_per_stride, memory_height, lm_hits); + LOG_WARN("TEMP-LMPROBE guest=0x{:08X} type={} mipcnt={} truemip={} swizzled={} bpp={} bsz={} src_so_far={}", + guest_base, static_cast(texture_type), static_cast(gxm_texture.mip_count), + static_cast(gxm_texture.true_mip_count()), is_swizzled, bpp, block_size, total_source_so_far); + LOG_WARN("TEMP-LMPROBE width={} height={} layout={}x{} align={}x{} pps={} memh={}", + width, height, layout_width, layout_height, align_width, align_height, pixels_per_stride, memory_height); + + // reproduce resolve_z_order_compressed_image's mapping to find, for a DEST + // block (bx,by), which SOURCE index it was taken from + const uint32_t bcx = pixels_per_stride / 4, bcy = memory_height / 4; + const uint32_t mn = std::min(bcx, bcy); + const uint32_t kk = std::bit_width(mn) - 1; + auto src_index_for = [&](uint32_t bx, uint32_t by) -> uint32_t { + for (uint32_t i = 0; i < bcx * bcy; i++) { + uint32_t x = renderer::texture::decode_morton2_x(i) & (mn - 1); + uint32_t y = renderer::texture::decode_morton2_y(i) & (mn - 1); + uint32_t upper = (i >> (2 * kk)) << kk; + if (bcx >= bcy) x |= upper; else y |= upper; + if (x == bx && y == by) + return i; + } + return 0xFFFFFFFFu; + }; + + struct { uint32_t bx, by; const char *tag; } probes[] = { + { 81, 32, "DARK (renders black)" }, + { 83, 36, "BRIGHT(renders fine)" }, + { 80, 32, "dark neighbour" }, + { 84, 36, "bright neighbour" }, + }; + for (auto &pr : probes) { + const uint32_t di = pr.by * bcx + pr.bx; + const uint32_t si = src_index_for(pr.bx, pr.by); + LOG_WARN("TEMP-LMPROBE block({:>3},{:>2}) {} destIdx={} srcIdx={} srcByteOff={}", + pr.bx, pr.by, pr.tag, di, si, si * 16); + LOG_WARN("TEMP-LMPROBE DEST bytes: {} | {}", hex16(dst + di * 16), blkdesc(dst + di * 16)); + if (si != 0xFFFFFFFFu) { + LOG_WARN("TEMP-LMPROBE SRC bytes: {} | {}", hex16(lm_src_before + si * 16), blkdesc(lm_src_before + si * 16)); + LOG_WARN("TEMP-LMPROBE (match={})", memcmp(dst + di * 16, lm_src_before + si * 16, 16) == 0); + } + // what a LINEAR (unswizzled) read would have given at this position + LOG_WARN("TEMP-LMPROBE SRC@linear: {} | {}", hex16(lm_src_before + di * 16), blkdesc(lm_src_before + di * 16)); + } + + // whole-texture character: how much of the SOURCE is black-endpoint / empty + uint32_t src_c1_black = 0, src_empty = 0, src_alpha_nz = 0, total_blocks = bcx * bcy; + for (uint32_t i = 0; i < total_blocks; i++) { + const uint8_t *p = lm_src_before + i * 16; + const uint16_t c1 = static_cast(p[10] | (p[11] << 8)); + if (c1 == 0) src_c1_black++; + bool empty = true; + for (int k = 8; k < 16; k++) + if (p[k]) empty = false; + if (empty) src_empty++; + for (int k = 0; k < 8; k++) + if (p[k]) { src_alpha_nz++; break; } + } + LOG_WARN("TEMP-LMPROBE SOURCE stats: blocks={} c1==0x0000:{} colourEmpty:{} alphaNonZero:{}", + total_blocks, src_c1_black, src_empty, src_alpha_nz); + + // guest-memory sanity: hash + first bytes, so a stale/partial stream shows up + uint32_t h = 2166136261u; + const uint32_t src_bytes = (pixels_per_stride / 4) * (memory_height / 4) * 16; + for (uint32_t i = 0; i < src_bytes; i++) { + h ^= lm_src_before[i]; + h *= 16777619u; + } + LOG_WARN("TEMP-LMPROBE SOURCE fnv32={:08x} bytes={} first16={}", h, src_bytes, hex16(lm_src_before)); + } + } + // ======================================================= + if (!support_dxt && gxm::is_bcn_format(base_format)) { // decompress the texture const int num_comp = gxm::get_num_components(base_format); @@ -718,6 +856,66 @@ void TextureCache::cache_and_bind_texture(const SceGxmTexture &gxm_texture, MemS upload = false; } + // ================= TEMP-LMPROBE-BIND (HARDCODED — remove after the run) ================= + // The black-section lightmap (1024x256 UBC2) is 256 KiB, i.e. >= 4 pages, so + // should_use_hash is FALSE and it takes the PAGE-PROTECTION path: upload = info->dirty. + // If any guest write to it escapes the protection tracking, dirty stays false, we never + // re-upload, and the GPU keeps stale texture content forever — which would explain a + // lightmap whose blocks encode black where the game has since written real lighting. + // Hash the guest bytes at EVERY bind and compare against what we last actually uploaded. + // STALE=true on any line is the smoking gun; it means the guest data changed while we + // decided not to re-upload. + { + const SceGxmTextureFormat lb_fmt = gxm::get_format(gxm_texture); + const SceGxmTextureBaseFormat lb_base = gxm::get_base_format(lb_fmt); + if (lb_base == SCE_GXM_TEXTURE_BASE_FORMAT_UBC2 + && gxm::get_width(gxm_texture) == 1024 && gxm::get_height(gxm_texture) == 256) { + static std::map lm_last_uploaded; + static std::map lm_seen_count; + static int lm_bind_logs = 0; + const Address lb_addr = gxm_texture.data_addr << 2; + const uint32_t lb_size = gxm::texture_size_first_mip(gxm_texture); + // the game can free streamed texture memory while a bind is queued — never read + // guest memory that is no longer valid, a crash here would waste the whole run + const bool lb_valid = lb_addr && lb_size + && is_valid_addr_range(mem, lb_addr, lb_addr + lb_size); + const uint8_t *lb_guest = lb_valid ? Ptr(lb_addr).get(mem) : nullptr; + // strided sample (every 64th byte, ~4 KiB read) — plenty to detect a content + // change, cheap enough to run on every bind without wrecking the frame rate + uint32_t lb_h = 2166136261u; + if (lb_guest) { + for (uint32_t i = 0; i < lb_size; i += 64) { + lb_h ^= lb_guest[i]; + lb_h *= 16777619u; + } + } + auto prev = lm_last_uploaded.find(lb_addr); + const bool lb_known = (prev != lm_last_uploaded.end()); + const bool lb_changed = lb_known && (prev->second != lb_h); + const bool lb_stale = lb_changed && !upload; + const uint32_t n = ++lm_seen_count[lb_addr]; + // always log the first binds, every change, every upload, and any stale hit + if (lm_bind_logs < 300 || lb_changed || upload || lb_stale) { + lm_bind_logs++; + // Protection covers only whole pages INSIDE the texture: the head is rounded + // up and the tail rounded down, so a partial head/tail page is never write + // protected and writes there never set dirty. Report exactly how many bytes + // of this texture are unprotected, and where the black block falls. + const int64_t head_gap = static_cast(range_protect_begin) - static_cast(lb_addr); + const int64_t tail_gap = static_cast(lb_addr + lb_size) - static_cast(range_protect_end); + LOG_WARN("TEMP-LMPROBE-BIND #{} addr=0x{:08X} size={} cached={} use_hash={} dirty={} UPLOAD={} guestFNV={:08x} lastUploadedFNV={} CHANGED={} STALE={}", + n, lb_addr, lb_size, cached_gxm_texture_index != -1, info->use_hash, info->dirty, upload, + lb_h, lb_known ? fmt::format("{:08x}", prev->second) : std::string("none"), + lb_changed, lb_stale); + LOG_WARN("TEMP-LMPROBE-BIND protect=[0x{:08X}..0x{:08X}) pagesz={} UNPROTECTED head={} tail={} (black block src is ~byte 102416 of {})", + range_protect_begin, range_protect_end, mem.host_page_size, head_gap, tail_gap, lb_size); + } + if (upload) + lm_last_uploaded[lb_addr] = lb_h; + } + } + // ======================================================================================= + if (upload && !info->use_hash && (import_textures || export_textures)) { // we still need to get a hash of the texture info->hash = hash_texture_nostride(gxm_texture, mem); @@ -764,13 +962,15 @@ void TextureCache::cache_and_bind_texture(const SceGxmTexture &gxm_texture, MemS if (!info->use_hash) { info->dirty = false; - add_protect(mem, range_protect_begin, range_protect_end - range_protect_begin, MemPerm::ReadOnly, [info, texture_repr](Address, bool) { - if (memcmp(&info->texture, &texture_repr, sizeof(SceGxmTexture)) == 0) { - info->dirty = true; - } - - return true; - }); + add_protect( + mem, range_protect_begin, range_protect_end - range_protect_begin, MemPerm::ReadOnly, [info, texture_repr](Address, bool) { + if (memcmp(&info->texture, &texture_repr, sizeof(SceGxmTexture)) == 0) { + info->dirty = true; + } + + return true; + }, + info); } upload_done(); diff --git a/vita3k/renderer/src/transfer.cpp b/vita3k/renderer/src/transfer.cpp index a79677c6..7095a03a 100644 --- a/vita3k/renderer/src/transfer.cpp +++ b/vita3k/renderer/src/transfer.cpp @@ -15,6 +15,8 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +#include + #include #include #include @@ -26,6 +28,7 @@ #include #include +#include // keywords.h must be after tracy.h for msvc compiler #include @@ -41,12 +44,42 @@ static void perform_transfer_copy_impl(MemState &mem, const SceGxmTransferImage T *__restrict__ src_ptr = src.address.cast().get(mem); T *__restrict__ dst_ptr = dst.address.cast().get(mem); + // Fast path: LINEAR→LINEAR, no color key, contiguous rows → use memmove + // (handles overlapping src/dst correctly, which the per-element loop does not) + if constexpr (src_type == SCE_GXM_TRANSFER_LINEAR && dst_type == SCE_GXM_TRANSFER_LINEAR && mode == SCE_GXM_TRANSFER_COLORKEY_NONE) { + const int32_t src_stride_pixel = src.stride / sizeof(T); + const int32_t dst_stride_pixel = dst.stride / sizeof(T); + if (src.x == 0 && src.y == 0 && dst.x == 0 && dst.y == 0 + && src_stride_pixel == static_cast(src.width) + && dst_stride_pixel == static_cast(dst.width)) { + memmove(dst_ptr, src_ptr, static_cast(src.width) * src.height * sizeof(T)); + return; + } + // Row-by-row memmove for strided LINEAR→LINEAR (handles overlap within each row) + if (src.x == 0 && dst.x == 0 + && src_stride_pixel >= static_cast(src.width) + && dst_stride_pixel >= static_cast(dst.width)) { + const uint8_t *s = reinterpret_cast(src_ptr) + src.y * src.stride; + uint8_t *d = reinterpret_cast(dst_ptr) + dst.y * dst.stride; + const size_t row_bytes = static_cast(src.width) * sizeof(T); + if (reinterpret_cast(d) <= reinterpret_cast(s)) { + for (uint32_t dy = 0; dy < src.height; dy++) { + memmove(d + dy * dst.stride, s + dy * src.stride, row_bytes); + } + } else { + for (int32_t dy = static_cast(src.height) - 1; dy >= 0; dy--) { + memmove(d + dy * dst.stride, s + dy * src.stride, row_bytes); + } + } + return; + } + } + auto compute_offset = [&](uint32_t dx, uint32_t dy, const SceGxmTransferImage &img, SceGxmTransferType type) -> int32_t { const int32_t stride_pixel = img.stride / sizeof(T); if (type == SCE_GXM_TRANSFER_LINEAR) { return dy * stride_pixel + dx; } else if (type == SCE_GXM_TRANSFER_TILED) { - // tiles are 32x32, you have the offset within the tile then the offset of the tile const uint32_t texel_offset_in_tile = ((dy % 32) * 32) + (dx % 32); const int32_t tile_address = (stride_pixel / 32) * (dy / 32) + (dx / 32); @@ -56,14 +89,27 @@ static void perform_transfer_copy_impl(MemState &mem, const SceGxmTransferImage } }; - for (uint32_t dx = 0; dx < src.width; dx++) { - for (uint32_t dy = 0; dy < src.height; dy++) { - // compute offset depending on the texture type used - // the function compute_offset gets inlined + // Check for overlap and use a snapshot of source data if needed + const uintptr_t src_start = reinterpret_cast(src_ptr); + const uintptr_t dst_start = reinterpret_cast(dst_ptr); + const size_t src_span = (src.y + src.height) * (src.stride ? src.stride : src.width * sizeof(T)); + const size_t dst_span = (dst.y + dst.height) * (dst.stride ? dst.stride : dst.width * sizeof(T)); + const bool overlaps = src_start < dst_start + dst_span && dst_start < src_start + src_span; + + std::vector src_copy; + const T *safe_src = src_ptr; + if (overlaps) { + const size_t src_elements = src_span / sizeof(T); + src_copy.assign(src_ptr, src_ptr + src_elements); + safe_src = src_copy.data(); + } + + for (uint32_t dy = 0; dy < src.height; dy++) { + for (uint32_t dx = 0; dx < src.width; dx++) { uint32_t src_offset = compute_offset(src.x + dx, src.y + dy, src, src_type); uint32_t dst_offset = compute_offset(dst.x + dx, dst.y + dy, dst, dst_type); - T value = src_ptr[src_offset]; + T value = safe_src[src_offset]; if constexpr (mode == SCE_GXM_TRANSFER_COLORKEY_PASS) { if ((value & key_mask) != key_value) continue; @@ -184,10 +230,21 @@ COMMAND(handle_transfer_copy) { delete[] images; }; + if (renderer.current_backend == Backend::Vulkan && renderer.features.enable_memory_mapping && !renderer.disable_surface_sync) { - if (dynamic_cast(renderer).surface_cache.check_for_surface(mem, images[0].address.address(), copy_operation, images[1].address.address())) - // let the vulkan surface cache handle it + const uint32_t src_read_size = images[0].stride * (images[0].y + images[0].height); + const uint32_t dst_write_size = images[1].stride * (images[1].y + images[1].height); + if (dynamic_cast(renderer).surface_cache.check_for_surface(mem, images[0].address.address(), copy_operation, images[1].address.address(), src_read_size, dst_write_size)) { + LOG_WARN_ONCE("transfer_copy: surface-synced src=0x{:08X}→dst=0x{:08X} fmt=0x{:X} {}x{} srcXY=({},{}) dstXY=({},{})", + images[0].address.address(), images[1].address.address(), + fmt::underlying(src_fmt), images[0].width, images[0].height, + images[0].x, images[0].y, images[1].x, images[1].y); return; + } + + LOG_WARN_ONCE("transfer_copy: no surface sync for src=0x{:08X}→dst=0x{:08X} fmt=0x{:X} {}x{}", + images[0].address.address(), images[1].address.address(), + fmt::underlying(src_fmt), images[0].width, images[0].height); } copy_operation(); @@ -276,8 +333,12 @@ COMMAND(handle_transfer_downscale) { delete dst; }; + if (renderer.current_backend == Backend::Vulkan && renderer.features.enable_memory_mapping && !renderer.disable_surface_sync) { - if (dynamic_cast(renderer).surface_cache.check_for_surface(mem, src->address.address(), downscale_operation, dst->address.address())) + // src->address has already been adjusted to the first read byte above + const uint32_t src_read_size = src->stride * src->height; + const uint32_t dst_write_size = dst->stride * dst->height; + if (dynamic_cast(renderer).surface_cache.check_for_surface(mem, src->address.address(), downscale_operation, dst->address.address(), src_read_size, dst_write_size)) // let the vulkan surface cache handle it return; } @@ -290,6 +351,7 @@ COMMAND(handle_transfer_fill) { const uint32_t fill_color = helper.pop(); const SceGxmTransferImage *dest = helper.pop(); + const auto bpp = gxm::get_bits_per_pixel(dest->format); const uint32_t bytes_per_pixel = (bpp + 7) >> 3; diff --git a/vita3k/renderer/src/vulkan/context.cpp b/vita3k/renderer/src/vulkan/context.cpp index b54615f4..2097f37e 100644 --- a/vita3k/renderer/src/vulkan/context.cpp +++ b/vita3k/renderer/src/vulkan/context.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -102,7 +103,16 @@ void VKContext::wait_thread_function(const MemState &mem) { } uint8_t *src = reinterpret_cast(std::get(mem_it->second.buffer_impl).mapped_data); src += request.location - mem_it->first; - memcpy(Ptr(request.location).get(mem), src, request.size); + // this is the emulator's own write-back, not a guest write: go + // through protection without firing or consuming any traps + surface_sync_internal_write = true; + // the wait thread only sees a const MemState; lifting/re-arming + // protection segments needs the mutable protect state + write_through_protection(const_cast(mem), request.location, request.size, [&]() { + memcpy(Ptr(request.location).get(mem), src, request.size); + }); + surface_sync_internal_write = false; + }, [&](PostSurfaceSyncRequest &request) { wait_for_fences(); @@ -112,7 +122,7 @@ void VKContext::wait_thread_function(const MemState &mem) { [&](SyncSignalRequest &request) { wait_for_fences(); - renderer::subject_done(request.sync, request.timestamp); + renderer::subject_done(request.sync, request.timestamp); // Nick }, [&](CallbackRequest &request) { if (request.callback) { @@ -132,10 +142,14 @@ void set_context(VKContext &context, MemState &mem, VKRenderTarget *rt, const Fe SceGxmColorSurface *color_surface_fin = &context.record.color_surface; // set these values for the pipeline cache context.record.color_base_format = gxm::get_base_format(color_surface_fin->colorFormat); - context.record.is_gamma_corrected = static_cast(color_surface_fin->gamma); - vk::Format vk_format = color::translate_format(context.record.color_base_format); + vk::Format vk_format = color::translate_surface_format(context.record.color_base_format); + + // GXM gamma mode only applies to 8-bit unorm formats (U8U8U8U8). + // For float/snorm/etc formats, the real Vita hardware ignores the gamma flag. + const bool format_supports_gamma = (vk_format == vk::Format::eR8G8B8A8Unorm); + context.record.is_gamma_corrected = static_cast(color_surface_fin->gamma) && format_supports_gamma; - if (color_surface_fin->gamma && vk_format == vk::Format::eR8G8B8A8Unorm) { + if (color_surface_fin->gamma && format_supports_gamma) { vk_format = vk::Format::eR8G8B8A8Srgb; } @@ -412,6 +426,24 @@ void VKContext::stop_recording(const SceGxmNotification ¬if1, const SceGxmNot if (in_renderpass) stop_render_pass(); + { + // GXM guarantees scene N's jobs complete before scene N+1 starts; games rely on + // this ordering (e.g. consuming shader-store output as vertex input in a later + // scene). The batched command buffers carry no implicit memory dependency, so a + // scene-boundary barrier is required. Catch-all form: a targeted + // ShaderWrite->VertexAttributeRead barrier was not sufficient in testing, and a + // prior in-code note reports BDA-write visibility quirks on some GPUs, so make + // every write visible to everything that follows. Once per scene — cheap. + const vk::MemoryBarrier scene_boundary{ + .srcAccessMask = vk::AccessFlagBits::eMemoryWrite, + .dstAccessMask = vk::AccessFlagBits::eMemoryRead | vk::AccessFlagBits::eMemoryWrite + }; + render_cmd.pipelineBarrier( + vk::PipelineStageFlagBits::eAllCommands, + vk::PipelineStageFlagBits::eAllCommands, + vk::DependencyFlags(), scene_boundary, {}, {}); + } + struct VisibilityRange { uint32_t offset; uint32_t size; @@ -479,6 +511,7 @@ void VKContext::stop_recording(const SceGxmNotification ¬if1, const SceGxmNot submit_info.setCommandBuffers(cmdbuffers_to_submit); state.general_queue.submit(submit_info, fence); + cmdbuffers_to_submit.clear(); state.frame().rendered_fences.push_back(fence); @@ -488,11 +521,12 @@ void VKContext::stop_recording(const SceGxmNotification ¬if1, const SceGxmNot if (state.mapping_method == MappingMethod::DoubleBuffer) { // sync all the visibility buffers + // (through the wait queue: it re-validates the memory mapping at execution time, + // which is required as the guest can unmap memory while results are in flight) for (auto &range : occlusion_ranges) { state.request_queue.push(BufferSyncRequest{ current_visibility_buffer->address + range.offset * 4, range.size * 4 }); } - // we must sync the two buffers if (surface_info && surface_info->need_buffer_sync) state.request_queue.push(BufferSyncRequest{ surface_info->data.address(), static_cast(surface_info->total_bytes) }); } diff --git a/vita3k/renderer/src/vulkan/creation.cpp b/vita3k/renderer/src/vulkan/creation.cpp index e0c848e2..1d7344f7 100644 --- a/vita3k/renderer/src/vulkan/creation.cpp +++ b/vita3k/renderer/src/vulkan/creation.cpp @@ -57,6 +57,7 @@ VKContext::VKContext(VKState &state, MemState &mem) // use the default buffer std::fill_n(vertex_stream_buffers, SCE_GXM_MAX_VERTEX_STREAMS, state.default_buffer.buffer); + // also initialize the gpu wait thread gpu_request_wait_thread = std::thread(&VKContext::wait_thread_function, this, std::ref(mem)); } else { diff --git a/vita3k/renderer/src/vulkan/gxm_to_vulkan.cpp b/vita3k/renderer/src/vulkan/gxm_to_vulkan.cpp index 0409b1d1..d831027d 100644 --- a/vita3k/renderer/src/vulkan/gxm_to_vulkan.cpp +++ b/vita3k/renderer/src/vulkan/gxm_to_vulkan.cpp @@ -476,6 +476,13 @@ vk::Format translate_format(SceGxmColorBaseFormat format) { return {}; } } + +vk::Format translate_surface_format(SceGxmColorBaseFormat base_format) { + if (base_format == SCE_GXM_COLOR_BASE_FORMAT_U4U4U4U4) + return vk::Format::eR8G8B8A8Unorm; + + return translate_format(base_format); +} } // namespace color namespace texture { diff --git a/vita3k/renderer/src/vulkan/pipeline_cache.cpp b/vita3k/renderer/src/vulkan/pipeline_cache.cpp index 5d79c9b2..8bb2f5d6 100644 --- a/vita3k/renderer/src/vulkan/pipeline_cache.cpp +++ b/vita3k/renderer/src/vulkan/pipeline_cache.cpp @@ -552,7 +552,7 @@ vk::RenderPass PipelineCache::retrieve_render_pass(vk::Format format, bool force }; vk::AttachmentReference ds_ref{ .attachment = no_color ? 0U : 1U, - .layout = vk::ImageLayout::eDepthStencilAttachmentOptimal + .layout = vk::ImageLayout::eDepthStencilReadOnlyOptimal /*vk::ImageLayout::eDepthStencilAttachmentOptimal*/ // Nick }; vk::SubpassDescription subpass{ .pipelineBindPoint = vk::PipelineBindPoint::eGraphics @@ -599,9 +599,10 @@ vk::RenderPass PipelineCache::retrieve_render_pass(vk::Format format, bool force .srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eLateFragmentTests, .dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests, .srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eDepthStencilAttachmentWrite, - .dstAccessMask = vk::AccessFlagBits::eColorAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentRead + .dstAccessMask = vk::AccessFlagBits::eColorAttachmentRead | vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eDepthStencilAttachmentRead | vk::AccessFlagBits::eDepthStencilAttachmentWrite }; + // BUG: This is dead code (as immediately overwritten below) if (state.features.support_shader_interlock && no_color) { // we must wait for the previous shaders to be done dependencies[1].dstStageMask = vk::PipelineStageFlagBits::eFragmentShader; @@ -840,6 +841,13 @@ vk::Pipeline PipelineCache::compile_pipeline(SceGxmPrimitiveType type, vk::Rende const bool use_shader_interlock = state.features.support_shader_interlock && gxm_fragment_shader->is_frag_color_used(); const vk::PipelineRasterizationStateCreateInfo rasterizer{ + // GXM (PowerVR) CLAMPS depth instead of clipping at the near/far planes; games + // place geometry just beyond z/w = 1 (e.g. KZM's shadow-mask stencil volumes, + // measured at z/w = 1.0008) and expect it rasterized. With Vulkan's default + // clipping, the clip boundary sweeps through such geometry as the view drifts, + // flipping stencil coverage of whole regions frame to frame (erratic shadow + // flicker). Depth clamp reproduces the GXM behavior. + .depthClampEnable = state.physical_device_features.depthClamp ? VK_TRUE : VK_FALSE, .polygonMode = translate_polygon_mode(record.front_polygon_mode), .cullMode = translate_cull_mode(record.cull_mode), // front face is always counter clockwise diff --git a/vita3k/renderer/src/vulkan/renderer.cpp b/vita3k/renderer/src/vulkan/renderer.cpp index 232d6659..399ae387 100644 --- a/vita3k/renderer/src/vulkan/renderer.cpp +++ b/vita3k/renderer/src/vulkan/renderer.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -610,6 +611,10 @@ bool VKState::create(std::unique_ptr &state, const Config &conf // use these features (because they are used by the vita GPU) if they are available vk::PhysicalDeviceFeatures enabled_features{ + // GXM/PowerVR does not clip depth at the near/far planes — it clamps. Games + // deliberately place geometry (e.g. stencil-volume far caps) just beyond + // z/w = 1 and rely on it being rasterized. + .depthClamp = physical_device_features.depthClamp, .fillModeNonSolid = physical_device_features.fillModeNonSolid, .wideLines = physical_device_features.wideLines, .samplerAnisotropy = physical_device_features.samplerAnisotropy, @@ -626,6 +631,7 @@ bool VKState::create(std::unique_ptr &state, const Config &conf bool support_buffer_device_address = false; bool support_external_memory = false; bool support_shader_interlock = false; + bool support_depth_bias_control_ext = false; const std::map optional_extensions = { { vk::KHRGetMemoryRequirements2ExtensionName, &temp_bool }, // can be used by vma to improve performance @@ -646,6 +652,10 @@ bool VKState::create(std::unique_ptr &state, const Config &conf { vk::KHRShaderFloat16Int8ExtensionName, &support_fsr }, // used for accurate programmable blending on desktop GPUs { vk::EXTFragmentShaderInterlockExtensionName, &support_shader_interlock }, + // exact GXM depth-bias semantics (absolute units, value-independent) — the + // constant-factor fallback under-biases small depth values (shadow-mask + // scenes) and lets near-coplanar casters z-fight + { VK_EXT_DEPTH_BIAS_CONTROL_EXTENSION_NAME, &support_depth_bias_control_ext }, #ifdef __APPLE__ // Needed to create the MoltenVK device { vk::KHRPortabilitySubsetExtensionName, &temp_bool }, @@ -750,18 +760,35 @@ bool VKState::create(std::unique_ptr &state, const Config &conf } support_shader_interlock &= static_cast(physical_device_features.fragmentStoresAndAtomics); + if (support_shader_interlock) { auto props = physical_device.getFeatures2KHR(); support_shader_interlock = static_cast(props.get().fragmentShaderSampleInterlock); features.support_shader_interlock = support_shader_interlock; } + bool dbc_float = false; + bool dbc_force_unorm = false; + if (support_depth_bias_control_ext) { + auto props = physical_device.getFeatures2KHR(); + const auto &dbc = props.get(); + dbc_float = static_cast(dbc.depthBiasControl) && static_cast(dbc.floatRepresentation); + dbc_force_unorm = static_cast(dbc.depthBiasControl) && static_cast(dbc.leastRepresentableValueForceUnormRepresentation); + } + support_depth_bias_control = dbc_float || dbc_force_unorm; + depth_bias_control_use_float = dbc_float; + // if this reports "ext=false" on a modern GPU, check for a global Vulkan + // Configurator layer override filtering device extensions, then the driver version + LOG_INFO("Depth bias control (exact GXM bias): ext={} float={} force_unorm={}", + support_depth_bias_control_ext, dbc_float, dbc_force_unorm); + vk::StructureChain + vk::PhysicalDeviceRasterizationOrderAttachmentAccessFeaturesEXT, + vk::PhysicalDeviceDepthBiasControlFeaturesEXT> device_info{ vk::DeviceCreateInfo{ .pEnabledFeatures = &enabled_features }, @@ -775,11 +802,20 @@ bool VKState::create(std::unique_ptr &state, const Config &conf vk::PhysicalDeviceFragmentShaderInterlockFeaturesEXT{ .fragmentShaderSampleInterlock = VK_TRUE }, vk::PhysicalDeviceRasterizationOrderAttachmentAccessFeaturesEXT{ - .rasterizationOrderColorAttachmentAccess = VK_TRUE } + .rasterizationOrderColorAttachmentAccess = VK_TRUE }, + vk::PhysicalDeviceDepthBiasControlFeaturesEXT{ + // request exactly the representations the driver reported, or + // vkCreateDevice fails with FEATURE_NOT_PRESENT + .depthBiasControl = VK_TRUE, + .leastRepresentableValueForceUnormRepresentation = dbc_force_unorm ? VK_TRUE : VK_FALSE, + .floatRepresentation = dbc_float ? VK_TRUE : VK_FALSE } }; device_info.get().setQueueCreateInfos(queue_infos); device_info.get().setPEnabledExtensionNames(device_extensions); + if (!support_depth_bias_control) + device_info.unlink(); + if (!support_memory_mapping) device_info.unlink(); @@ -984,7 +1020,7 @@ void VKState::late_init(const Config &cfg, const std::string_view game_id, MemSt pipeline_cache.init(support_rasterized_order_access); - texture_cache.init(true, texture_folder(), game_id); + texture_cache.init(true, texture_folder(), game_id); // Nick } void VKState::cleanup() { @@ -1529,7 +1565,7 @@ void VKState::unmap_memory(MemState &mem, Ptr address) { case MappingMethod::DoubleBuffer: remove_external_mapping(mem, address.cast().get(mem), ite->second.size); // remove all the trapping related to these locations - buffer_trapping.remove_range(address.address(), address.address() + ite->second.size); + buffer_trapping.remove_range(mem, address.address(), address.address() + ite->second.size); break; #ifdef __ANDROID__ @@ -1794,8 +1830,13 @@ BufferTrapping::BufferTrapping(VKState &state) TrappedBuffer *BufferTrapping::access_buffer(Address addr, uint32_t size, MemState &mem, bool always_trap, bool cover_everything) { const bool is_buffer_small = (size < 3 * KiB(4)); - if (is_buffer_small && always_trap) { - // overwise we may end up with trapping nothing + if (always_trap) { + // The partial head/tail pages must be protected too: a CPU write landing there + // never faults, so the page-granular refresh skips it and the mirror's edge + // bytes stay stale forever. KZM: a settled shadow caster's double-buffered ring + // slots then flap between two old edge-poses every other frame = the erratic + // shadow/decal flicker. The page-refresh copy is already clamped to the bound + // range, and cross-buffer faults on shared pages are handled per-block. cover_everything = true; } else if (is_buffer_small) { // not big enough to apply buffer trapping @@ -1828,18 +1869,36 @@ TrappedBuffer *BufferTrapping::access_buffer(Address addr, uint32_t size, MemSta } { - // remove the following overlapping dirty buffers + // remove the following overlapping dirty buffers — including their protect blocks, + // whose callbacks capture the map node and must never outlive it (a fault would + // otherwise write through a dangling iterator: heap corruption) auto next_it = it; next_it++; while (next_it != trapped_buffers.end() && next_it->first < addr + size) { - if (next_it->second.dirty) + if (next_it->second.dirty) { + remove_protect_owner(mem, &next_it->second); next_it = trapped_buffers.erase(next_it); - else + } else { next_it++; + } } } + // decide whether to refresh the mapped mirror from guest RAM before resetting the + // flags: for shader-store buffers the mirror can hold GPU-written data that guest RAM + // never sees, so only genuinely game-written PAGES may be overwritten — the emulator's + // own surface-sync write-backs must not clobber it, and a whole-range refresh would + // wipe the GPU's output (Killzone Mercenary's GPU-skinned shadow casters explode + // into stretched shards otherwise) + const bool full_refresh = is_new || !always_trap; + std::vector
dirty_pages; + { + std::lock_guard lock(dirty_pages_mutex); + dirty_pages.swap(it->second.guest_dirty_pages); + } + it->second.size = size; it->second.dirty = false; + it->second.guest_dirty = false; it->second.extra = ~0; if (is_new) { @@ -1863,21 +1922,48 @@ TrappedBuffer *BufferTrapping::access_buffer(Address addr, uint32_t size, MemSta aligned_addr = align(addr, KiB(4)); aligned_size = align_down(addr + size - aligned_addr, KiB(4)); } - add_protect(mem, aligned_addr, aligned_size, MemPerm::ReadOnly, [it](Address addr, bool write) { - it->second.dirty = true; - return true; - }); + add_protect( + mem, aligned_addr, aligned_size, MemPerm::ReadOnly, [this, it](Address fault_addr, bool write) { + it->second.dirty = true; + if (!surface_sync_internal_write) { + it->second.guest_dirty = true; + std::lock_guard lock(dirty_pages_mutex); + it->second.guest_dirty_pages.push_back(align_down(fault_addr, KiB(4))); + } + return true; + }, + &it->second, true); // copy back the data as it was non-existent or dirty - memcpy(it->second.mapped_location, Ptr(addr).get(mem), size); + if (full_refresh) { + memcpy(it->second.mapped_location, Ptr(addr).get(mem), size); + } else if (!dirty_pages.empty()) { + // page-granular refresh: copy only the pages the game actually wrote, preserving + // GPU shader-store output elsewhere in the buffer + std::sort(dirty_pages.begin(), dirty_pages.end()); + dirty_pages.erase(std::unique(dirty_pages.begin(), dirty_pages.end()), dirty_pages.end()); + const uint64_t range_end = static_cast(addr) + size; + for (const Address page : dirty_pages) { + const uint64_t chunk_start = std::max(page, addr); + const uint64_t chunk_end = std::min(static_cast(page) + KiB(4), range_end); + if (chunk_start >= chunk_end) + continue; + memcpy(it->second.mapped_location + (chunk_start - addr), + Ptr(static_cast
(chunk_start)).get(mem), static_cast(chunk_end - chunk_start)); + } + } return &it->second; } -void BufferTrapping::remove_range(Address start, Address end) { +void BufferTrapping::remove_range(MemState &mem, Address start, Address end) { auto it = trapped_buffers.lower_bound(start); - while (it != trapped_buffers.end() && it->first < end) + while (it != trapped_buffers.end() && it->first < end) { + // the protect blocks' callbacks capture this map node: remove them first so a + // later fault cannot write through a dangling iterator + remove_protect_owner(mem, &it->second); it = trapped_buffers.erase(it); + } } } // namespace renderer::vulkan diff --git a/vita3k/renderer/src/vulkan/scene.cpp b/vita3k/renderer/src/vulkan/scene.cpp index e7f1a73f..c9795494 100644 --- a/vita3k/renderer/src/vulkan/scene.cpp +++ b/vita3k/renderer/src/vulkan/scene.cpp @@ -27,17 +27,37 @@ namespace renderer::vulkan { + void set_uniform_buffer(VKContext &context, MemState &mem, const ShaderProgram *program, const bool vertex_shader, const int block_num, const int size, Ptr data) { auto offset = program->uniform_buffer_data_offsets.at(block_num); if (offset == static_cast(-1)) { return; } - const uint32_t data_size_upload = std::min(size, program->uniform_buffer_sizes.at(block_num) * 4); if (context.state.features.enable_memory_mapping) { - if (context.state.mapping_method == MappingMethod::DoubleBuffer) { + // a uniform buffer can alias a rendered color surface: some games (e.g. Killzone + // Mercenary's bloom/exposure chain) read their render target in the shader through + // raw buffer addresses. GPU reads never trigger the mprotect-based lazy sync, so + // make sure the surface content is synced into the mapped buffer before the draw. + const bool aliases_surface = context.state.surface_cache.sync_surface_for_gpu_read(data.address(), data_size_upload); + + uint32_t upload_size = data_size_upload; + if (upload_size == 0 && !aliases_surface) { + // pointer-only binds (declared block size 0): the shader reads through the raw + // address with an extent unknowable here, and uploading nothing leaves the + // mapped mirror holding stale bytes for CPU-written data (Killzone Mercenary's + // lighting accumulator inputs) — upload a bounded window instead + auto mem_it = context.state.mapped_memories.lower_bound(data.address()); + if (mem_it != context.state.mapped_memories.end() && data.address() >= mem_it->first + && data.address() < mem_it->first + mem_it->second.size) { + const uint64_t available = mem_it->first + mem_it->second.size - data.address(); + upload_size = static_cast(std::min(16 * 1024, available)); + } + } + + if (!aliases_surface && upload_size > 0 && context.state.mapping_method == MappingMethod::DoubleBuffer) { // we must always cover everything as some small part of the buffer may get changed only - context.state.buffer_trapping.access_buffer(data.address(), data_size_upload, mem, false, true); + context.state.buffer_trapping.access_buffer(data.address(), upload_size, mem, false, true); } const uint64_t buffer_address = context.state.get_matching_device_address(data.address()); @@ -284,6 +304,7 @@ static void bind_vertex_streams(VKContext &context, MemState &mem, uint32_t inst // on the PS Vita, shader stores are used most of the time to write to a vertex buffer context.state.buffer_trapping.access_buffer(state.vertex_streams[i].data.address(), static_cast(state.vertex_streams[i].size), mem, context.state.has_shader_store); } + } if (max_stream_idx == 0) @@ -374,7 +395,8 @@ void draw(VKContext &context, SceGxmPrimitiveType type, SceGxmIndexFormat format context.last_draw_was_framebuffer_fetch = fragment_program_gxp.is_frag_color_used(); } - if (context.current_visibility_buffer != nullptr && context.current_query_idx != -1 && !context.is_in_query) { + if (context.current_visibility_buffer != nullptr && context.current_query_idx != -1 && !context.is_in_query + && static_cast(context.current_query_idx) < context.current_visibility_buffer->size) { if (context.current_visibility_buffer->queries_used[context.current_query_idx]) { LOG_WARN_ONCE("Visibility buffer entry is used more than once in a scene"); // still let this happen, this is a validation error but I think most GPUs should be fine with it diff --git a/vita3k/renderer/src/vulkan/surface_cache.cpp b/vita3k/renderer/src/vulkan/surface_cache.cpp index c1a4f7ad..90f0bd88 100644 --- a/vita3k/renderer/src/vulkan/surface_cache.cpp +++ b/vita3k/renderer/src/vulkan/surface_cache.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -25,10 +26,15 @@ #include +#include #include #include #include +#include +#include +#include + extern "C" { #include } @@ -59,6 +65,34 @@ static bool format_need_additional_memory(SceGxmColorBaseFormat format) { namespace renderer::vulkan { +static bool surface_sync_needs_u4u4u4u4_repack(const ColorSurfaceCacheInfo &surface) { + return surface.format == SCE_GXM_COLOR_BASE_FORMAT_U4U4U4U4 + && (surface.texture.format == vk::Format::eR8G8B8A8Unorm + || surface.texture.format == vk::Format::eR8G8B8A8Srgb); +} + +static uint8_t unorm8_to_unorm4(uint8_t value) { + return static_cast((static_cast(value) * 15 + 127) / 255); +} + +static void pack_rgba8_to_r4g4b4a4(uint8_t *dst, const uint8_t *src, uint32_t pixel_stride, uint32_t height) { + for (uint32_t y = 0; y < height; y++) { + uint16_t *dst_row = reinterpret_cast(dst + y * pixel_stride * sizeof(uint16_t)); + const uint8_t *src_row = src + y * pixel_stride * 4; + + for (uint32_t x = 0; x < pixel_stride; x++) { + const uint8_t r = unorm8_to_unorm4(src_row[x * 4 + 0]); + const uint8_t g = unorm8_to_unorm4(src_row[x * 4 + 1]); + const uint8_t b = unorm8_to_unorm4(src_row[x * 4 + 2]); + const uint8_t a = unorm8_to_unorm4(src_row[x * 4 + 3]); + + dst_row[x] = static_cast(r | (g << 4) | (b << 8) | (a << 12)); + } + } +} + +thread_local bool surface_sync_internal_write = false; + static void protect_surface(MemState &mem, ColorSurfaceCacheInfo &info) { const bool trap_reads = (info.tiling == SurfaceTiling::Linear && format_support_surface_sync(info.format)); @@ -78,15 +112,35 @@ static void protect_surface(MemState &mem, ColorSurfaceCacheInfo &info) { std::shared_ptr need_sync = trap_reads ? info.need_surface_sync : nullptr; // Don't track dirty for small surfaces to avoid false positives from unrelated writes std::shared_ptr dirty = small_surface ? nullptr : info.dirty; - - add_protect(mem, addr_start, addr_end - addr_start, perm, - [dirty, need_sync](Address, bool write) { - if (write && dirty) - *dirty = true; + std::shared_ptr> guest_written = small_surface ? nullptr : info.guest_written_range; + + add_protect( + mem, addr_start, addr_end - addr_start, perm, + [dirty, need_sync, guest_written](Address addr, bool write) { + if (write) { + // the trap is one-shot: dirty drives the re-protection on the next frame, + // so it must be set for ANY write — otherwise an emulator write-back would + // silently disarm the trap and later guest writes would go undetected + if (dirty) + *dirty = true; + if (guest_written && !surface_sync_internal_write) { + // grow the union of genuinely guest-written ranges (page granularity) + const Address page_start = align_down(addr, KiB(4)); + const Address page_end = page_start + KiB(4); + if (guest_written->second == 0) { + guest_written->first = page_start; + guest_written->second = page_end; + } else { + guest_written->first = std::min(guest_written->first, page_start); + guest_written->second = std::max(guest_written->second, page_end); + } + } + } if (need_sync) *need_sync = true; return true; - }); + }, + &info); } ColorSurfaceCacheInfo::~ColorSurfaceCacheInfo() { @@ -113,11 +167,26 @@ void VKSurfaceCache::destroy_surface(ColorSurfaceCacheInfo &info) { // don't forget to destroy in the right order for (auto &casted : info.casted_textures) { destroy_queue.add_buffer(casted.transition_buffer); + destroy_queue.add(casted.reinterpret_view); destroy_queue.add_image(casted.texture); } info.casted_textures.clear(); destroy_queue.add(info.alternate_view); + destroy_queue.add(info.reinterpret_store_view); + + // these are specific to the surface's format and size — a recycled cache entry must not + // inherit them (a stale blit_image with the previous owner's format silently corrupts + // the surface sync of the new owner, e.g. blitting an RGBA16F surface through a leftover + // RGBA8-SRGB image, which fills the synced memory with reinterpreted garbage) + if (info.blit_image) { + destroy_queue.add_image(*info.blit_image); + info.blit_image.reset(); + } + if (info.copy_buffer) { + destroy_queue.add_buffer(*info.copy_buffer); + info.copy_buffer.reset(); + } destroy_framebuffers(info.texture.view); destroy_queue.add_image(info.texture); @@ -156,6 +225,10 @@ void VKSurfaceCache::cleanup() { auto &info = item.content; for (auto &casted : info.casted_textures) { casted.transition_buffer.destroy(); + if (casted.reinterpret_view) { + state.device.destroy(casted.reinterpret_view); + casted.reinterpret_view = nullptr; + } casted.texture.destroy(); } info.casted_textures.clear(); @@ -165,6 +238,11 @@ void VKSurfaceCache::cleanup() { info.alternate_view = nullptr; } + if (info.reinterpret_store_view) { + state.device.destroy(info.reinterpret_store_view); + info.reinterpret_store_view = nullptr; + } + if (info.blit_image) info.blit_image->destroy(); if (info.copy_buffer) @@ -193,6 +271,20 @@ void VKSurfaceCache::cleanup() { info.texture.destroy(); } + if (reinterpret_pipeline) { + state.device.destroy(reinterpret_pipeline); + state.device.destroy(reinterpret_pipeline_layout); + state.device.destroy(reinterpret_desc_layout); + state.device.destroy(reinterpret_desc_pool); + state.device.destroy(reinterpret_shader); + if (reinterpret_sampler) { + state.device.destroy(reinterpret_sampler); + reinterpret_sampler = nullptr; + } + reinterpret_pipeline = nullptr; + reinterpret_desc_sets.clear(); + } + color_address_lookup.clear(); depth_address_lookup.clear(); stencil_address_lookup.clear(); @@ -208,6 +300,7 @@ SurfaceRetrieveResult VKSurfaceCache::retrieve_color_surface_for_framebuffer(Mem const uint32_t original_width = color->width; const uint32_t original_height = color->height; + uint32_t width = static_cast(original_width * state.res_multiplier); uint32_t height = static_cast(original_height * state.res_multiplier); @@ -225,7 +318,7 @@ SurfaceRetrieveResult VKSurfaceCache::retrieve_color_surface_for_framebuffer(Mem overlap = (overlap && (ite->first + ite->second->total_bytes) > address); const SceGxmColorBaseFormat base_format = gxm::get_base_format(color->colorFormat); - vk::Format vk_format = color::translate_format(base_format); + vk::Format vk_format = color::translate_surface_format(base_format); SurfaceTiling tiling; if (color->surfaceType == SCE_GXM_COLOR_SURFACE_LINEAR) @@ -308,9 +401,9 @@ SurfaceRetrieveResult VKSurfaceCache::retrieve_color_surface_for_framebuffer(Mem // get the least recently used (probably unused) color surface ColorSurfaceCacheInfo &info_added = *color_surface_queue.get_lru(); - if (info_added.texture.image) - // deferred destruction of the existing surface + if (info_added.texture.image) { destroy_surface(info_added); + } if (info_added.data) color_address_lookup.erase(info_added.data.address()); @@ -338,10 +431,15 @@ SurfaceRetrieveResult VKSurfaceCache::retrieve_color_surface_for_framebuffer(Mem image.layout = vkutil::ImageLayout::Undefined; // we might have to create a non-srgb/linear view later if this surface is used for presentation - const bool need_mutable = (vk_format == vk::Format::eR8G8B8A8Unorm || vk_format == vk::Format::eR8G8B8A8Srgb); + const bool need_mutable_rgba8 = (vk_format == vk::Format::eR8G8B8A8Unorm || vk_format == vk::Format::eR8G8B8A8Srgb); + // 64-bit surfaces may be read back through an R32G32_UINT view by the typeless + // reinterpret compute pass (any 64-bit format is in the same compatibility class), + // which also needs a mutable format. + const bool need_mutable_64bit = (vk::blockSize(vk_format) == 8); + const bool need_mutable = need_mutable_rgba8 || need_mutable_64bit; const vk::ImageCreateFlags image_create_flags = need_mutable ? vk::ImageCreateFlagBits::eMutableFormat : vk::ImageCreateFlags(); const void *image_info_pNext = nullptr; - if (support_image_format_specifier && need_mutable) { + if (support_image_format_specifier && need_mutable_rgba8) { static const vk::Format view_formats[] = { vk::Format::eR8G8B8A8Unorm, vk::Format::eR8G8B8A8Srgb }; static const vk::ImageFormatListCreateInfoKHR image_info_formats{ .viewFormatCount = 2, @@ -368,6 +466,11 @@ SurfaceRetrieveResult VKSurfaceCache::retrieve_color_surface_for_framebuffer(Mem info_added.need_surface_sync.reset(); info_added.need_surface_sync = std::make_shared(false); info_added.dirty = std::make_shared(false); + info_added.guest_written_range = std::make_shared>(0, 0); + // don't inherit the previous owner's sync state + info_added.gpu_read_sync_only = false; + info_added.cpu_read_range_start = 0; + info_added.cpu_read_range_size = 0; // we only support surface sync of linear surfaces for now if (!can_mprotect_mapped_memory) { @@ -394,27 +497,24 @@ std::optional VKSurfaceCache::retrieve_color_surface_as_tex const uint32_t width = static_cast(original_width * state.res_multiplier); const uint32_t height = static_cast(original_height * state.res_multiplier); - bool overlap = true; - // Of course, this works under the assumption that range must be unique :D - auto ite = color_address_lookup.upper_bound(address); - if (ite == color_address_lookup.begin()) - // no match - overlap = false; - else - --ite; - // ite is now the first item with an address lower or equal to key - - overlap = (overlap && (ite->first + ite->second->total_bytes) > address); - - if (!overlap) - return std::nullopt; + // With memory pools, multiple (possibly stale) cache entries can overlap the same + // address: pick the most recently rendered entry containing the address, so that a + // stale aliased entry never shadows fresh content. + auto scan = color_address_lookup.upper_bound(address); + auto ite = color_address_lookup.end(); + while (scan != color_address_lookup.begin()) { + --scan; + if (scan->first + scan->second->total_bytes > address) { + if (ite == color_address_lookup.end() || scan->second->last_frame_rendered > ite->second->last_frame_rendered) + ite = scan; + } + } - if (*ite->second->dirty) - // Guest wrote to the surface backing memory since it was rendered, so GPU data is stale. + if (ite == color_address_lookup.end()) return std::nullopt; const vk::ComponentMapping swizzle = texture::translate_swizzle(gxm::get_format(texture)); - vk::Format vk_format = color::translate_format(base_format); + vk::Format vk_format = color::translate_surface_format(base_format); const bool is_srgb = texture.gamma_mode != 0; if (is_srgb) { @@ -455,6 +555,33 @@ std::optional VKSurfaceCache::retrieve_color_surface_as_tex ColorSurfaceCacheInfo &info = *ite->second; + // Serve RECENT TRANSFER DESTINATIONS from guest RAM: the game transfer-copied data + // (e.g. a per-light shadow-mask snapshot) into this range and now binds it as a + // texture — the CPU-written bytes live in guest RAM, not in the surface image. Unlike + // the removed union-based bail (see NOTE below), this only triggers on explicit + // recent transfer writes, so GPU-rendered sub-regions are unaffected. + { + const uint64_t cur_frame = reinterpret_cast(state.context)->frame_timestamp; + const Address bind_end = address + total_surface_size; + for (const auto &tw : *info.guest_transfer_writes) { + if (tw.start < bind_end && address < tw.end && cur_frame - tw.frame <= 60) + return std::nullopt; + } + } + + // NOTE: an earlier revision bailed to the RAM-upload path when guest_written_range + // intersected the texture's read range. That was wrong for memory pools: the pool's + // address range legitimately contains CPU-written ring allocations, so once trapping + // became reliable the union always grew and permanently forced GPU-rendered sub-regions + // (Killzone Mercenary's shadow masks) onto the RAM path, whose content is never written + // back — serving stale data. Telemetry showed the case the bail was protecting against + // (texture binds of transfer destinations) never occurs, so recently-rendered surfaces + // are always served from the GPU image. + // Surfaces that are no longer actively rendered but have been written by the CPU since + // (video frames, UI uploads into recycled pool memory) must still be read from guest RAM. + if (*info.dirty && info.last_frame_rendered + 2 <= reinterpret_cast(state.context)->frame_timestamp) + return std::nullopt; + if ((base_format == SCE_GXM_COLOR_BASE_FORMAT_U8U8U8 || info.format == SCE_GXM_COLOR_BASE_FORMAT_U8U8U8) && base_format != info.format) // don't even try to match u8u8u8 with something else @@ -488,6 +615,19 @@ std::optional VKSurfaceCache::retrieve_color_surface_as_tex if (static_cast(start_sourced_line + height) > info.height) LOG_WARN_ONCE("Trying to use texture partially in the surface cache"); + // The compute de-interleave below rewrites the cast's byte layout, so it must only engage for the exact pattern it is correct for + const uint32_t guard_native_byte_offset = data_delta % stride_bytes; + const uint32_t guard_sub_texel_byte = bytes_per_pixel_in_store ? (guard_native_byte_offset % bytes_per_pixel_in_store) : 0u; + const uint32_t guard_native_store_col = bytes_per_pixel_in_store ? (guard_native_byte_offset / bytes_per_pixel_in_store) : 0u; + const uint32_t guard_ratio = bytes_per_pixel_requested ? (bytes_per_pixel_in_store / bytes_per_pixel_requested) : 0u; + + const bool use_compute_deinterleave = state.res_multiplier != 1.0f + && bytes_per_pixel_in_store == 8 && bytes_per_pixel_requested == 4 && guard_ratio == 2 + && guard_native_store_col == 0 && start_sourced_line == 0 + && (guard_sub_texel_byte % bytes_per_pixel_requested) == 0 + && info.original_width > 0 && info.original_height > 0; + // ---------------------------------------------------------------------------- + // We should be able to use this texture, so set it as mru color_surface_queue.set_as_mru(&info); @@ -495,6 +635,11 @@ std::optional VKSurfaceCache::retrieve_color_surface_as_tex const bool is_same_image = (color_handle_view == info.texture.view) || (color_handle_view == info.alternate_view); if (state.features.use_texture_viewport && base_format == info.format) { + LOG_WARN_ONCE("surface_as_texture: viewport addr=0x{:08X} fmt={} {}x{} as {}x{} ratio=({:.3f},{:.3f})", + address, vk::to_string(info.texture.format), + info.original_width, info.original_height, original_width, original_height, + original_width / static_cast(info.original_width), + original_height / static_cast(info.original_height)); // use a texture viewport *texture_viewport = { .ratio = { @@ -533,15 +678,39 @@ std::optional VKSurfaceCache::retrieve_color_surface_as_tex } if (is_same_image || (start_sourced_line != 0) || (start_x != 0) || (info.width != width) || (info.height != height) || (info.format != base_format)) { + LOG_WARN_ONCE("surface_as_texture: cast addr=0x{:08X} same_img={} surf_fmt={} tex_fmt={} surf={}x{} tex={}x{} offset=({},{}) bpp_store={} bpp_req={}", + address, is_same_image, vk::to_string(info.texture.format), vk::to_string(vk_format), + info.original_width, info.original_height, original_width, original_height, + start_x, start_sourced_line, bytes_per_pixel_in_store, bytes_per_pixel_requested); const uint64_t scene_timestamp = reinterpret_cast(state.context)->scene_timestamp; + // Games sample render-target casts at non-base mips (explicit LOD or lod_bias in the + // texture words, e.g. KZM's exposure-adaptation strip); a single-mip cast silently + // serves full-res mip 0 there, which destabilizes feedback loops that are bit-stable + // on hardware. Serve the declared mip count; the chain is generated on the GPU right + // after the cast blit. Only the plain same-bpp blit path supports this. + uint16_t wanted_mips = 1; + if (bytes_per_pixel_requested == bytes_per_pixel_in_store && !use_compute_deinterleave) + wanted_mips = renderer::texture::get_upload_mip(texture.true_mip_count(), + static_cast(original_width), static_cast(original_height)); + if (wanted_mips > 1 && (bytes_per_pixel_requested != bytes_per_pixel_in_store || use_compute_deinterleave)) + wanted_mips = 1; + + // Shadow-mask pool casts (384x192 surface, sub-128 row offset): force single mip. + // The pool's sub-regions are 128x128; serving a multi-level mip chain causes the + // sampler to pick a downscaled level, producing blocky shadows. + if (info.original_width == 384 && info.original_height == 192 + && start_sourced_line < 128 && state.res_multiplier == 1.0f + && bytes_per_pixel_requested == bytes_per_pixel_in_store && !use_compute_deinterleave) + wanted_mips = 1; + std::vector &casted_vec = info.casted_textures; CastedTexture *casted = nullptr; // Look in cast cache and grab one. The cache really does not store immediate grab on now, but rather to reduce the synchronization in the pipeline (use different texture) for (size_t i = 0; i < casted_vec.size();) { - if ((casted_vec[i].cropped_height == height) && (casted_vec[i].cropped_width == width) && (casted_vec[i].cropped_y == start_sourced_line) && (casted_vec[i].cropped_x == start_x) && (casted_vec[i].format == base_format)) { + if ((casted_vec[i].cropped_height == height) && (casted_vec[i].cropped_width == width) && (casted_vec[i].cropped_y == start_sourced_line) && (casted_vec[i].cropped_x == start_x) && (casted_vec[i].format == base_format) && (casted_vec[i].mip_count == wanted_mips)) { casted = &casted_vec[i]; if (casted->scene_timestamp == scene_timestamp) { @@ -572,11 +741,49 @@ std::optional VKSurfaceCache::retrieve_color_surface_as_tex .cropped_y = start_sourced_line, .cropped_width = width, .cropped_height = height, - .format = base_format + .format = base_format, + .mip_count = wanted_mips + }; + if (bytes_per_pixel_requested == bytes_per_pixel_in_store) { + casted->texture.width = original_width; + casted->texture.height = original_height; + } else { + casted->texture.width = width; + casted->texture.height = height; + } + + auto store_is_f16 = [](SceGxmColorBaseFormat f) { + return f == SCE_GXM_COLOR_BASE_FORMAT_F16 + || f == SCE_GXM_COLOR_BASE_FORMAT_F16F16 + || f == SCE_GXM_COLOR_BASE_FORMAT_F16F16F16F16; }; - casted->texture.width = width; - casted->texture.height = height; - casted->texture.format = vk_format; + + auto force_unsigned_reinterpret_format = [](vk::Format fmt) { + switch (fmt) { + case vk::Format::eR8Snorm: return vk::Format::eR8Unorm; + case vk::Format::eR8G8Snorm: return vk::Format::eR8G8Unorm; + case vk::Format::eR8G8B8A8Snorm: return vk::Format::eR8G8B8A8Unorm; + case vk::Format::eR16Snorm: return vk::Format::eR16Unorm; + case vk::Format::eR16G16Snorm: return vk::Format::eR16G16Unorm; + case vk::Format::eR16G16B16A16Snorm: return vk::Format::eR16G16B16A16Unorm; + case vk::Format::eR8Sint: return vk::Format::eR8Uint; + case vk::Format::eR8G8Sint: return vk::Format::eR8G8Uint; + case vk::Format::eR8G8B8A8Sint: return vk::Format::eR8G8B8A8Uint; + default: return fmt; + } + }; + + casted->texture.format = (bytes_per_pixel_requested != bytes_per_pixel_in_store && store_is_f16(info.format)) ? force_unsigned_reinterpret_format(vk_format) : vk_format; + + // Vita render targets store linear data as raw bytes — no gamma encoding. + // When a game reads this surface as an sRGB texture, the decode must happen + // only at sampling time. Create the image in the source (UNORM) format so + // the blit copies raw bytes, then attach an sRGB view for the sampler. + const bool needs_srgb_view = is_srgb + && casted->texture.format == vk::Format::eR8G8B8A8Srgb + && info.texture.format == vk::Format::eR8G8B8A8Unorm; + if (needs_srgb_view) + casted->texture.format = vk::Format::eR8G8B8A8Unorm; // find the swizzle we need to apply const std::uint8_t components_in_store = vk::componentCount(info.texture.format); @@ -589,64 +796,224 @@ std::optional VKSurfaceCache::retrieve_color_surface_as_tex else resulting_swizzle = swizzle; - casted->texture.init_image(vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst, resulting_swizzle); - casted->texture.transition_to(cmd_buffer, vkutil::ImageLayout::TransferDst); + if (use_compute_deinterleave) { + // The compute pass writes this image through an R32_UINT storage view (created lazily at dispatch) + // the consumer still samples it through its real format view + casted->texture.init_image(vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eStorage, resulting_swizzle, vk::ImageCreateFlagBits::eMutableFormat); + casted->texture.transition_to(cmd_buffer, vkutil::ImageLayout::StorageImage); + } else { + casted->texture.mip_count = wanted_mips; + vk::ImageUsageFlags usage = vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst; + if (wanted_mips > 1) + // the mip chain is generated by blitting level i-1 -> i + usage |= vk::ImageUsageFlagBits::eTransferSrc; + casted->texture.init_image(usage, resulting_swizzle, + needs_srgb_view ? vk::ImageCreateFlagBits::eMutableFormat : vk::ImageCreateFlags()); + casted->texture.transition_to(cmd_buffer, vkutil::ImageLayout::TransferDst); + } + + if (needs_srgb_view) { + state.device.destroyImageView(casted->texture.view); + vk::ImageSubresourceRange srgb_range = vkutil::color_subresource_range; + srgb_range.levelCount = casted->texture.mip_count; + vk::ImageViewCreateInfo srgb_view_info{ + .image = casted->texture.image, + .viewType = vk::ImageViewType::e2D, + .format = vk::Format::eR8G8B8A8Srgb, + .components = resulting_swizzle, + .subresourceRange = srgb_range + }; + casted->texture.view = state.device.createImageView(srgb_view_info); + casted->texture.format = vk::Format::eR8G8B8A8Srgb; + } } else { - casted->texture.transition_to_discard(cmd_buffer, vkutil::ImageLayout::TransferDst); + casted->texture.transition_to_discard(cmd_buffer, use_compute_deinterleave ? vkutil::ImageLayout::StorageImage : vkutil::ImageLayout::TransferDst); } casted->scene_timestamp = scene_timestamp; if (bytes_per_pixel_requested == bytes_per_pixel_in_store) { - vk::ImageCopy image_copy{ + const int32_t src_w = static_cast(std::min(width, info.width - start_x)); + const int32_t src_h = static_cast(std::min(height, info.height - start_sourced_line)); + const int32_t dst_w = static_cast(original_width); + const int32_t dst_h = static_cast(original_height); + + vk::ImageBlit blit{ .srcSubresource = vkutil::color_subresource_layer, - .srcOffset = { static_cast(start_x), static_cast(start_sourced_line), 0 }, + .srcOffsets = std::array{ + vk::Offset3D{ static_cast(start_x), static_cast(start_sourced_line), 0 }, + vk::Offset3D{ static_cast(start_x) + src_w, static_cast(start_sourced_line) + src_h, 1 } }, .dstSubresource = vkutil::color_subresource_layer, - .dstOffset = { 0, - 0, - 0 }, - .extent = { - // Don't try to copy what is in the stride - std::min(width, info.width), - std::min(height, info.height), - 1 } + .dstOffsets = std::array{ vk::Offset3D{ 0, 0, 0 }, vk::Offset3D{ dst_w, dst_h, 1 } } }; - cmd_buffer.copyImage(info.texture.image, vk::ImageLayout::eGeneral, casted->texture.image, vk::ImageLayout::eTransferDstOptimal, image_copy); + + cmd_buffer.blitImage(info.texture.image, vk::ImageLayout::eGeneral, casted->texture.image, vk::ImageLayout::eTransferDstOptimal, blit, vk::Filter::eLinear); + + // Generate the declared mip chain from the freshly-blitted mip 0 so non-base-LOD + // samples read real downscaled data (the game generates these mips on hardware, + // e.g. via transfer downscales into the memory following mip 0). + for (uint32_t level = 1; level < casted->texture.mip_count; level++) { + const vk::ImageMemoryBarrier to_src{ + .srcAccessMask = vk::AccessFlagBits::eTransferWrite, + .dstAccessMask = vk::AccessFlagBits::eTransferRead, + .oldLayout = vk::ImageLayout::eTransferDstOptimal, + .newLayout = vk::ImageLayout::eTransferSrcOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = casted->texture.image, + .subresourceRange = { vk::ImageAspectFlagBits::eColor, level - 1, 1, 0, 1 } + }; + cmd_buffer.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, to_src); + + const vk::ImageBlit mip_blit{ + .srcSubresource = { vk::ImageAspectFlagBits::eColor, level - 1, 0, 1 }, + .srcOffsets = std::array{ + vk::Offset3D{ 0, 0, 0 }, + vk::Offset3D{ std::max(dst_w >> (level - 1), 1), std::max(dst_h >> (level - 1), 1), 1 } }, + .dstSubresource = { vk::ImageAspectFlagBits::eColor, level, 0, 1 }, + .dstOffsets = std::array{ + vk::Offset3D{ 0, 0, 0 }, + vk::Offset3D{ std::max(dst_w >> level, 1), std::max(dst_h >> level, 1), 1 } } + }; + cmd_buffer.blitImage(casted->texture.image, vk::ImageLayout::eTransferSrcOptimal, + casted->texture.image, vk::ImageLayout::eTransferDstOptimal, mip_blit, vk::Filter::eLinear); + } + if (casted->texture.mip_count > 1) { + // bring the source levels back to TransferDst so the single transition to + // SampledImage below covers every level from the same layout + const vk::ImageMemoryBarrier back_to_dst{ + .srcAccessMask = vk::AccessFlagBits::eTransferRead, + .dstAccessMask = vk::AccessFlagBits::eTransferWrite, + .oldLayout = vk::ImageLayout::eTransferSrcOptimal, + .newLayout = vk::ImageLayout::eTransferDstOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = casted->texture.image, + .subresourceRange = { vk::ImageAspectFlagBits::eColor, 0, casted->texture.mip_count - 1u, 0, 1 } + }; + cmd_buffer.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, back_to_dst); + } } else { LOG_INFO_ONCE("Game is doing typeless copies"); - // We must use a transition buffer - vk::DeviceSize buffer_size = stride_bytes * static_cast(state.res_multiplier * align(height, 4)) + start_x * bytes_per_pixel_requested; - if (!casted->transition_buffer.buffer || casted->transition_buffer.size < buffer_size) { - // create or re-create the buffer - state.frame().destroy_queue.add_buffer(casted->transition_buffer); - casted->transition_buffer = vkutil::Buffer(buffer_size); - casted->transition_buffer.init_buffer(vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eTransferSrc); - } - // copy the image to the buffer - const uint32_t src_pixel_stride = static_cast((info.stride_bytes / bytes_per_pixel_in_store) * state.res_multiplier); - vk::BufferImageCopy copy_image_buffer{ - .bufferOffset = 0, - .bufferRowLength = src_pixel_stride, - .bufferImageHeight = height, - .imageSubresource = vkutil::color_subresource_layer, - .imageOffset = { 0, - static_cast(start_sourced_line), - 0 }, - .imageExtent = { info.width, height, 1 } - }; - cmd_buffer.copyImageToBuffer(info.texture.image, vk::ImageLayout::eGeneral, casted->transition_buffer.buffer, copy_image_buffer); - - // then the buffer to the image - const uint32_t dst_pixel_stride = (stride_bytes / bytes_per_pixel_requested) * state.res_multiplier; - copy_image_buffer - .setBufferOffset(start_x * bytes_per_pixel_requested) - .setBufferRowLength(dst_pixel_stride) - .setImageOffset({ 0, 0, 0 }) - .setImageExtent({ width, height, 1 }); - cmd_buffer.copyBufferToImage(casted->transition_buffer.buffer, casted->texture.image, vk::ImageLayout::eTransferDstOptimal, copy_image_buffer); + const uint32_t ratio = bytes_per_pixel_in_store / bytes_per_pixel_requested; + + const uint32_t native_byte_offset = data_delta % stride_bytes; + const uint32_t sub_texel_byte = native_byte_offset % bytes_per_pixel_in_store; + + if (use_compute_deinterleave) { + ensure_reinterpret_pipeline(); + + const uint32_t half_index = sub_texel_byte / bytes_per_pixel_requested; + + if (!casted->reinterpret_view) { + vk::ImageViewCreateInfo reinterpret_view_info{ + .image = casted->texture.image, + .viewType = vk::ImageViewType::e2D, + .format = vk::Format::eR32Uint, + .components = {}, + .subresourceRange = vkutil::color_subresource_range + }; + casted->reinterpret_view = state.device.createImageView(reinterpret_view_info); + } + + // Read the 64-bit store as raw 32-bit word pairs through an R32G32_UINT + if (!info.reinterpret_store_view) { + vk::ImageViewCreateInfo store_view_info{ + .image = info.texture.image, + .viewType = vk::ImageViewType::e2D, + .format = vk::Format::eR32G32Uint, + .components = {}, + .subresourceRange = vkutil::color_subresource_range + }; + info.reinterpret_store_view = state.device.createImageView(store_view_info); + } + + // Make the freshly-rendered store visible to the compute read. + vk::ImageMemoryBarrier store_to_compute{ + .srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eShaderWrite, + .dstAccessMask = vk::AccessFlagBits::eShaderRead, + .oldLayout = vk::ImageLayout::eGeneral, + .newLayout = vk::ImageLayout::eGeneral, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = info.texture.image, + .subresourceRange = vkutil::color_subresource_range + }; + cmd_buffer.pipelineBarrier( + vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eFragmentShader, + vk::PipelineStageFlagBits::eComputeShader, {}, {}, {}, store_to_compute); + + vk::DescriptorSet dset = reinterpret_desc_sets[reinterpret_desc_idx]; + reinterpret_desc_idx = (reinterpret_desc_idx + 1) % static_cast(reinterpret_desc_sets.size()); + + vk::DescriptorImageInfo store_ii{ reinterpret_sampler, info.reinterpret_store_view, vk::ImageLayout::eGeneral }; + vk::DescriptorImageInfo cast_ii{ nullptr, casted->reinterpret_view, vk::ImageLayout::eGeneral }; + std::array writes; + writes[0] = vk::WriteDescriptorSet{ .dstSet = dset, .dstBinding = 0, .dstArrayElement = 0, .descriptorType = vk::DescriptorType::eCombinedImageSampler }; + writes[0].setImageInfo(store_ii); + writes[1] = vk::WriteDescriptorSet{ .dstSet = dset, .dstBinding = 1, .dstArrayElement = 0, .descriptorType = vk::DescriptorType::eStorageImage }; + writes[1].setImageInfo(cast_ii); + state.device.updateDescriptorSets(writes, {}); + + ReinterpretPushConstants pc{ + .out_width = width, + .out_height = height, + .scaled_store_w = info.texture.width, + .scaled_store_h = info.texture.height, + .ratio = ratio, + .half_index = half_index + }; + cmd_buffer.bindPipeline(vk::PipelineBindPoint::eCompute, reinterpret_pipeline); + cmd_buffer.bindDescriptorSets(vk::PipelineBindPoint::eCompute, reinterpret_pipeline_layout, 0, dset, {}); + cmd_buffer.pushConstants(reinterpret_pipeline_layout, vk::ShaderStageFlagBits::eCompute, 0, sizeof(pc), &pc); + // 2D dispatch keeps the per-axis workgroup count well under the limit at high multipliers. + cmd_buffer.dispatch((width + 7u) / 8u, (height + 7u) / 8u, 1); + } else { + // Direct byte reinterpret (cropped / partial reads) + const uint32_t src_pixel_stride = static_cast((info.stride_bytes / bytes_per_pixel_in_store) * state.res_multiplier); + const uint32_t scaled_store_col = static_cast((native_byte_offset / bytes_per_pixel_in_store) * state.res_multiplier); + const uint32_t src_byte_offset = scaled_store_col * bytes_per_pixel_in_store + sub_texel_byte; + const uint32_t dst_pixel_stride = src_pixel_stride * ratio; + + const vk::DeviceSize buffer_size = static_cast(src_pixel_stride) * bytes_per_pixel_in_store * align(height, 4) + src_byte_offset + bytes_per_pixel_in_store; + + if (!casted->transition_buffer.buffer || casted->transition_buffer.size < buffer_size) { + state.frame().destroy_queue.add_buffer(casted->transition_buffer); + casted->transition_buffer = vkutil::Buffer(buffer_size); + casted->transition_buffer.init_buffer(vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eTransferSrc); + } + + vk::BufferImageCopy copy_image_buffer{ + .bufferOffset = 0, + .bufferRowLength = src_pixel_stride, + .bufferImageHeight = height, + .imageSubresource = vkutil::color_subresource_layer, + .imageOffset = { 0, static_cast(start_sourced_line), 0 }, + .imageExtent = { info.width, height, 1 } + }; + vk::ImageMemoryBarrier img_barrier{ + .srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eShaderWrite, + .dstAccessMask = vk::AccessFlagBits::eTransferRead, + .oldLayout = vk::ImageLayout::eGeneral, + .newLayout = vk::ImageLayout::eGeneral, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = info.texture.image, + .subresourceRange = vkutil::color_subresource_range + }; + cmd_buffer.pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eFragmentShader, vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, img_barrier); + cmd_buffer.copyImageToBuffer(info.texture.image, vk::ImageLayout::eTransferSrcOptimal, casted->transition_buffer.buffer, copy_image_buffer); + + copy_image_buffer + .setBufferOffset(src_byte_offset) + .setBufferRowLength(dst_pixel_stride) + .setImageOffset({ 0, 0, 0 }) + .setImageExtent({ width, height, 1 }); + cmd_buffer.copyBufferToImage(casted->transition_buffer.buffer, casted->texture.image, vk::ImageLayout::eTransferDstOptimal, copy_image_buffer); + } } - casted->texture.transition_to(cmd_buffer, vkutil::ImageLayout::ColorAttachmentReadWrite); + casted->texture.transition_to(cmd_buffer, vkutil::ImageLayout::SampledImage); return TextureLookupResult{ casted->texture.view, @@ -689,6 +1056,7 @@ SurfaceRetrieveResult VKSurfaceCache::retrieve_depth_stencil_for_framebuffer(Sce int32_t memory_width = static_cast(width / state.res_multiplier); int32_t memory_height = static_cast(height / state.res_multiplier); + const SurfaceTiling tiling = (depth_stencil->get_type() == SCE_GXM_DEPTH_STENCIL_SURFACE_LINEAR) ? SurfaceTiling::Linear : SurfaceTiling::Tiled; // check if MSAA is used, the depth buffer is never downscaled @@ -839,8 +1207,7 @@ std::optional VKSurfaceCache::retrieve_depth_stencil_as_tex return std::nullopt; } - if (stride_samples % 32 != 0) - // a depth/stencil always has a stride which is a multiple of the tile size + if (stride_samples % 32 != 0) // a depth/stencil always has a stride which is a multiple of the tile size return std::nullopt; // take upscaling into account @@ -1111,25 +1478,128 @@ Framebuffer &VKSurfaceCache::retrieve_framebuffer_handle(MemState &mem, SceGxmCo return (framebuffer_array[key] = { fb_standard, fb_interlock, color_result.base_image }); } -bool VKSurfaceCache::check_for_surface(MemState &mem, Address source_address, CallbackRequestFunction &callback, Address target_address) { +void VKSurfaceCache::push_guest_write_callback(CallbackRequestFunction &&callback, Address target_address, uint32_t target_size) { + if (target_address && target_size) { + uint64_t pending_id; + { + std::lock_guard lock(pending_guest_writes_mutex); + pending_id = ++next_pending_write_id; + pending_guest_writes.push_back({ target_address, target_address + target_size, pending_id }); + pending_guest_write_count.store(static_cast(pending_guest_writes.size()), std::memory_order_release); + } + CallbackRequestFunction wrapped = [this, inner = std::move(callback), pending_id]() { + inner(); + complete_pending_guest_write(pending_id); + }; + state.request_queue.push(CallbackRequest{ new CallbackRequestFunction(std::move(wrapped)) }); + } else { + state.request_queue.push(CallbackRequest{ new CallbackRequestFunction(std::move(callback)) }); + } +} + +void VKSurfaceCache::complete_pending_guest_write(uint64_t id) { + { + std::lock_guard lock(pending_guest_writes_mutex); + for (size_t i = 0; i < pending_guest_writes.size(); i++) { + if (pending_guest_writes[i].id == id) { + pending_guest_writes.erase(pending_guest_writes.begin() + i); + break; + } + } + pending_guest_write_count.store(static_cast(pending_guest_writes.size()), std::memory_order_release); + } + pending_guest_writes_cv.notify_all(); +} + +void VKSurfaceCache::wait_for_pending_guest_write(Address addr, uint32_t size) { + if (pending_guest_write_count.load(std::memory_order_acquire) == 0) + return; + + const Address end = addr + size; + std::unique_lock lock(pending_guest_writes_mutex); + const auto no_intersect = [&] { + for (const auto &pending : pending_guest_writes) { + if (pending.start < end && addr < pending.end) + return false; + } + return true; + }; + if (no_intersect()) + return; + + const bool drained = pending_guest_writes_cv.wait_for(lock, std::chrono::milliseconds(200), no_intersect); + if (!drained) { + LOG_WARN_ONCE("Timed out waiting for queued guest write intersecting 0x{:08X}-0x{:08X}", addr, end); + } +} + +bool VKSurfaceCache::check_for_surface(MemState &mem, Address source_address, CallbackRequestFunction &callback, Address target_address, uint32_t source_size, uint32_t target_size) { if (!state.features.enable_memory_mapping || state.disable_surface_sync) return false; + // The destination of this CPU operation is a guest write. If it lands inside a cached + // surface's address range, record it in that surface's guest-written range so texture + // reads of this data are served from guest RAM instead of the surface image (games copy + // e.g. per-light shadow-mask snapshots into buffers sharing a memory pool with render + // targets). The one-shot write trap cannot be relied upon for this: the emulator's own + // write-backs can consume the trap before the guest write happens. + if (target_address && target_size) { + auto t_scan = color_address_lookup.upper_bound(target_address); + while (t_scan != color_address_lookup.begin()) { + --t_scan; + if (t_scan->first + t_scan->second->total_bytes > target_address) { + auto &wr = *t_scan->second->guest_written_range; + const Address w_end = target_address + target_size; + if (wr.second == 0) { + wr.first = target_address; + wr.second = w_end; + } else { + wr.first = std::min(wr.first, target_address); + wr.second = std::max(wr.second, w_end); + } + + // precise per-transfer record (see header): consumers binding this range + // as a texture must be served the CPU-transferred data from guest RAM + auto &tw = *t_scan->second->guest_transfer_writes; + if (tw.size() >= 16) + tw.erase(tw.begin()); + tw.push_back({ target_address, w_end, + reinterpret_cast(state.context)->frame_timestamp }); + } + } + } + if (vector_utils::find_index(cpu_surfaces_changed, source_address) != -1) { - // there is a transfer operation pending on this surface, just add the callback after and we are done - state.request_queue.push(CallbackRequest{ new CallbackRequestFunction(std::move(callback)) }); + push_guest_write_callback(std::move(callback), target_address, target_size); if (target_address) cpu_surfaces_changed.push_back(target_address); return true; } - // for now, only look if the address matches exactly a color surface - auto it = color_address_lookup.find(source_address); - if (it == color_address_lookup.end()) + // Range-based lookup: find the most recently rendered color surface containing the + // address (memory pools can have multiple overlapping — possibly stale — entries) + ColorSurfaceCacheInfo *color_match = nullptr; + Address color_base = 0; + auto scan = color_address_lookup.upper_bound(source_address); + while (scan != color_address_lookup.begin()) { + --scan; + if (scan->first + scan->second->total_bytes > source_address) { + if (color_match == nullptr || scan->second->last_frame_rendered > color_match->last_frame_rendered) { + color_match = scan->second; + color_base = scan->first; + } + } + } + + if (color_match == nullptr) { + LOG_WARN_ONCE("check_for_surface: no surface at 0x{:08X}", source_address); return false; + } + + auto &surface = *color_match; + auto it = color_address_lookup.find(color_base); - auto &surface = *it->second; VKContext &context = *static_cast(state.context); // if the frame is already rendered skip // Note: that's not the best behavior but it should be fine @@ -1138,58 +1608,185 @@ bool VKSurfaceCache::check_for_surface(MemState &mem, Address source_address, Ca return false; // we found something - if (!*surface.need_surface_sync) { - // first send the command to sync the surface with the GPU - *surface.need_surface_sync = true; + // the CPU is about to read this memory, so a GPU-only sync is not enough anymore + const bool was_gpu_read_only = surface.gpu_read_sync_only; + surface.gpu_read_sync_only = false; + + // Grow the union of CPU-read ranges: guest RAM write-backs for this surface (both the + // recurring scene-end syncs and the immediate sync below) are restricted to it, so that + // CPU-written data elsewhere in the surface's address range (e.g. transfer destinations + // of Killzone Mercenary's exposure-meter chain) is never clobbered with image content. + const uint64_t read_end = std::min(static_cast(source_address) + (source_size ? source_size : surface.total_bytes), + static_cast(it->first) + surface.total_bytes); + if (surface.cpu_read_range_size == 0) { + surface.cpu_read_range_start = source_address; + surface.cpu_read_range_size = static_cast(read_end - source_address); + } else { + const Address new_start = std::min(surface.cpu_read_range_start, source_address); + const uint64_t new_end = std::max(surface.cpu_read_range_start + surface.cpu_read_range_size, read_end); + surface.cpu_read_range_start = new_start; + surface.cpu_read_range_size = static_cast(new_end - new_start); + } + + // The callback must stay asynchronous (wait-thread ordered): transfers can be issued + // before the scene producing their source data is submitted, relying on GPU-order + // execution — the recurring scene-end syncs (need_surface_sync stays latched) deliver + // the data before the queued callback runs. + if (!*surface.need_surface_sync || was_gpu_read_only) + submit_immediate_surface_sync(surface, nullptr); - // we shouldn't have a command buffer being used, but just in case - vk::CommandBuffer prev_cmd = context.render_cmd; + // now push the callback + push_guest_write_callback(std::move(callback), target_address, target_size); - // for the time being, just create a temp command buffer / fence - // That's not the best approach but I guess it works - vk::CommandBuffer surface_cmd = nullptr; - vk::Fence fence = state.device.createFence({}); - ColorSurfaceCacheInfo *returned_info = nullptr; + if (target_address) + cpu_surfaces_changed.push_back(target_address); + + return true; +} + +void VKSurfaceCache::submit_immediate_surface_sync(ColorSurfaceCacheInfo &surface, MemState *mem, Address sync_addr, uint32_t sync_size) { + VKContext &context = *static_cast(state.context); + + // first send the command to sync the surface with the GPU + *surface.need_surface_sync = true; + + // we shouldn't have a command buffer being used, but just in case + vk::CommandBuffer prev_cmd = context.render_cmd; + // this can be called mid-scene, so preserve the scene's last written surface + ColorSurfaceCacheInfo *prev_last_written = last_written_surface; + + // for the time being, just create a temp command buffer / fence + // That's not the best approach but I guess it works + vk::CommandBuffer surface_cmd = nullptr; + vk::Fence fence = state.device.createFence({}); + ColorSurfaceCacheInfo *returned_info = nullptr; + { + std::lock_guard lock(state.multithread_pool_mutex); + surface_cmd = vkutil::create_single_time_command(state.device, state.multithread_command_pool); + + context.render_cmd = surface_cmd; + last_written_surface = &surface; + returned_info = perform_surface_sync(); + context.render_cmd = prev_cmd; + last_written_surface = (prev_last_written == &surface) ? nullptr : prev_last_written; + + surface_cmd.end(); + } + // submit this command + vk::SubmitInfo submit_info{}; + submit_info.setCommandBuffers(surface_cmd); + state.general_queue.submit(submit_info, fence); + + if (mem != nullptr) { + // synchronous completion: the caller (a CPU operation like sceGxmTransfer) needs the + // guest memory content to be correct and ordered exactly like the hardware transfer + // unit would — wait for the copy and write guest RAM before returning + auto result = state.device.waitForFences(fence, vk::True, std::numeric_limits::max()); + if (result != vk::Result::eSuccess) + LOG_ERROR("Could not wait for fences."); + state.device.destroyFence(fence); { std::lock_guard lock(state.multithread_pool_mutex); - surface_cmd = vkutil::create_single_time_command(state.device, state.multithread_command_pool); + state.device.freeCommandBuffers(state.multithread_command_pool, surface_cmd); + } - context.render_cmd = surface_cmd; - last_written_surface = &surface; - returned_info = perform_surface_sync(); - context.render_cmd = prev_cmd; + if (returned_info) { + if (returned_info->need_buffer_sync && state.mapping_method == MappingMethod::DoubleBuffer) { + // restrict the guest RAM write-back to the requested region if one was given + const Address location = sync_size ? sync_addr : returned_info->data.address(); + const uint32_t size = sync_size ? sync_size : static_cast(returned_info->total_bytes); + auto mem_it = state.mapped_memories.lower_bound(location); + if (mem_it == state.mapped_memories.end() || mem_it->first + mem_it->second.size < location + size) { + LOG_ERROR("Immediate surface sync for {}-{} is not fully mapped", log_hex(location), log_hex(location + size)); + } else { + const uint8_t *src = reinterpret_cast(std::get(mem_it->second.buffer_impl).mapped_data); + // this is the emulator's own write-back, not a guest write: go through + // protection without firing or consuming any traps + surface_sync_internal_write = true; + write_through_protection(*mem, location, size, [&]() { + memcpy(Ptr(location).get(*mem), src + (location - mem_it->first), size); + }); + surface_sync_internal_write = false; + } + } - surface_cmd.end(); + if (returned_info->need_post_surface_sync) + perform_post_surface_sync(*mem, returned_info); } - // submit this command - vk::SubmitInfo submit_info{}; - submit_info.setCommandBuffers(surface_cmd); - state.general_queue.submit(submit_info, fence); + return; + } - // now we need to wait for the fence, then destroy it along with the command buffer - // to prevent memory leaks - CallbackRequestFunction vk_callback = [&state = this->state, fence, surface_cmd]() { - auto result = state.device.waitForFences(fence, vk::True, std::numeric_limits::max()); - if (result != vk::Result::eSuccess) - LOG_ERROR("Could not wait for fences."); + // asynchronous completion: wait for the fence on the wait thread, then destroy it along + // with the command buffer to prevent memory leaks + CallbackRequestFunction vk_callback = [&state = this->state, fence, surface_cmd]() { + auto result = state.device.waitForFences(fence, vk::True, std::numeric_limits::max()); + if (result != vk::Result::eSuccess) + LOG_ERROR("Could not wait for fences."); - // destroy the objects - state.device.destroyFence(fence); + // destroy the objects + state.device.destroyFence(fence); - std::lock_guard lock(state.multithread_pool_mutex); - state.device.freeCommandBuffers(state.multithread_command_pool, surface_cmd); - }; - state.request_queue.push(CallbackRequest{ new CallbackRequestFunction(std::move(vk_callback)) }); + std::lock_guard lock(state.multithread_pool_mutex); + state.device.freeCommandBuffers(state.multithread_command_pool, surface_cmd); + }; + state.request_queue.push(CallbackRequest{ new CallbackRequestFunction(std::move(vk_callback)) }); + + if (returned_info) { + if (returned_info->need_buffer_sync && state.mapping_method == MappingMethod::DoubleBuffer) + state.request_queue.push(BufferSyncRequest{ returned_info->data.address(), static_cast(returned_info->total_bytes) }); - if (returned_info) + if (returned_info->need_post_surface_sync) state.request_queue.push(PostSurfaceSyncRequest{ returned_info }); } +} - // now push the callback - state.request_queue.push(CallbackRequest{ new CallbackRequestFunction(std::move(callback)) }); +bool VKSurfaceCache::sync_surface_for_gpu_read(Address address, uint32_t size) { + if (!state.features.enable_memory_mapping || state.disable_surface_sync) + return false; - if (target_address) - cpu_surfaces_changed.push_back(target_address); + + // Range-based lookup: find the surface whose base address is <= address + auto it = color_address_lookup.upper_bound(address); + if (it == color_address_lookup.begin()) + return false; + --it; + + auto &surface = *it->second; + // Treat the bind as a raw surface read when it is the surface's exact base address, OR + // when a LARGE range lies inside the surface: Killzone Mercenary's lighting accumulator + // binds sub-regions of rendered surfaces as raw buffers (offset != base). Small non-base + // binds stay excluded — uniform data is often sub-allocated from ring buffers landing + // inside a cached surface's range and carries CPU-written data that must be uploaded. + // Serving surface reads GPU-side matters beyond freshness: the fallback path snapshots + // guest RAM at renderer-processing time, racing the wait thread's asynchronous + // write-backs — the shader then reads a nondeterministic mix of frame generations, + // which destabilizes the game's temporal lighting accumulation (erratic shadow flicker). + if (it->first != address) { + // Non-base binds are raw surface reads when they carry no uniform data to upload: + // genuine CPU uniform reserves (ring buffers landing inside a surface's range) + // always have a nonzero declared block size, while Killzone Mercenary's lighting + // accumulator binds bare pointers (declared size 0) at row offsets into rendered + // surfaces. Large declared sizes are also treated as surface reads. + constexpr uint32_t min_surface_read_size = KiB(16); + const bool contained = address >= it->first + && static_cast(address) + size <= static_cast(it->first) + surface.total_bytes; + const bool engaged = contained && (size == 0 || size >= min_surface_read_size); + if (!engaged) + return false; + } + + VKContext &context = *static_cast(state.context); + // if the surface has not been rendered recently, its memory content is as up to date as it gets + if (surface.last_frame_rendered + MAX_FRAMES_RENDERING <= context.frame_timestamp) + return false; + + // once need_surface_sync is set, the end-of-scene sync in perform_surface_sync keeps the + // memory up to date every time the surface is rendered, so only the first read needs this + if (!*surface.need_surface_sync) { + // GPU-only readers don't need the data written back to guest RAM + surface.gpu_read_sync_only = true; + submit_immediate_surface_sync(surface, nullptr); + } return true; } @@ -1251,14 +1848,17 @@ ColorSurfaceCacheInfo *VKSurfaceCache::perform_surface_sync() { vk::Buffer buffer; uint32_t offset; - if (format_need_additional_memory(last_written_surface->format)) { + const uint32_t pixel_stride = (last_written_surface->stride_bytes * 8) / gxm::bits_per_pixel(last_written_surface->format); + const bool needs_copy_buffer = format_need_additional_memory(last_written_surface->format) || surface_sync_needs_u4u4u4u4_repack(*last_written_surface); + + if (needs_copy_buffer) { if (!last_written_surface->copy_buffer) last_written_surface->copy_buffer = std::make_unique(); vkutil::Buffer ©_buffer = *last_written_surface->copy_buffer; if (!copy_buffer.buffer) { - copy_buffer.size = last_written_surface->stride_bytes * last_written_surface->original_height; + copy_buffer.size = static_cast(pixel_stride) * last_written_surface->original_height * vk::blockSize(last_written_surface->texture.format); copy_buffer.init_buffer(vk::BufferUsageFlagBits::eTransferDst, vkutil::vma_mapped_alloc); } @@ -1268,11 +1868,13 @@ ColorSurfaceCacheInfo *VKSurfaceCache::perform_surface_sync() { last_written_surface->need_buffer_sync = false; last_written_surface->need_post_surface_sync = true; } else { - last_written_surface->need_buffer_sync = true; + // when the surface is only read by the GPU through raw buffer addresses, don't copy + // it back to guest RAM: the CPU write-back would mark the surface dirty and force a + // full re-upload from RAM on the next texture use (very slow + tearing) + last_written_surface->need_buffer_sync = !last_written_surface->gpu_read_sync_only; last_written_surface->need_post_surface_sync = !is_swizzle_identity; std::tie(buffer, offset) = state.get_matching_mapping(last_written_surface->data); } - const uint32_t pixel_stride = (last_written_surface->stride_bytes * 8) / gxm::bits_per_pixel(last_written_surface->format); vk::BufferImageCopy copy{ .bufferOffset = offset, .bufferRowLength = pixel_stride, @@ -1289,6 +1891,7 @@ ColorSurfaceCacheInfo *VKSurfaceCache::perform_surface_sync() { return return_value; } + template static void swizzle_text_T_2(T *pixels, uint32_t nb_pixel) { for (uint32_t i = 0; i < nb_pixel; i++) { @@ -1349,36 +1952,51 @@ void VKSurfaceCache::perform_post_surface_sync(const MemState &mem, ColorSurface if (surface == nullptr) return; + // all writes below are the emulator's own write-backs, not guest writes: flag them and + // go through protection without firing or consuming any traps + surface_sync_internal_write = true; + struct InternalWriteGuard { + ~InternalWriteGuard() { surface_sync_internal_write = false; } + } guard; + const uint32_t pixel_stride = (surface->stride_bytes * 8) / gxm::bits_per_pixel(surface->format); const uint32_t nb_pixels = pixel_stride * surface->original_height; uint8_t *pixels = surface->data.cast().get(mem); - if (format_need_additional_memory(surface->format)) { - // special case, use a custom function - const bool is_swizzle_identity = surface->swizzle.r == vk::ComponentSwizzle::eR; - if (!surface->sws_context) { - const AVPixelFormat dst_fmt = is_swizzle_identity ? AV_PIX_FMT_RGB24 : AV_PIX_FMT_BGR24; - surface->sws_context = sws_getContext(surface->original_width, surface->original_height, AV_PIX_FMT_RGB0, surface->original_width, surface->original_height, dst_fmt, 0, nullptr, nullptr, nullptr); - assert(surface->sws_context != NULL); + const auto write_back = [&]() { + if (surface_sync_needs_u4u4u4u4_repack(*surface)) { + pack_rgba8_to_r4g4b4a4(pixels, static_cast(surface->copy_buffer->mapped_data), pixel_stride, surface->original_height); + return; } - int src_stride = pixel_stride * 4; - int dst_stride = pixel_stride * 3; - sws_scale(surface->sws_context, reinterpret_cast(&surface->copy_buffer->mapped_data), &src_stride, 0, surface->original_height, &pixels, &dst_stride); - return; - } + if (format_need_additional_memory(surface->format)) { + // special case, use a custom function + const bool is_swizzle_identity = surface->swizzle.r == vk::ComponentSwizzle::eR; + if (!surface->sws_context) { + const AVPixelFormat dst_fmt = is_swizzle_identity ? AV_PIX_FMT_RGB24 : AV_PIX_FMT_BGR24; + surface->sws_context = sws_getContext(surface->original_width, surface->original_height, AV_PIX_FMT_RGB0, surface->original_width, surface->original_height, dst_fmt, 0, nullptr, nullptr, nullptr); + assert(surface->sws_context != NULL); + } - switch (vk::componentBits(surface->texture.format, 0)) { - case 8: - swizzle_text_T(pixels, nb_pixels, surface); - break; - case 16: - swizzle_text_T(reinterpret_cast(pixels), nb_pixels, surface); - break; - case 32: - swizzle_text_T(reinterpret_cast(pixels), nb_pixels, surface); - break; - } + int src_stride = pixel_stride * 4; + int dst_stride = pixel_stride * 3; + sws_scale(surface->sws_context, reinterpret_cast(&surface->copy_buffer->mapped_data), &src_stride, 0, surface->original_height, &pixels, &dst_stride); + return; + } + + switch (vk::componentBits(surface->texture.format, 0)) { + case 8: + swizzle_text_T(pixels, nb_pixels, surface); + break; + case 16: + swizzle_text_T(reinterpret_cast(pixels), nb_pixels, surface); + break; + case 32: + swizzle_text_T(reinterpret_cast(pixels), nb_pixels, surface); + break; + } + }; + write_through_protection(const_cast(mem), surface->data.address(), surface->total_bytes, write_back); } void VKSurfaceCache::destroy_associated_framebuffers(const VKRenderTarget *render_target) { @@ -1497,4 +2115,77 @@ std::vector VKSurfaceCache::dump_frame(Ptr address, uint32 return frame; } +void VKSurfaceCache::ensure_reinterpret_pipeline() { + if (reinterpret_pipeline) + return; + + const fs::path shader_path = state.static_assets / "shaders-builtin/vulkan" / "surface_cast_reinterpret.comp.spv"; + reinterpret_shader = vkutil::load_shader(state.device, shader_path); + + // point sampler used only so the shader can texelFetch the store + vk::SamplerCreateInfo sampler_info{ + .magFilter = vk::Filter::eNearest, + .minFilter = vk::Filter::eNearest, + .mipmapMode = vk::SamplerMipmapMode::eNearest, + .addressModeU = vk::SamplerAddressMode::eClampToEdge, + .addressModeV = vk::SamplerAddressMode::eClampToEdge, + .addressModeW = vk::SamplerAddressMode::eClampToEdge + }; + reinterpret_sampler = state.device.createSampler(sampler_info); + + // set 0: binding 0 = store (combined image sampler), binding 1 = cast (storage image) + std::array bindings{}; + bindings[0] = vk::DescriptorSetLayoutBinding{ + .binding = 0, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eCompute + }; + bindings[1] = vk::DescriptorSetLayoutBinding{ + .binding = 1, + .descriptorType = vk::DescriptorType::eStorageImage, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eCompute + }; + vk::DescriptorSetLayoutCreateInfo layout_info{}; + layout_info.setBindings(bindings); + reinterpret_desc_layout = state.device.createDescriptorSetLayout(layout_info); + + vk::PushConstantRange push_range{ + .stageFlags = vk::ShaderStageFlagBits::eCompute, + .offset = 0, + .size = sizeof(ReinterpretPushConstants) + }; + vk::PipelineLayoutCreateInfo pl_info{}; + pl_info.setSetLayouts(reinterpret_desc_layout); + pl_info.setPushConstantRanges(push_range); + reinterpret_pipeline_layout = state.device.createPipelineLayout(pl_info); + + vk::PipelineShaderStageCreateInfo stage{ + .stage = vk::ShaderStageFlagBits::eCompute, + .module = reinterpret_shader, + .pName = "main" + }; + vk::ComputePipelineCreateInfo pipeline_info{ + .stage = stage, + .layout = reinterpret_pipeline_layout + }; + reinterpret_pipeline = state.device.createComputePipeline(nullptr, pipeline_info).value; + + constexpr uint32_t NB_SETS = 256; + std::array pool_sizes{ + vk::DescriptorPoolSize{ vk::DescriptorType::eCombinedImageSampler, NB_SETS }, + vk::DescriptorPoolSize{ vk::DescriptorType::eStorageImage, NB_SETS } + }; + vk::DescriptorPoolCreateInfo pool_info{ .maxSets = NB_SETS }; + pool_info.setPoolSizes(pool_sizes); + reinterpret_desc_pool = state.device.createDescriptorPool(pool_info); + + std::vector layouts(NB_SETS, reinterpret_desc_layout); + vk::DescriptorSetAllocateInfo alloc_info{ .descriptorPool = reinterpret_desc_pool }; + alloc_info.setSetLayouts(layouts); + reinterpret_desc_sets = state.device.allocateDescriptorSets(alloc_info); + reinterpret_desc_idx = 0; +} + } // namespace renderer::vulkan diff --git a/vita3k/renderer/src/vulkan/sync_state.cpp b/vita3k/renderer/src/vulkan/sync_state.cpp index ea2e6b99..e865553b 100644 --- a/vita3k/renderer/src/vulkan/sync_state.cpp +++ b/vita3k/renderer/src/vulkan/sync_state.cpp @@ -103,7 +103,46 @@ void sync_depth_bias(VKContext &context) { if (!context.is_recording) return; - context.render_cmd.setDepthBias(static_cast(context.record.depth_bias_unit), 0.0, static_cast(context.record.depth_bias_slope)); + if (context.state.support_depth_bias_control) { + // Exact GXM semantics via VK_EXT_depth_bias_control: GXM bias units are ABSOLUTE + // depth steps (~2^-16 granularity) independent of the current depth value. The + // float representation expresses exactly that. The constant-factor fallback below + // is only correct for depth values in [0.5, 1): at the small light-space depths of + // shadow-mask scenes it under-biases by orders of magnitude, letting near-coplanar + // casters z-fight — a sub-pixel view change then flips huge mask regions between + // depth winners (KZM's erratic shadow flicker). + const bool use_float = context.state.depth_bias_control_use_float; + const vk::DepthBiasRepresentationInfoEXT representation{ + .depthBiasRepresentation = use_float + ? vk::DepthBiasRepresentationEXT::eFloat + : vk::DepthBiasRepresentationEXT::eLeastRepresentableValueForceUnorm, + .depthBiasExact = VK_FALSE + }; + // float: constant is an absolute depth offset -> units * 2^-16. + // FORCE_UNORM: constant is in minimum-resolvable-difference steps of a 24-bit + // UNORM depth (2^-24) -> units * 2^-16 / 2^-24 = units * 256. + const float constant = use_float + ? static_cast(context.record.depth_bias_unit) / 65536.0f + : static_cast(context.record.depth_bias_unit) * 256.0f; + const vk::DepthBiasInfoEXT bias_info{ + .pNext = &representation, + .depthBiasConstantFactor = constant, + .depthBiasClamp = 0.0f, + .depthBiasSlopeFactor = static_cast(context.record.depth_bias_slope) + }; + context.render_cmd.setDepthBias2EXT(bias_info); + return; + } + + // GXM depth bias units are absolute depth steps (~2^-16 granularity), while Vulkan's + // constant factor on a float depth buffer is scaled by the primitive-relative ULP + // (~2^-23 for depth values in [0.5, 1)). Scale the units to match, otherwise decal + // passes (soft shadows, bullet holes, ...) receive a negligible bias and z-fight with + // the geometry beneath them — visible as view-dependent decal flicker in e.g. + // Killzone Mercenary, which uses units of only -4. + // (2^-16 / 2^-23 = 128; exact for depth in [0.5, 1), conservative below that.) + constexpr float gxm_depth_bias_unit_scale = 128.0f; + context.render_cmd.setDepthBias(static_cast(context.record.depth_bias_unit) * gxm_depth_bias_unit_scale, 0.0, static_cast(context.record.depth_bias_slope)); } void sync_depth_data(VKContext &context) { @@ -229,17 +268,22 @@ void sync_visibility_buffer(VKContext &context, Ptr buffer, uint32_t s } void sync_visibility_index(VKContext &context, bool enable, uint32_t index, bool is_increment) { + // Out-of-range indices (e.g. 0xDEADBEEF sentinels) must never reach the query + // machinery: libgxm rejects them and the previous index stays in effect. The old + // behavior of redirecting them to slot 0 polluted slot 0's occlusion count and + // could begin its query twice in one scene (undefined results per Vulkan). + const uint32_t slot_count = context.current_visibility_buffer ? context.current_visibility_buffer->size : 16384u; + if (enable && index >= slot_count) { + LOG_WARN_ONCE("Visibility index out of range: 0x{:X} (slots={})", index, slot_count); + return; + } + if (context.current_visibility_buffer == nullptr) { context.current_query_idx = enable ? index : -1; context.is_query_op_increment = is_increment; return; } - if (index >= context.current_visibility_buffer->size) { - LOG_WARN_ONCE("Using visibility index {} which is too big for the buffer", index); - index = 0; - } - if (!enable) { if (context.is_in_query) { context.render_cmd.endQuery(context.current_visibility_buffer->query_pool, context.current_query_idx); diff --git a/vita3k/renderer/src/vulkan/texture.cpp b/vita3k/renderer/src/vulkan/texture.cpp index 5956d5f9..89e3bb4f 100644 --- a/vita3k/renderer/src/vulkan/texture.cpp +++ b/vita3k/renderer/src/vulkan/texture.cpp @@ -25,6 +25,10 @@ #include #include #include +#include + +#include +#include #include namespace renderer::vulkan { @@ -127,6 +131,15 @@ void sync_texture(VKContext &context, MemState &mem, std::size_t index, SceGxmTe // get the sampler now context.state.texture_cache.cache_and_bind_sampler(texture, is_depth_surface); } else { + // the upload path hashes guest RAM: if a queued (GPU-ordered) transfer writing this + // texture's memory hasn't landed yet, wait for it, otherwise the previous frame's + // data gets uploaded (per-light shadow-mask snapshots in Killzone Mercenary) + const Address data_addr = texture.data_addr << 2; + if (data_addr) { + const uint32_t data_size = std::max( + (gxm::get_width(texture) * gxm::get_height(texture) * gxm::bits_per_pixel(base_format)) / 8, 1); + context.state.surface_cache.wait_for_pending_guest_write(data_addr, data_size); + } context.state.texture_cache.cache_and_bind_texture(texture, mem); auto &image = context.state.texture_cache.current_texture->texture; lookup_result = TextureLookupResult{ @@ -586,7 +599,7 @@ void VKTextureCache::configure_sampler(size_t index, const SceGxmTexture &textur .mipLodBias = (static_cast(texture.lod_bias) - 31.f) / 8.f, .maxAnisotropy = static_cast(anisotropic_filtering), .compareEnable = VK_FALSE, - .minLod = static_cast(texture.lod_min0 | (texture.lod_min1 << 2)), + .minLod = static_cast(texture.true_lod_min()), .maxLod = VK_LOD_CLAMP_NONE, .unnormalizedCoordinates = VK_FALSE, }; diff --git a/vita3k/shader/include/shader/uniform_block.h b/vita3k/shader/include/shader/uniform_block.h index dfc850e7..9a71d7c6 100644 --- a/vita3k/shader/include/shader/uniform_block.h +++ b/vita3k/shader/include/shader/uniform_block.h @@ -30,11 +30,11 @@ enum VertUniformFieldId : uint32_t { }; struct RenderFragUniformBlock { - float back_disabled; - float front_disabled; - float writing_mask; - float use_raw_image; - float res_multiplier; + float back_disabled = 0.0f; + float front_disabled = 0.0f; + float writing_mask = 0.0f; + float use_raw_image = 0.0f; + float res_multiplier = 1.0f; }; enum FragUniformFieldId : uint32_t { diff --git a/vita3k/shader/include/shader/usse_translator_types.h b/vita3k/shader/include/shader/usse_translator_types.h index c758431f..1ec1a22f 100644 --- a/vita3k/shader/include/shader/usse_translator_types.h +++ b/vita3k/shader/include/shader/usse_translator_types.h @@ -89,6 +89,9 @@ struct SpirvShaderParameters { int viewport_ratio_id; int viewport_offset_id; + // number of entries in the buffer_addresses[] uniform array + int buffer_count = 0; + // when using a thread, texture or literal buffer, if not -1, this fields contain the sa register // with the matching address, this assumes of course that this address is not copied somewhere // else and that this register is not overwritten diff --git a/vita3k/shader/include/shader/usse_types.h b/vita3k/shader/include/shader/usse_types.h index e31942f2..280c1e4c 100644 --- a/vita3k/shader/include/shader/usse_types.h +++ b/vita3k/shader/include/shader/usse_types.h @@ -48,6 +48,7 @@ enum class ExtPredicate : uint8_t { P3, NEGP0, NEGP1, + NEGP2, PN }; @@ -99,10 +100,7 @@ inline ExtPredicate ext_vec_predicate_to_ext(ExtVecPredicate pred) { case ExtVecPredicate::NEGP1: return ExtPredicate::NEGP1; case ExtVecPredicate::NEGP2: - // TODO - assert(false); - LOG_CRITICAL("ExtVecPredicate::NEGP2 case hit, report this to devs."); - [[fallthrough]]; + return ExtPredicate::NEGP2; default: return ExtPredicate::NONE; } diff --git a/vita3k/shader/include/shader/usse_utilities.h b/vita3k/shader/include/shader/usse_utilities.h index 7f60020b..bf56f80c 100644 --- a/vita3k/shader/include/shader/usse_utilities.h +++ b/vita3k/shader/include/shader/usse_utilities.h @@ -61,6 +61,7 @@ spv::Id convert_to_float(spv::Builder &b, const SpirvUtilFunctions &utils, spv:: spv::Id convert_to_int(spv::Builder &b, const SpirvUtilFunctions &utils, spv::Id opr, DataType type, bool normal); spv::Id add_uvec2_uint(spv::Builder &b, spv::Id vec, spv::Id to_add); +spv::Id add_uvec2_int(spv::Builder &b, spv::Id vec, spv::Id to_add_signed); size_t dest_mask_to_comp_count(shader::usse::Imm4 dest_mask); diff --git a/vita3k/shader/src/spirv_recompiler.cpp b/vita3k/shader/src/spirv_recompiler.cpp index 194075fe..ec5e0419 100644 --- a/vita3k/shader/src/spirv_recompiler.cpp +++ b/vita3k/shader/src/spirv_recompiler.cpp @@ -889,13 +889,13 @@ static SpirvShaderParameters create_parameters(spv::Builder &b, const SceGxmProg spv::Id o_arr_type = b.makeArrayType(f32_v4_type, b.makeIntConstant(REG_O_COUNT / 4), 0); // Create register banks - spv_params.ins = b.createVariable(spv::NoPrecision, spv::StorageClassPrivate, pa_arr_type, "pa"); - spv_params.uniforms = b.createVariable(spv::NoPrecision, spv::StorageClassPrivate, sa_arr_type, "sa"); - spv_params.internals = b.createVariable(spv::NoPrecision, spv::StorageClassPrivate, i_arr_type, "internals"); - spv_params.temps = b.createVariable(spv::NoPrecision, spv::StorageClassPrivate, temp_arr_type, "r"); - spv_params.predicates = b.createVariable(spv::NoPrecision, spv::StorageClassPrivate, pred_arr_type, "p"); - spv_params.indexes = b.createVariable(spv::NoPrecision, spv::StorageClassPrivate, index_arr_type, "idx"); - spv_params.outs = b.createVariable(spv::NoPrecision, spv::StorageClassPrivate, o_arr_type, "outs"); + spv_params.ins = b.createVariable(spv::NoPrecision, spv::StorageClassPrivate, pa_arr_type, "pa", b.makeNullConstant(pa_arr_type)); + spv_params.uniforms = b.createVariable(spv::NoPrecision, spv::StorageClassPrivate, sa_arr_type, "sa", b.makeNullConstant(sa_arr_type)); + spv_params.internals = b.createVariable(spv::NoPrecision, spv::StorageClassPrivate, i_arr_type, "internals", b.makeNullConstant(i_arr_type)); + spv_params.temps = b.createVariable(spv::NoPrecision, spv::StorageClassPrivate, temp_arr_type, "r", b.makeNullConstant(temp_arr_type)); + spv_params.predicates = b.createVariable(spv::NoPrecision, spv::StorageClassPrivate, pred_arr_type, "p", b.makeNullConstant(pred_arr_type)); + spv_params.indexes = b.createVariable(spv::NoPrecision, spv::StorageClassPrivate, index_arr_type, "idx", b.makeNullConstant(index_arr_type)); + spv_params.outs = b.createVariable(spv::NoPrecision, spv::StorageClassPrivate, o_arr_type, "outs", b.makeNullConstant(o_arr_type)); SamplerMap samplers; @@ -1000,6 +1000,8 @@ static SpirvShaderParameters create_parameters(spv::Builder &b, const SceGxmProg const uint16_t uniform_buffer_count = features.enable_memory_mapping ? buffer_count : 0; const uint16_t uniform_texture_count = features.use_texture_viewport ? texture_count : 0; + spv_params.buffer_count = uniform_buffer_count; + if (program_type == SceGxmProgramType::Vertex) { // Create the default reg uniform buffer std::vector uniform_composition = { v4, f32, f32, f32, f32, f32 }; @@ -1744,7 +1746,11 @@ static spv::Function *make_vert_finalize_function(spv::Builder &b, const SpirvSh z = b.createBinOp(spv::OpFAdd, f32, z, z_offset); // z values below 0 get clamped - z = b.createBuiltinCall(f32, utils.std_builtins, GLSLstd450FMax, { z, zero }); + // z = b.createBuiltinCall(f32, utils.std_builtins, GLSLstd450FMax, { z, zero }); // Nick causes bad clamping - should be removed + + // Per-fragment depth clamping is handled by depthClampEnable (Vulkan) + // or GL_ARB_depth_clamp (OpenGL), matching PowerVR hardware behavior. + // Per-vertex clamping here would warp the triangle's depth plane equation. if (!translation_state.is_vulkan) { // convert [0,1] depth range (gxp, vulkan) to [-1,1] depth range (opengl) diff --git a/vita3k/shader/src/translator/alu.cpp b/vita3k/shader/src/translator/alu.cpp index ac4449ac..d2a0bf30 100644 --- a/vita3k/shader/src/translator/alu.cpp +++ b/vita3k/shader/src/translator/alu.cpp @@ -758,16 +758,16 @@ bool USSETranslatorVisitor::vcomp( } case Opcode::VLOG: { - // src0 = e^y => return y - result = m_b.createBuiltinCall(m_b.getTypeId(result), std_builtins, GLSLstd450Log, { result }); + // src0 = 2^y => return y (USSE complex unit is base-2) + result = m_b.createBuiltinCall(m_b.getTypeId(result), std_builtins, GLSLstd450Log2, { result }); break; } case Opcode::VEXP: { - // y = e^src0 => return y + // y = 2^src0 => return y (USSE complex unit is base-2) // hack (kind of) : // define exp(Nan) as 1.0, this is needed for Freedom Wars to render properly - const spv::Id exp_val = m_b.createBuiltinCall(m_b.getTypeId(result), std_builtins, GLSLstd450Exp, { result }); + const spv::Id exp_val = m_b.createBuiltinCall(m_b.getTypeId(result), std_builtins, GLSLstd450Exp2, { result }); const spv::Id ones = utils::make_uniform_vector_from_type(m_b, m_b.getTypeId(result), 1.0f); const spv::Id is_nan = m_b.createUnaryOp(spv::OpIsNan, m_b.makeBoolType(), result); result = m_b.createTriOp(spv::OpSelect, m_b.getTypeId(result), is_nan, ones, exp_val); @@ -1860,12 +1860,13 @@ bool USSETranslatorVisitor::vdual( const spv::Id second = load(ops[1], write_mask_source); const spv::Op op = (m_b.getNumComponents(first) > 1) ? spv::OpDot : spv::OpFMul; result = m_b.createBinOp(op, type_f32, first, second); + result = postprocess_dot_result_for_store(m_b, result, write_mask_dest); break; } case Opcode::FEXP: { // hack: set exp(nan) = 1.0 (see VEXP) const spv::Id source = load(ops[0], write_mask_source); - result = m_b.createBuiltinCall(m_b.getTypeId(source), std_builtins, GLSLstd450Exp, { source }); + result = m_b.createBuiltinCall(m_b.getTypeId(source), std_builtins, GLSLstd450Exp2, { source }); const int num_comp = m_b.getNumComponents(source); const spv::Id ones = utils::make_uniform_vector_from_type(m_b, m_b.getTypeId(result), 1.0f); const spv::Id is_nan = m_b.createUnaryOp(spv::OpIsNan, utils::make_vector_or_scalar_type(m_b, m_b.makeBoolType(), num_comp), result); @@ -1874,13 +1875,14 @@ bool USSETranslatorVisitor::vdual( } case Opcode::FLOG: { const spv::Id source = load(ops[0], write_mask_source); - result = m_b.createBuiltinCall(m_b.getTypeId(source), std_builtins, GLSLstd450Log, { source }); + result = m_b.createBuiltinCall(m_b.getTypeId(source), std_builtins, GLSLstd450Log2, { source }); break; } case Opcode::VSSQ: { const spv::Id source = load(ops[0], write_mask_source); const spv::Op op = (m_b.getNumComponents(source) > 1) ? spv::OpDot : spv::OpFMul; result = m_b.createBinOp(op, type_f32, source, source); + result = postprocess_dot_result_for_store(m_b, result, write_mask_dest); break; } case Opcode::FMAD: diff --git a/vita3k/shader/src/translator/data.cpp b/vita3k/shader/src/translator/data.cpp index 9c679a5f..cde1ca42 100644 --- a/vita3k/shader/src/translator/data.cpp +++ b/vita3k/shader/src/translator/data.cpp @@ -520,7 +520,59 @@ bool USSETranslatorVisitor::vpck( source = utils::convert_to_int(m_b, m_util_funcs, source, inst.opr.dest.type, scale); } - store(inst.opr.dest, source, dest_mask, dest_repeat_offset); + //store(inst.opr.dest, source, dest_mask, dest_repeat_offset); + + + // VPCK writes complete packed slots — zero-fill unmasked components within each + // 32-bit register slot to match real SGX543 behavior. Without this, old register + // contents leak into subsequent 32-bit operations (e.g. IMAD) that read the full slot. + Imm4 store_mask = dest_mask; + if (is_integer_data_type(inst.opr.dest.type) && get_data_type_size(inst.opr.dest.type) < 4) { + const int pack = (get_data_type_size(inst.opr.dest.type) == 1) ? 4 : 2; + Imm4 expanded = 0; + for (int s = 0; s < 4; s += pack) { + Imm4 slot_bits = ((1 << pack) - 1) << s; + if (dest_mask & slot_bits) + expanded |= slot_bits; + } + + if (expanded != dest_mask) { + spv::Id comp_type = m_b.isScalar(source) + ? m_b.getTypeId(source) + : m_b.getContainedTypeId(m_b.getTypeId(source)); + spv::Id zero_val = m_b.isUintType(comp_type) + ? m_b.makeUintConstant(0) + : m_b.makeIntConstant(0); + + std::vector comps; + int src_idx = 0; + for (int i = 0; i < 4; i++) { + if (!(expanded & (1 << i))) + continue; + if (dest_mask & (1 << i)) { + if (m_b.getNumComponents(source) == 1) + comps.push_back(source); + else + comps.push_back(m_b.createCompositeExtract(source, comp_type, src_idx)); + src_idx++; + } else { + comps.push_back(zero_val); + } + } + + if (comps.size() == 1) { + source = comps[0]; + } else { + source = m_b.createCompositeConstruct( + utils::make_vector_or_scalar_type(m_b, comp_type, static_cast(comps.size())), + comps); + } + store_mask = expanded; + } + } + + store(inst.opr.dest, source, store_mask, dest_repeat_offset); + END_REPEAT() reset_repeat_multiplier(); diff --git a/vita3k/shader/src/usse_disasm.cpp b/vita3k/shader/src/usse_disasm.cpp index 00c041e1..9f82cbd9 100644 --- a/vita3k/shader/src/usse_disasm.cpp +++ b/vita3k/shader/src/usse_disasm.cpp @@ -50,6 +50,7 @@ const char *e_predicate_str(ExtPredicate p) { case ExtPredicate::P3: return "p3 "; case ExtPredicate::NEGP0: return "!p0 "; case ExtPredicate::NEGP1: return "!p1 "; + case ExtPredicate::NEGP2: return "!p2 "; case ExtPredicate::PN: return "pN "; default: return "invalid"; } diff --git a/vita3k/shader/src/usse_program_analyzer.cpp b/vita3k/shader/src/usse_program_analyzer.cpp index 92835a2d..23559dd3 100644 --- a/vita3k/shader/src/usse_program_analyzer.cpp +++ b/vita3k/shader/src/usse_program_analyzer.cpp @@ -441,16 +441,23 @@ void analyze(USSEBlockNode &root, USSEOffset end_offset, const AnalyzeReadFuncti // Some ifs may be trying to jump to parent's merge point. It's also means there's no more content // further in this block. if (branch_from_result->second.pred == 0) { - // Execution changed direction, follow it + // Execution changed direction, follow it. Emit the code before the branch. current_code->size = baddr - current_code->offset; request.block_node->add_children(current_code_inst); - baddr = branch_from_result->second.dest - 1; + const std::uint32_t follow_dest = branch_from_result->second.dest; - std::unique_ptr current_code_inst = std::make_unique(request.block_node); - USSECodeNode *current_code = reinterpret_cast(current_code_inst.get()); + if (follow_dest > request.end_offset) { + current_code_inst = nullptr; + break; + } + + baddr = follow_dest - 1; + + current_code_inst = std::make_unique(request.block_node); + current_code = reinterpret_cast(current_code_inst.get()); - current_code->offset = baddr; + current_code->offset = follow_dest; } else { bool else_exist = false; auto branch_from_else_result = branches_from.find(branch_from_result->second.dest - 1); diff --git a/vita3k/shader/src/usse_translator_entry.cpp b/vita3k/shader/src/usse_translator_entry.cpp index e9f0e42f..b24d3440 100644 --- a/vita3k/shader/src/usse_translator_entry.cpp +++ b/vita3k/shader/src/usse_translator_entry.cpp @@ -899,7 +899,7 @@ spv::Id USSERecompiler::get_condition_value(const std::uint8_t pred, const bool if (predicator >= ExtPredicate::P0 && predicator <= ExtPredicate::P3) { pred_opr.num = static_cast(predicator) - static_cast(ExtPredicate::P0); - } else if (predicator >= ExtPredicate::NEGP0 && predicator <= ExtPredicate::NEGP1) { + } else if (predicator >= ExtPredicate::NEGP0 && predicator <= ExtPredicate::NEGP2) { pred_opr.num = static_cast(predicator) - static_cast(ExtPredicate::NEGP0); do_neg = !do_neg; } diff --git a/vita3k/shader/src/usse_utilities.cpp b/vita3k/shader/src/usse_utilities.cpp index 8ae6b391..a3e2c587 100644 --- a/vita3k/shader/src/usse_utilities.cpp +++ b/vita3k/shader/src/usse_utilities.cpp @@ -653,22 +653,71 @@ void buffer_address_access(spv::Builder &b, const SpirvShaderParameters ¶ms, const spv::Id i32 = b.makeIntType(32); const spv::Id zero = b.makeIntConstant(0); + spv::Id buffer_address; spv::Id buffer_idx_val; + spv::Id addr_oob = spv::NoResult; if (buffer_idx == -1) { - // buffer index is in the upper 4 bits of addr - buffer_idx_val = b.createBinOp(spv::OpShiftRightLogical, i32, addr, b.makeIntConstant(28)); - // remove the buffer index bits from the address - addr = b.createBinOp(spv::OpBitwiseAnd, i32, addr, b.makeIntConstant((1 << 28) - 1)); + const spv::Id biased = b.createBinOp(spv::OpIAdd, i32, addr, b.makeIntConstant(1 << 27)); + buffer_idx_val = b.createBinOp(spv::OpShiftRightLogical, i32, biased, b.makeIntConstant(28)); + + if (params.buffer_count > 0) { + const spv::Id max_idx = b.makeIntConstant(params.buffer_count - 1); + spv::Id too_big = b.createBinOp(spv::OpSGreaterThan, b.makeBoolType(), buffer_idx_val, max_idx); + spv::Id negative = b.createBinOp(spv::OpSLessThan, b.makeBoolType(), buffer_idx_val, zero); + addr_oob = b.createBinOp(spv::OpLogicalOr, b.makeBoolType(), too_big, negative); + buffer_idx_val = b.createTriOp(spv::OpSelect, i32, too_big, max_idx, buffer_idx_val); + buffer_idx_val = b.createTriOp(spv::OpSelect, i32, negative, zero, buffer_idx_val); + } + + // signed offset = addr - (index << 28) (may be negative) + const spv::Id idx_hi = b.createBinOp(spv::OpShiftLeftLogical, i32, buffer_idx_val, b.makeIntConstant(28)); + addr = b.createBinOp(spv::OpISub, i32, addr, idx_hi); + + // crash safety: treat out-of-range decoded buffer offsets as 0 instead of allowing large invalid GPU addresses that would fault + constexpr int safe_offset_max = 0x1000000; // 16 MiB, far larger than any real uniform buffer + spv::Id off_hi = b.createBinOp(spv::OpSGreaterThan, b.makeBoolType(), addr, b.makeIntConstant(safe_offset_max)); + spv::Id off_lo = b.createBinOp(spv::OpSLessThan, b.makeBoolType(), addr, b.makeIntConstant(-safe_offset_max)); + spv::Id off_bad = b.createBinOp(spv::OpLogicalOr, b.makeBoolType(), off_hi, off_lo); + if (addr_oob != spv::NoResult) + addr_oob = b.createBinOp(spv::OpLogicalOr, b.makeBoolType(), addr_oob, off_bad); + else + addr_oob = off_bad; + addr = b.createTriOp(spv::OpSelect, i32, off_bad, zero, addr); } else { buffer_idx_val = b.makeIntConstant(buffer_idx); } - spv::Id buffer_address = utils::create_access_chain(b, spv::StorageClassUniform, params.render_info_id, { b.makeIntConstant(params.buffer_addresses_id), buffer_idx_val }); + buffer_address = utils::create_access_chain(b, spv::StorageClassUniform, params.render_info_id, { b.makeIntConstant(params.buffer_addresses_id), buffer_idx_val }); buffer_address = b.createLoad(buffer_address, spv::NoPrecision); + + if (buffer_idx == -1 && !is_buffer_store) { + const spv::Id u32 = b.makeUintType(32); + const spv::Id addr_lo = b.createCompositeExtract(buffer_address, u32, 0); + const spv::Id addr_hi = b.createCompositeExtract(buffer_address, u32, 1); + const spv::Id both = b.createBinOp(spv::OpBitwiseOr, u32, addr_lo, addr_hi); + const spv::Id is_null = b.createBinOp(spv::OpIEqual, b.makeBoolType(), both, b.makeUintConstant(0)); + if (addr_oob != spv::NoResult) + addr_oob = b.createBinOp(spv::OpLogicalOr, b.makeBoolType(), addr_oob, is_null); + else + addr_oob = is_null; + + // substitute buffer_addresses[0] when null to prevent GPU fault on the read + spv::Id safe_address = utils::create_access_chain(b, spv::StorageClassUniform, params.render_info_id, { b.makeIntConstant(params.buffer_addresses_id), zero }); + safe_address = b.createLoad(safe_address, spv::NoPrecision); + const spv::Id uvec2_type = b.getTypeId(buffer_address); + const spv::Id bvec2_type = b.makeVectorType(b.makeBoolType(), 2); + const spv::Id null_splat = b.createCompositeConstruct(bvec2_type, {is_null, is_null}); + buffer_address = b.createTriOp(spv::OpSelect, uvec2_type, null_splat, safe_address, buffer_address); + } + // add the offset from the base address - buffer_address = add_uvec2_uint(b, buffer_address, addr); + buffer_address = add_uvec2_int(b, buffer_address, addr); if (component_size == sizeof(uint32_t)) { + spv::Id aligned_low_bits = b.createCompositeExtract(buffer_address, i32, 0); + aligned_low_bits = b.createBinOp(spv::OpBitwiseAnd, i32, aligned_low_bits, b.makeIntConstant(~0b11)); + buffer_address = b.createCompositeInsert(aligned_low_bits, buffer_address, b.getTypeId(buffer_address), 0); + int buffer_idx_vec4 = 0; if (nb_components >= 4) { // first copy them 4 by 4 (using the fact that we can do 4-byte aligned reads) @@ -682,6 +731,15 @@ void buffer_address_access(spv::Builder &b, const SpirvShaderParameters ¶ms, b.createStore(data, accessed, spv::MemoryAccessAlignedMask, spv::ScopeMax, 4); } else { accessed = b.createLoad(accessed, spv::NoPrecision, spv::MemoryAccessAlignedMask, spv::ScopeMax, 4); + if (addr_oob != spv::NoResult) { + const spv::Id f32_type = b.makeFloatType(32); + const spv::Id f32_zero = b.makeFloatConstant(0.0f); + const spv::Id vec4_type = b.makeVectorType(f32_type, 4); + const spv::Id zero_vec4 = b.makeCompositeConstant(vec4_type, {f32_zero, f32_zero, f32_zero, f32_zero}); + const spv::Id bvec4_type = b.makeVectorType(b.makeBoolType(), 4); + const spv::Id oob_splat = b.createCompositeConstruct(bvec4_type, {addr_oob, addr_oob, addr_oob, addr_oob}); + accessed = b.createTriOp(spv::OpSelect, vec4_type, oob_splat, zero_vec4, accessed); + } store(b, params, utils, features, dest, accessed, 0b1111, dest_offset); } @@ -704,6 +762,21 @@ void buffer_address_access(spv::Builder &b, const SpirvShaderParameters ¶ms, b.createStore(data, accessed, spv::MemoryAccessAlignedMask, spv::ScopeMax, 4); } else { accessed = b.createLoad(accessed, spv::NoPrecision, spv::MemoryAccessAlignedMask, spv::ScopeMax, 4); + if (addr_oob != spv::NoResult) { + const spv::Id f32_type = b.makeFloatType(32); + const spv::Id f32_zero = b.makeFloatConstant(0.0f); + if (nb_components == 1) { + accessed = b.createTriOp(spv::OpSelect, f32_type, addr_oob, f32_zero, accessed); + } else { + const spv::Id vec_type = b.makeVectorType(f32_type, nb_components); + std::vector zeros(nb_components, f32_zero); + const spv::Id zero_vec = b.makeCompositeConstant(vec_type, zeros); + const spv::Id bvec_type = b.makeVectorType(b.makeBoolType(), nb_components); + std::vector oob_comps(nb_components, addr_oob); + const spv::Id oob_splat = b.createCompositeConstruct(bvec_type, oob_comps); + accessed = b.createTriOp(spv::OpSelect, vec_type, oob_splat, zero_vec, accessed); + } + } store(b, params, utils, features, dest, accessed, (1 << nb_components) - 1, dest_offset); } } @@ -737,6 +810,9 @@ void buffer_address_access(spv::Builder &b, const SpirvShaderParameters ¶ms, spv::Id shift = b.createBinOp(spv::OpShiftLeftLogical, i32, alignment, b.makeIntConstant(3)); // 1 byte = 8 bits loaded = b.createOp(spv::OpBitFieldSExtract, i32, { loaded, shift, b.makeIntConstant(component_size * 8) }); + if (addr_oob != spv::NoResult) + loaded = b.createTriOp(spv::OpSelect, i32, addr_oob, zero, loaded); + loaded_components.push_back(loaded); if (loaded_components.size() == 4 || component_idx == nb_components - 1) { @@ -1634,4 +1710,25 @@ spv::Id add_uvec2_uint(spv::Builder &b, spv::Id vec, spv::Id to_add) { return b.createCompositeConstruct(uvec2, { lower, upper }); } +spv::Id add_uvec2_int(spv::Builder &b, spv::Id vec, spv::Id to_add_signed) { + const spv::Id u32 = b.makeUintType(32); + const spv::Id i32 = b.makeIntType(32); + const spv::Id uvec2 = b.makeVectorType(u32, 2); + const spv::Id add_result_type = b.makeStructResultType(u32, u32); + + spv::Id to_add_i = b.isUintType(b.getTypeId(to_add_signed)) ? b.createUnaryOp(spv::OpBitcast, i32, to_add_signed) : to_add_signed; + spv::Id sign = b.createBinOp(spv::OpShiftRightArithmetic, i32, to_add_i, b.makeIntConstant(31)); + sign = b.createUnaryOp(spv::OpBitcast, u32, sign); + spv::Id to_add = b.createUnaryOp(spv::OpBitcast, u32, to_add_i); + + spv::Id lower = b.createCompositeExtract(vec, u32, 0); + spv::Id lower_add = b.createBinOp(spv::OpIAddCarry, add_result_type, lower, to_add); + spv::Id carry = b.createCompositeExtract(lower_add, u32, 1); + spv::Id upper = b.createCompositeExtract(vec, u32, 1); + upper = b.createBinOp(spv::OpIAdd, u32, upper, carry); + upper = b.createBinOp(spv::OpIAdd, u32, upper, sign); // sign extension of the offset + lower = b.createCompositeExtract(lower_add, u32, 0); + return b.createCompositeConstruct(uvec2, { lower, upper }); +} + } // namespace shader::usse::utils diff --git a/vita3k/vkutil/include/vkutil/objects.h b/vita3k/vkutil/include/vkutil/objects.h index 81ce13b5..8e1d63b4 100644 --- a/vita3k/vkutil/include/vkutil/objects.h +++ b/vita3k/vkutil/include/vkutil/objects.h @@ -34,6 +34,9 @@ struct Image { uint32_t width; uint32_t height; + // number of mip levels backing this image; the view covers all of them and + // the default-range transitions are widened to match + uint32_t mip_count = 1; vk::Format format; ImageLayout layout = ImageLayout::Undefined; @@ -99,6 +102,7 @@ protected: uint32_t capacity; public: + uint32_t wrap_count = 0; // any buffer alignment on vulkan is at most 256 on 99% of instances uint32_t alignment = 256; uint32_t data_offset = 0; diff --git a/vita3k/vkutil/src/objects.cpp b/vita3k/vkutil/src/objects.cpp index 57761caa..37d995f4 100644 --- a/vita3k/vkutil/src/objects.cpp +++ b/vita3k/vkutil/src/objects.cpp @@ -91,7 +91,7 @@ void Image::init_image(vk::ImageUsageFlags usage, vk::ComponentMapping mapping, .width = width, .height = height, .depth = 1 }, - .mipLevels = 1, + .mipLevels = mip_count, .arrayLayers = 1, .samples = vk::SampleCountFlagBits::e1, .tiling = vk::ImageTiling::eOptimal, @@ -115,6 +115,7 @@ void Image::init_image(vk::ImageUsageFlags usage, vk::ComponentMapping mapping, } else { range = vkutil::color_subresource_range; } + range.levelCount = mip_count; vk::ImageViewCreateInfo view_info{ .image = image, @@ -127,12 +128,18 @@ void Image::init_image(vk::ImageUsageFlags usage, vk::ComponentMapping mapping, } void Image::transition_to(vk::CommandBuffer buffer, ImageLayout new_layout, const vk::ImageSubresourceRange &range) { - transition_image_layout(buffer, image, layout, new_layout, range); + vk::ImageSubresourceRange effective = range; + if (mip_count > 1 && effective.levelCount == 1) + effective.levelCount = mip_count; + transition_image_layout(buffer, image, layout, new_layout, effective); layout = new_layout; } void Image::transition_to_discard(vk::CommandBuffer buffer, ImageLayout new_layout, const vk::ImageSubresourceRange &range) { - transition_image_layout_discard(buffer, image, layout, new_layout, range); + vk::ImageSubresourceRange effective = range; + if (mip_count > 1 && effective.levelCount == 1) + effective.levelCount = mip_count; + transition_image_layout_discard(buffer, image, layout, new_layout, effective); layout = new_layout; } @@ -202,8 +209,10 @@ RingBuffer::RingBuffer(vk::BufferUsageFlags usage, const size_t capacity) } void RingBuffer::allocate(const uint32_t data_size) { - if (cursor + data_size > capacity) + if (cursor + data_size > capacity) { cursor = 0; + wrap_count++; + } data_offset = cursor;