Files
MihawkandClaude 48607e12da PPSSPP: a resolution change from the Quick Menu no longer restarts video and crashes
PPSSPP reported its current internal resolution as its maximum geometry, so
any increase made RetroArch restart the video driver (runloop.c compares
max_width/max_height). RetroArch keeps the cached Vulkan device across that
restart and calls neither context_destroy nor context_reset, so PPSSPP's render
thread kept the freed vk_t's interface and called a null function (issue 63:
signal 11, rip=0, [rsp] inside the core). The maximum is now always 10x
(4800x2720, the largest option and the PS5 default profile), so a change is a
geometry update; with hardware rendering RetroArch creates no textures from
the maximum, and default users already had it.

Evidence (python3 tools/evidence.py compare evidence/: 146 replayed, 0 failed):
- evidence/ppsspp-resolution-crash-before: build a2c7800a, tests/pad/
  ppsspp-resolution-change.txt (2880x1632 to 3840x2176 at 30 s): the second
  video_vulkan init, then the reported crash, [rsp]=0x000000040084aabb.
- evidence/ppsspp-resolution-change: build 191ff52c, the same script:
  SET_SYSTEM_AV_INFO 3840x2160 and NotifyDisplayResized 3840x2160 with one
  video_vulkan init; the 45 s screenshot and the 55 s STOP, no crash.
Gates: tools/verify.sh, all five (format, unit 215, build, integration 215,
evidence 141), before the evidence was added.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-18 22:00:16 -04:00

