Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Added `VulkanVideoDriver`, a headless Vulkan video driver
implementing the full `retro_hw_render_interface_vulkan` (version 5)
and the Vulkan context negotiation interface,
enabling Vulkan-only cores (e.g. Azahar) to run under libretro.py.
Requires the new `libretro.py[vulkan]` extra
and a Vulkan loader (MoltenVK on macOS).
- Added ctypes bindings for the types in `libretro_vulkan.h`
to `libretro.api.video`.
- `VulkanVideoDriver` uploads software-rendered frames to a `VkImage`
and reads them back through its capture path,
mirroring the OpenGL driver's texture upload;
if Vulkan is unavailable, initialization fails
rather than silently falling back to CPU-side frames,
since the driver exists to test cores' Vulkan support.
- `VulkanVideoDriver` implements `RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER`
by handing the core a pointer to persistently-mapped host-visible Vulkan memory.
- Implemented `RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE`
and `RETRO_ENVIRONMENT_GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT`,
and added a `context_negotiation_interface` property to `VideoDriver`.
- Added `VideoDriver.context_negotiation_version`,
which lets each driver report the negotiation interface version it supports;
`VulkanVideoDriver` accepts a `negotiation_version` argument
so cores can be tested against the version 1 negotiation interface.

### Fixed

- Fixed `RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE` writing the interface struct
by value instead of writing a pointer to it.
- Fixed `RETRO_ENVIRONMENT_GET_PERF_INTERFACE` crashing
by passing bound methods instead of function-pointer instances
to the `retro_perf_callback` struct (affected e.g. Beetle PSX HW).
- Fixed VFS file opens failing for the `WRITE | UPDATE_EXISTING` access mode
(affected e.g. Flycast).
- The VFS `seek` handler now returns 0 on success instead of the new position.
libretro.h documents returning the position,
but RetroArch returns 0 for ordinary buffered files
and cores are written against that behavior
(PPSSPP treats any non-zero return as an error,
making every file appear empty under a spec-conforming frontend).
- Fixed core options without an explicit default value tripping an assertion;
libretro.h treats the first value as the default (affected e.g. Mupen64Plus-Next).
- Sample cores now build with the `.dylib` suffix on macOS,
matching what `libretro.samples` expects to load.
- Added `VideoDriver.destroy_hw_context()`,
which calls the core's `context_destroy` without releasing
the driver's own graphics resources.
`Session` now calls it immediately before `retro_unload_game`
(matching RetroArch's `core_unload_game`),
then releases the video driver's GPU resources after `retro_deinit`,
because cores may have background threads submitting GPU work
until unload stops them.
Fixes crashes on session teardown with SwanStation, PPSSPP,
mupen64plus-next (paraLLEl-RDP), and Azahar.
- `MultiVideoDriver` now shuts down the outgoing video driver
(including its hardware context) when switching drivers at runtime.
- `VulkanVideoDriver` now permanently retains retired render-interface structs
with their callbacks replaced by a native no-op,
because some cores keep the interface pointer
in objects destroyed at process exit.
- Fixed `retro_video_refresh_t` rejecting NULL frame duping:
ctypes exposes a NULL pointer's value as `None`, not 0
(affected e.g. SwanStation).

## [0.8.2] - 2026-07-14

### Fixed

- Remove stray references to `SessionBuilder` from the documentation.
Expand Down
24 changes: 24 additions & 0 deletions cmake/LibretroSamples.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,27 @@ else()
"skipping GL sample cores.")
endif()
endif()

# --- video/vulkan/ --------------------------------------------------------
#
# The Vulkan core resolves every entry point at runtime through
# ``get_instance_proc_addr`` (via the upstream ``vulkan_symbol_wrapper``),
# so only the Vulkan *headers* are needed at build time — no loader library
# is linked. Skip the core when the headers aren't installed.
find_package(Vulkan QUIET)

if(Vulkan_INCLUDE_DIRS)
add_sample_core(
NAME vulkan_rendering
CATEGORY video
SOURCES
"${_lrs_src}/video/vulkan/vk_rendering/libretro-test.c"
"${_lrs_src}/video/vulkan/vk_rendering/vulkan_symbol_wrapper.c"
INCLUDES
"${_lrs_src}/video/vulkan/vk_rendering"
${Vulkan_INCLUDE_DIRS}
COMPILE_DEFINITIONS ${_lrs_common_defs} VK_NO_PROTOTYPES
)
else()
message(STATUS "libretro.py: Vulkan headers not found; skipping the Vulkan sample core.")
endif()
6 changes: 6 additions & 0 deletions cmake/SampleCore.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ function(add_sample_core)
OUTPUT_NAME "${SC_NAME}_libretro"
)

