vulkan api support - #29
Conversation
In software mode the driver now brings up a real Vulkan device (instance/device/capture resources, no negotiation or render interface since there's no core context), and every software frame is uploaded to a VkImage via staging + vkCmdCopyBufferToImage, then read back through the same capture path used for hardware frames
JesseTG
left a comment
There was a problem hiding this comment.
Thanks for your contribution! I do have some feedback I'd like you to address before I'm ready to merge (and therefore maintain) this.
|
Also, could you retarget this PR against |
…negotiation version Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JesseTG
left a comment
There was a problem hiding this comment.
Nearly there, thank you for your patience. Got a few more changes I'd like you to make.
| case int(): | ||
| return handle | ||
| case _: | ||
| return handle.value or 0 |
There was a problem hiding this comment.
Use libretro.api._utils.address instead, please.
| ``lock_queue``/``wait_sync_index`` after the Python interpreter has | ||
| finalized — which would crash if they still pointed at ctypes closures. | ||
| ``free`` is a safe stand-in: every callback receives this driver's | ||
| ``handle``, which is ``NULL``, and ``free(NULL)`` is a no-op. |
There was a problem hiding this comment.
Where in mupen64plus-next does this happen?
|
|
||
| # Interface structs whose Python callbacks have been replaced with native stubs; | ||
| # kept alive forever because cores may hold the pointer in exit-time destructors | ||
| _RETIRED_INTERFACES: list[retro_hw_render_interface_vulkan] = [] |
There was a problem hiding this comment.
Does this really have to be global?
| )[0] | ||
|
|
||
| assert isinstance(interface, retro_hw_render_context_negotiation_interface_vulkan) | ||
| self._negotiation = interface |
There was a problem hiding this comment.
Please make this a deepcopy of the argument so that cores don't have to keep an interface struct alive. (And to protect against accidental modification.)
| if not create_info_ptr: | ||
| return 0 | ||
|
|
||
| info = ctypes.cast(create_info_ptr, POINTER(_VkDeviceCreateInfo))[0] |
There was a problem hiding this comment.
Use TypedPointer instead of plain POINTER, please. (Not just here, everywhere in the file.) This way the type checker can catch mistakes.
| return (1 << self._sync_index_count) - 1 | ||
|
|
||
| def _set_command_buffers(_handle: c_void_ptr | None, num_cmd: int, cmd: Any) -> None: | ||
| # cmd is a POINTER(VkCommandBuffer): an array of void pointers |
There was a problem hiding this comment.
Cast cmd to TypedPointer[VkCommandBuffer], please.
| self._interface = retro_hw_render_interface_vulkan( | ||
| interface_type=HardwareRenderInterfaceType.VULKAN, | ||
| interface_version=RETRO_HW_RENDER_INTERFACE_VULKAN_VERSION, | ||
| handle=None, |
There was a problem hiding this comment.
I'd like it if handle was id(self), so that the driver can verify that the core passes in the handle pointer like it's supposed to.
|
|
||
| interface[0] = driver_interface | ||
| # The data is a retro_hw_render_interface **; | ||
| # write the address of the driver's (long-lived) interface struct into it | ||
| cast(interface, POINTER(c_void_p))[0] = addressof(driver_interface) |
There was a problem hiding this comment.
If I got the type wrong, then maybe it would be best to retype interface as TypedPointer[TypedPointer[retro_hw_render_interface]].
I one-shotted this with fable and haven't even read it yet
(The model wrote the summary below; a human has still not read the diff. All claims are backed by the test suite and by screenshots from real cores.)
This adds the Vulkan video driver that
HardwareContext.VULKAN's docstring has been asking for. It was developed against, and validated with, seven real Vulkan cores on macOS/MoltenVK: Azahar, Beetle PSX HW, Flycast, Dolphin, Mupen64Plus-Next (paraLLEl-RDP/RSP), PPSSPP, and SwanStation — each boots, renders, and screenshots correctly through this driver.What's new
VulkanVideoDriver(drivers/video/vulkan/): a headless driver implementing the full version-5retro_hw_render_interface_vulkan(set_image, sync indices, queue locking,set_command_buffers,set_signal_semaphore) plus the context negotiation interface (v1get_application_info/create_deviceand v2create_instance/create_device2wrapper paths, with fallback device creation). Frames are captured to host memory withvkCmdCopyImageToBuffer, soscreenshot()works like the GL/software drivers. Supported capture formats: RGBA8/BGRA8 (+sRGB), A1R5G5B5/R5G6B5/B5G6R5 (Beetle scans out 16-bit when dithering), and A2B10G10R10/A2R10G10B10 (Dolphin).libretro_vulkan.h(api/video/vulkan.py), with struct layouts unit-tested against values computed from the C headers.vulkanpackage (newlibretro.py[vulkan]extra) and bridges handles as integers at the ABI boundary. On macOS it enablesVK_KHR_portability_enumerationand creates aVK_EXT_headless_surfaceto hand to cores — PPSSPP crashes ifcreate_devicereceives a NULL surface.VideoDriverprotocol members:context_negotiation_interface(backs the previously-stubbedSET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE) anddestroy_hw_context()(see lifecycle below).DEFAULT_DRIVER_MAP, documented in a new guide page, exercised by a new integration test using the upstreamvk_renderingsample core (gated on Vulkan headers at build time).Pre-existing bugs fixed along the way
Each of these was found because a real core tripped over it:
GET_HW_RENDER_INTERFACEwrote the interface struct by value into aretro_hw_render_interface **instead of writing a pointer (never exercised before — no driver returned an interface).GET_PERF_INTERFACEcrashed: bound methods were assigned toretro_perf_callbackfields instead of CFUNCTYPE instances (Beetle PSX HW requests perf atretro_init).WRITE | UPDATE_EXISTINGaccess mode (write without truncation) was rejected (Flycast uses it).seeknow returns 0 on success instead of the new position. libretro.h documents returning the position, but RetroArch returnsfseek's result for ordinary files, and cores are written against that — PPSSPP treats any non-zero return as an error, which made every file look empty under a spec-conforming frontend. De facto beats de jure here; there's an ABI-level regression test.default_valuetripped an assertion; per libretro.h the first value is the default (Mupen64Plus-Next has such an option).retro_video_refresh_trejected NULL frame dupes: ctypes exposes a NULL pointer's.valueasNone, not0(SwanStation dupes frames)..so, butlibretro.samples._loaderexpects.dylib.Hardware-context lifecycle
Session.__exit__now mirrors RetroArch'score_unload_game: the core'scontext_destroyfires immediately beforeretro_unload_game(SwanStation and PPSSPP need their emulated system alive for it), while the driver's own GPU objects are released only afterretro_deinit(Azahar has GPU threads that only stop during unload). Retired interface structs are kept for the life of the process with their callbacks swapped to a native no-op, because paraLLEl-RDP holds the interface pointer in exit-time static destructors that would otherwise call into a finalized interpreter.Testing
pytestpasses (526 tests here): struct-layout unit tests, env-call dispatch tests, driver tests markedvulkanthat run against a real GPU (a simulated core clears aVkImage, hands it over viaset_image, and pixel values are asserted), VFS ABI tests, and thevk_renderingend-to-end test. pyright (strict) and ruff are clean.vulkanextra or a loader is missing; everything else runs without Vulkan installed.Known limitations (documented): headless only (no window path yet), and the capture path reads the underlying image rather than sampling the
VkImageView, so view swizzles are ignored — correct for all cores tested.