2024-08-24 13:18:12 +00:00
|
|
|
|
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
2024-04-14 14:09:51 +00:00
|
|
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
|
|
|
|
|
|
#include <pthread.h>
|
2024-08-30 13:48:21 +00:00
|
|
|
|
|
2024-04-14 14:09:51 +00:00
|
|
|
|
#include "common/assert.h"
|
2024-07-28 13:54:09 +00:00
|
|
|
|
#include "common/config.h"
|
|
|
|
|
#include "common/debug.h"
|
|
|
|
|
#include "common/thread.h"
|
2024-04-14 14:09:51 +00:00
|
|
|
|
#include "core/libraries/error_codes.h"
|
|
|
|
|
#include "core/libraries/kernel/time_management.h"
|
|
|
|
|
#include "core/libraries/videoout/driver.h"
|
2024-08-14 09:36:11 +00:00
|
|
|
|
#include "core/platform.h"
|
2024-04-14 14:09:51 +00:00
|
|
|
|
#include "video_core/renderer_vulkan/renderer_vulkan.h"
|
|
|
|
|
|
2024-05-21 22:35:12 +00:00
|
|
|
|
extern std::unique_ptr<Vulkan::RendererVulkan> renderer;
|
2024-07-28 13:54:09 +00:00
|
|
|
|
extern std::unique_ptr<AmdGpu::Liverpool> liverpool;
|
2024-04-14 14:09:51 +00:00
|
|
|
|
|
|
|
|
|
namespace Libraries::VideoOut {
|
|
|
|
|
|
|
|
|
|
constexpr static bool Is32BppPixelFormat(PixelFormat format) {
|
|
|
|
|
switch (format) {
|
|
|
|
|
case PixelFormat::A8R8G8B8Srgb:
|
|
|
|
|
case PixelFormat::A8B8G8R8Srgb:
|
|
|
|
|
case PixelFormat::A2R10G10B10:
|
|
|
|
|
case PixelFormat::A2R10G10B10Srgb:
|
|
|
|
|
case PixelFormat::A2R10G10B10Bt2020Pq:
|
|
|
|
|
return true;
|
|
|
|
|
default:
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
constexpr u32 PixelFormatBpp(PixelFormat pixel_format) {
|
|
|
|
|
switch (pixel_format) {
|
|
|
|
|
case PixelFormat::A16R16G16B16Float:
|
|
|
|
|
return 8;
|
|
|
|
|
default:
|
|
|
|
|
return 4;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VideoOutDriver::VideoOutDriver(u32 width, u32 height) {
|
|
|
|
|
main_port.resolution.fullWidth = width;
|
|
|
|
|
main_port.resolution.fullHeight = height;
|
|
|
|
|
main_port.resolution.paneWidth = width;
|
|
|
|
|
main_port.resolution.paneHeight = height;
|
2024-07-28 13:54:09 +00:00
|
|
|
|
present_thread = std::jthread([&](std::stop_token token) { PresentThread(token); });
|
2024-04-14 14:09:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VideoOutDriver::~VideoOutDriver() = default;
|
|
|
|
|
|
|
|
|
|
int VideoOutDriver::Open(const ServiceThreadParams* params) {
|
|
|
|
|
if (main_port.is_open) {
|
|
|
|
|
return ORBIS_VIDEO_OUT_ERROR_RESOURCE_BUSY;
|
|
|
|
|
}
|
|
|
|
|
main_port.is_open = true;
|
2024-07-28 13:54:09 +00:00
|
|
|
|
liverpool->SetVoPort(&main_port);
|
|
|
|
|
return 1;
|
2024-04-14 14:09:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void VideoOutDriver::Close(s32 handle) {
|
|
|
|
|
std::scoped_lock lock{mutex};
|
|
|
|
|
|
|
|
|
|
main_port.is_open = false;
|
|
|
|
|
main_port.flip_rate = 0;
|
|
|
|
|
ASSERT(main_port.flip_events.empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VideoOutPort* VideoOutDriver::GetPort(int handle) {
|
|
|
|
|
if (handle != 1) [[unlikely]] {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
return &main_port;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int VideoOutDriver::RegisterBuffers(VideoOutPort* port, s32 startIndex, void* const* addresses,
|
|
|
|
|
s32 bufferNum, const BufferAttribute* attribute) {
|
|
|
|
|
const s32 group_index = port->FindFreeGroup();
|
|
|
|
|
if (group_index >= MaxDisplayBufferGroups) {
|
|
|
|
|
return ORBIS_VIDEO_OUT_ERROR_NO_EMPTY_SLOT;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (startIndex + bufferNum > MaxDisplayBuffers || startIndex > MaxDisplayBuffers ||
|
|
|
|
|
bufferNum > MaxDisplayBuffers) {
|
|
|
|
|
LOG_ERROR(Lib_VideoOut,
|
|
|
|
|
"Attempted to register too many buffers startIndex = {}, bufferNum = {}",
|
|
|
|
|
startIndex, bufferNum);
|
|
|
|
|
return ORBIS_VIDEO_OUT_ERROR_INVALID_VALUE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const s32 end_index = startIndex + bufferNum;
|
|
|
|
|
if (bufferNum > 0 &&
|
|
|
|
|
std::any_of(port->buffer_slots.begin() + startIndex, port->buffer_slots.begin() + end_index,
|
|
|
|
|
[](auto& buffer) { return buffer.group_index != -1; })) {
|
|
|
|
|
return ORBIS_VIDEO_OUT_ERROR_SLOT_OCCUPIED;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (attribute->reserved0 != 0 || attribute->reserved1 != 0) {
|
2024-08-26 12:22:11 +00:00
|
|
|
|
LOG_ERROR(Lib_VideoOut, "Invalid reserved members");
|
2024-04-14 14:09:51 +00:00
|
|
|
|
return ORBIS_VIDEO_OUT_ERROR_INVALID_VALUE;
|
|
|
|
|
}
|
|
|
|
|
if (attribute->aspect_ratio != 0) {
|
|
|
|
|
LOG_ERROR(Lib_VideoOut, "Invalid aspect ratio = {}", attribute->aspect_ratio);
|
|
|
|
|
return ORBIS_VIDEO_OUT_ERROR_INVALID_ASPECT_RATIO;
|
|
|
|
|
}
|
|
|
|
|
if (attribute->width > attribute->pitch_in_pixel) {
|
|
|
|
|
LOG_ERROR(Lib_VideoOut, "Buffer width {} is larger than pitch {}", attribute->width,
|
|
|
|
|
attribute->pitch_in_pixel);
|
|
|
|
|
return ORBIS_VIDEO_OUT_ERROR_INVALID_PITCH;
|
|
|
|
|
}
|
|
|
|
|
if (attribute->tiling_mode < TilingMode::Tile || attribute->tiling_mode > TilingMode::Linear) {
|
|
|
|
|
LOG_ERROR(Lib_VideoOut, "Invalid tilingMode = {}",
|
|
|
|
|
static_cast<u32>(attribute->tiling_mode));
|
|
|
|
|
return ORBIS_VIDEO_OUT_ERROR_INVALID_TILING_MODE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO(Lib_VideoOut,
|
|
|
|
|
"startIndex = {}, bufferNum = {}, pixelFormat = {}, aspectRatio = {}, "
|
|
|
|
|
"tilingMode = {}, width = {}, height = {}, pitchInPixel = {}, option = {:#x}",
|
|
|
|
|
startIndex, bufferNum, GetPixelFormatString(attribute->pixel_format),
|
|
|
|
|
attribute->aspect_ratio, static_cast<u32>(attribute->tiling_mode), attribute->width,
|
|
|
|
|
attribute->height, attribute->pitch_in_pixel, attribute->option);
|
|
|
|
|
|
|
|
|
|
auto& group = port->groups[group_index];
|
|
|
|
|
std::memcpy(&group.attrib, attribute, sizeof(BufferAttribute));
|
|
|
|
|
group.is_occupied = true;
|
|
|
|
|
|
|
|
|
|
for (u32 i = 0; i < bufferNum; i++) {
|
|
|
|
|
const uintptr_t address = reinterpret_cast<uintptr_t>(addresses[i]);
|
|
|
|
|
port->buffer_slots[startIndex + i] = VideoOutBuffer{
|
|
|
|
|
.group_index = group_index,
|
|
|
|
|
.address_left = address,
|
|
|
|
|
.address_right = 0,
|
|
|
|
|
};
|
|
|
|
|
|
2024-06-15 19:18:48 +00:00
|
|
|
|
renderer->RegisterVideoOutSurface(group, address);
|
2024-04-14 14:09:51 +00:00
|
|
|
|
LOG_INFO(Lib_VideoOut, "buffers[{}] = {:#x}", i + startIndex, address);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return group_index;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int VideoOutDriver::UnregisterBuffers(VideoOutPort* port, s32 attributeIndex) {
|
|
|
|
|
if (attributeIndex >= MaxDisplayBufferGroups || !port->groups[attributeIndex].is_occupied) {
|
|
|
|
|
LOG_ERROR(Lib_VideoOut, "Invalid attribute index {}", attributeIndex);
|
|
|
|
|
return ORBIS_VIDEO_OUT_ERROR_INVALID_VALUE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto& group = port->groups[attributeIndex];
|
|
|
|
|
group.is_occupied = false;
|
|
|
|
|
|
|
|
|
|
for (auto& buffer : port->buffer_slots) {
|
|
|
|
|
if (buffer.group_index != attributeIndex) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
buffer.group_index = -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ORBIS_OK;
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-28 13:54:09 +00:00
|
|
|
|
std::chrono::microseconds VideoOutDriver::Flip(const Request& req) {
|
|
|
|
|
const auto start = std::chrono::high_resolution_clock::now();
|
|
|
|
|
|
2024-05-15 18:54:45 +00:00
|
|
|
|
// Whatever the game is rendering show splash if it is active
|
|
|
|
|
if (!renderer->ShowSplash(req.frame)) {
|
2024-05-16 13:58:14 +00:00
|
|
|
|
// Present the frame.
|
|
|
|
|
renderer->Present(req.frame);
|
|
|
|
|
}
|
2024-04-14 14:09:51 +00:00
|
|
|
|
|
|
|
|
|
// Update flip status.
|
2024-07-28 13:54:09 +00:00
|
|
|
|
auto* port = req.port;
|
2024-08-14 09:36:11 +00:00
|
|
|
|
{
|
|
|
|
|
std::unique_lock lock{port->port_mutex};
|
|
|
|
|
auto& flip_status = port->flip_status;
|
|
|
|
|
flip_status.count++;
|
|
|
|
|
flip_status.processTime = Libraries::Kernel::sceKernelGetProcessTime();
|
|
|
|
|
flip_status.tsc = Libraries::Kernel::sceKernelReadTsc();
|
|
|
|
|
flip_status.flipArg = req.flip_arg;
|
|
|
|
|
flip_status.currentBuffer = req.index;
|
|
|
|
|
if (req.eop) {
|
|
|
|
|
--flip_status.gcQueueNum;
|
|
|
|
|
}
|
|
|
|
|
--flip_status.flipPendingNum;
|
|
|
|
|
}
|
2024-04-14 14:09:51 +00:00
|
|
|
|
|
|
|
|
|
// Trigger flip events for the port.
|
2024-07-28 13:54:09 +00:00
|
|
|
|
for (auto& event : port->flip_events) {
|
2024-04-14 14:09:51 +00:00
|
|
|
|
if (event != nullptr) {
|
2024-07-09 10:12:15 +00:00
|
|
|
|
event->TriggerEvent(SCE_VIDEO_OUT_EVENT_FLIP, Kernel::SceKernelEvent::Filter::VideoOut,
|
2024-04-14 14:09:51 +00:00
|
|
|
|
reinterpret_cast<void*>(req.flip_arg));
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-05-09 20:59:35 +00:00
|
|
|
|
|
|
|
|
|
// Reset flip label
|
2024-06-15 11:36:07 +00:00
|
|
|
|
if (req.index != -1) {
|
2024-07-28 13:54:09 +00:00
|
|
|
|
port->buffer_labels[req.index] = 0;
|
|
|
|
|
port->SignalVoLabel();
|
2024-06-15 11:36:07 +00:00
|
|
|
|
}
|
2024-07-28 13:54:09 +00:00
|
|
|
|
|
|
|
|
|
const auto end = std::chrono::high_resolution_clock::now();
|
|
|
|
|
return std::chrono::duration_cast<std::chrono::microseconds>(end - start);
|
2024-04-14 14:09:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-09-08 19:50:32 +00:00
|
|
|
|
void VideoOutDriver::DrawBlankFrame() {
|
|
|
|
|
const auto empty_frame = renderer->PrepareBlankFrame(false);
|
|
|
|
|
renderer->Present(empty_frame);
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-10 19:48:01 +00:00
|
|
|
|
bool VideoOutDriver::SubmitFlip(VideoOutPort* port, s32 index, s64 flip_arg,
|
|
|
|
|
bool is_eop /*= false*/) {
|
2024-08-14 09:36:11 +00:00
|
|
|
|
{
|
|
|
|
|
std::unique_lock lock{port->port_mutex};
|
|
|
|
|
if (index != -1 && port->flip_status.flipPendingNum >= port->NumRegisteredBuffers()) {
|
|
|
|
|
LOG_ERROR(Lib_VideoOut, "Flip queue is full");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (is_eop) {
|
|
|
|
|
++port->flip_status.gcQueueNum;
|
|
|
|
|
}
|
|
|
|
|
++port->flip_status.flipPendingNum; // integral GPU and CPU pending flips counter
|
|
|
|
|
port->flip_status.submitTsc = Libraries::Kernel::sceKernelReadTsc();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!is_eop) {
|
|
|
|
|
// Before processing the flip we need to ask GPU thread to flush command list as at this
|
|
|
|
|
// point VO surface is ready to be presented, and we will need have an actual state of
|
|
|
|
|
// Vulkan image at the time of frame presentation.
|
|
|
|
|
liverpool->SendCommand([=, this]() {
|
|
|
|
|
renderer->FlushDraw();
|
|
|
|
|
SubmitFlipInternal(port, index, flip_arg, is_eop);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
SubmitFlipInternal(port, index, flip_arg, is_eop);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void VideoOutDriver::SubmitFlipInternal(VideoOutPort* port, s32 index, s64 flip_arg,
|
|
|
|
|
bool is_eop /*= false*/) {
|
2024-06-15 11:36:07 +00:00
|
|
|
|
Vulkan::Frame* frame;
|
|
|
|
|
if (index == -1) {
|
2024-08-14 09:36:11 +00:00
|
|
|
|
frame = renderer->PrepareBlankFrame(is_eop);
|
2024-06-15 11:36:07 +00:00
|
|
|
|
} else {
|
|
|
|
|
const auto& buffer = port->buffer_slots[index];
|
|
|
|
|
const auto& group = port->groups[buffer.group_index];
|
2024-07-28 13:54:09 +00:00
|
|
|
|
frame = renderer->PrepareFrame(group, buffer.address_left, is_eop);
|
2024-06-15 11:36:07 +00:00
|
|
|
|
}
|
2024-04-14 14:09:51 +00:00
|
|
|
|
|
2024-07-28 13:54:09 +00:00
|
|
|
|
std::scoped_lock lock{mutex};
|
2024-04-14 14:09:51 +00:00
|
|
|
|
requests.push({
|
|
|
|
|
.frame = frame,
|
|
|
|
|
.port = port,
|
|
|
|
|
.flip_arg = flip_arg,
|
2024-08-24 13:18:12 +00:00
|
|
|
|
.index = index,
|
2024-05-10 19:48:01 +00:00
|
|
|
|
.eop = is_eop,
|
2024-04-14 14:09:51 +00:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-28 13:54:09 +00:00
|
|
|
|
void VideoOutDriver::PresentThread(std::stop_token token) {
|
|
|
|
|
static constexpr std::chrono::milliseconds VblankPeriod{16};
|
|
|
|
|
Common::SetCurrentThreadName("PresentThread");
|
|
|
|
|
|
|
|
|
|
const auto receive_request = [this] -> Request {
|
|
|
|
|
std::scoped_lock lk{mutex};
|
|
|
|
|
if (!requests.empty()) {
|
|
|
|
|
const auto request = requests.front();
|
|
|
|
|
requests.pop();
|
|
|
|
|
return request;
|
|
|
|
|
}
|
|
|
|
|
return {};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto vblank_period = VblankPeriod / Config::vblankDiv();
|
|
|
|
|
auto delay = std::chrono::microseconds{0};
|
|
|
|
|
while (!token.stop_requested()) {
|
|
|
|
|
// Sleep for most of the vblank duration.
|
|
|
|
|
std::this_thread::sleep_for(vblank_period - delay);
|
|
|
|
|
|
|
|
|
|
// Check if it's time to take a request.
|
|
|
|
|
auto& vblank_status = main_port.vblank_status;
|
|
|
|
|
if (vblank_status.count % (main_port.flip_rate + 1) == 0) {
|
|
|
|
|
const auto request = receive_request();
|
2024-09-08 19:50:32 +00:00
|
|
|
|
if (!request) {
|
|
|
|
|
delay = std::chrono::microseconds{0};
|
|
|
|
|
if (!main_port.is_open) {
|
|
|
|
|
DrawBlankFrame();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
delay = Flip(request);
|
2024-09-17 07:45:10 +00:00
|
|
|
|
FRAME_END;
|
2024-09-08 19:50:32 +00:00
|
|
|
|
}
|
2024-07-28 13:54:09 +00:00
|
|
|
|
}
|
2024-04-14 14:09:51 +00:00
|
|
|
|
|
2024-07-28 13:54:09 +00:00
|
|
|
|
{
|
|
|
|
|
// Needs lock here as can be concurrently read by `sceVideoOutGetVblankStatus`
|
|
|
|
|
std::unique_lock lock{main_port.vo_mutex};
|
|
|
|
|
vblank_status.count++;
|
|
|
|
|
vblank_status.processTime = Libraries::Kernel::sceKernelGetProcessTime();
|
|
|
|
|
vblank_status.tsc = Libraries::Kernel::sceKernelReadTsc();
|
|
|
|
|
main_port.vblank_cv.notify_all();
|
|
|
|
|
}
|
2024-05-17 21:31:55 +00:00
|
|
|
|
|
2024-07-28 13:54:09 +00:00
|
|
|
|
// Trigger flip events for the port.
|
|
|
|
|
for (auto& event : main_port.vblank_events) {
|
|
|
|
|
if (event != nullptr) {
|
|
|
|
|
event->TriggerEvent(SCE_VIDEO_OUT_EVENT_VBLANK,
|
|
|
|
|
Kernel::SceKernelEvent::Filter::VideoOut, nullptr);
|
|
|
|
|
}
|
2024-05-17 21:31:55 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2024-04-14 14:09:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace Libraries::VideoOut
|