if(APPLE)
# MODULE libraries default to ".so" on macOS,
# but frontends (and libretro.samples._loader) expect ".dylib"
set_target_properties(${SC_NAME} PROPERTIES SUFFIX ".dylib")
endif()

if(WIN32)
# libretro.h declares the public entry points with __declspec(dllexport),
# so explicit per-symbol exports already cover the API. Setting this
Expand Down
59 changes: 59 additions & 0 deletions docs/guide/vulkan.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
Running Vulkan Cores
====================

libretro.py includes :class:`.VulkanVideoDriver`,
a headless video driver for cores that render with Vulkan
(``RETRO_HW_CONTEXT_VULKAN``),
such as Azahar or other 3D-heavy emulators
whose OpenGL renderers are unavailable or too slow on some platforms.

The driver implements the full version 5
:class:`.retro_hw_render_interface_vulkan`
and the Vulkan context negotiation interface;
each hardware frame is copied into host memory,
so :meth:`.VideoDriver.screenshot` works the same way it does
with the OpenGL and software drivers.
There is no window or swapchain.

Installation
------------

Install the ``vulkan`` extra alongside libretro.py::

pip install libretro.py[vulkan]

The driver also needs a Vulkan loader library at runtime:

Linux
Install the Vulkan loader from your package manager
(e.g. ``libvulkan1`` on Debian/Ubuntu).

Windows
``vulkan-1.dll`` ships with your graphics driver.

macOS
Install MoltenVK_ and the Vulkan loader,
e.g. with Homebrew::

brew install molten-vk vulkan-loader

A Homebrew-installed Python will find ``libvulkan.dylib`` automatically.
Other Python builds may need the library path set at launch::

DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib python my_script.py

Usage
-----

When the ``vulkan`` extra is installed,
:data:`.DEFAULT_DRIVER_MAP` automatically maps
:attr:`.HardwareContext.VULKAN` to :class:`.VulkanVideoDriver`,
so a default :class:`.Session` will use it
whenever a core requests a Vulkan context.
To use it explicitly::

from libretro.drivers.video.vulkan import VulkanVideoDriver

driver = VulkanVideoDriver()

.. _MoltenVK: https://github.com/KhronosGroup/MoltenVK
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Ease of use, flexibility, and complete API support are top priorities.
:caption: Guides

guide/env
guide/vulkan
guide/renderdoc
guide/glossary

Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ dev = [
cli = ['typer == 0.25.1']
opengl = ['moderngl[headless] >= 5.12', 'PyOpenGL == 3.1.*']
opengl-window = ['moderngl-window == 3.*', "libretro.py[opengl]"]
all = ["libretro.py[cli,opengl,opengl-window]"]
vulkan = ['vulkan == 1.3.*']
all = ["libretro.py[cli,opengl,opengl-window,vulkan]"]

[project.urls]
Homepage = "https://github.com/JesseTG/libretro.py"
Expand Down Expand Up @@ -166,6 +167,7 @@ testpaths = ["tests", "src"]
markers = [
"isolated: run the test in a fresh subprocess (provided by pytest-isolated)",
"opengl: requires the libretro.py[opengl] extra and a working GL context",
"vulkan: requires the libretro.py[vulkan] extra and a working Vulkan installation",
"slow: long-running; excluded from PR CI by default",
]
required_plugins = ["pytest-assert-type", "hypothesis", "pytest-isolated"]
Expand Down
5 changes: 5 additions & 0 deletions src/libretro/api/vfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ class VfsFileAccess(IntFlag):
UPDATE_EXISTING = RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING

READ_WRITE_EXISTING = READ_WRITE | UPDATE_EXISTING
WRITE_EXISTING = WRITE | UPDATE_EXISTING

@property
def open_flag(self) -> Literal["rb", "wb", "w+b", "r+b"]:
Expand All @@ -206,6 +207,10 @@ def open_flag(self) -> Literal["rb", "wb", "w+b", "r+b"]:
return "w+b"
case VfsFileAccess.READ_WRITE_EXISTING:
return "r+b"
case VfsFileAccess.WRITE_EXISTING:
# Write-only without truncation; Python has no such mode,
# so the closest match is read-write without truncation
return "r+b"
case _:
raise ValueError(f"Invalid VfsFileAccess: {self}")

Expand Down
1 change: 1 addition & 0 deletions src/libretro/api/video/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
from .frame import *
from .negotiate import *
from .render import *
from .vulkan import *
Loading