InitSwapchain deliberately refuses the 270 degree pre-transform. The driver
then reports VK_SUBOPTIMAL_KHR on every present, which we counted towards
recreating the swapchain - and the new one got the same treatment, so in
reverse landscape we rebuilt it every single frame.
Remember that the mismatch is ours and stop treating SUBOPTIMAL as a reason
to recreate when it is. Any other cause still counts.
Split out of the Android rotation work, where it was found: it's a
platform-independent Vulkan fix and stands on its own.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
The EGL path from the previous commit is now the only one, so GLSurfaceView
and everything that existed to serve it are gone:
- NativeRenderer.java and NativeGLSurfaceView.java, deleted.
- The javaGL branches in PpssppActivity. GL and Vulkan now share one
lifecycle: a plain SurfaceView, startRenderLoopThread() when the surface
arrives, joinRenderLoopThread() when it goes away.
- The "androidJavaGL" queryConfig, and the AndroidNativeEGL setting and its
developer-tools checkbox.
- displayInit/displayRender, and with them the "Second time, joining the
emuthread and starting it up again" dance - restarting the emu thread from
inside a JNI callback because a lost context was only observable as a
second onSurfaceCreated.
- GraphicsContext::NotifyContextLost, which existed solely because that join
drained the render queue on a thread with no current context. The render
thread now joins the emu thread with the context still current, so the
drain is real work and nothing has to be skipped.
- g_emuThread, now that nothing starts an emu thread outside the render loop.
Three things displayInit/displayRender did that nothing else did, moved
rather than dropped:
- EnterVR and the per-frame StartVRRender/UpdateVRInput/FinishVRRender hooks
now live in RenderLoopThread's GL branch. Java's GL thread could simply
return early and wait for the next vsync when the VR session was inactive;
this loop drives itself, so that case sleeps 16ms instead of spinning.
Not tested - no headset here.
- RECREATE_VIEWS after the emu thread starts, as before. Vulkan never did
this, so it's a candidate for removal when the two paths converge.
- The thin3d error callback, which surfaces GL errors as OSD messages. That
one moved into AndroidEGLGraphicsContext::InitSurface, next to the rest of
the context's own setup, matching WindowsGLContext.
Tested on a nubia Z2464N (Unisoc UMS9632, Mali-G57, Android 15): cold start,
UI, booting a homebrew, backgrounding and resuming both in the menu and
in-game, and a density change, which destroys and recreates the activity the
way a rotation does. Every cycle tears the EGL context down and builds a new
one, and the renderer comes back with FBOs and textures intact. The config
ladder's first choice, RGBA8888 D24S8, is what the Java chooser landed on
here too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
PreprocessSteps moved the clear of an empty clear pass into the next render
pass on the same framebuffer, stopping only at copies out of it. Resizing a
framebuffer clears the new one, copies the old contents in, then renders, so
the clear landed after the copy and wiped it. Stop at any copy, blit or
readback that touches the framebuffer.
Fixes black or missing layers in Asphalt Urban GT 2, FF Type-0, Street Riders,
Kurohyou, Split/Second and Super Stardust.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
If InitSurface failed, the thread returned but stayed joinable, so the next
surface was refused as "Already running" until the next pause. Tell a live
thread from a finished one with renderLoopRunning, and reap the latter.
Also make VulkanGraphicsContext::InitSurface clean up when it fails after
creating the draw context. Callers don't call ShutdownSurface on failure,
so draw_ (with its render manager) and the surface leaked, and the next
attempt overwrote the pointer.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Pipelines still in the compile queue weren't in flight yet, so the
shader cache load could stop waiting (and the spinner) early.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There's no swapchain, so reading the backbuffer asserted. Fail the
readback, and have gpu.buffer.screenshot fall back to the displayed
framebuffer.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
These own GPU objects, memory or refcounts in their destructors (or assert
there that they were torn down), so a copy would double-free. Nothing copies
them today; this keeps it that way. The manager base classes cover every
backend's subclass.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
GLSLtoSPV takes an optional SPIRVCache, keyed on a 32-bit hash of the
source, stage and variant, plus the source length. A changed shader
simply misses. thin3d's shaders and the other fixed ones use a global
cache in PSP/SYSTEM/CACHE/vulkan_spirv.cache, loaded on first use and
saved after graphics init, when a game's cache is saved, and at
shutdown; it's flushed once it reaches 32 entries, about twice what a
session compiles, so outdated ones don't pile up. Game shaders keep
theirs in the .vkshadercache, ahead of the shader IDs so that the
compiles on load find it (version 60), and only what the session used
is saved.
A cold glslang costs about 40ms before its first shader here, and
0.3-0.9ms per shader after that.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Executables outside a bundle (headless) found no Vulkan library. Also try
the app bundle built next to them, the Vulkan SDK's install and Homebrew's.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A frame that never draws to the backbuffer never acquires an image, but
its final submit still waited on the acquire semaphore, which nothing
signals, hanging the GPU. Skip the swap for such frames when finishing
them. This replaces the check for a frame with no steps at all, which
could also set it partway through a frame that acquires later.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
For when there's nothing to present to. Instead of a surface and swapchain,
it renders into images of its own through the VulkanPresentation interface
libretro uses, picking the graphics queue without a surface. Acquiring and
presenting signal and wait on the frame's semaphores with empty submits.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The name and value columns were hardcoded at x=17 and x=77. The control has
a fixed width in the dialog and can not be widened, so the 8 hex digits of a
GPR only just fit - and once the register list got a vertical scrollbar, the
~17px it takes off the client width pushed the last digits off the edge.
Derive the value column from the client width each paint instead, pulling it
in far enough for a whole value to fit and clipping anything longer rather
than letting it spill. Same for the category tab labels.
Float registers print with %g rather than %f: in a column that narrow, a
clipped %f of a large value is worse than useless (1e20 would read as
100000000), while %g keeps six significant digits and stays short for the
values you normally see.
imgui 1.92 hands the backend a list of dirty rectangles when its font atlas
grows, but thin3d could only replace a whole mip level, so every new glyph
re-uploaded the entire atlas.
Adds DrawContext::UpdateTextureRegions, taking a batch of regions so each
backend can submit them together:
- Vulkan: packs the regions into the push pool and issues a single
vkCmdCopyBufferToImage from the init command buffer, transitioning only
the level being written.
- OpenGL: new TEXTURE_SUBIMAGE init step. The existing sub-image path is a
render command needing an active render pass and a texture slot, which
doesn't fit here. Rows are packed caller-side since GLES2 lacks
GL_UNPACK_ROW_LENGTH.
- D3D11: UpdateSubresource with a box, no staging texture needed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing has generated or used a geometry shader since the GS paths were removed
- GeometryShaderGenerator is gone, and ShaderWriter's BeginGSMain/EndGSMain had
no callers at all. Removes ShaderStage::Geometry and everything hanging off it:
the GS preambles and GSMain helpers in ShaderWriter, the stage mappings in all
three thin3d backends, the D3D11 geometry shader plumbing (curGS_, the pipeline
and module members, gs_4_0 compilation), CreateGeometryShaderD3D11, the unused
PipelineFlags::USES_GEOMETRY_SHADER and PipelineManagerVulkan's
UsesGeometryShader().
Also stop enabling the Vulkan geometryShader device feature, since we no longer
have any use for it.
Kept on purpose: the device feature is still listed in the feature dumps (like
other capabilities we don't use), and the Vulkan shader cache header keeps its
now-always-zero geometry shader count so the on-disk format stays compatible.
VulkanGraphicsContext::InitSurface() threw away VulkanContext::InitSurface()'s
VkResult and carried on, so a failed surface init surfaced as
_dbg_assert_(GetAvailablePresentModes().size() > 0) in the VKContext
constructor rather than as a graphics error with the usual backend fallback.
The vkCreate*SurfaceKHR failure path in ReinitSurface() didn't log anything
either, so the assert was the only trace of it.
Now ReinitSurface() logs and sets init_error_ for all three ways it can bail
(surface creation, ChooseQueue, present mode enumeration), InitSurface()
checks the result, and MainThreadFunc() passes the message back out instead of
writing it to a local it then drops - Windows/main.cpp was reporting
"Failed to initialize main thread function." to the user.
Also deletes the Application on that failure path, which was leaked.
The loop queue-deleted the VkPipeline and nulled the slot without deleting the
Promise the array owns - DestroyVariantsInstant right below it shows the intended
ownership. That's one leaked Promise per destroyed variant per cached pipeline,
on every MSAA or resolution change.
It also wrote pipeline[] without taking mutex_, which the header documents as
protecting that array and which the render thread holds while reading and
replacing the same slots in PerformRenderPass. The two have to be fixed together:
the missing delete was the only thing keeping this a leak rather than a
use-after-free.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vd8ntC2brCUtCrDJMqLbs8
GetRenderPass() looks up and inserts into renderPasses_ from the main thread
(EndCurRenderStep, CreateGraphicsPipeline) and from the render thread
(PerformBindFramebufferAsRenderTarget), unsynchronized. The render thread really
does insert rather than only hit: PreprocessSteps rewrites the load actions to
CLEAR when it merges a clear-only pass into a later one, after the main thread
already looked up the pre-merge key. DenseHashMap::Insert can Grow(), which
reallocates the buckets out from under a concurrent Get().
VKRRenderPass::Get() has the same problem one level down - it creates the passes
lazily and is called from both threads on the same object, so two threads hitting
an empty slot each create a pass and one gets overwritten and leaked, while the
sample-count branch can queue a pass for deletion that the other thread is about
to hand to vkCreateGraphicsPipelines.
A mutex each. Handing the VKRRenderPass pointer out from under the map lock is
fine, entries are only ever erased all at once in DestroyDeviceObjects.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vd8ntC2brCUtCrDJMqLbs8
* TransitionDepthStencilImageAuto set dstAccessMask to TRANSFER_READ_BIT for
TRANSFER_DST_OPTIMAL. The color path and this function's own source-side switch
both use TRANSFER_WRITE_BIT - it's a copy-paste from the TRANSFER_SRC case two
lines up. Every depth copy and blit went through it.
* VulkanMayBeAvailable's per-device loop did anyGood = !blacklisted, overwriting
the verdict from earlier devices, so a blacklisted GPU enumerated after a good
one hid the Vulkan backend entirely. Hybrid-GPU machines are exactly what the
blacklist targets.
* The instance extension scan stopped as soon as it found the platform surface
extension, so a driver reporting that before VK_KHR_surface made us give up
with "Platform surface extension not found". Enumeration order isn't specified.
* CreateDevice only logged when vkCreateDevice failed, then carried on to report
success, call VulkanSetAvailable(true) and build a VMA allocator on a null
device behind an assert that's live in release builds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vd8ntC2brCUtCrDJMqLbs8
The previous commit moved everything out of the list before running callbacks, to
avoid appending to a vector being iterated. That regressed device teardown: a
callback can queue more deletes (~VKFramebuffer does, via ~VKRFramebuffer, which
queues image views, image allocations and framebuffers), and those land back on a
list that used to be picked up by the object loops later in the same pass.
That's harmless for the per-frame lists, since callbacks queue onto the global
list and a later frame drains it. But PerformPendingDeletes() drains the global
list itself, and DestroyDevice() calls it immediately before vmaDestroyAllocator
and vkDestroyDevice - so the re-queued objects were never destroyed at all.
Loop instead. In the per-frame case that's one extra empty lap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vd8ntC2brCUtCrDJMqLbs8
pipelineLayouts_ was mutated from the main thread (CreatePipelineLayout, and the
deferred callback queued by DestroyPipelineLayout) while the render thread walked
it every frame in FlushDescriptors. Exiting a game in Vulkan mode hits this
reliably: ~GPU_Vulkan stops the render thread and destroys the draw engine's
layout, but the destruction is deferred onto the delete list and doesn't actually
run until a BeginFrame two frames later, with the render thread running again.
Guard the list, and the lifetime of the layouts in it, with a mutex.
The global delete list had the same problem - VulkanDescSetPool::Recreate queues
the old pool from FlushDescSets on the render thread, which happens for real once
a game goes past the initial 1024 descriptors, while the main thread moves the
list into the current frame's list in EndFrame(). Lock the queueing functions and
Take's source list.
While in there:
* Take() didn't move queryPools_, so query pools queued for deletion sat on the
global list until device teardown instead of being deleted a few frames later.
* PerformDeletes now drains into a local list before destroying anything. A
callback is allowed to queue further deletes (~VKFramebuffer's does, via
~VKRFramebuffer), which used to append to the very vector being iterated.
They now get the normal deferral instead of running in the same pass.
* Missing semicolon in BeginFrame that only compiles because VLOG is empty.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vd8ntC2brCUtCrDJMqLbs8
libretro/libretro_vulkan.cpp got PPSSPP's libretro core working with
RetroArch's Vulkan integration by globally monkey-patching PPSSPP's Vulkan
loader function pointers (vkCreateInstance, vkCreateDevice,
vkCreateSwapchainKHR, vkAcquireNextImageKHR, vkQueuePresentKHR,
vkQueueSubmit, etc.) so the unmodified VulkanContext class would end up
wrapping RetroArch's already-existing VkInstance/VkDevice instead of
creating its own, and so a fake VkSwapchainKHR (a self-managed array of
images synced against RetroArch's retro_hw_render_interface_vulkan
callbacks) could stand in for the real swapchain that libretro's Vulkan
model doesn't have. Flagged in-code as "a wacky wrapper".
Replaces that with first-class support in VulkanContext for the two things
libretro actually needs:
- Adopting an externally-created instance/device instead of faking
vkCreateInstance/vkCreateDevice: VulkanContext::CreateInstanceExternal()
adopts RetroArch's VkInstance; CreateDevice() gained optional
extraDeviceExtensions/extraRequiredFeatures params so RetroArch's
requirements get merged into a real vkCreateDevice() call;
ownsInstance_/ownsDevice_ flags (the latter set via
SetDeviceExternallyOwned()) mean DestroyInstance()/DestroyDevice() skip
the real vkDestroy* calls when something else owns the object, without
needing to intercept anything. VulkanLoader gained
VulkanLoadFromGetInstanceProcAddr() for bootstrapping from a
host-supplied proc-addr getter instead of dlopen/dlsym-ing the loader
ourselves - vkGetDeviceProcAddr is resolved via the real instance handle
(not NULL), since per the Vulkan spec it's not one of the handful of
commands queryable with a NULL instance.
- A pluggable presentation backend (Common/GPU/Vulkan/VulkanPresentation.h)
for hosts with no real VK_KHR_swapchain, replacing the fake-swapchain-
handle trick. VulkanContext::GetPresentation() is null by default, so
every existing platform's real-swapchain code path is untouched;
libretro/LibretroVulkanPresentation implements this interface directly
against retro_hw_render_interface_vulkan, as real class state instead of
file-scope globals. Several pieces of state that are normally only
populated as a side effect of ReinitSurface()/InitSwapchain() - the
graphics queue/queue family index (ChooseQueue() is entangled with
real-surface presentation-support checks), the swapchain format, and
the available present modes - needed presentation-aware fallbacks since
libretro never calls that real-surface path at all.
libretro/LibretroVulkanContext.cpp now drives VulkanContext's real, public
API directly - no more hijacked function pointers, no more fake surface or
swapchain. libretro/libretro_vulkan.cpp is deleted.
Verified with a full build+run in RetroArch (not just compile-time
checks): the libretro Makefile doesn't track header dependencies
(cl.exe doesn't support -MMD/-MP, and Makefile.common never sets up an
equivalent), so a `make clean` full rebuild is required after any header
change to avoid linking stale object code from before the change - several
of the fixes above were initially masked by exactly that.
It was removed during https://github.com/hrydgard/ppsspp/pull/21982
On Linux Wayland however this can be observed without:
34:25:550 Vulkan/VulkanContext.cpp:1401 I[G3D]: surfCapabilities_.current: -1x-1
34:25:550 Vulkan/VulkanContext.cpp:1406 I[G3D]: surfCapabilities_.current after clamp: -1x-1 min: 1x1 max: 32768x32768 computed: 1x1 cbdraw