diff --git a/src/core/hle/service/gsp/gsp_gpu.cpp b/src/core/hle/service/gsp/gsp_gpu.cpp index aeb2f0360..a85a6968a 100644 --- a/src/core/hle/service/gsp/gsp_gpu.cpp +++ b/src/core/hle/service/gsp/gsp_gpu.cpp @@ -1,10 +1,11 @@ // Copyright 2014-2026 Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the misc/licenses/gplv2.txt file included. +// Licensed under GPLv3 or any later version +// Refer to the LICENSE.txt file included. #include #include #include +#include #include #include #include "common/archives.h" @@ -39,13 +40,29 @@ namespace ErrCodes { enum { // TODO(purpasmart): Check if this name fits its actual usage OutofRangeOrMisalignedAddress = 513, - NoSaveVRAMSysAreaPerm = 518, + NoPermission = 518, FirstInitialization = 519, + ForceModeCanceled = 520, }; } -constexpr Result ResultNoSaveVRAMSysAreaPerm(ErrCodes::NoSaveVRAMSysAreaPerm, ErrorModule::GX, +constexpr Result ResultNoSaveVRAMSysAreaPerm(ErrCodes::NoPermission, ErrorModule::GX, ErrorSummary::NothingHappened, ErrorLevel::Permanent); +constexpr Result ResultNoGpuRight(ErrCodes::NoPermission, ErrorModule::GX, + ErrorSummary::NothingHappened, + ErrorLevel::Permanent); // 0xD8202A06 +constexpr Result ResultInvalidBuffer(ErrorDescription::InvalidAddress, ErrorModule::GX, + ErrorSummary::InvalidArgument, + ErrorLevel::Usage); // 0xE0E02BF5 +constexpr Result ResultInvalidPriority(ErrorDescription::InvalidSection, ErrorModule::GX, + ErrorSummary::InvalidArgument, + ErrorLevel::Usage); // 0xE0E02BE8 +constexpr Result ResultNotSupported(ErrorDescription::NotImplemented, ErrorModule::GX, + ErrorSummary::NotSupported, ErrorLevel::Usage); // 0xE0C02BF4 +constexpr Result ResultForceModeCanceled(ErrCodes::ForceModeCanceled, ErrorModule::GX, + ErrorSummary::Canceled, ErrorLevel::Success); // 0x01202A08 +constexpr Result ResultGpuRightBusy(ErrorDescription::Busy, ErrorModule::GX, + ErrorSummary::WouldBlock, ErrorLevel::Status); // 0xC8402BF0 constexpr Result ResultFirstInitialization(ErrCodes::FirstInitialization, ErrorModule::GX, ErrorSummary::Success, ErrorLevel::Success); constexpr Result ResultRegsOutOfRangeOrMisaligned(ErrCodes::OutofRangeOrMisalignedAddress, @@ -88,15 +105,11 @@ std::pair GSP_GPU::GetFrameBufferInfo(u32 thread_id, u32 // If the thread ID is invalid, the last cached // data is used. if (thread_id != std::numeric_limits::max()) { - - // For each thread there are two FrameBufferUpdate fields - const u32 offset = 0x200 + (2 * thread_id + screen_index) * sizeof(FrameBufferUpdate); - FrameBufferUpdate* screen = - reinterpret_cast(shared_memory->GetPointer(offset)); + FrameBufferUpdate* screen = GetFrameBufferUpdate(thread_id, screen_index); if (screen->is_dirty || force_update) { - cached_framebuffer_infos[screen_index] = screen->framebuffer_info[screen->index]; - screen->is_dirty.Assign(false); + cached_framebuffer_infos[screen_index] = screen->framebuffer_info[screen->index & 1]; + screen->is_dirty = 0; is_dirty = true; } } @@ -104,18 +117,39 @@ std::pair GSP_GPU::GetFrameBufferInfo(u32 thread_id, u32 return {is_dirty, &cached_framebuffer_infos[screen_index]}; } +FrameBufferUpdate* GSP_GPU::GetFrameBufferUpdate(u32 thread_id, u32 screen_index) { + const u32 offset = 0x200 + (2 * thread_id + screen_index) * sizeof(FrameBufferUpdate); + return reinterpret_cast(shared_memory->GetPointer(offset)); +} + InterruptRelayQueue* GSP_GPU::GetInterruptRelayQueue(u32 thread_id) { u8* ptr = shared_memory->GetPointer(sizeof(InterruptRelayQueue) * thread_id); return reinterpret_cast(ptr); } +bool GSP_GPU::HasGpuRight(Kernel::HLERequestContext& ctx) { + return GetSessionData(ctx.Session())->thread_id == thread_id_with_rights; +} + void GSP_GPU::ClientDisconnected(std::shared_ptr server_session) { SessionData* session_data = GetSessionData(server_session); session_data->interrupt_event = nullptr; session_data->registered = false; + std::erase_if(right_waiters, [session_data](const RightWaitContext& waiter) { + return waiter.thread_id == session_data->thread_id; + }); if (thread_id_with_rights == session_data->thread_id) { ReleaseRight(session_data); } + // Clear the framebuffer update slots of the thread. + for (u32 screen_id = 0; screen_id < 2; ++screen_id) { + auto* update = GetFrameBufferUpdate(session_data->thread_id, screen_id); + update->index = 0; + update->is_dirty = 0; + update->pad1 = 0; + update->framebuffer_info[0].address_left = 0; + update->framebuffer_info[1].address_left = 0; + } if (True(session_data->relay_queue_flags & RelayEventQueueFlags::AllowSaveVramSysArea)) { vram_backup_handler.ResetState(); } @@ -225,6 +259,7 @@ public: enum class Type : u32 { SaveVRAMSysArea, RestoreVRAMSysArea, + AcquireRight, }; explicit ThreadCallback(Core::System& system_, std::string name_, Type type_) @@ -232,16 +267,19 @@ public: void WakeUp(std::shared_ptr thread, Kernel::HLERequestContext& ctx, Kernel::ThreadWakeupReason reason) { - if (type == Type::SaveVRAMSysArea || type == Type::RestoreVRAMSysArea) { - IPC::RequestBuilder rb = IPC::RequestBuilder(ctx, 1, 0); - rb.Push(ResultSuccess); - } + IPC::RequestBuilder rb = IPC::RequestBuilder(ctx, 1, 0); + rb.Push(result); + } + + void SetResult(Result result_) { + result = result_; } private: Core::System& system; std::string name; Type type; + Result result = ResultSuccess; ThreadCallback() : system(Core::Global()) {} @@ -250,6 +288,7 @@ private: ar& boost::serialization::base_object(*this); ar & name; ar & type; + ar & result.raw; } friend class boost::serialization::access; }; @@ -257,22 +296,82 @@ private: void GSP_GPU::WriteHWRegs(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const u32 reg_addr = rp.Pop(); - const u32 size = rp.Pop(); + // Real HW ignores this. + [[maybe_unused]] const u32 size = rp.Pop(); const auto src_data = rp.PopStaticBuffer(); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(GSP::WriteHWRegs(reg_addr, size, src_data, system.GPU())); + if (!HasGpuRight(ctx)) { + rb.Push(ResultNoGpuRight); + return; + } + + rb.Push(GSP::WriteHWRegs(reg_addr, static_cast(src_data.size()), src_data, system.GPU())); } void GSP_GPU::WriteHWRegsWithMask(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const u32 reg_addr = rp.Pop(); - const u32 size = rp.Pop(); + // Real HW ignores this. + [[maybe_unused]] const u32 size = rp.Pop(); const auto src_data = rp.PopStaticBuffer(); - const auto mask_data = rp.PopStaticBuffer(); + auto mask_data = rp.PopStaticBuffer(); + + // If the mask data is smaller, it is treated as zeroes. + if (mask_data.size() < src_data.size()) { + mask_data.resize(src_data.size(), 0); + } IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(GSP::WriteHWRegsWithMask(reg_addr, size, src_data, mask_data, system.GPU())); + if (!HasGpuRight(ctx)) { + rb.Push(ResultNoGpuRight); + return; + } + + // Real hardware explicitly checks this combination to reset the LCD mode state, and writes are + // skipped. + // TODO(PabloMK7): Handle LCD state here properly. + if (reg_addr == 0x400424 && src_data.size() == 0x50) { + rb.Push(ResultSuccess); + return; + } + + rb.Push(GSP::WriteHWRegsWithMask(reg_addr, static_cast(src_data.size()), src_data, + mask_data, system.GPU())); +} + +void GSP_GPU::WriteHWRegRepeat(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 reg_addr = rp.Pop(); + // Real HW ignores this. + [[maybe_unused]] const u32 size = rp.Pop(); + const auto src_data = rp.PopStaticBuffer(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + if (!HasGpuRight(ctx)) { + rb.Push(ResultNoGpuRight); + return; + } + + const u32 size_bytes = static_cast(src_data.size()); + if (reg_addr & 3 || reg_addr >= 0x420000) { + rb.Push(ResultRegsOutOfRangeOrMisaligned); + return; + } + if (size_bytes > 0x80) { + rb.Push(ResultRegsInvalidSize); + return; + } + if (size_bytes & 3) { + rb.Push(ResultRegsMisaligned); + return; + } + for (u32 offset = 0; offset < size_bytes; offset += sizeof(u32)) { + u32 value; + std::memcpy(&value, &src_data[offset], sizeof(u32)); + system.GPU().WriteReg(REGS_BEGIN + reg_addr, value); + } + rb.Push(ResultSuccess); } void GSP_GPU::ReadHWRegs(Kernel::HLERequestContext& ctx) { @@ -283,6 +382,12 @@ void GSP_GPU::ReadHWRegs(Kernel::HLERequestContext& ctx) { static constexpr u32 MaxReadSize = 0x80; u32 size = std::min(input_size, MaxReadSize); + if (!HasGpuRight(ctx)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultNoGpuRight); + return; + } + if ((reg_addr % 4) != 0 || reg_addr >= 0x420000) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(ResultRegsOutOfRangeOrMisaligned); @@ -314,40 +419,101 @@ void GSP_GPU::SetBufferSwap(Kernel::HLERequestContext& ctx) { u32 screen_id = rp.Pop(); auto fb_info = rp.PopRaw(); - system.GPU().SetBufferSwap(screen_id, fb_info); + if (!HasGpuRight(ctx)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultNoGpuRight); + return; + } + + // Real hardware ignores invalid screens and still returns success. + if (screen_id < 2) { + system.GPU().SetBufferSwap(screen_id, fb_info); + } else { + LOG_ERROR(Service_GSP, "Invalid screen id {}", screen_id); + } IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(ResultSuccess); } +void GSP_GPU::SetCommandList(Kernel::HLERequestContext& ctx) { + // Stubbed command, which was once implemented in older GSP versions. + // Only returns success. + IPC::RequestParser rp(ctx); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void GSP_GPU::RequestDma(Kernel::HLERequestContext& ctx) { + // Stubbed command, which was once implemented in older GSP versions. + // Only returns success. + IPC::RequestParser rp(ctx); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +/// Checks if the given address is valid for cache operations. +static bool IsValidCacheRange(VAddr address, u32 size, bool allow_vram) { + const VAddr end = address + size; + const auto contains = [address, end](VAddr range_start, VAddr range_end) { + return address >= range_start && address <= range_end && end >= range_start && + end <= range_end; + }; + return contains(Memory::LINEAR_HEAP_VADDR, Memory::LINEAR_HEAP_VADDR_END) || + contains(Memory::NEW_LINEAR_HEAP_VADDR, Memory::NEW_LINEAR_HEAP_VADDR_END) || + contains(Memory::N3DS_EXTRA_RAM_VADDR, Memory::N3DS_EXTRA_RAM_VADDR_END) || + (allow_vram && address >= Memory::VRAM_VADDR && address <= Memory::VRAM_VADDR_END); +} + +void GSP_GPU::DoCacheOperation(Kernel::HLERequestContext& ctx, bool allow_vram) { + IPC::RequestParser rp(ctx); + const u32 address = rp.Pop(); + const u32 size = rp.Pop(); + [[maybe_unused]] auto process = rp.PopObject(); + + // The CPU cache is not emulated, only the address check. + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + if (!IsValidCacheRange(address, size, allow_vram)) { + LOG_ERROR(Service_GSP, "Invalid cache operation range address=0x{:08X}, size=0x{:08X}", + address, size); + rb.Push(ResultInvalidBuffer); + return; + } + rb.Push(ResultSuccess); +} + void GSP_GPU::FlushDataCache(Kernel::HLERequestContext& ctx) { - IPC::RequestParser rp(ctx); - [[maybe_unused]] u32 address = rp.Pop(); - [[maybe_unused]] u32 size = rp.Pop(); - [[maybe_unused]] auto process = rp.PopObject(); - - // TODO(purpasmart96): Verify return header on HW - - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ResultSuccess); - - LOG_TRACE(Service_GSP, "(STUBBED) called address=0x{:08X}, size=0x{:08X}, process={}", address, - size, process->process_id); + DoCacheOperation(ctx, false); } void GSP_GPU::InvalidateDataCache(Kernel::HLERequestContext& ctx) { + DoCacheOperation(ctx, true); +} + +void GSP_GPU::RegisterInterruptEvents(Kernel::HLERequestContext& ctx) { + Unsupported(ctx); +} + +void GSP_GPU::StoreDataCache(Kernel::HLERequestContext& ctx) { + DoCacheOperation(ctx, false); +} + +void GSP_GPU::ResetGpuCore(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - [[maybe_unused]] u32 address = rp.Pop(); - [[maybe_unused]] u32 size = rp.Pop(); - [[maybe_unused]] auto process = rp.PopObject(); - - // TODO(purpasmart96): Verify return header on HW - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + if (!HasGpuRight(ctx)) { + rb.Push(ResultNoGpuRight); + return; + } + DoResetGpuCore(); rb.Push(ResultSuccess); +} - LOG_TRACE(Service_GSP, "(STUBBED) called address=0x{:08X}, size=0x{:08X}, process={}", address, - size, process->process_id); +void GSP_GPU::Unsupported(Kernel::HLERequestContext& ctx) { + // Stubbed commands, which were once implemented in older GSP versions. + IPC::RequestParser rp(ctx); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultNotSupported); } void GSP_GPU::SetAxiConfigQoSMode(Kernel::HLERequestContext& ctx) { @@ -357,6 +523,8 @@ void GSP_GPU::SetAxiConfigQoSMode(Kernel::HLERequestContext& ctx) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(ResultSuccess); + // TODO(PabloMK7): Implement the AXI registers even if they do not + // do anything. For now this function remains stubbed. LOG_DEBUG(Service_GSP, "(STUBBED) called mode=0x{:08X}", mode); } @@ -383,15 +551,28 @@ void GSP_GPU::RegisterInterruptRelayQueue(Kernel::HLERequestContext& ctx) { auto relay_queue_flags = static_cast(rp.Pop()); auto interrupt_event = rp.PopObject(); - ASSERT_MSG(interrupt_event, "handle is not valid!"); - interrupt_event->SetName("GSP_GPU::interrupt_event"); + // Real hardware stores the handle without checking it. + if (interrupt_event) { + interrupt_event->SetName("GSP_GPU::interrupt_event"); + } else { + LOG_ERROR(Service_GSP, "Invalid interrupt event handle"); + } SessionData* session_data = GetSessionData(ctx.Session()); session_data->interrupt_event = std::move(interrupt_event); session_data->registered = true; session_data->relay_queue_flags = relay_queue_flags; + // Clear the relevant queue information, as real hardware does. + auto* interrupt_relay_queue = GetInterruptRelayQueue(session_data->thread_id); + interrupt_relay_queue->index = 0; + interrupt_relay_queue->number_interrupts = 0; + interrupt_relay_queue->error_code = 0; + interrupt_relay_queue->config = 0; + interrupt_relay_queue->missed_PDC0 = 0; + interrupt_relay_queue->missed_PDC1 = 0; + IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); if (first_initialization) { @@ -584,15 +765,6 @@ void Service::GSP::GSP_GPU::ProcessPendingInterruptImpl(InterruptId interrupt_id } } - // Update framebuffer information if requested - const s32 screen_id = (interrupt_id == InterruptId::PDC0) ? 0 : 1; - - auto info = GetFrameBufferInfo(thread_id, screen_id); - if (info.first || force_buffer_swap[screen_id]) { - force_buffer_swap[screen_id] = false; - system.GPU().SetBufferSwap(screen_id, *info.second); - } - // If it's the bottom screen interrupt and a HLE event is pending, // signal it and reset. if (interrupt_id == InterruptId::PDC1 && session_data->on_vblank_event) { @@ -615,6 +787,15 @@ void GSP_GPU::SignalInterrupt(InterruptId interrupt_id, u64 wait_delay_ns) { // Normal interrupts are only signaled for the active thread (ie, the thread that has the GPU // right), but the PDC0/1 interrupts are signaled for every registered thread. if (interrupt_id == InterruptId::PDC0 || interrupt_id == InterruptId::PDC1) { + // Update the framebuffer if requested. Only the framebuffer info of the thread with the + // GPU right is used. + const u32 screen_id = (interrupt_id == InterruptId::PDC0) ? 0 : 1; + auto info = GetFrameBufferInfo(thread_id_with_rights, screen_id); + if (info.first || force_buffer_swap[screen_id]) { + force_buffer_swap[screen_id] = false; + system.GPU().SetBufferSwap(screen_id, *info.second); + } + for (u32 thread_id = 0; thread_id < MaxGSPThreads; ++thread_id) { SignalInterruptForThread(interrupt_id, thread_id, wait_delay_ns); } @@ -633,6 +814,14 @@ void GSP_GPU::SetLcdForceBlack(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const bool enable_black = rp.Pop(); + // After the GPU core reset of a forced AcquireRight, only the right holder can turn the + // screens back on. + if (gpu_core_reset_by_force && !enable_black && !HasGpuRight(ctx)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultNoGpuRight); + return; + } + Pica::ColorFill data{}; data.is_enabled.Assign(enable_black); system.GPU().SetColorFill(data); @@ -649,6 +838,7 @@ void GSP_GPU::TriggerCmdReqQueue(Kernel::HLERequestContext& ctx) { // the function always succeeds. IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(ResultSuccess); + return; } auto* command_buffer = GetCommandBuffer(thread_id_with_rights); @@ -679,9 +869,15 @@ void GSP_GPU::TriggerCmdReqQueue(Kernel::HLERequestContext& ctx) { // Decode and execute command system.perf_stats->BeginGPUProcessing(); - gpu.Execute(command); + const Result result = gpu.Execute(command); system.perf_stats->EndGPUProcessing(); + // The GSP module stores the error of a failed command and keeps going. + if (result.IsError()) { + command_buffer->unk[0] = result.raw; + command_buffer->status.Assign(CommandBuffer::STATUS_CMD_FAILED); + } + if (command.stop) { command_buffer->status.Assign(CommandBuffer::STATUS_STOPPED); } @@ -703,6 +899,18 @@ void GSP_GPU::TriggerCmdReqQueue(Kernel::HLERequestContext& ctx) { } } +void GSP_GPU::SetDisplayTransfer(Kernel::HLERequestContext& ctx) { + Unsupported(ctx); +} + +void GSP_GPU::SetTextureCopy(Kernel::HLERequestContext& ctx) { + Unsupported(ctx); +} + +void GSP_GPU::SetMemoryFill(Kernel::HLERequestContext& ctx) { + Unsupported(ctx); +} + void GSP_GPU::ImportDisplayCaptureInfo(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); @@ -908,61 +1116,96 @@ void GSP_GPU::RestoreVramSysArea(Kernel::HLERequestContext& ctx) { "GSP_GPU::RestoreVramSysArea", std::chrono::nanoseconds(-1), vblank_callback); } -Result GSP_GPU::AcquireGpuRight(const Kernel::HLERequestContext& ctx, - const std::shared_ptr& process, u32 flag, - bool blocking) { - const auto session_data = GetSessionData(ctx.Session()); - - LOG_DEBUG(Service_GSP, "called flag={:08X} process={} thread_id={}", flag, process->process_id, - session_data->thread_id); - +void GSP_GPU::ApplyPerProgramSettings(const Kernel::Process& process) { bool right_eye_disable_allow = Common::Hacks::hack_manager.GetHackAllowMode(Common::Hacks::HackType::RIGHT_EYE_DISABLE, - process->codeset->program_id) != + process.codeset->program_id) != Common::Hacks::HackAllowMode::DISALLOW; bool requires_shader_fixup = Common::Hacks::hack_manager.GetHackAllowMode( - Common::Hacks::HackType::REQUIRES_SHADER_FIXUP, process->codeset->program_id, + Common::Hacks::HackType::REQUIRES_SHADER_FIXUP, process.codeset->program_id, Common::Hacks::HackAllowMode::DISALLOW) != Common::Hacks::HackAllowMode::DISALLOW; delay_texture_copy_completion = Common::Hacks::hack_manager.GetHackAllowMode( - Common::Hacks::HackType::DELAY_TEXTURE_COPY_COMPLETION, process->codeset->program_id, + Common::Hacks::HackType::DELAY_TEXTURE_COPY_COMPLETION, process.codeset->program_id, Common::Hacks::HackAllowMode::DISALLOW) != Common::Hacks::HackAllowMode::DISALLOW; auto& gpu = system.GPU(); - gpu.ApplyPerProgramSettings(process->codeset->program_id); + gpu.ApplyPerProgramSettings(process.codeset->program_id); gpu.GetRightEyeDisabler().SetEnabled(right_eye_disable_allow); gpu.PicaCore().vs_setup.SetRequiresShaderFixup(requires_shader_fixup); gpu.PicaCore().gs_setup.SetRequiresShaderFixup(requires_shader_fixup); +} + +void GSP_GPU::GrantGpuRight(u32 thread_id, u32 client_thread_id, const Kernel::Process& process) { + ApplyPerProgramSettings(process); + system.GPU().Renderer().Rasterizer()->SwitchDiskResources(process.codeset->program_id); + + thread_id_with_rights = thread_id; + active_client_thread_id = client_thread_id; + + for (u32 screen_id = 0; screen_id < 2; ++screen_id) { + GetFrameBufferUpdate(thread_id_with_rights, screen_id)->is_dirty = 0; + } +} + +Result GSP_GPU::AcquireGpuRight(const Kernel::HLERequestContext& ctx, + const std::shared_ptr& process, bool force, + bool try_only) { + const auto session_data = GetSessionData(ctx.Session()); + + LOG_DEBUG(Service_GSP, "called force={} try={} process={} thread_id={}", force, try_only, + process->process_id, session_data->thread_id); if (thread_id_with_rights == session_data->thread_id) { return {ErrorDescription::AlreadyDone, ErrorModule::GX, ErrorSummary::Success, ErrorLevel::Success}; } - gpu.Renderer().Rasterizer()->SwitchDiskResources(process->codeset->program_id); - - if (blocking) { - // TODO: The thread should be put to sleep until acquired. - ASSERT_MSG(thread_id_with_rights == std::numeric_limits::max(), - "Sleeping for GPU right is not yet supported."); - } else if (thread_id_with_rights != std::numeric_limits::max()) { - return {ErrorDescription::Busy, ErrorModule::GX, ErrorSummary::WouldBlock, - ErrorLevel::Status}; + if (force && !force_mode) { + // The first forced AcquireRight steals the right and switches the GSP + // module to force mode until the system restarts. + if (thread_id_with_rights != std::numeric_limits::max()) { + LOG_WARNING(Service_GSP, "GPU right taken from thread {} by force", + thread_id_with_rights); + } + force_mode = true; + GrantGpuRight(session_data->thread_id, ctx.ClientThread()->thread_id, *process); + return ResultSuccess; } - thread_id_with_rights = session_data->thread_id; - active_client_thread_id = ctx.ClientThread()->thread_id; + if (thread_id_with_rights != std::numeric_limits::max()) { + return ResultGpuRightBusy; + } + + // In force mode, if AcquireRight is used without force, then it returns a + // special success error code. + if (force_mode && !force && !try_only) { + return ResultForceModeCanceled; + } + + GrantGpuRight(session_data->thread_id, ctx.ClientThread()->thread_id, *process); return ResultSuccess; } +void GSP_GPU::DoResetGpuCore() { + // The GSP module turns on the LCD force black of both screens, resets and reinitialises the + // GPU and applies the framebuffers again. The screens stay black until SetLcdForceBlack(0) + // is called. TODO(PabloMK7): Emulate the low level details of a GPU reset. + Pica::ColorFill data{}; + data.is_enabled.Assign(1); + system.GPU().SetColorFill(data); + force_buffer_swap[0] = true; + force_buffer_swap[1] = true; +} + void GSP_GPU::TryAcquireRight(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const auto process = rp.PopObject(); - const auto result = AcquireGpuRight(ctx, process, 0, false); + const auto result = AcquireGpuRight(ctx, process, false, true); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(result); @@ -970,19 +1213,55 @@ void GSP_GPU::TryAcquireRight(Kernel::HLERequestContext& ctx) { void GSP_GPU::AcquireRight(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - const auto flag = rp.Pop(); + const bool force = rp.Pop() != 0; const auto process = rp.PopObject(); - const auto result = AcquireGpuRight(ctx, process, flag, true); + const auto result = AcquireGpuRight(ctx, process, force, false); + if (result == ResultGpuRightBusy) { + // The GSP module waits until the holder releases the right, then gives it to the thread. + // Sleep the caller thread until the right is available. + const auto session_data = GetSessionData(ctx.Session()); + auto callback = std::make_shared(system, "GSP_GPU::AcquireRight", + ThreadCallback::Type::AcquireRight); + auto event = + ctx.SleepClientThread("GSP_GPU::AcquireRight", std::chrono::nanoseconds(-1), callback); + right_waiters.push_back({session_data->thread_id, ctx.ClientThread()->thread_id, force, + process, std::move(event), std::move(callback)}); + return; + } + + // The first forced AcquireRight resets the GPU core and + // makes SetLcdForceBlack(0) require the right. + if (force && !gpu_core_reset_by_force) { + gpu_core_reset_by_force = true; + DoResetGpuCore(); + } IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(result); } void GSP_GPU::ReleaseRight(const SessionData* session_data) { - if (thread_id_with_rights == session_data->thread_id) { - thread_id_with_rights = std::numeric_limits::max(); - active_client_thread_id = std::numeric_limits::max(); + if (thread_id_with_rights != session_data->thread_id) { + return; + } + + thread_id_with_rights = std::numeric_limits::max(); + active_client_thread_id = std::numeric_limits::max(); + + // Wake up the first thread waiting in AcquireRight. In force mode, threads waiting without + // the flag are woken with ResultForceModeCanceled and do not get the right. + while (!right_waiters.empty()) { + auto wait_context = std::move(right_waiters.front()); + right_waiters.pop_front(); + if (force_mode && !wait_context.force) { + wait_context.callback->SetResult(ResultForceModeCanceled); + wait_context.event->Signal(); + continue; + } + GrantGpuRight(wait_context.thread_id, wait_context.client_thread_id, *wait_context.process); + wait_context.event->Signal(); + break; } } @@ -999,20 +1278,6 @@ void GSP_GPU::ReleaseRight(Kernel::HLERequestContext& ctx) { LOG_WARNING(Service_GSP, "called"); } -void GSP_GPU::StoreDataCache(Kernel::HLERequestContext& ctx) { - IPC::RequestParser rp(ctx); - - [[maybe_unused]] u32 address = rp.Pop(); - [[maybe_unused]] u32 size = rp.Pop(); - auto process = rp.PopObject(); - - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ResultSuccess); - - LOG_TRACE(Service_GSP, "(STUBBED) called address=0x{:08X}, size=0x{:08X}, process={}", address, - size, process->process_id); -} - void GSP_GPU::SetLedForceOff(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); @@ -1025,12 +1290,21 @@ void GSP_GPU::SetLedForceOff(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_GSP, "(STUBBED) called"); } +void GSP_GPU::SetTestCommand(Kernel::HLERequestContext& ctx) { + Unsupported(ctx); +} + void GSP_GPU::SetInternalPriorities(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const auto priority = rp.Pop(); const auto priority_with_rights = rp.Pop(); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + if (priority < 0x10 || priority > 0x3F || priority_with_rights < 0x11 || + priority_with_rights > 0x3F) { + rb.Push(ResultInvalidPriority); + return; + } rb.Push(ResultSuccess); LOG_DEBUG(Service_GSP, "(STUBBED) called priority={:#02X}, priority_with_rights={:#02X}", @@ -1063,6 +1337,9 @@ void GSP_GPU::serialize(Archive& ar, const unsigned int) { ar & cached_framebuffer_infos; ar & force_buffer_swap; ar & vram_backup_handler; + ar & right_waiters; + ar & force_mode; + ar & gpu_core_reset_by_force; } SERIALIZE_IMPL(GSP_GPU) @@ -1071,19 +1348,19 @@ GSP_GPU::GSP_GPU(Core::System& system) : ServiceFramework("gsp::Gpu", 4), system // clang-format off {0x0001, &GSP_GPU::WriteHWRegs, "WriteHWRegs"}, {0x0002, &GSP_GPU::WriteHWRegsWithMask, "WriteHWRegsWithMask"}, - {0x0003, nullptr, "WriteHWRegRepeat"}, + {0x0003, &GSP_GPU::WriteHWRegRepeat, "WriteHWRegRepeat"}, {0x0004, &GSP_GPU::ReadHWRegs, "ReadHWRegs"}, {0x0005, &GSP_GPU::SetBufferSwap, "SetBufferSwap"}, - {0x0006, nullptr, "SetCommandList"}, - {0x0007, nullptr, "RequestDma"}, + {0x0006, &GSP_GPU::SetCommandList, "SetCommandList"}, + {0x0007, &GSP_GPU::RequestDma, "RequestDma"}, {0x0008, &GSP_GPU::FlushDataCache, "FlushDataCache"}, {0x0009, &GSP_GPU::InvalidateDataCache, "InvalidateDataCache"}, - {0x000A, nullptr, "RegisterInterruptEvents"}, + {0x000A, &GSP_GPU::RegisterInterruptEvents, "RegisterInterruptEvents"}, {0x000B, &GSP_GPU::SetLcdForceBlack, "SetLcdForceBlack"}, {0x000C, &GSP_GPU::TriggerCmdReqQueue, "TriggerCmdReqQueue"}, - {0x000D, nullptr, "SetDisplayTransfer"}, - {0x000E, nullptr, "SetTextureCopy"}, - {0x000F, nullptr, "SetMemoryFill"}, + {0x000D, &GSP_GPU::SetDisplayTransfer, "SetDisplayTransfer"}, + {0x000E, &GSP_GPU::SetTextureCopy, "SetTextureCopy"}, + {0x000F, &GSP_GPU::SetMemoryFill, "SetMemoryFill"}, {0x0010, &GSP_GPU::SetAxiConfigQoSMode, "SetAxiConfigQoSMode"}, {0x0011, &GSP_GPU::SetPerfLogMode, "SetPerfLogMode"}, {0x0012, &GSP_GPU::GetPerfLog, "GetPerfLog"}, @@ -1095,9 +1372,9 @@ GSP_GPU::GSP_GPU(Core::System& system) : ServiceFramework("gsp::Gpu", 4), system {0x0018, &GSP_GPU::ImportDisplayCaptureInfo, "ImportDisplayCaptureInfo"}, {0x0019, &GSP_GPU::SaveVramSysArea, "SaveVramSysArea"}, {0x001A, &GSP_GPU::RestoreVramSysArea, "RestoreVramSysArea"}, - {0x001B, nullptr, "ResetGpuCore"}, + {0x001B, &GSP_GPU::ResetGpuCore, "ResetGpuCore"}, {0x001C, &GSP_GPU::SetLedForceOff, "SetLedForceOff"}, - {0x001D, nullptr, "SetTestCommand"}, + {0x001D, &GSP_GPU::SetTestCommand, "SetTestCommand"}, {0x001E, &GSP_GPU::SetInternalPriorities, "SetInternalPriorities"}, {0x001F, &GSP_GPU::StoreDataCache, "StoreDataCache"}, // clang-format on diff --git a/src/core/hle/service/gsp/gsp_gpu.h b/src/core/hle/service/gsp/gsp_gpu.h index 17246ee23..3e056b5ae 100644 --- a/src/core/hle/service/gsp/gsp_gpu.h +++ b/src/core/hle/service/gsp/gsp_gpu.h @@ -1,11 +1,12 @@ // Copyright 2014-2026 Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the misc/licenses/gplv2.txt file included. +// Licensed under GPLv3 or any later version +// Refer to the LICENSE.txt file included. #pragma once #include #include +#include #include #include #include @@ -61,8 +62,8 @@ struct FrameBufferInfo { static_assert(sizeof(FrameBufferInfo) == 0x1c, "Struct has incorrect size"); struct FrameBufferUpdate { - BitField<0, 1, u8> index; // Index used for GSP::SetBufferSwap - BitField<0, 1, u8> is_dirty; // true if GSP should update GPU framebuffer registers + u8 index; // Index of the framebuffer info to use + u8 is_dirty; // Non-zero if GSP should update GPU framebuffer registers u16 pad1; FrameBufferInfo framebuffer_info[2]; @@ -204,7 +205,7 @@ public: /** * Retrieves the framebuffer info stored in the GSP shared memory for the - * specified screen index and thread id. + * specified screen index and thread id, and clears its update flag. * @param thread_id GSP thread id of the process that accesses the structure that we are * requesting. * @param screen_index Index of the screen we are requesting (Top = 0, Bottom = 1). @@ -216,6 +217,9 @@ public: /// Gets a pointer to a thread command buffer in GSP shared memory CommandBuffer* GetCommandBuffer(u32 thread_id); + /// Gets the pointer to the thread's framebuffer update slot for a screen in GSP shared memory + FrameBufferUpdate* GetFrameBufferUpdate(u32 thread_id, u32 screen_index); + /// Gets a pointer to the interrupt relay queue for a given thread index InterruptRelayQueue* GetInterruptRelayQueue(u32 thread_id); @@ -246,6 +250,8 @@ private: void ProcessPendingInterrupt(size_t pending_interrupt_id); + bool HasGpuRight(Kernel::HLERequestContext& ctx); + void ProcessPendingInterruptImpl(InterruptId interrupt_id, u32 thread_id); /** @@ -273,9 +279,19 @@ private: */ void WriteHWRegsWithMask(Kernel::HLERequestContext& ctx); + /// Write the data to the same register repeatedly. + void WriteHWRegRepeat(Kernel::HLERequestContext& ctx); + /// Read a GSP GPU hardware register void ReadHWRegs(Kernel::HLERequestContext& ctx); + /// Helper for stubbed commands. + void Unsupported(Kernel::HLERequestContext& ctx); + + void DoCacheOperation(Kernel::HLERequestContext& ctx, bool allow_vram); + + void DoResetGpuCore(); + /** * GSP_GPU::SetBufferSwap service function * @@ -289,6 +305,10 @@ private: */ void SetBufferSwap(Kernel::HLERequestContext& ctx); + void SetCommandList(Kernel::HLERequestContext& ctx); + + void RequestDma(Kernel::HLERequestContext& ctx); + /** * GSP_GPU::FlushDataCache service function * @@ -319,6 +339,8 @@ private: */ void InvalidateDataCache(Kernel::HLERequestContext& ctx); + void RegisterInterruptEvents(Kernel::HLERequestContext& ctx); + /** * GSP_GPU::SetLcdForceBlack service function * @@ -334,6 +356,12 @@ private: /// This triggers handling of the GX command written to the command buffer in shared memory. void TriggerCmdReqQueue(Kernel::HLERequestContext& ctx); + void SetDisplayTransfer(Kernel::HLERequestContext& ctx); + + void SetTextureCopy(Kernel::HLERequestContext& ctx); + + void SetMemoryFill(Kernel::HLERequestContext& ctx); + /** * GSP_GPU::SetAxiConfigQoSMode service function * Inputs: @@ -468,6 +496,8 @@ private: /// Force the 3D LED State (0 = On, Non-Zero = Off) void SetLedForceOff(Kernel::HLERequestContext& ctx); + void SetTestCommand(Kernel::HLERequestContext& ctx); + /** * GSP_GPU::SetInternalPriorities service function * Inputs: @@ -479,6 +509,8 @@ private: */ void SetInternalPriorities(Kernel::HLERequestContext& ctx); + void ResetGpuCore(Kernel::HLERequestContext& ctx); + /// Returns the session data for the specified registered thread id, or nullptr if not found. SessionData* FindRegisteredThreadData(u32 thread_id); @@ -487,8 +519,40 @@ private: std::unique_ptr MakeSessionData() override; Result AcquireGpuRight(const Kernel::HLERequestContext& ctx, - const std::shared_ptr& process, u32 flag, - bool blocking); + const std::shared_ptr& process, bool force, + bool try_only); + + /// Set by forced AcquireRight, from then onwards AcquireRight without the force flag does + /// not give the right (until the system restarts). + bool force_mode = false; + bool gpu_core_reset_by_force = false; + + void ApplyPerProgramSettings(const Kernel::Process& process); + + /// Gives the GPU right to a GSP thread. + void GrantGpuRight(u32 thread_id, u32 client_thread_id, const Kernel::Process& process); + + struct RightWaitContext { + u32 thread_id; + u32 client_thread_id; + bool force; + std::shared_ptr process; + std::shared_ptr event; + std::shared_ptr callback; + + template + void serialize(Archive& ar, const unsigned int) { + ar & thread_id; + ar & client_thread_id; + ar & force; + ar & process; + ar & event; + ar & callback; + } + }; + + /// Threads waiting for the GPU right in request order. + std::deque right_waiters; Core::System& system; diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp index 87b770705..30cfc2029 100644 --- a/src/video_core/gpu.cpp +++ b/src/video_core/gpu.cpp @@ -1,6 +1,6 @@ // Copyright 2023-2026 Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the misc/licenses/gplv2.txt file included. +// Licensed under GPLv3 or any later version +// Refer to the LICENSE.txt file included. #include "common/archives.h" #include "common/hacks/hack_manager.h" @@ -101,28 +101,45 @@ GPU::GPU(Core::System& system, Frontend::EmuWindow& emu_window, GPU::~GPU() = default; -PAddr GPU::VirtualToPhysicalAddress(VAddr addr) { - if (addr == 0) { - return 0; - } +constexpr Result ResultInvalidGpuBuffer(ErrorDescription::InvalidAddress, ErrorModule::GX, + ErrorSummary::InvalidArgument, + ErrorLevel::Usage); // 0xE0E02BF5 +constexpr Result ResultUnknownGxCommand(ErrorDescription::NotImplemented, ErrorModule::GX, + ErrorSummary::NotSupported, + ErrorLevel::Usage); // 0xE0C02BF4 +std::optional GPU::VirtualToPhysicalAddress(VAddr addr, u32 size) { + // Address translation of the GSP module. + // The linear heaps and the New 3DS extra memory must contain both the start, + // and the end of the buffer, VRAM only the start. The ends of the + // ranges are inclusive. + const VAddr end = addr + size; + const auto contains = [addr, end](VAddr range_start, VAddr range_end) { + return addr >= range_start && addr <= range_end && end >= range_start && end <= range_end; + }; + + if (contains(Memory::LINEAR_HEAP_VADDR, Memory::LINEAR_HEAP_VADDR_END)) { + return addr - Memory::LINEAR_HEAP_VADDR + Memory::FCRAM_PADDR; + } + if (contains(Memory::NEW_LINEAR_HEAP_VADDR, Memory::NEW_LINEAR_HEAP_VADDR_END)) { + return addr - Memory::NEW_LINEAR_HEAP_VADDR + Memory::FCRAM_PADDR; + } if (addr >= Memory::VRAM_VADDR && addr <= Memory::VRAM_VADDR_END) { return addr - Memory::VRAM_VADDR + Memory::VRAM_PADDR; } - if (addr >= Memory::LINEAR_HEAP_VADDR && addr <= Memory::LINEAR_HEAP_VADDR_END) { - return addr - Memory::LINEAR_HEAP_VADDR + Memory::FCRAM_PADDR; - } - if (addr >= Memory::NEW_LINEAR_HEAP_VADDR && addr <= Memory::NEW_LINEAR_HEAP_VADDR_END) { - return addr - Memory::NEW_LINEAR_HEAP_VADDR + Memory::FCRAM_PADDR; + if (contains(Memory::N3DS_EXTRA_RAM_VADDR, Memory::N3DS_EXTRA_RAM_VADDR_END)) { + return addr - Memory::N3DS_EXTRA_RAM_VADDR + Memory::N3DS_EXTRA_RAM_PADDR; } + + // TODO(PabloMK7): Find a better way to handle this. PAddr plg_fb_addr; - if (addr >= Memory::PLUGIN_3GX_FB_VADDR && addr <= Memory::PLUGIN_3GX_FB_VADDR_END && + if (contains(Memory::PLUGIN_3GX_FB_VADDR, Memory::PLUGIN_3GX_FB_VADDR_END) && (plg_fb_addr = impl->system.Memory().Plugin3GXFramebufferAddress())) { return addr - Memory::PLUGIN_3GX_FB_VADDR + plg_fb_addr; } - LOG_ERROR(HW_Memory, "Unknown virtual address @ 0x{:08X}", addr); - return addr; + LOG_ERROR(HW_GPU, "Invalid GPU buffer address 0x{:08X} (size 0x{:X})", addr, size); + return std::nullopt; } void GPU::SetInterruptHandler(Service::GSP::InterruptHandler handler) { @@ -142,9 +159,10 @@ void GPU::ClearAll(bool flush) { impl->rasterizer->ClearAll(flush); } -void GPU::Execute(const Service::GSP::Command& command) { +Result GPU::Execute(const Service::GSP::Command& command) { using Service::GSP::CommandId; auto& regs = impl->pica.regs; + Result result = ResultSuccess; switch (command.id) { case CommandId::RequestDma: { @@ -176,8 +194,10 @@ void GPU::Execute(const Service::GSP::Command& command) { auto& params = command.submit_gpu_cmdlist; auto& cmdbuffer = regs.internal.pipeline.command_buffer; - // Write to the command buffer GPU registers - cmdbuffer.addr[0].Assign(VirtualToPhysicalAddress(params.address) >> 3); + // Write to the command buffer GPU registers. + // The GSP module uses address 0 when the buffer is not in GPU visible memory. + const PAddr address = VirtualToPhysicalAddress(params.address, params.size).value_or(0); + cmdbuffer.addr[0].Assign(address >> 3); cmdbuffer.size[0].Assign(params.size >> 3); cmdbuffer.trigger[0] = 1; @@ -189,19 +209,36 @@ void GPU::Execute(const Service::GSP::Command& command) { auto& params = command.memory_fill; auto& memfill = regs.memory_fill_config; + const auto translate = [this](u32 start, u32 end) -> std::optional { + if (start >= end) { + return std::nullopt; + } + return VirtualToPhysicalAddress(start, end - start); + }; + // Write to the memory fill GPU registers. // If both buffers are set GSP dispatches PSC0 only. const bool has_both_bufs = params.start1 != 0 && params.start2 != 0; if (params.start1 != 0) { - memfill[0].address_start = VirtualToPhysicalAddress(params.start1) >> 3; - memfill[0].address_end = VirtualToPhysicalAddress(params.end1) >> 3; + const auto start = translate(params.start1, params.end1); + if (!start) { + result = ResultInvalidGpuBuffer; + break; + } + memfill[0].address_start = *start >> 3; + memfill[0].address_end = (*start + params.end1 - params.start1) >> 3; memfill[0].value_32bit = params.value1; memfill[0].control = params.control1; MemoryFill(0, has_both_bufs ? std::numeric_limits::max() : 0); } if (params.start2 != 0) { - memfill[1].address_start = VirtualToPhysicalAddress(params.start2) >> 3; - memfill[1].address_end = VirtualToPhysicalAddress(params.end2) >> 3; + const auto start = translate(params.start2, params.end2); + if (!start) { + result = ResultInvalidGpuBuffer; + break; + } + memfill[1].address_start = *start >> 3; + memfill[1].address_end = (*start + params.end2 - params.start2) >> 3; memfill[1].value_32bit = params.value2; memfill[1].control = params.control2; MemoryFill(1, has_both_bufs ? 0 : 1); @@ -213,8 +250,12 @@ void GPU::Execute(const Service::GSP::Command& command) { auto& display_transfer = regs.display_transfer_config; // Write to the transfer engine GPU registers. - display_transfer.input_address = VirtualToPhysicalAddress(params.in_buffer_address) >> 3; - display_transfer.output_address = VirtualToPhysicalAddress(params.out_buffer_address) >> 3; + // The GSP module uses address 0 when the buffer is not in GPU visible memory (only the + // first byte is checked). + display_transfer.input_address = + VirtualToPhysicalAddress(params.in_buffer_address, 1).value_or(0) >> 3; + display_transfer.output_address = + VirtualToPhysicalAddress(params.out_buffer_address, 1).value_or(0) >> 3; display_transfer.input_size = params.in_buffer_size; display_transfer.output_size = params.out_buffer_size; display_transfer.flags = params.flags; @@ -229,8 +270,11 @@ void GPU::Execute(const Service::GSP::Command& command) { auto& texture_copy = regs.display_transfer_config; // Write to the transfer engine GPU registers. - texture_copy.input_address = VirtualToPhysicalAddress(params.in_buffer_address) >> 3; - texture_copy.output_address = VirtualToPhysicalAddress(params.out_buffer_address) >> 3; + // The GSP module uses address 0 when the buffer is not in GPU visible memory. + texture_copy.input_address = + VirtualToPhysicalAddress(params.in_buffer_address, params.size).value_or(0) >> 3; + texture_copy.output_address = + VirtualToPhysicalAddress(params.out_buffer_address, params.size).value_or(0) >> 3; texture_copy.texture_copy.size = params.size; texture_copy.texture_copy.input_size = params.in_width_gap; texture_copy.texture_copy.output_size = params.out_width_gap; @@ -248,31 +292,48 @@ void GPU::Execute(const Service::GSP::Command& command) { } default: LOG_ERROR(HW_GPU, "Unknown command {:#08X}", command.id.Value()); + result = ResultUnknownGxCommand; } // Notify debugger that a GSP command was processed. if (impl->debug_context) { impl->debug_context->OnEvent(Pica::DebugContext::Event::GSPCommandProcessed, &command); } + return result; } void GPU::SetBufferSwap(u32 screen_id, const Service::GSP::FrameBufferInfo& info) { - const PAddr phys_address_left = VirtualToPhysicalAddress(info.address_left); - const PAddr phys_address_right = VirtualToPhysicalAddress(info.address_right); + // The GSP module does not apply a framebuffer outside GPU visible memory. The right address + // is used only for the top screen in stereoscopic mode and when it is not zero. + // Otherwise the left address is used for both eyes. + const auto phys_address_left = VirtualToPhysicalAddress(info.address_left, info.stride); + if (!phys_address_left) { + return; + } // Update framebuffer properties. auto& framebuffer = impl->pica.regs.framebuffer_config[screen_id]; + if (screen_id == 0) { + const bool stereoscopic = (info.format & 0x70) == 0x20; + const PAddr phys_address_right = + (!stereoscopic || info.address_right == 0) + ? *phys_address_left + : VirtualToPhysicalAddress(info.address_right, info.stride).value_or(0); + if (info.active_fb == 0) { + framebuffer.address_right1 = phys_address_right; + } else { + framebuffer.address_right2 = phys_address_right; + } + } if (info.active_fb == 0) { - framebuffer.address_left1 = phys_address_left; - framebuffer.address_right1 = phys_address_right; + framebuffer.address_left1 = *phys_address_left; } else { - framebuffer.address_left2 = phys_address_left; - framebuffer.address_right2 = phys_address_right; + framebuffer.address_left2 = *phys_address_left; } framebuffer.stride = info.stride; - framebuffer.format = info.format; - framebuffer.active_fb = info.shown_fb; + framebuffer.format = (framebuffer.format & 0xFFFF0000) | (info.format & 0xFFFF); + framebuffer.active_fb = info.shown_fb & 0xFF; // Notify debugger about the buffer swap. if (impl->debug_context) { @@ -407,6 +468,17 @@ void GPU::SubmitCmdList(u32 index) { // Forward command list processing to the PICA core. const PAddr addr = config.GetPhysicalAddress(index); const u32 size = config.GetSize(index); + + // Do not process the list if the memory range is not sane. + auto& memory = impl->system.Memory(); + const u8* head = memory.GetPhysicalPointer(addr); + if (!head || (size != 0 && memory.GetPhysicalPointer(addr + size - 1) != head + size - 1)) { + LOG_ERROR(HW_GPU, "Invalid command list address 0x{:08X} (size 0x{:X})", addr, size); + impl->signal_interrupt(Service::GSP::InterruptId::P3D, 0); + config.trigger[index] = 0; + return; + } + impl->pica.ProcessCmdList(addr, size, !right_eye_disabler->ShouldAllowCmdQueueTrigger(addr, size)); config.trigger[index] = 0; diff --git a/src/video_core/gpu.h b/src/video_core/gpu.h index ac19b4c65..2552e02b1 100644 --- a/src/video_core/gpu.h +++ b/src/video_core/gpu.h @@ -1,13 +1,15 @@ // Copyright 2023-2026 Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the misc/licenses/gplv2.txt file included. +// Licensed under GPLv3 or any later version +// Refer to the LICENSE.txt file included. #pragma once #include #include +#include #include +#include "core/hle/result.h" #include "core/hle/service/gsp/gsp_interrupt.h" namespace Service::GSP { @@ -61,7 +63,7 @@ public: void ClearAll(bool flush); /// Executes the provided GSP command. - void Execute(const Service::GSP::Command& command); + Result Execute(const Service::GSP::Command& command); /// Updates GPU display framebuffer configuration using the specified parameters. void SetBufferSwap(u32 screen_id, const Service::GSP::FrameBufferInfo& info); @@ -120,7 +122,9 @@ private: struct Impl; std::unique_ptr impl; - PAddr VirtualToPhysicalAddress(VAddr addr); + /// Translates the address of a GPU buffer as the GSP module does, or returns std::nullopt when + /// the buffer is not in GPU visible memory. + std::optional VirtualToPhysicalAddress(VAddr addr, u32 size); }; } // namespace VideoCore