feat(sceVideoOut): implement sdl2 window and present thread

This commit is contained in:
boykopovar 2026-09-12 17:32:24 +03:00
parent 49e200c88a
commit d225df7449
7 changed files with 346 additions and 61 deletions

View File

@ -9,9 +9,9 @@ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -static-libgcc -st
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
set(SDL_STATIC ON CACHE BOOL "" FORCE)
set(SDL_TEST OFF CACHE BOOL "" FORCE)
set(SDL_VIDEO OFF CACHE BOOL "" FORCE)
set(SDL_VIDEO ON CACHE BOOL "" FORCE)
set(SDL_RENDER OFF CACHE BOOL "" FORCE)
set(SDL_X11 OFF CACHE BOOL "" FORCE)
set(SDL_X11 ON CACHE BOOL "" FORCE)
set(SDL_WAYLAND OFF CACHE BOOL "" FORCE)
set(SDL_OPENGL OFF CACHE BOOL "" FORCE)
set(SDL_OPENGLES OFF CACHE BOOL "" FORCE)

View File

@ -5,5 +5,8 @@ add_library(libSceVideoOut SHARED EXCLUDE_FROM_ALL
${CMAKE_CURRENT_SOURCE_DIR}/src/VideoOutDriver.cpp
)
target_include_directories(libSceVideoOut PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(libSceVideoOut PRIVATE libc)
target_include_directories(libSceVideoOut PRIVATE
${LIBS_INCLUDE_DIR}
${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include
)
target_link_libraries(libSceVideoOut PRIVATE libc libkernel SDL2-static)

View File

@ -4,8 +4,12 @@
#include <array>
#include <condition_variable>
#include <cstdint>
#include <list>
#include <mutex>
#include <thread>
#include <vector>
#include "SDL.h"
#include "SceTypes.hpp"
static constexpr int VIDEO_OUT_ERROR_INVALID_VALUE = -2144796671;
@ -29,6 +33,7 @@ static constexpr int VIDEO_OUT_BUS_TYPE_SUB = 2;
static constexpr int VIDEO_OUT_BUFFER_NUM_MAX = 16;
static constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX = 4;
static constexpr int VIDEO_OUT_NUM_MAX = 4;
static constexpr std::size_t VIDEO_OUT_FLIP_QUEUE_CAPACITY = 16;
static constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED = 0;
static constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED = 1;
@ -64,10 +69,20 @@ struct BufferAttributeGroup {
bool occupied = false;
};
struct EventRegistration {
KernelEqueue eq = 0;
uint64_t generation = 0;
};
struct VideoOutConfig {
std::mutex mutex;
std::condition_variable_any vblankCond;
std::vector<EventRegistration> flipEvents;
std::vector<EventRegistration> vblankEvents;
std::vector<EventRegistration> preVblankEvents;
std::vector<EventRegistration> outputModeEvents;
uint32_t width = 1920;
uint32_t height = 1080;
uint64_t generation = 0;
@ -85,16 +100,49 @@ struct VideoOutConfig {
std::array<BufferAttributeGroup, VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX> groups{};
};
struct VideoOutDriver {
std::mutex mutex;
VideoOutConfig contexts[VIDEO_OUT_NUM_MAX];
struct FlipRequest {
VideoOutConfig* cfg = nullptr;
uint64_t generation = 0;
int index = 0;
int flipMode = 0;
int64_t flipArg = 0;
};
class VideoOutDriver {
public:
static VideoOutDriver& Get();
VideoOutDriver();
~VideoOutDriver();
VideoOutDriver(const VideoOutDriver&) = delete;
VideoOutDriver& operator=(const VideoOutDriver&) = delete;
int Open(int busType);
bool Close(int handle);
VideoOutConfig* GetConfig(int handle);
bool IsOpen(int handle);
void SubmitFlip(VideoOutConfig* cfg, int index, int flipMode, int64_t flipArg);
private:
void presentLoop(std::stop_token token);
void vblankBegin();
void vblankEnd();
void processFlip(const FlipRequest& req);
void triggerEvents(VideoOutConfig& cfg, int eventKind, void* triggerData);
std::mutex mutex;
VideoOutConfig contexts[VIDEO_OUT_NUM_MAX];
std::mutex flipMutex;
std::condition_variable flipCond;
std::list<FlipRequest> flipQueue;
SDL_Window* window = nullptr;
SDL_Surface* windowSurface = nullptr;
std::jthread presentThread;
};
#endif

View File

@ -1,30 +1,30 @@
#include <cstring>
#include <mutex>
#include <stdexcept>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libSceVideoOut/include/Buffer.hpp"
#include "prx/libSceVideoOut/include/VideoOutDriver.hpp"
extern "C" {
void APS5_VABI sceVideoOutSetBufferAttribute2(VideoOutBufferAttribute2* attribute, uint64_t pixel_format, uint32_t tiling_mode, uint32_t width, uint32_t height, uint64_t option, uint32_t dcc_control, uint64_t dcc_cb_register_clear_color) {
void APS5_VABI sceVideoOutSetBufferAttribute2(VideoOutBufferAttribute2* attribute, uint64_t pixelFormat, uint32_t tilingMode, uint32_t width, uint32_t height, uint64_t option, uint32_t dccControl, uint64_t dccCbRegisterClearColor) {
if (attribute == nullptr) {
return;
throw std::runtime_error("sceVideoOutSetBufferAttribute2: null attribute");
}
std::memset(attribute, 0, sizeof(VideoOutBufferAttribute2));
attribute->tiling_mode = tiling_mode;
attribute->tiling_mode = tilingMode;
attribute->aspect_ratio = 0;
attribute->width = width;
attribute->height = height;
attribute->pitch_in_pixel = 0;
attribute->option = option;
attribute->pixel_format = pixel_format;
attribute->dcc_cb_register_clear_color = dcc_cb_register_clear_color;
attribute->dcc_control = dcc_control;
attribute->pixel_format = pixelFormat;
attribute->dcc_cb_register_clear_color = dccCbRegisterClearColor;
attribute->dcc_control = dccControl;
}
int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int set_index, int buffer_index_start, const VideoOutBuffers* buffers, int buffer_num, const VideoOutBufferAttribute2* attribute, int category, void* option) {
int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int setIndex, int bufferIndexStart, const VideoOutBuffers* buffers, int bufferNum, const VideoOutBufferAttribute2* attribute, int category, void* option) {
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg == nullptr) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
@ -38,7 +38,7 @@ int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int set_index, int buffer_
if (option != nullptr) {
return VIDEO_OUT_ERROR_INVALID_OPTION;
}
if (set_index < 0 || set_index >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX || buffer_index_start < 0 || buffer_index_start >= VIDEO_OUT_BUFFER_NUM_MAX || buffer_num < 1 || buffer_num > VIDEO_OUT_BUFFER_NUM_MAX || buffer_index_start + buffer_num > VIDEO_OUT_BUFFER_NUM_MAX) {
if (setIndex < 0 || setIndex >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX || bufferIndexStart < 0 || bufferIndexStart >= VIDEO_OUT_BUFFER_NUM_MAX || bufferNum < 1 || bufferNum > VIDEO_OUT_BUFFER_NUM_MAX || bufferIndexStart + bufferNum > VIDEO_OUT_BUFFER_NUM_MAX) {
return VIDEO_OUT_ERROR_INVALID_VALUE;
}
if (category != VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED && category != VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED) {
@ -48,11 +48,11 @@ int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int set_index, int buffer_
if (cfg->closing) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
}
if (cfg->groups[set_index].occupied) {
if (cfg->groups[setIndex].occupied) {
return VIDEO_OUT_ERROR_INVALID_INDEX;
}
for (int i = 0; i < buffer_num; i++) {
if (cfg->buffers[buffer_index_start + i].Occupied()) {
for (int i = 0; i < bufferNum; i++) {
if (cfg->buffers[bufferIndexStart + i].Occupied()) {
return VIDEO_OUT_ERROR_SLOT_OCCUPIED;
}
}
@ -60,18 +60,20 @@ int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int set_index, int buffer_
group.attribute = *attribute;
group.category = category;
group.occupied = true;
cfg->groups[set_index] = group;
for (int i = 0; i < buffer_num; i++) {
cfg->groups[setIndex] = group;
cfg->width = attribute->width;
cfg->height = attribute->height;
for (int i = 0; i < bufferNum; i++) {
VideoOutBuffer buf{};
buf.groupIndex = set_index;
buf.groupIndex = setIndex;
buf.dataAddress = reinterpret_cast<uint64_t>(buffers[i].data);
buf.metadataAddress = reinterpret_cast<uint64_t>(buffers[i].metadata);
cfg->buffers[buffer_index_start + i] = buf;
cfg->buffers[bufferIndexStart + i] = buf;
}
return 0;
}
int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int set_index, const VideoOutBufferAttribute2* attribute, void* option) {
int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int setIndex, const VideoOutBufferAttribute2* attribute, void* option) {
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg == nullptr) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
@ -79,51 +81,50 @@ int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int set_index,
if (attribute == nullptr) {
return VIDEO_OUT_ERROR_INVALID_OPTION;
}
if (set_index < 0 || set_index >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX) {
if (setIndex < 0 || setIndex >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX) {
return VIDEO_OUT_ERROR_INVALID_INDEX;
}
if (option != nullptr) {
return VIDEO_OUT_ERROR_INVALID_OPTION;
}
std::unique_lock lock(cfg->mutex);
if (cfg->closing || !cfg->groups[set_index].occupied) {
if (cfg->closing || !cfg->groups[setIndex].occupied) {
return VIDEO_OUT_ERROR_INVALID_INDEX;
}
cfg->groups[set_index].attribute = *attribute;
cfg->groups[setIndex].attribute = *attribute;
return 0;
}
int APS5_VABI sceVideoOutUnregisterBuffers(int handle, int set_index) {
int APS5_VABI sceVideoOutUnregisterBuffers(int handle, int setIndex) {
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg == nullptr) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
}
if (set_index < 0 || set_index >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX) {
if (setIndex < 0 || setIndex >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX) {
return VIDEO_OUT_ERROR_INVALID_INDEX;
}
std::unique_lock lock(cfg->mutex);
if (cfg->closing) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
}
if (!cfg->groups[set_index].occupied) {
if (!cfg->groups[setIndex].occupied) {
return VIDEO_OUT_ERROR_INVALID_INDEX;
}
cfg->groups[set_index] = BufferAttributeGroup{};
cfg->groups[setIndex] = BufferAttributeGroup{};
for (auto& buf : cfg->buffers) {
if (buf.groupIndex == set_index) {
if (buf.groupIndex == setIndex) {
buf = VideoOutBuffer{};
}
}
return 0;
}
int APS5_VABI sceVideoOutSubmitFlip(int handle, int index, int flip_mode, int64_t flip_arg) {
(void)flip_arg;
int APS5_VABI sceVideoOutSubmitFlip(int handle, int index, int flipMode, int64_t flipArg) {
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg == nullptr) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
}
if (flip_mode < VIDEO_OUT_FLIP_MODE_VSYNC || flip_mode > VIDEO_OUT_FLIP_MODE_VSYNC_MULTI) {
if (flipMode < VIDEO_OUT_FLIP_MODE_VSYNC || flipMode > VIDEO_OUT_FLIP_MODE_VSYNC_MULTI) {
return VIDEO_OUT_ERROR_INVALID_VALUE;
}
if (index < VIDEO_OUT_BUFFER_INDEX_BLACK || index >= VIDEO_OUT_BUFFER_NUM_MAX) {
@ -137,9 +138,8 @@ int APS5_VABI sceVideoOutSubmitFlip(int handle, int index, int flip_mode, int64_
if (!special && !cfg->buffers[index].Occupied()) {
return VIDEO_OUT_ERROR_INVALID_INDEX;
}
cfg->flipStatus.currentBuffer = index;
cfg->flipStatus.flipArg = flip_arg;
cfg->flipStatus.count++;
lock.unlock();
VideoOutDriver::Get().SubmitFlip(cfg, index, flipMode, flipArg);
return 0;
}

View File

@ -1,12 +1,22 @@
#include <algorithm>
#include <chrono>
#include <mutex>
#include <stdexcept>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libkernel/Equeue/Equeue.hpp"
#include "prx/libSceVideoOut/include/Event.hpp"
#include "prx/libSceVideoOut/include/VideoOutDriver.hpp"
static int RegisterVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind, void* udata) {
static std::vector<EventRegistration>* getEventList(VideoOutConfig& cfg, int16_t eventKind) {
if (eventKind == VIDEO_OUT_EVENT_FLIP) return &cfg.flipEvents;
if (eventKind == VIDEO_OUT_EVENT_VBLANK) return &cfg.vblankEvents;
if (eventKind == VIDEO_OUT_EVENT_PRE_VBLANK_START) return &cfg.preVblankEvents;
if (eventKind == VIDEO_OUT_EVENT_SET_MODE) return &cfg.outputModeEvents;
throw std::runtime_error("getEventList: unknown event kind");
}
static int registerVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind, void* udata) {
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg == nullptr) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
@ -57,16 +67,30 @@ static int RegisterVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind,
e->event.data = 0;
};
const int result = EqueueAddEvent_nid_postfix(eq, event);
return result == EQUEUE_ERROR_EBADF ? VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE : result;
if (result == EQUEUE_ERROR_EBADF) {
return VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE;
}
std::unique_lock lock(cfg->mutex);
EventRegistration reg;
reg.eq = eq;
reg.generation = cfg->generation;
getEventList(*cfg, eventKind)->push_back(reg);
return result;
}
static int DeleteVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind) {
static int deleteVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind) {
if (!VideoOutDriver::Get().IsOpen(handle)) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
}
if (eq == 0 || !EqueuePin_nid_postfix(eq)) {
return VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE;
}
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg != nullptr) {
std::unique_lock lock(cfg->mutex);
auto* events = getEventList(*cfg, eventKind);
events->erase(std::remove_if(events->begin(), events->end(), [eq](const EventRegistration& r) { return r.eq == eq; }), events->end());
}
const int result = EqueueDeleteEvent_nid_postfix(eq, static_cast<uintptr_t>(eventKind), EVFILT_VIDEO_OUT);
if (result == EQUEUE_ERROR_EBADF) {
return VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE;
@ -77,31 +101,31 @@ static int DeleteVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind) {
extern "C" {
int APS5_VABI sceVideoOutAddFlipEvent(KernelEqueue eq, int handle, void* udata) {
return RegisterVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_FLIP, udata);
return registerVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_FLIP, udata);
}
int APS5_VABI sceVideoOutAddVblankEvent(KernelEqueue eq, int handle, void* udata) {
return RegisterVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_VBLANK, udata);
return registerVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_VBLANK, udata);
}
int APS5_VABI sceVideoOutAddPreVblankStartEvent(KernelEqueue eq, int handle, void* udata) {
return RegisterVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_PRE_VBLANK_START, udata);
return registerVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_PRE_VBLANK_START, udata);
}
int APS5_VABI sceVideoOutAddOutputModeEvent(KernelEqueue eq, int handle, void* udata) {
return RegisterVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_SET_MODE, udata);
return registerVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_SET_MODE, udata);
}
int APS5_VABI sceVideoOutDeleteFlipEvent(KernelEqueue eq, int handle) {
return DeleteVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_FLIP);
return deleteVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_FLIP);
}
int APS5_VABI sceVideoOutDeleteVblankEvent(KernelEqueue eq, int handle) {
return DeleteVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_VBLANK);
return deleteVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_VBLANK);
}
int APS5_VABI sceVideoOutDeletePreVblankStartEvent(KernelEqueue eq, int handle) {
return DeleteVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_PRE_VBLANK_START);
return deleteVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_PRE_VBLANK_START);
}
int APS5_VABI sceVideoOutGetEventId(const KernelEvent* ev) {

View File

@ -1,12 +1,12 @@
#include <cstring>
#include <mutex>
#include <stdexcept>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libSceVideoOut/include/Output.hpp"
#include "prx/libSceVideoOut/include/VideoOutDriver.hpp"
static int ValidateOutputConfig(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) {
static int validateOutputConfig(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) {
if (!VideoOutDriver::Get().IsOpen(handle)) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
}
@ -28,18 +28,18 @@ static int ValidateOutputConfig(int handle, uint64_t mode, const VideoOutOutputO
extern "C" {
int APS5_VABI sceVideoOutOpen(int user_id, int bus_type, int index, const void* param) {
int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* param) {
(void)param;
if (user_id != 255 && user_id != 0) {
if (userId != 255 && userId != 0) {
return VIDEO_OUT_ERROR_INVALID_VALUE;
}
if (bus_type != VIDEO_OUT_BUS_TYPE_MAIN && bus_type != VIDEO_OUT_BUS_TYPE_OVERLAY && bus_type != VIDEO_OUT_BUS_TYPE_SUB) {
if (busType != VIDEO_OUT_BUS_TYPE_MAIN && busType != VIDEO_OUT_BUS_TYPE_OVERLAY && busType != VIDEO_OUT_BUS_TYPE_SUB) {
return VIDEO_OUT_ERROR_INVALID_VALUE;
}
if (index != 0) {
return VIDEO_OUT_ERROR_INVALID_VALUE;
}
const int handle = VideoOutDriver::Get().Open(bus_type);
const int handle = VideoOutDriver::Get().Open(busType);
if (handle < 0) {
return VIDEO_OUT_ERROR_RESOURCE_BUSY;
}
@ -100,7 +100,7 @@ int APS5_VABI sceVideoOutGetOutputStatus(int handle, VideoOutOutputStatus* statu
std::unique_lock lock(cfg->mutex);
status->resolution = (cfg->width >= 3840 || cfg->height >= 2160) ? 2u : 1u;
status->dynamicRange = 1;
status->refreshRate = VIDEO_OUT_REFRESH_RATE_59_94HZ;
status->refreshRate = (cfg->outputMode == VIDEO_OUT_OUTPUT_MODE_119_88HZ) ? VIDEO_OUT_REFRESH_RATE_119_88HZ : VIDEO_OUT_REFRESH_RATE_59_94HZ;
status->flags = 0;
status->reserved[0] = 0;
status->reserved[1] = 0;
@ -138,16 +138,16 @@ int APS5_VABI sceVideoOutInitializeOutputOptions(VideoOutOutputOptions* options)
return 0;
}
int APS5_VABI sceVideoOutIsOutputSupported(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reserved_ptr, uint64_t reserved) {
const int result = ValidateOutputConfig(handle, mode, options, reserved_ptr, reserved);
int APS5_VABI sceVideoOutIsOutputSupported(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) {
const int result = validateOutputConfig(handle, mode, options, reservedPtr, reserved);
if (result != 0) {
return result;
}
return (mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ) ? 0 : 1;
}
int APS5_VABI sceVideoOutConfigureOutput(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reserved_ptr, uint64_t reserved) {
const int supported = sceVideoOutIsOutputSupported(handle, mode, options, reserved_ptr, reserved);
int APS5_VABI sceVideoOutConfigureOutput(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) {
const int supported = sceVideoOutIsOutputSupported(handle, mode, options, reservedPtr, reserved);
if (supported < 0) {
return supported;
}

View File

@ -1,12 +1,39 @@
#include <chrono>
#include <stdexcept>
#include "SDL.h"
#include "prx/libkernel/Equeue/Equeue.hpp"
#include "prx/libkernel/Time/include/Time.hpp"
#include "prx/libSceVideoOut/include/VideoOutDriver.hpp"
#include <stdexcept>
static constexpr uint32_t VBLANK_FREQ_DEFAULT = 60;
static constexpr uint32_t VBLANK_FREQ_119 = 120;
VideoOutDriver& VideoOutDriver::Get() {
static VideoOutDriver instance;
return instance;
}
VideoOutDriver::VideoOutDriver() {
if (SDL_InitSubSystem(SDL_INIT_VIDEO) < 0) {
throw std::runtime_error(std::string("SDL_InitSubSystem(VIDEO) failed: ") + SDL_GetError());
}
presentThread = std::jthread([this](std::stop_token token) { presentLoop(token); });
}
VideoOutDriver::~VideoOutDriver() {
if (presentThread.joinable()) {
presentThread.request_stop();
flipCond.notify_all();
presentThread.join();
}
if (window != nullptr) {
SDL_DestroyWindow(window);
window = nullptr;
}
SDL_QuitSubSystem(SDL_INIT_VIDEO);
}
int VideoOutDriver::Open(int busType) {
std::unique_lock lock(mutex);
const int handle = busType + 1;
@ -34,6 +61,10 @@ int VideoOutDriver::Open(int busType) {
for (auto& grp : cfg.groups) {
grp = BufferAttributeGroup{};
}
cfg.flipEvents.clear();
cfg.vblankEvents.clear();
cfg.preVblankEvents.clear();
cfg.outputModeEvents.clear();
if (++cfg.generation == 0) {
throw std::runtime_error("VideoOut port generation wrapped");
}
@ -62,6 +93,10 @@ bool VideoOutDriver::Close(int handle) {
grp = BufferAttributeGroup{};
}
cfg.flipRate = 0;
cfg.flipEvents.clear();
cfg.vblankEvents.clear();
cfg.preVblankEvents.clear();
cfg.outputModeEvents.clear();
cfg.vblankCond.notify_all();
return true;
}
@ -78,3 +113,178 @@ bool VideoOutDriver::IsOpen(int handle) {
std::unique_lock lock(mutex);
return handle > 0 && handle < VIDEO_OUT_NUM_MAX && contexts[handle].opened;
}
void VideoOutDriver::SubmitFlip(VideoOutConfig* cfg, int index, int flipMode, int64_t flipArg) {
std::unique_lock lock(flipMutex);
if (flipQueue.size() >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) {
throw std::runtime_error("VideoOut flip queue full");
}
FlipRequest req;
req.cfg = cfg;
req.generation = cfg->generation;
req.index = index;
req.flipMode = flipMode;
req.flipArg = flipArg;
{
std::unique_lock cfgLock(cfg->mutex);
cfg->flipStatus.flipPendingNum++;
}
flipQueue.push_back(req);
flipCond.notify_one();
}
void VideoOutDriver::triggerEvents(VideoOutConfig& cfg, int eventKind, void* triggerData) {
std::vector<EventRegistration>* events = nullptr;
if (eventKind == VIDEO_OUT_EVENT_FLIP) {
events = &cfg.flipEvents;
} else if (eventKind == VIDEO_OUT_EVENT_VBLANK) {
events = &cfg.vblankEvents;
} else if (eventKind == VIDEO_OUT_EVENT_PRE_VBLANK_START) {
events = &cfg.preVblankEvents;
} else if (eventKind == VIDEO_OUT_EVENT_SET_MODE) {
events = &cfg.outputModeEvents;
} else {
throw std::runtime_error("triggerEvents: unknown event kind");
}
for (auto& reg : *events) {
if (reg.generation != cfg.generation) {
continue;
}
EqueueTriggerEvent_nid_postfix(reg.eq, static_cast<uintptr_t>(eventKind), EVFILT_VIDEO_OUT, triggerData);
}
}
void VideoOutDriver::vblankBegin() {
std::unique_lock lock(mutex);
for (int i = 1; i < VIDEO_OUT_NUM_MAX; i++) {
auto& cfg = contexts[i];
if (!cfg.opened) {
continue;
}
std::unique_lock cfgLock(cfg.mutex);
cfg.preVblankStatus.count++;
cfg.preVblankStatus.processTime = sceKernelGetProcessTime();
cfg.preVblankStatus.processTimeCounter = sceKernelGetProcessTimeCounter();
triggerEvents(cfg, VIDEO_OUT_EVENT_PRE_VBLANK_START, reinterpret_cast<void*>(cfg.preVblankStatus.count));
}
}
void VideoOutDriver::vblankEnd() {
std::unique_lock lock(mutex);
for (int i = 1; i < VIDEO_OUT_NUM_MAX; i++) {
auto& cfg = contexts[i];
if (!cfg.opened) {
continue;
}
std::unique_lock cfgLock(cfg.mutex);
cfg.vblankStatus.count++;
cfg.vblankStatus.processTime = sceKernelGetProcessTime();
cfg.vblankStatus.processTimeCounter = sceKernelGetProcessTimeCounter();
triggerEvents(cfg, VIDEO_OUT_EVENT_VBLANK, reinterpret_cast<void*>(cfg.vblankStatus.count));
cfg.vblankCond.notify_all();
}
}
void VideoOutDriver::processFlip(const FlipRequest& req) {
VideoOutConfig* cfg = req.cfg;
{
std::unique_lock cfgLock(cfg->mutex);
if (!cfg->opened || cfg->closing || cfg->generation != req.generation) {
cfg->flipStatus.flipPendingNum--;
return;
}
}
const bool isBlank = req.index == VIDEO_OUT_BUFFER_INDEX_BLANK;
const bool isBlack = req.index == VIDEO_OUT_BUFFER_INDEX_BLACK;
if (isBlank || isBlack) {
if (window != nullptr) {
SDL_FillRect(windowSurface, nullptr, isBlack ? SDL_MapRGB(windowSurface->format, 0, 0, 0) : 0);
SDL_UpdateWindowSurface(window);
}
} else {
throw std::runtime_error("VideoOut Vulkan rendering not implemented");
}
std::unique_lock cfgLock(cfg->mutex);
if (!cfg->opened || cfg->closing || cfg->generation != req.generation) {
cfg->flipStatus.flipPendingNum--;
return;
}
cfg->flipStatus.count++;
cfg->flipStatus.processTime = sceKernelGetProcessTime();
cfg->flipStatus.processTimeCounter = sceKernelGetProcessTimeCounter();
cfg->flipStatus.flipArg = req.flipArg;
cfg->flipStatus.currentBuffer = req.index;
cfg->flipStatus.flipPendingNum--;
triggerEvents(*cfg, VIDEO_OUT_EVENT_FLIP, reinterpret_cast<void*>(req.flipArg));
}
void VideoOutDriver::presentLoop(std::stop_token token) {
while (!token.stop_requested()) {
uint64_t outputMode = VIDEO_OUT_OUTPUT_MODE_DEFAULT;
{
std::unique_lock lock(mutex);
for (int i = 1; i < VIDEO_OUT_NUM_MAX; i++) {
if (contexts[i].opened) {
outputMode = contexts[i].outputMode;
break;
}
}
}
const uint32_t freq = (outputMode == VIDEO_OUT_OUTPUT_MODE_119_88HZ) ? VBLANK_FREQ_119 : VBLANK_FREQ_DEFAULT;
const auto period = std::chrono::nanoseconds(1'000'000'000ULL / freq);
const auto frameStart = std::chrono::steady_clock::now();
vblankBegin();
FlipRequest req{};
bool hasFlip = false;
{
std::unique_lock lock(flipMutex);
if (!flipQueue.empty()) {
req = flipQueue.front();
flipQueue.pop_front();
hasFlip = true;
}
}
if (hasFlip) {
if (window == nullptr) {
uint32_t w = 1920;
uint32_t h = 1080;
{
std::unique_lock lock(mutex);
if (req.cfg != nullptr) {
std::unique_lock cfgLock(req.cfg->mutex);
w = req.cfg->width;
h = req.cfg->height;
}
}
window = SDL_CreateWindow("PS5", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, static_cast<int>(w), static_cast<int>(h), SDL_WINDOW_SHOWN);
if (window == nullptr) {
throw std::runtime_error(std::string("SDL_CreateWindow failed: ") + SDL_GetError());
}
windowSurface = SDL_GetWindowSurface(window);
if (windowSurface == nullptr) {
throw std::runtime_error(std::string("SDL_GetWindowSurface failed: ") + SDL_GetError());
}
}
processFlip(req);
}
vblankEnd();
SDL_Event sdlEvent;
while (SDL_PollEvent(&sdlEvent)) {
if (sdlEvent.type == SDL_QUIT) {
return;
}
}
const auto elapsed = std::chrono::steady_clock::now() - frameStart;
if (elapsed < period) {
const auto sleepUs = std::chrono::duration_cast<std::chrono::microseconds>(period - elapsed).count();
if (sleepUs > 0) {
sceKernelUsleep_nid_postfix(static_cast<KernelUseconds>(sleepUs));
}
}
}
}