1241 lines
51 KiB
Diff

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 910f32214..8e18a1387 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -308,7 +308,8 @@ elseif(NOT OPENGL_LIBRARIES AND USING_GLES2)
set(OPENGL_LIBRARIES GLESv2 EGL)
endif()
-if(NOT OPENGL_LIBRARIES)
+# PS5: no GL library exists and none is used; the port is Vulkan only.
+if(NOT OPENGL_LIBRARIES AND NOT PPSSPP_PS5)
if(POLICY CMP0072)
cmake_policy(SET CMP0072 NEW)
endif()
@@ -974,6 +975,13 @@ endif()
target_compile_definitions(Common PRIVATE Z7_CRC_NUM_TABLES=1)
+# PS5: the GL backend is not built. Its gl*() calls would become imports nothing on
+# the console resolves, and the native core loader refuses a core with one.
+if(PPSSPP_PS5)
+ file(GLOB PS5_GL_SOURCES ${CMAKE_SOURCE_DIR}/Common/GPU/OpenGL/*.cpp ${CMAKE_SOURCE_DIR}/Common/GPU/OpenGL/*.c)
+ set_source_files_properties(${PS5_GL_SOURCES} TARGET_DIRECTORY Common PROPERTIES HEADER_FILE_ONLY TRUE)
+endif()
+
if(TARGET Ext::GLEW)
target_link_libraries(Common Ext::GLEW)
endif()
@@ -1743,7 +1751,8 @@ if(ANDROID)
target_link_libraries(native log EGL OpenSLES)
elseif(WIN32)
target_link_libraries(native ws2_32 winmm)
-elseif(${CMAKE_SYSTEM_NAME} MATCHES "^(DragonFly|FreeBSD|NetBSD)$")
+elseif(${CMAKE_SYSTEM_NAME} MATCHES "^(DragonFly|FreeBSD|NetBSD)$" AND NOT PPSSPP_PS5)
+ # PS5 has no libexecinfo, and nothing the core builds calls backtrace().
target_link_libraries(native execinfo)
endif()
@@ -1985,7 +1994,10 @@ set(GPU_D3D11
)
# We build Vulkan even on Apple to avoid annoying build differences.
-set(GPU_IMPLS ${GPU_GLES} ${GPU_VULKAN})
+set(GPU_IMPLS ${GPU_VULKAN})
+if(NOT PPSSPP_PS5)
+ list(APPEND GPU_IMPLS ${GPU_GLES})
+endif()
if(WIN32)
list(APPEND GPU_IMPLS ${GPU_D3D11})
endif()
diff --git a/Common/CPUDetect.cpp b/Common/CPUDetect.cpp
index f45ef3ce5..03c07091b 100644
--- a/Common/CPUDetect.cpp
+++ b/Common/CPUDetect.cpp
@@ -30,6 +30,8 @@
#include <fcntl.h>
#elif PPSSPP_PLATFORM(MAC)
#include <sys/sysctl.h>
+#elif PPSSPP_PLATFORM(PS5)
+#include <unistd.h>
#endif
#include <cstdint>
@@ -465,6 +467,28 @@ void CPUInfo::Detect() {
logical_cpu_count = num / std::max(num_cores, 1);
}
}
+#elif PPSSPP_PLATFORM(PS5)
+ // The payload SDK answers the online hardware-thread count through sysconf; there
+ // is no /proc and no sysctl name for it. The two fields are NOT two totals:
+ // ThreadManager::Init multiplies them (numRealCores * numLogicalCoresPerCpu), so
+ // they must be physical cores and threads per core. Setting both from sysconf
+ // (16 and 16 on this console) asked for 256 compute threads and 512 in total,
+ // which is what the first successful console load reported. Take the physical
+ // core count from CPUID leaf 0x80000008 like the AMD branch above, and derive
+ // SMT siblings from the online count.
+ {
+ long online = sysconf(_SC_NPROCESSORS_ONLN);
+ if (online > 0) {
+ num_cores = (int)online;
+ if (max_ex_fn >= 0x80000008) {
+ do_cpuid(cpu_id, 0x80000008);
+ const int physical = (cpu_id[2] & 0xFF) + 1;
+ if (physical > 0 && physical <= online)
+ num_cores = physical;
+ }
+ logical_cpu_count = (int)(online / std::max(num_cores, 1));
+ }
+ }
#endif
if (logical_cpu_count <= 0)
logical_cpu_count = 1;
diff --git a/Common/GPU/Vulkan/VulkanContext.cpp b/Common/GPU/Vulkan/VulkanContext.cpp
index 9b384731e..4c761c842 100644
--- a/Common/GPU/Vulkan/VulkanContext.cpp
+++ b/Common/GPU/Vulkan/VulkanContext.cpp
@@ -566,7 +566,10 @@ VkResult VulkanContext::CreateDevice(int physical_device) {
physical_device_ = physical_device;
INFO_LOG(Log::G3D, "Chose physical device %d: %s", physical_device, physicalDeviceProperties_[physical_device].properties.deviceName);
- vulkanDeviceApiVersion_ = physicalDeviceProperties_[physical_device].properties.apiVersion;
+ // The device offers no more of Vulkan than the instance was created for: a core command above
+ // that version is not there (vkGetDeviceProcAddr returns null for it), so what it promoted must
+ // still be enabled as an extension.
+ vulkanDeviceApiVersion_ = std::min(physicalDeviceProperties_[physical_device].properties.apiVersion, vulkanInstanceApiVersion_);
queue_count = 0;
vkGetPhysicalDeviceQueueFamilyProperties(physical_devices_[physical_device_], &queue_count, nullptr);
diff --git a/Common/GPU/Vulkan/VulkanContext.h b/Common/GPU/Vulkan/VulkanContext.h
index 617d231de..36269c3b7 100644
--- a/Common/GPU/Vulkan/VulkanContext.h
+++ b/Common/GPU/Vulkan/VulkanContext.h
@@ -1,5 +1,7 @@
#pragma once
+#include <algorithm>
+#include <atomic>
#include <cstdint>
#include <cstring>
#include <string>
@@ -227,6 +229,8 @@ class VulkanContext {
void DestroyDevice();
void PerformPendingDeletes();
+ VkResult FramebufferFailure() const { return (VkResult)framebufferFailure_.load(); }
+ void SetFramebufferFailure(VkResult result) { framebufferFailure_.store(result); }
void WaitUntilQueueIdle();
// Utility functions for shorter code
@@ -429,6 +433,10 @@ class VulkanContext {
return frame_[curFrame_].deleteList.GetLastDeleteCount();
}
+ // A frontend that creates the instance itself (libretro) created it for this API version.
+ void LimitInstanceApiVersion(u32 apiVersion) {
+ vulkanInstanceApiVersion_ = std::min(vulkanInstanceApiVersion_, apiVersion);
+ }
u32 InstanceApiVersion() const {
return vulkanInstanceApiVersion_;
}
@@ -515,6 +523,7 @@ class VulkanContext {
// At the end of the frame, this is copied into the frame's delete list, so it can be processed
// the next time the frame comes around again.
VulkanDeleteList globalDeleteList_;
+ std::atomic<int> framebufferFailure_{VK_SUCCESS};
std::vector<VkDebugUtilsMessengerEXT> utils_callbacks;
diff --git a/Common/GPU/Vulkan/VulkanFramebuffer.cpp b/Common/GPU/Vulkan/VulkanFramebuffer.cpp
index f36b1eb4c..54c070ed9 100644
--- a/Common/GPU/Vulkan/VulkanFramebuffer.cpp
+++ b/Common/GPU/Vulkan/VulkanFramebuffer.cpp
@@ -61,23 +61,39 @@ VKRFramebuffer::VKRFramebuffer(VulkanContext *vk, VulkanBarrierBatch *barriers,
_dbg_assert_(tag);
- CreateImage(vulkan_, barriers, color, width, height, numLayers, VK_SAMPLE_COUNT_1_BIT, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, true, tag);
+ if ((result_ = CreateImage(vulkan_, color, width, height, numLayers, VK_SAMPLE_COUNT_1_BIT, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, true, tag)) != VK_SUCCESS)
+ return;
if (createDepthStencilBuffer) {
- CreateImage(vulkan_, barriers, depth, width, height, numLayers, VK_SAMPLE_COUNT_1_BIT, vulkan_->GetDeviceInfo().preferredDepthStencilFormat, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, false, tag);
+ if ((result_ = CreateImage(vulkan_, depth, width, height, numLayers, VK_SAMPLE_COUNT_1_BIT, vulkan_->GetDeviceInfo().preferredDepthStencilFormat, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, false, tag)) != VK_SUCCESS)
+ return;
}
if (_multiSampleLevel > 0) {
sampleCount = MultiSampleLevelToFlagBits(_multiSampleLevel);
// TODO: Create a different tag for these?
- CreateImage(vulkan_, barriers, msaaColor, width, height, numLayers, sampleCount, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, true, tag);
+ if ((result_ = CreateImage(vulkan_, msaaColor, width, height, numLayers, sampleCount, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, true, tag)) != VK_SUCCESS)
+ return;
if (createDepthStencilBuffer) {
- CreateImage(vulkan_, barriers, msaaDepth, width, height, numLayers, sampleCount, vulkan_->GetDeviceInfo().preferredDepthStencilFormat, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, false, tag);
+ if ((result_ = CreateImage(vulkan_, msaaDepth, width, height, numLayers, sampleCount, vulkan_->GetDeviceInfo().preferredDepthStencilFormat, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, false, tag)) != VK_SUCCESS)
+ return;
}
} else {
sampleCount = VK_SAMPLE_COUNT_1_BIT;
}
+ // All images/views exist. Only now may command buffers refer to them.
+ for (VKRImage *img : {&color, &depth, &msaaColor, &msaaDepth}) {
+ if (!img->image) continue;
+ const bool isColor = img == &color || img == &msaaColor;
+ barriers->TransitionImage(img->image, 0, 1, numLayers,
+ isColor ? VK_IMAGE_ASPECT_COLOR_BIT : VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT,
+ VK_IMAGE_LAYOUT_UNDEFINED, img->layout, 0,
+ isColor ? VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT : VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
+ VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, isColor ? VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT :
+ VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT);
+ }
+ result_ = VK_SUCCESS;
UpdateTag(tag);
// We create the actual framebuffer objects on demand, because some combinations might not make sense.
@@ -148,6 +164,15 @@ VkFramebuffer VKRFramebuffer::Get(VKRRenderPass *compatibleRenderPass, RenderPas
}
VKRFramebuffer::~VKRFramebuffer() {
+ if (!Valid()) {
+ // No commands have ever referenced an incomplete framebuffer.
+ for (VKRImage *img : {&color, &depth, &msaaColor, &msaaDepth}) {
+ for (VkImageView view : {img->rtView, img->texAllLayersView, img->texLayerViews[0], img->texLayerViews[1]})
+ if (view) vkDestroyImageView(vulkan_->GetDevice(), view, nullptr);
+ if (img->image) vmaDestroyImage(vulkan_->Allocator(), img->image, img->alloc);
+ }
+ return;
+ }
color.Delete(vulkan_);
depth.Delete(vulkan_);
msaaColor.Delete(vulkan_);
@@ -162,7 +187,7 @@ VKRFramebuffer::~VKRFramebuffer() {
// NOTE: If numLayers > 1, it will create an array texture, rather than a normal 2D texture.
// This requires a different sampling path!
-void VKRFramebuffer::CreateImage(VulkanContext *vulkan, VulkanBarrierBatch *barriers, VKRImage &img, int width, int height, int numLayers, VkSampleCountFlagBits sampleCount, VkFormat format, VkImageLayout initialLayout, bool color, const char *tag) {
+VkResult VKRFramebuffer::CreateImage(VulkanContext *vulkan, VKRImage &img, int width, int height, int numLayers, VkSampleCountFlagBits sampleCount, VkFormat format, VkImageLayout initialLayout, bool color, const char *tag) {
// We don't support more exotic layer setups for now. Mono or stereo.
_dbg_assert_(numLayers == 1 || numLayers == 2);
@@ -192,7 +217,11 @@ void VKRFramebuffer::CreateImage(VulkanContext *vulkan, VulkanBarrierBatch *barr
VmaAllocationInfo allocInfo{};
VkResult res = vmaCreateImage(vulkan->Allocator(), &ici, &allocCreateInfo, &img.image, &img.alloc, &allocInfo);
- _dbg_assert_(res == VK_SUCCESS);
+ if (res != VK_SUCCESS) {
+ img.image = VK_NULL_HANDLE;
+ img.alloc = VK_NULL_HANDLE;
+ return res;
+ }
vulkan->SetDebugName(img.image, VK_OBJECT_TYPE_IMAGE, tag);
@@ -207,8 +236,11 @@ void VKRFramebuffer::CreateImage(VulkanContext *vulkan, VulkanBarrierBatch *barr
ivci.subresourceRange.layerCount = numLayers;
ivci.subresourceRange.levelCount = 1;
res = vkCreateImageView(vulkan->GetDevice(), &ivci, nullptr, &img.rtView);
+ if (res != VK_SUCCESS) {
+ img.rtView = VK_NULL_HANDLE;
+ return res;
+ }
vulkan->SetDebugName(img.rtView, VK_OBJECT_TYPE_IMAGE_VIEW, tag);
- _dbg_assert_(res == VK_SUCCESS);
// Separate view for texture sampling all layers together.
if (!color) {
@@ -217,7 +249,10 @@ void VKRFramebuffer::CreateImage(VulkanContext *vulkan, VulkanBarrierBatch *barr
ivci.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY; // layered for consistency, even if single image.
res = vkCreateImageView(vulkan->GetDevice(), &ivci, nullptr, &img.texAllLayersView);
- _dbg_assert_(res == VK_SUCCESS);
+ if (res != VK_SUCCESS) {
+ img.texAllLayersView = VK_NULL_HANDLE;
+ return res;
+ }
vulkan->SetDebugName(img.texAllLayersView, VK_OBJECT_TYPE_IMAGE_VIEW, tag);
// Create 2D views for both layers.
@@ -227,47 +262,24 @@ void VKRFramebuffer::CreateImage(VulkanContext *vulkan, VulkanBarrierBatch *barr
ivci.subresourceRange.layerCount = 1;
ivci.subresourceRange.baseArrayLayer = i;
res = vkCreateImageView(vulkan->GetDevice(), &ivci, nullptr, &img.texLayerViews[i]);
+ if (res != VK_SUCCESS) {
+ img.texLayerViews[i] = VK_NULL_HANDLE;
+ return res;
+ }
if (vulkan->DebugLayerEnabled()) {
char temp[128];
snprintf(temp, sizeof(temp), "%s_layer%d", tag, i);
vulkan->SetDebugName(img.texLayerViews[i], VK_OBJECT_TYPE_IMAGE_VIEW, temp);
}
- _dbg_assert_(res == VK_SUCCESS);
- }
-
- VkPipelineStageFlags dstStage;
- VkAccessFlagBits dstAccessMask;
- switch (initialLayout) {
- case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:
- dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
- dstStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
- break;
- case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:
- dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
- dstStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
- break;
- case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL:
- dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
- dstStage = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
- break;
- default:
- Crash();
- return;
}
- VkImageMemoryBarrier *barrier = barriers->Add(img.image, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, dstStage, 0);
- barrier->subresourceRange.layerCount = numLayers;
- barrier->subresourceRange.aspectMask = aspects;
- barrier->oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
- barrier->newLayout = initialLayout;
- barrier->srcAccessMask = 0;
- barrier->dstAccessMask = dstAccessMask;
img.layout = initialLayout;
img.format = format;
img.sampleCount = sampleCount;
img.tag = tag ? tag : "N/A";
img.numLayers = numLayers;
+ return VK_SUCCESS;
}
static VkAttachmentLoadOp ConvertLoadAction(VKRRenderPassLoadAction action) {
diff --git a/Common/GPU/Vulkan/VulkanFramebuffer.h b/Common/GPU/Vulkan/VulkanFramebuffer.h
index a21cfbc37..db6c3d8e6 100644
--- a/Common/GPU/Vulkan/VulkanFramebuffer.h
+++ b/Common/GPU/Vulkan/VulkanFramebuffer.h
@@ -80,6 +80,8 @@ class VKRFramebuffer {
}
void UpdateTag(const char *newTag);
+ bool Valid() const { return result_ == VK_SUCCESS; }
+ VkResult Result() const { return result_; }
bool HasDepth() const {
return depth.image != VK_NULL_HANDLE;
@@ -95,7 +97,8 @@ class VKRFramebuffer {
VulkanContext *Vulkan() const { return vulkan_; }
private:
- static void CreateImage(VulkanContext *vulkan, VulkanBarrierBatch *barriers, VKRImage &img, int width, int height, int numLayers, VkSampleCountFlagBits sampleCount, VkFormat format, VkImageLayout initialLayout, bool color, const char *tag);
+ VkResult result_ = VK_NOT_READY;
+ static VkResult CreateImage(VulkanContext *vulkan, VKRImage &img, int width, int height, int numLayers, VkSampleCountFlagBits sampleCount, VkFormat format, VkImageLayout initialLayout, bool color, const char *tag);
VkFramebuffer framebuf[(size_t)RenderPassType::TYPE_COUNT]{};
diff --git a/Common/GPU/Vulkan/VulkanRenderManager.cpp b/Common/GPU/Vulkan/VulkanRenderManager.cpp
index 10abc5cbe..9ec843512 100644
--- a/Common/GPU/Vulkan/VulkanRenderManager.cpp
+++ b/Common/GPU/Vulkan/VulkanRenderManager.cpp
@@ -1732,6 +1732,16 @@ void VulkanRenderManager::FlushSync() {
}
}
+bool VulkanRenderManager::CollectGarbage() {
+ if (!insideFrame_) return false;
+ EndCurRenderStep();
+ FlushSync(); // Wait for the recording worker and GPU, not only the Vulkan queue.
+ vulkan_->PerformPendingDeletes();
+ // Deleting a queued framebuffer can queue its images/views in turn.
+ vulkan_->PerformPendingDeletes();
+ return true;
+}
+
void VulkanRenderManager::ResetStats() {
initTimeMs_.Reset();
totalGPUTimeMs_.Reset();
diff --git a/Common/GPU/Vulkan/VulkanRenderManager.h b/Common/GPU/Vulkan/VulkanRenderManager.h
index d4885a8e4..985e22e72 100644
--- a/Common/GPU/Vulkan/VulkanRenderManager.h
+++ b/Common/GPU/Vulkan/VulkanRenderManager.h
@@ -539,6 +539,7 @@ class VulkanRenderManager {
}
void ResetStats();
+ bool CollectGarbage();
void StartThreads();
void StopThreads();
diff --git a/Common/GPU/Vulkan/thin3d_vulkan.cpp b/Common/GPU/Vulkan/thin3d_vulkan.cpp
index d0f2fd37f..57c72a0c9 100644
--- a/Common/GPU/Vulkan/thin3d_vulkan.cpp
+++ b/Common/GPU/Vulkan/thin3d_vulkan.cpp
@@ -28,6 +28,7 @@
#include "Common/GPU/thin3d.h"
#include "Common/GPU/Vulkan/VulkanRenderManager.h"
#include "Common/GPU/Vulkan/VulkanContext.h"
+#include "Common/PS5Memory.h"
#include "Common/GPU/Vulkan/VulkanImage.h"
#include "Common/GPU/Vulkan/VulkanMemory.h"
#include "Common/GPU/Vulkan/VulkanLoader.h"
@@ -44,6 +45,10 @@
using namespace PPSSPP_VK;
+#if PPSSPP_PLATFORM(PS5) && defined(__LIBRETRO__)
+bool PS5TestFlag(const char *name); // libretro/libretro.cpp
+#endif
+
namespace Draw {
// This can actually be replaced with a cast as the values are in the right order.
@@ -940,6 +945,13 @@ VKContext::VKContext(VulkanContext *vulkan, bool useRenderThread)
caps_.preferredDepthBufferFormat = DataFormatFromVulkanDepth(vulkan->GetDeviceInfo().preferredDepthStencilFormat);
caps_.texture3DSupported = true;
caps_.textureDepthSupported = true;
+#if PPSSPP_PLATFORM(PS5) && defined(__LIBRETRO__)
+ {
+ // PS5 A/B test flag (libretro/libretro.cpp): no sampled depth.
+ if (::PS5TestFlag("ps5_no_depth_texture"))
+ caps_.textureDepthSupported = false;
+ }
+#endif
caps_.fragmentShaderInt32Supported = true;
caps_.textureNPOTFullySupported = true;
caps_.fragmentShaderDepthWriteSupported = true;
@@ -1792,11 +1804,28 @@ Framebuffer *VKContext::CreateFramebuffer(const FramebufferDesc &desc) {
_assert_(desc.width > 0);
_assert_(desc.height > 0);
- VKRFramebuffer *vkrfb = new VKRFramebuffer(vulkan_, &renderManager_.PostInitBarrier(), desc.width, desc.height, desc.numLayers, desc.multiSampleLevel, desc.z_stencil, desc.tag);
- return new VKFramebuffer(vkrfb, desc.multiSampleLevel);
+ if (vulkan_->FramebufferFailure() != VK_SUCCESS) return nullptr;
+ // Estimate attachment storage, including the single-sample resolve images.
+ const size_t bytes = size_t(desc.width) * desc.height * desc.numLayers * 4 *
+ (desc.z_stencil ? 2 : 1) * (desc.multiSampleLevel ? 1 + (1u << desc.multiSampleLevel) : 1);
+ if (ps5_memory_pressure(bytes) && renderManager_.CollectGarbage())
+ ps5_memory_report("ppsspp-pressure-reclaim", bytes, 0);
+ for (int attempt = 0; attempt < 2; ++attempt) {
+ VKRFramebuffer *vkrfb = new VKRFramebuffer(vulkan_, &renderManager_.PostInitBarrier(), desc.width, desc.height, desc.numLayers, desc.multiSampleLevel, desc.z_stencil, desc.tag);
+ if (vkrfb->Valid()) return new VKFramebuffer(vkrfb, desc.multiSampleLevel);
+ const VkResult result = vkrfb->Result();
+ delete vkrfb; // Immediately release the incomplete, never-submitted resources.
+ ps5_memory_report("ppsspp-framebuffer-failed", bytes, result);
+ if (attempt == 0 && (result == VK_ERROR_OUT_OF_DEVICE_MEMORY || result == VK_ERROR_OUT_OF_HOST_MEMORY) && renderManager_.CollectGarbage())
+ continue;
+ vulkan_->SetFramebufferFailure(result);
+ return nullptr;
+ }
+ return nullptr;
}
void VKContext::CopyFramebufferImage(Framebuffer *srcfb, int level, int x, int y, int z, Framebuffer *dstfb, int dstLevel, int dstX, int dstY, int dstZ, int width, int height, int depth, Aspect aspects, const char *tag) {
+ if (!srcfb || !dstfb) return;
VKFramebuffer *src = (VKFramebuffer *)srcfb;
VKFramebuffer *dst = (VKFramebuffer *)dstfb;
@@ -1809,6 +1838,7 @@ void VKContext::CopyFramebufferImage(Framebuffer *srcfb, int level, int x, int y
}
bool VKContext::BlitFramebuffer(Framebuffer *srcfb, int srcX1, int srcY1, int srcX2, int srcY2, Framebuffer *dstfb, int dstX1, int dstY1, int dstX2, int dstY2, Aspect aspects, FBBlitFilter filter, const char *tag) {
+ if (!srcfb || !dstfb) return false;
VKFramebuffer *src = (VKFramebuffer *)srcfb;
VKFramebuffer *dst = (VKFramebuffer *)dstfb;
@@ -1822,6 +1852,7 @@ bool VKContext::BlitFramebuffer(Framebuffer *srcfb, int srcX1, int srcY1, int sr
}
bool VKContext::CopyFramebufferToMemory(Framebuffer *srcfb, Aspect aspects, int x, int y, int w, int h, Draw::DataFormat format, void *pixels, int pixelStride, ReadbackMode mode, const char *tag) {
+ if (!srcfb) return false;
VKFramebuffer *src = (VKFramebuffer *)srcfb;
int aspectMask = 0;
@@ -1856,6 +1887,7 @@ void VKContext::BindFramebufferAsRenderTarget(Framebuffer *fbo, const RenderPass
}
void VKContext::BindFramebufferAsTexture(Framebuffer *fbo, int binding, Aspect channelBit, int layer) {
+ if (!fbo) return;
VKFramebuffer *fb = (VKFramebuffer *)fbo;
_assert_(binding >= 0 && binding < MAX_BOUND_TEXTURES);
diff --git a/Common/MemArena.h b/Common/MemArena.h
index 9a905b134..0b3e806cc 100644
--- a/Common/MemArena.h
+++ b/Common/MemArena.h
@@ -25,8 +25,13 @@
#include <mach/mach.h>
#endif
+#include "ppsspp_config.h"
#include "Common/CommonTypes.h"
+#if PPSSPP_PLATFORM(PS5)
+#include <ps5platform/shm.h>
+#endif
+
// This class lets you create a block of anonymous RAM, and then arbitrarily map views into it.
// Multiple views can mirror the same section of the block, which makes it very convient for emulating
// memory mirrors.
@@ -55,4 +60,9 @@ class MemArena {
#else
int fd = -1;
#endif
+#if PPSSPP_PLATFORM(PS5)
+ // PS5: guest memory is one direct-memory object of the platform layer
+ // (MemArenaPosix.cpp, ps5platform/shm.h).
+ ps5_shm shm_{};
+#endif
};
diff --git a/Common/MemArenaPosix.cpp b/Common/MemArenaPosix.cpp
index a413f4eaf..6c064fcca 100644
--- a/Common/MemArenaPosix.cpp
+++ b/Common/MemArenaPosix.cpp
@@ -51,7 +51,31 @@ bool MemArena::NeedsProbing() {
return false;
}
+#if PPSSPP_PLATFORM(PS5)
+// PS5: guest memory is direct memory, through the platform layer (my payload
+// SDK fork, ps5platform/shm.h). Every view of a shared-memory object is charged
+// to the title's 448 MiB of flexible memory; one direct-memory object is
+// charged to nothing but the 12 GiB direct pool, and maps at any number of
+// addresses, which is what PPSSPP's mirrors are.
+static void *ps5_reserved = nullptr;
+static const size_t ps5_reserved_bytes = 0x100000000ULL;
+
+static bool ps5_in_reserved(const void *view, size_t size) {
+ const uintptr_t at = (uintptr_t)view, base = (uintptr_t)ps5_reserved;
+ return ps5_reserved && at >= base && at + size <= base + ps5_reserved_bytes;
+}
+#endif
+
bool MemArena::GrabMemSpace(size_t size) {
+#if PPSSPP_PLATFORM(PS5)
+ const int result = ps5_shm_create(size, &shm_);
+ if (result != 0) {
+ ERROR_LOG(Log::MemMap, "PS5: %08x bytes of direct memory for guest memory failed: %08x", (int)size, (unsigned)result);
+ return false;
+ }
+ INFO_LOG(Log::MemMap, "PS5: guest memory is %08x bytes of direct memory", (int)size);
+ return true;
+#endif
#ifndef NO_MMAP
constexpr mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
@@ -104,6 +128,10 @@ bool MemArena::GrabMemSpace(size_t size) {
}
void MemArena::ReleaseSpace() {
+#if PPSSPP_PLATFORM(PS5)
+ ps5_shm_destroy(&shm_);
+ return;
+#endif
#ifndef NO_MMAP
close(fd);
#endif
@@ -111,6 +139,18 @@ void MemArena::ReleaseSpace() {
void *MemArena::CreateView(s64 offset, size_t size, void *base)
{
+#if PPSSPP_PLATFORM(PS5)
+ // At base, inside the reserved 4 GiB (Find4GBBase); with none, anywhere
+ // outside the Vulkan driver's GPU window.
+ void *view = nullptr;
+ const int result = ps5_shm_map(&shm_, (size_t)offset, size, base, PS5_SHM_READ | PS5_SHM_WRITE,
+ base ? PS5_SHM_FIXED : 0, &view);
+ if (result != 0) {
+ ERROR_LOG(Log::MemMap, "PS5: view %08x+%08x at %p failed: %08x", (int)offset, (int)size, base, (unsigned)result);
+ return 0;
+ }
+ return view;
+#endif
#ifdef NO_MMAP
return (void*) base;
#else
@@ -122,7 +162,7 @@ void *MemArena::CreateView(s64 offset, size_t size, void *base)
((base == 0) ? 0 : MAP_FIXED), fd, offset);
if (retval == MAP_FAILED) {
- NOTICE_LOG(Log::MemMap, "mmap on %s (fd: %d) failed: %s", ram_temp_file.c_str(), (int)fd, strerror(errno));
+ ERROR_LOG(Log::MemMap, "mmap on %s (fd: %d) failed: %s (view %08x+%08x at %p)", ram_temp_file.c_str(), (int)fd, strerror(errno), (int)offset, (int)size, base);
return 0;
}
return retval;
@@ -130,12 +170,52 @@ void *MemArena::CreateView(s64 offset, size_t size, void *base)
}
void MemArena::ReleaseView(s64 offset, void* view, size_t size) {
+#if PPSSPP_PLATFORM(PS5)
+ // A view in the reserved 4 GiB leaves its hole reserved, so nothing else is
+ // mapped there before the next instance maps its views again.
+ ps5_shm_unmap(view, size, ps5_in_reserved(view, size) ? PS5_SHM_KEEP_RESERVED : 0);
+ return;
+#endif
#ifndef NO_MMAP
munmap(view, size);
#endif
}
u8* MemArena::Find4GBBase() {
+#if PPSSPP_PLATFORM(PS5)
+ // PS5: an anonymous PROT_NONE mapping of that size is charged to the title's
+ // small flexible-memory budget and fails, and the hardcoded fallback below
+ // would map over whatever lives there. The platform layer reserves 4 GiB
+ // without backing it, outside the Vulkan driver's GPU window; views are then
+ // mapped into it at fixed addresses. It is kept while the core is loaded and
+ // reused. The address asked for is 0x6_0000_0000.
+ if (!ps5_reserved) {
+ void *const hint = (void *)0x600000000ULL;
+ void *address = nullptr;
+ int result = ps5_vrange_reserve(ps5_reserved_bytes, hint, 0x10000, &address);
+ // The static above lives in the core image, and RetroArch unloads the
+ // core when content closes: a reloaded core starts with it null while
+ // the previous instance's reservation (and any view it left mapped)
+ // still holds the range, and a second game failed with 8002000c
+ // ("Memory init failed", 2026-09-24). The range is this arena's alone,
+ // so whatever an earlier instance left there is released and the
+ // reservation made again.
+ if (result == 0 && address != hint) {
+ ps5_vrange_release(address, ps5_reserved_bytes);
+ ps5_vrange_release(hint, ps5_reserved_bytes);
+ result = ps5_vrange_reserve(ps5_reserved_bytes, hint, 0x10000, &address);
+ if (result == 0)
+ INFO_LOG(Log::MemMap, "PS5: released the previous instance's 4 GiB and reserved it again");
+ }
+ if (result != 0) {
+ ERROR_LOG(Log::MemMap, "PS5: reserving 4 GiB failed: %08x", (unsigned)result);
+ return nullptr;
+ }
+ ps5_reserved = address;
+ INFO_LOG(Log::MemMap, "PS5: guest address space reserved at %p (4 GiB)", ps5_reserved);
+ }
+ return static_cast<u8 *>(ps5_reserved);
+#endif
// Now, create views in high memory where there's plenty of space.
#if PPSSPP_ARCH(64BIT) && !defined(USE_ASAN) && !defined(NO_MMAP)
// We should probably just go look in /proc/self/maps for some free space.
diff --git a/Common/MemoryUtil.cpp b/Common/MemoryUtil.cpp
index be2a14ddb..dbd608bd1 100644
--- a/Common/MemoryUtil.cpp
+++ b/Common/MemoryUtil.cpp
@@ -43,6 +43,10 @@
#ifndef _WIN32
#include <unistd.h>
+
+#if PPSSPP_PLATFORM(PS5)
+#include <ps5platform/exec.h>
+#endif
#endif
static int hint_location;
#ifdef __APPLE__
@@ -189,7 +193,20 @@ void *AllocateExecutableMemory(size_t size) {
if (PlatformIsWXExclusive())
prot = PROT_READ | PROT_WRITE; // POST_EXEC is added later in this case.
+#if PPSSPP_PLATFORM(PS5)
+ // PS5: code lives in direct memory through the platform layer (my payload
+ // SDK fork, ps5platform/exec.h): read, write and execute at once, within a
+ // 32-bit displacement of this core's own code, and out of the 12 GiB direct
+ // pool rather than the title's 448 MiB of flexible memory.
+ (void)map_hint;
+ void* ptr = ps5_exec_allocate(size, (uintptr_t)&AllocateExecutableMemory);
+ if (!ptr) {
+ ERROR_LOG(Log::MemMap, "PS5: no executable memory for %d bytes", (int)size);
+ ptr = MAP_FAILED;
+ }
+#else
void* ptr = mmap(map_hint, size, prot, MAP_ANON | MAP_PRIVATE, -1, 0);
+#endif
if (ptr == MAP_FAILED) {
ptr = nullptr;
@@ -276,6 +293,12 @@ void FreeMemoryPages(void *ptr, size_t size) {
}
void FreeExecutableMemory(void *ptr, size_t size) {
+#if PPSSPP_PLATFORM(PS5)
+ if (ptr) {
+ ps5_exec_release(ptr);
+ return;
+ }
+#endif
FreeMemoryPages(ptr, size);
}
diff --git a/Common/PS5Memory.h b/Common/PS5Memory.h
new file mode 100644
index 000000000..2a60c055e
--- /dev/null
+++ b/Common/PS5Memory.h
@@ -0,0 +1,9 @@
+#pragma once
+#include <cstddef>
+#if defined(__PROSPERO__)
+extern "C" int ps5_memory_pressure(size_t request);
+extern "C" void ps5_memory_report(const char *event, size_t request, int result);
+#else
+inline int ps5_memory_pressure(size_t) { return 0; }
+inline void ps5_memory_report(const char *, size_t, int) {}
+#endif
diff --git a/Common/Thread/ThreadUtil.cpp b/Common/Thread/ThreadUtil.cpp
index e842a0312..12a87d20b 100644
--- a/Common/Thread/ThreadUtil.cpp
+++ b/Common/Thread/ThreadUtil.cpp
@@ -143,7 +143,11 @@ void SetCurrentThreadName(const char *threadName) {
pthread_setname_np(pthread_self(), threadName);
#elif defined(__APPLE__)
pthread_setname_np(threadName);
-#elif defined(__DragonFly__) || defined(__FreeBSD__) || defined(__OpenBSD__)
+// PS5 is x86_64-sie-ps5, which defines __FreeBSD__, but its libkernel's
+// pthread_set_name_np is not usable from a title: calling it on a fresh worker thread
+// faults inside libkernel (fault address 0x70) and takes the process down. Skip it and
+// keep the name in the TLS variable the way the unknown-platform path does.
+#elif (defined(__DragonFly__) || defined(__FreeBSD__) || defined(__OpenBSD__)) && !PPSSPP_PLATFORM(PS5)
pthread_set_name_np(pthread_self(), threadName);
#elif defined(__NetBSD__)
pthread_setname_np(pthread_self(), "%s", (void*)threadName);
diff --git a/Core/FrameTiming.cpp b/Core/FrameTiming.cpp
index 79a6e46f6..ef2af194a 100644
--- a/Core/FrameTiming.cpp
+++ b/Core/FrameTiming.cpp
@@ -88,6 +88,18 @@ void FrameTiming::ComputePresentMode(Draw::DrawContext *draw, bool fastForward)
_dbg_assert_(draw->GetDeviceCaps().presentModesSupported != (Draw::PresentMode)0);
+#if PPSSPP_PLATFORM(PS5)
+ // PS5 port: RetroArch presents, and paces fast-forward itself (its
+ // fast-forward frame skipping). Skipping flips here as well -- a FIFO-only
+ // device's fallback -- kept retro_run from returning for most frames, and
+ // with the port's fast-forward frameskip the frames that were both drawn
+ // and allowed to flip came about twice a second: choppy fast-forward
+ // (2026-09-24). Every frame flips; the frontend decides what is shown.
+ fastForwardSkipFlip_ = false;
+ presentMode_ = Draw::PresentMode::FIFO;
+ return;
+#endif
+
if (draw->GetDeviceCaps().presentModesSupported == Draw::PresentMode::FIFO) {
// Only FIFO mode is supported (like on iOS and some GLES backends).
fastForwardSkipFlip_ = true;
diff --git a/GPU/Common/FramebufferManagerCommon.cpp b/GPU/Common/FramebufferManagerCommon.cpp
index 18bc85c09..77ee810a0 100644
--- a/GPU/Common/FramebufferManagerCommon.cpp
+++ b/GPU/Common/FramebufferManagerCommon.cpp
@@ -34,6 +34,7 @@
#include "Core/Debugger/MemBlockInfo.h"
#include "GPU/Common/DrawEngineCommon.h"
#include "GPU/Common/FramebufferManagerCommon.h"
+#include "Common/PS5Memory.h"
#include "GPU/Common/PresentationCommon.h"
#include "GPU/Common/TextureCacheCommon.h"
#include "GPU/Common/ReinterpretFramebuffer.h"
@@ -1770,9 +1771,10 @@ void FramebufferManagerCommon::DecimateFBOs() {
}
}
+ const int scratchAge = ps5_memory_pressure(0) ? 2 : FBO_OLD_AGE;
for (auto it = tempFBOs_.begin(); it != tempFBOs_.end(); ) {
int age = frameLastFramebufUsed_ - it->second.last_frame_used;
- if (age > FBO_OLD_AGE && !persistentFramebuffers) {
+ if (age > scratchAge && !persistentFramebuffers) {
it->second.fbo->Release();
it = tempFBOs_.erase(it);
} else {
diff --git a/GPU/Common/TextureCacheCommon.cpp b/GPU/Common/TextureCacheCommon.cpp
index a5216d548..aef15daed 100644
--- a/GPU/Common/TextureCacheCommon.cpp
+++ b/GPU/Common/TextureCacheCommon.cpp
@@ -37,6 +37,7 @@
#include "Core/HW/Display.h"
#include "GPU/Common/FramebufferManagerCommon.h"
#include "GPU/Common/TextureCacheCommon.h"
+#include "Common/PS5Memory.h"
#include "GPU/Common/TextureDecoder.h"
#include "GPU/Common/GPUStateUtils.h"
#include "GPU/ge_constants.h"
@@ -813,7 +814,9 @@ bool TextureCacheCommon::GetBestFramebufferCandidate(const TextureDefinition &en
// Removes old textures.
void TextureCacheCommon::Decimate(TexCacheEntry *exceptThisOne, bool forcePressure) {
- if (--decimationCounter_ <= 0) {
+ const bool poolPressure = ps5_memory_pressure(0) != 0;
+ forcePressure |= poolPressure;
+ if (forcePressure || --decimationCounter_ <= 0) {
decimationCounter_ = TEXCACHE_DECIMATION_INTERVAL;
} else {
return;
@@ -830,7 +833,7 @@ void TextureCacheCommon::Decimate(TexCacheEntry *exceptThisOne, bool forcePressu
continue;
}
bool hasClut = (iter->second->status & TexCacheEntry::STATUS_CLUT_VARIANTS) != 0;
- int killAge = hasClut ? TEXTURE_KILL_AGE_CLUT : killAgeBase;
+ int killAge = poolPressure ? 2 : (hasClut ? TEXTURE_KILL_AGE_CLUT : killAgeBase);
if (iter->second->lastFrame + killAge < gpuStats.numFlips) {
DeleteTexture(iter++);
} else {
diff --git a/GPU/Vulkan/GPU_Vulkan.cpp b/GPU/Vulkan/GPU_Vulkan.cpp
index a36f54c68..5f28c3b22 100644
--- a/GPU/Vulkan/GPU_Vulkan.cpp
+++ b/GPU/Vulkan/GPU_Vulkan.cpp
@@ -266,7 +266,13 @@ u32 GPU_Vulkan::CheckGPUFeatures() const {
}
}
- if (!draw_->GetBugs().Has(Draw::Bugs::PVR_BAD_16BIT_TEXFORMATS)) {
+#if PPSSPP_PLATFORM(PS5) && defined(__LIBRETRO__)
+ extern bool PS5TestFlag(const char *name);
+ const bool ps5_no_16bit = PS5TestFlag("ps5_no_16bit_formats");
+#else
+ const bool ps5_no_16bit = false;
+#endif
+ if (!ps5_no_16bit && !draw_->GetBugs().Has(Draw::Bugs::PVR_BAD_16BIT_TEXFORMATS)) {
// These are VULKAN_4444_FORMAT and friends.
// Note that we are now using the correct set of formats - the only cases where some may be missing
// are non-conformant implementations like MoltenVK.
diff --git a/GPU/Vulkan/TextureCacheVulkan.cpp b/GPU/Vulkan/TextureCacheVulkan.cpp
index 242421e7f..6fbbc6a07 100644
--- a/GPU/Vulkan/TextureCacheVulkan.cpp
+++ b/GPU/Vulkan/TextureCacheVulkan.cpp
@@ -570,7 +570,27 @@ void TextureCacheVulkan::ReleaseTexture(TexCacheEntry *entry, bool delete_them)
entry->vkTex = nullptr;
}
+#if PPSSPP_PLATFORM(PS5) && defined(__LIBRETRO__)
+// PS5 A/B test flags (libretro/libretro.cpp, PS5TestFlag): one 16-bit format
+// decoded to 8888 instead, to find which one the console's driver samples wrong.
+extern bool PS5TestFlag(const char *name);
+static VkFormat PS5FilterFormat(VkFormat format) {
+ if ((format == VULKAN_4444_FORMAT && PS5TestFlag("ps5_no_4444")) ||
+ (format == VULKAN_1555_FORMAT && PS5TestFlag("ps5_no_1555")) ||
+ (format == VULKAN_565_FORMAT && PS5TestFlag("ps5_no_565")))
+ return VULKAN_8888_FORMAT;
+ return format;
+}
+#else
+static VkFormat PS5FilterFormat(VkFormat format) { return format; }
+#endif
+
+static VkFormat getClutDestFormatVulkanUnfiltered(GEPaletteFormat format);
VkFormat getClutDestFormatVulkan(GEPaletteFormat format) {
+ return PS5FilterFormat(getClutDestFormatVulkanUnfiltered(format));
+}
+
+static VkFormat getClutDestFormatVulkanUnfiltered(GEPaletteFormat format) {
switch (format) {
case GE_CMODE_16BIT_ABGR4444:
return VULKAN_4444_FORMAT;
@@ -970,11 +990,11 @@ VkFormat TextureCacheVulkan::GetDestFormat(GETextureFormat format, GEPaletteForm
case GE_TFMT_CLUT32:
return getClutDestFormatVulkan(clutFormat);
case GE_TFMT_4444:
- return VULKAN_4444_FORMAT;
+ return PS5FilterFormat(VULKAN_4444_FORMAT);
case GE_TFMT_5551:
- return VULKAN_1555_FORMAT;
+ return PS5FilterFormat(VULKAN_1555_FORMAT);
case GE_TFMT_5650:
- return VULKAN_565_FORMAT;
+ return PS5FilterFormat(VULKAN_565_FORMAT);
case GE_TFMT_8888:
case GE_TFMT_DXT1:
case GE_TFMT_DXT3:
diff --git a/ext/glew/CMakeLists.txt b/ext/glew/CMakeLists.txt
index 317b4b231..72fc2e63a 100644
--- a/ext/glew/CMakeLists.txt
+++ b/ext/glew/CMakeLists.txt
@@ -1,5 +1,6 @@
find_package(GLEW)
-if(NOT USE_LIBNX)
+# PS5: no GL, so no GLEW (the port is Vulkan only).
+if(NOT USE_LIBNX AND NOT PPSSPP_PS5)
if((NOT APPLE OR USE_SYSTEM_FFMPEG) AND GLEW_FOUND)
add_library(system_glew INTERFACE)
add_library(Ext::GLEW ALIAS system_glew)
diff --git a/libretro/CMakeLists.txt b/libretro/CMakeLists.txt
index d304b4dba..3944a7418 100644
--- a/libretro/CMakeLists.txt
+++ b/libretro/CMakeLists.txt
@@ -2,11 +2,19 @@
set(LIBRETRO_SRCS
libretro.cpp
LibretroGraphicsContext.cpp
- LibretroGLContext.cpp
- LibretroGLCoreContext.cpp
LibretroVulkanContext.cpp
libretro_vulkan.cpp)
+# PS5 has no GL context to create (see the root CMakeLists.txt).
+if(NOT PPSSPP_PS5)
+ set(LIBRETRO_SRCS ${LIBRETRO_SRCS}
+ LibretroGLContext.cpp
+ LibretroGLCoreContext.cpp)
+else()
+ set(LIBRETRO_SRCS ${LIBRETRO_SRCS}
+ ps5_gl_stubs.cpp)
+endif()
+
if(WIN32)
set(LIBRETRO_SRCS ${LIBRETRO_SRCS}
LibretroD3D11Context.cpp)
diff --git a/libretro/LibretroGraphicsContext.cpp b/libretro/LibretroGraphicsContext.cpp
index cc7a272e2..c6edc680b 100644
--- a/libretro/LibretroGraphicsContext.cpp
+++ b/libretro/LibretroGraphicsContext.cpp
@@ -1,7 +1,9 @@
#include "libretro/LibretroGraphicsContext.h"
+#if !PPSSPP_PLATFORM(PS5)
#include "libretro/LibretroGLContext.h"
#include "libretro/LibretroGLCoreContext.h"
+#endif
#include "libretro/LibretroVulkanContext.h"
#ifdef _WIN32
#include "libretro/LibretroD3D11Context.h"
@@ -101,6 +103,8 @@ LibretroGraphicsContext *LibretroGraphicsContext::CreateGraphicsContext() {
if (Libretro::backend != RETRO_HW_CONTEXT_DUMMY)
preferred = Libretro::backend;
+// PS5: Vulkan only; the GL contexts are not built.
+#if !PPSSPP_PLATFORM(PS5)
#ifndef USING_GLES2
if (preferred == RETRO_HW_CONTEXT_DUMMY || preferred == RETRO_HW_CONTEXT_OPENGL_CORE) {
ctx = new LibretroGLCoreContext();
@@ -120,6 +124,7 @@ LibretroGraphicsContext *LibretroGraphicsContext::CreateGraphicsContext() {
}
delete ctx;
}
+#endif // !PPSSPP_PLATFORM(PS5)
#ifndef HAVE_LIBNX
if (preferred == RETRO_HW_CONTEXT_DUMMY || preferred == RETRO_HW_CONTEXT_VULKAN) {
diff --git a/libretro/LibretroVulkanContext.cpp b/libretro/LibretroVulkanContext.cpp
index d222cd03c..0ad209614 100644
--- a/libretro/LibretroVulkanContext.cpp
+++ b/libretro/LibretroVulkanContext.cpp
@@ -31,6 +31,10 @@ void LibretroVulkanContext::SwapBuffers() {
LibretroHWRenderContext::SwapBuffers();
}
+// The API version asked of the frontend, which creates the instance: RetroArch raises 1.0 to 1.1
+// where the loader has it, and asking for 1.1 makes that the version this core knows it has.
+static const uint32_t libretro_api_version = VK_API_VERSION_1_1;
+
static bool create_device(retro_vulkan_context *context, VkInstance instance, VkPhysicalDevice gpu, VkSurfaceKHR surface, PFN_vkGetInstanceProcAddr get_instance_proc_addr, const char **required_device_extensions, unsigned num_required_device_extensions, const char **required_device_layers, unsigned num_required_device_layers, const VkPhysicalDeviceFeatures *required_features) {
init_glslang();
@@ -41,6 +45,7 @@ static bool create_device(retro_vulkan_context *context, VkInstance instance, Vk
// TODO: Here we'll inject the instance and all of the stuff into the VulkanContext.
vk->CreateInstance({});
+ vk->LimitInstanceApiVersion(libretro_api_version);
int physical_device = 0;
while (gpu && vk->GetPhysicalDevice(physical_device) != gpu) {
@@ -86,7 +91,7 @@ static const VkApplicationInfo *GetApplicationInfo(void) {
app_info.applicationVersion = Version(PPSSPP_GIT_VERSION).ToInteger();
app_info.pEngineName = "PPSSPP";
app_info.engineVersion = 2;
- app_info.apiVersion = VK_API_VERSION_1_0;
+ app_info.apiVersion = libretro_api_version;
return &app_info;
}
diff --git a/libretro/libretro.cpp b/libretro/libretro.cpp
index 3b2f10317..6b9ab8d44 100644
--- a/libretro/libretro.cpp
+++ b/libretro/libretro.cpp
@@ -48,6 +48,8 @@
#include <libretro.h>
#include "libretro/LibretroGraphicsContext.h"
+#include "Common/GPU/Vulkan/VulkanContext.h"
+#include "Common/PS5Memory.h"
#include "libretro/libretro_core_options.h"
#if PPSSPP_PLATFORM(ANDROID)
@@ -391,8 +393,131 @@ static bool set_variable_visibility(void)
return updated;
}
+#if PPSSPP_PLATFORM(PS5)
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+// PS5 test aid: /app0/ppsspp-options.txt, when present, holds `key=value` lines
+// that answer RETRO_ENVIRONMENT_GET_VARIABLE for those keys instead of the
+// frontend. The console's FTP server cannot read or write the frontend's
+// per-core options folder, so this is how a run sets options without a person
+// at the pad. The file is never shipped.
+static retro_environment_t ps5_frontend_environ_cb;
+static std::vector<std::pair<std::string, std::string>> ps5_option_overrides;
+
+static bool ps5_environ_cb(unsigned cmd, void *data) {
+ if (cmd == RETRO_ENVIRONMENT_GET_VARIABLE && data) {
+ struct retro_variable *var = (struct retro_variable *)data;
+ for (const auto &entry : ps5_option_overrides) {
+ if (var->key && entry.first == var->key) {
+ var->value = entry.second.c_str();
+ return true;
+ }
+ }
+ }
+ return ps5_frontend_environ_cb(cmd, data);
+}
+
+// A test flag from the same file: `name=1` present. Read by the GPU backend for
+// A/B runs (see GPU/Vulkan/GPU_Vulkan.cpp).
+bool PS5TestFlag(const char *name) {
+ for (const auto &entry : ps5_option_overrides)
+ if (entry.first == name && entry.second == "1")
+ return true;
+ return false;
+}
+
+static retro_environment_t ps5_wrap_environment(retro_environment_t cb) {
+ ps5_frontend_environ_cb = cb;
+ ps5_option_overrides.clear();
+ // POSIX reads: this file's FILE is libretro-common's RFILE.
+ int fd = open("/app0/ppsspp-options.txt", O_RDONLY);
+ if (fd >= 0) {
+ std::string text;
+ char chunk[512];
+ ssize_t got;
+ while ((got = read(fd, chunk, sizeof(chunk))) > 0)
+ text.append(chunk, (size_t)got);
+ close(fd);
+ size_t at = 0;
+ while (at < text.size()) {
+ size_t end = text.find('\n', at);
+ if (end == std::string::npos)
+ end = text.size();
+ std::string line = text.substr(at, end - at);
+ at = end + 1;
+ size_t eq = line.find('=');
+ if (eq == std::string::npos)
+ continue;
+ std::string key = line.substr(0, eq), value = line.substr(eq + 1);
+ while (!value.empty() && (value.back() == '\r' || value.back() == ' '))
+ value.pop_back();
+ while (!key.empty() && key.back() == ' ')
+ key.pop_back();
+ if (!key.empty())
+ ps5_option_overrides.emplace_back(key, value);
+ }
+ }
+ return ps5_option_overrides.empty() ? cb : ps5_environ_cb;
+}
+#endif
+
+#if PPSSPP_PLATFORM(PS5)
+// PS5: the port's default profile, the heaviest the console is meant to hold:
+// Vulkan, the JIT, 10x internal resolution (4800x2720), 8x MSAA, 16x
+// anisotropic filtering, "Auto max quality" texture filtering, hardware
+// transform and software skinning on, high spline quality, full-resolution
+// effects, no frameskip and no speed hacks. These are defaults: a value the
+// player saved in RetroArch's core options file still wins.
+static void ps5_apply_default_profile() {
+ static const struct { const char *key; const char *value; } profile[] = {
+ { "ppsspp_backend", "vulkan" },
+ { "ppsspp_cpu_core", "JIT" },
+ { "ppsspp_internal_resolution", "4800x2720" },
+ { "ppsspp_mulitsample_level", "x8" },
+ { "ppsspp_texture_anisotropic_filtering", "16x" },
+ { "ppsspp_texture_filtering", "Auto max quality" },
+ { "ppsspp_gpu_hardware_transform", "enabled" },
+ { "ppsspp_software_skinning", "enabled" },
+ { "ppsspp_spline_quality", "High" },
+ { "ppsspp_lower_resolution_for_effects", "disabled" },
+ { "ppsspp_frameskip", "disabled" },
+ { "ppsspp_auto_frameskip", "disabled" },
+ { "ppsspp_skip_buffer_effects", "disabled" },
+ { "ppsspp_skip_gpu_readbacks", "disabled" },
+ { "ppsspp_lazy_texture_caching", "disabled" },
+ { "ppsspp_disable_range_culling", "disabled" },
+ };
+ for (auto &def : option_defs_us) {
+ if (!def.key)
+ break;
+ for (const auto &entry : profile)
+ if (!strcmp(def.key, entry.key))
+ def.default_value = entry.value;
+ }
+
+ // Once: an options file saved by an earlier core would keep its values over
+ // these defaults (RetroArch reads it when the options are set, just after
+ // this), so the first run of this profile sets it aside, recoverable.
+ const char *saved = "/app0/config/PPSSPP/PPSSPP.opt";
+ const char *marker = "/app0/config/PPSSPP/ps5-default-profile-v1";
+ struct stat st;
+ if (stat(marker, &st) != 0) {
+ if (stat(saved, &st) == 0)
+ rename(saved, "/app0/config/PPSSPP/PPSSPP.opt.before-ps5-profile");
+ int fd = open(marker, O_WRONLY | O_CREAT | O_TRUNC, 0666);
+ if (fd >= 0)
+ close(fd);
+ }
+}
+#endif
+
void retro_set_environment(retro_environment_t cb)
{
+#if PPSSPP_PLATFORM(PS5)
+ cb = ps5_wrap_environment(cb);
+ ps5_apply_default_profile();
+#endif
environ_cb = cb;
bool option_categories = false;
@@ -476,6 +601,36 @@ static std::string map_psp_language_to_i18n_locale(int val)
}
}
+// PS5 port: fast-forward unthrottles and skips drawing. The libretro build draws every
+// emulated frame while RetroArch fast-forwards, so at a high internal
+// resolution fast-forward barely outran the GPU (PSP Test Game at 10x: ~1.3x).
+// PPSSPP's own frontends skip frames when fast-forwarding; here, while
+// RetroArch reports it, three of every four frames are not drawn, and the
+// frameskip the options set is back the moment it stops. Normal play keeps
+// the option's value (the PS5 default is off).
+static void ps5_fast_forward_frameskip() {
+ static bool active = false;
+ static int saved = 0;
+ bool fastForwarding = false;
+ if (!environ_cb(RETRO_ENVIRONMENT_GET_FASTFORWARDING, &fastForwarding))
+ fastForwarding = false;
+ // PPSSPP's own frame throttle is off only when it knows it is being fast-
+ // forwarded, which upstream tells it only with "force lag sync" on. With it
+ // off, __DisplayFlip kept sleeping to hold real time while RetroArch fast-
+ // forwarded, and the console's short sleeps overshoot: fast-forward crawled
+ // at a few frames a second (sampled in __DisplayFlip, 2026-09-24).
+ PSP_CoreParameter().fastForward = fastForwarding;
+ if (fastForwarding) {
+ if (!active)
+ saved = g_Config.iFrameSkip;
+ active = true;
+ g_Config.iFrameSkip = 3;
+ } else if (active) {
+ g_Config.iFrameSkip = saved;
+ active = false;
+ }
+}
+
static void check_dynamic_variables(CoreParameter &coreParam) {
if (g_Config.bForceLagSync)
{
@@ -1338,8 +1493,11 @@ void retro_get_system_av_info(struct retro_system_av_info *info)
info->geometry.base_width = g_Config.iInternalResolution * NATIVEWIDTH;
info->geometry.base_height = g_Config.iInternalResolution * NATIVEHEIGHT;
- info->geometry.max_width = g_Config.iInternalResolution * NATIVEWIDTH;
- info->geometry.max_height = g_Config.iInternalResolution * NATIVEHEIGHT;
+ // The largest internal resolution (10x, 4800x2720, the PS5 default) is always the
+ // maximum: a change from the Quick Menu then never restarts RetroArch's video driver,
+ // whose cached Vulkan context the core's render thread kept using (issue 63).
+ info->geometry.max_width = 10 * NATIVEWIDTH;
+ info->geometry.max_height = 10 * NATIVEHEIGHT;
if (g_Config.bDisplayCropTo16x9)
info->geometry.base_height -= g_Config.iInternalResolution * 2;
@@ -1702,8 +1860,21 @@ static void retro_input(void)
__CtrlSetAnalogXY(CTRL_STICK_RIGHT, x_right, y_right);
}
+static bool ReportFramebufferFailure() {
+ if (!ctx || ctx->GetGPUCore() != GPUCORE_VULKAN) return false;
+ auto *vk = static_cast<VulkanContext *>(ctx->GetAPIContext());
+ if (!vk || vk->FramebufferFailure() == VK_SUCCESS) return false;
+ const char *text = "PPSSPP could not allocate a framebuffer. Reduce internal resolution or MSAA, then reopen the game.";
+ log_cb(RETRO_LOG_ERROR, "%s (Vulkan result %d)\n", text, int(vk->FramebufferFailure()));
+ retro_message message{text, 600};
+ environ_cb(RETRO_ENVIRONMENT_SET_MESSAGE, &message);
+ environ_cb(RETRO_ENVIRONMENT_SHUTDOWN, nullptr);
+ return true;
+}
+
void retro_run(void)
{
+ if (ReportFramebufferFailure()) return;
if (g_pendingBoot) {
BootState state = PSP_InitUpdate(&g_bootErrorString);
switch (state) {
@@ -1755,6 +1926,7 @@ void retro_run(void)
check_variables(PSP_CoreParameter());
else
check_dynamic_variables(PSP_CoreParameter());
+ ps5_fast_forward_frameskip();
retro_input();
@@ -1780,6 +1952,7 @@ void retro_run(void)
else
EmuFrame();
+ if (ReportFramebufferFailure()) return;
VsyncSwapIntervalDetect();
ctx->SwapBuffers();
upload_output_audio_buffer();
diff --git a/libretro/libretro_vulkan.cpp b/libretro/libretro_vulkan.cpp
index 8fb670003..7a5ee3c7c 100644
--- a/libretro/libretro_vulkan.cpp
+++ b/libretro/libretro_vulkan.cpp
@@ -140,6 +140,18 @@ static VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice_libretro(VkPhysicalDevice p
}
VkResult res = vkCreateDevice_org(physicalDevice, &newInfo, pAllocator, pDevice);
+#if PPSSPP_PLATFORM(PS5)
+ // PS5: a refused device is otherwise silent; name what was asked for.
+ if (res != VK_SUCCESS) {
+ ERROR_LOG(Log::G3D, "vkCreateDevice returned %d with %u extensions:", (int)res, newInfo.enabledExtensionCount);
+ for (uint32_t i = 0; i < newInfo.enabledExtensionCount; i++)
+ ERROR_LOG(Log::G3D, " extension %s", newInfo.ppEnabledExtensionNames[i]);
+ for (const auto& pair : originalFeaturePointers)
+ for (uint32_t i = 0; i < sizeof(VkPhysicalDeviceFeatures) / sizeof(VkBool32); i++)
+ if (((VkBool32 *)pair.first)[i])
+ ERROR_LOG(Log::G3D, " feature index %u enabled", i);
+ }
+#endif
// The above code potentially modifies application memory. Restore it to avoid unexpected side effects.
for (const auto& pair : originalFeaturePointers) {
diff --git a/libretro/ps5_gl_stubs.cpp b/libretro/ps5_gl_stubs.cpp
new file mode 100644
index 000000000..ac7e7b60f
--- /dev/null
+++ b/libretro/ps5_gl_stubs.cpp
@@ -0,0 +1,11 @@
+// PS5: the GL backend is not built (see the root CMakeLists.txt), but the shared
+// shader generators still read the GL feature set when they are asked for a GLSL
+// target. This port only asks them for Vulkan, so the set is left empty.
+#include "ppsspp_config.h"
+#include "Common/GPU/OpenGL/GLFeatures.h"
+
+GLExtensions gl_extensions;
+
+int GLExtensions::GLSLVersion() {
+ return 0;
+}
diff --git a/ppsspp_config.h b/ppsspp_config.h
index faef7e478..5c2ed0f93 100644
--- a/ppsspp_config.h
+++ b/ppsspp_config.h
@@ -124,10 +124,17 @@
#define PPSSPP_PLATFORM_LINUX 1
#elif defined(__OpenBSD__)
#define PPSSPP_PLATFORM_OPENBSD 1
+#elif defined(__PROSPERO__)
+ // PlayStation 5 homebrew through the public payload SDK. The target is a
+ // FreeBSD-ABI x86-64 ELF (clang -target x86_64-sie-ps5), which is neither
+ // Linux nor any of the BSDs above: it has no /proc, no dlopen of project
+ // shared objects, and its allocator is the console's own pools.
+ #define PPSSPP_PLATFORM_PS5 1
#endif
// Windows ARM/ARM64, and Windows UWP (all), are the only platform that don't do GL at all (until Apple finally removes it)
-#if !PPSSPP_PLATFORM(WINDOWS) || ((!PPSSPP_ARCH(ARM) && !PPSSPP_ARCH(ARM64)) && !PPSSPP_PLATFORM(UWP))
+// PS5: no GL. The port presents through the frontend's Vulkan device only.
+#if (!PPSSPP_PLATFORM(WINDOWS) || ((!PPSSPP_ARCH(ARM) && !PPSSPP_ARCH(ARM64)) && !PPSSPP_PLATFORM(UWP))) && !PPSSPP_PLATFORM(PS5)
#define PPSSPP_API_ANY_GL 1
#endif