/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2022 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** * * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include REXCVAR_DEFINE_STRING(trace_gpu_prefix, "", "GPU", "GPU trace file prefix"); REXCVAR_DEFINE_BOOL(trace_gpu_stream, false, "GPU", "Enable GPU trace streaming"); REXCVAR_DEFINE_BOOL(guest_vblank_sync_to_refresh, false, "GPU", "Keep guest VBlank cadence tied to the guest refresh rate even when host " "vsync is disabled"); REXCVAR_DEFINE_STRING(swap_post_effect, "none", "GPU", "Swap post effect: none, fxaa, fxaa_extreme") .allowed({"none", "fxaa", "fxaa_extreme"}) .lifecycle(rex::cvar::Lifecycle::kRequiresRestart); namespace { constexpr bool kStoreShaders = true; bool HasMeaningfulTextureFetch(const rex::system::GraphicsSwapSubmission& submission) { for (uint32_t word : submission.texture_fetch) { if (word != 0) { return true; } } return false; } rex::system::GraphicsSwapSubmission MergeSwapSubmission( const rex::system::GraphicsSwapSubmission& base, const rex::system::GraphicsSwapSubmission& incoming) { rex::system::GraphicsSwapSubmission merged = base; if (incoming.frontbuffer_virtual_address) { merged.frontbuffer_virtual_address = incoming.frontbuffer_virtual_address; } if (incoming.frontbuffer_physical_address) { merged.frontbuffer_physical_address = incoming.frontbuffer_physical_address; } if (incoming.frontbuffer_width) { merged.frontbuffer_width = incoming.frontbuffer_width; } if (incoming.frontbuffer_height) { merged.frontbuffer_height = incoming.frontbuffer_height; } if (incoming.texture_format) { merged.texture_format = incoming.texture_format; } if (incoming.color_space) { merged.color_space = incoming.color_space; } if (HasMeaningfulTextureFetch(incoming)) { merged.texture_fetch = incoming.texture_fetch; } return merged; } rex::graphics::CommandProcessor::SwapPostEffect ParseSwapPostEffect( const std::string& effect_name) { std::string lowered = effect_name; std::transform(lowered.begin(), lowered.end(), lowered.begin(), [](unsigned char c) { c = static_cast(std::tolower(c)); return c == '-' ? '_' : char(c); }); if (lowered == "fxaa") { return rex::graphics::CommandProcessor::SwapPostEffect::kFxaa; } if (lowered == "fxaa_extreme" || lowered == "extreme") { return rex::graphics::CommandProcessor::SwapPostEffect::kFxaaExtreme; } return rex::graphics::CommandProcessor::SwapPostEffect::kNone; } } // namespace namespace rex::graphics { // Nvidia Optimus/AMD PowerXpress support. // These exports force the process to trigger the discrete GPU in multi-GPU // systems. // https://developer.download.nvidia.com/devzone/devcenter/gamegraphics/files/OptimusRenderingPolicies.pdf // https://stackoverflow.com/questions/17458803/amd-equivalent-to-nvoptimusenablement #if REX_PLATFORM_WIN32 extern "C" { __declspec(dllexport) uint32_t NvOptimusEnablement = 0x00000001; __declspec(dllexport) uint32_t AmdPowerXpressRequestHighPerformance = 1; } // extern "C" #endif // REX_PLATFORM_WIN32 GraphicsSystem::GraphicsSystem() : vsync_worker_running_(false) {} GraphicsSystem::~GraphicsSystem() = default; X_STATUS GraphicsSystem::SetupPresentation(ui::WindowedAppContext* app_context) { if (presenter_) { return X_STATUS_SUCCESS; } if (!provider_) { CreateProvider(true); if (!provider_) { REXGPU_ERROR("Unable to create graphics provider"); return X_STATUS_UNSUCCESSFUL; } provider_supports_presentation_ = true; } else if (!provider_supports_presentation_) { // A prior SetupGuestGpu built a headless provider; backends like Vulkan // need swapchain support baked in at provider creation time. REXGPU_ERROR("SetupPresentation called after headless SetupGuestGpu; call order is reversed"); return X_STATUS_UNSUCCESSFUL; } app_context_ = app_context; auto loss_cb = [this](bool is_responsible, bool statically_from_ui_thread) { OnHostGpuLossFromAnyThread(is_responsible); }; if (app_context_) { // Presenter creation must happen on the UI thread. app_context_->CallInUIThreadSynchronous( [this, loss_cb]() { presenter_ = provider_->CreatePresenter(loss_cb); }); } else { // Offscreen path (e.g. capturing guest output without a window). presenter_ = provider_->CreatePresenter(loss_cb); } if (!presenter_) { REXGPU_ERROR("Unable to create presenter"); return X_STATUS_UNSUCCESSFUL; } return X_STATUS_SUCCESS; } X_STATUS GraphicsSystem::SetupGuestGpu(runtime::FunctionDispatcher* function_dispatcher, system::KernelState* kernel_state) { memory_ = function_dispatcher->memory(); function_dispatcher_ = function_dispatcher; kernel_state_ = kernel_state; // Headless path: no one set up presentation, so build a no-presentation // provider just for the command processor. if (!provider_) { CreateProvider(false); provider_supports_presentation_ = false; } // Create command processor. This will spin up a thread to process all // incoming ringbuffer packets. command_processor_ = CreateCommandProcessor(); if (!command_processor_->Initialize()) { REXGPU_ERROR("Unable to initialize command processor"); return X_STATUS_UNSUCCESSFUL; } command_processor_->SetDesiredSwapPostEffect(ParseSwapPostEffect(REXCVAR_GET(swap_post_effect))); // Register GPU MMIO handlers // GPU registers are at 0x7FC80000-0x7FCFFFFF memory_->AddVirtualMappedRange(0x7FC80000, // base address 0xFFFF0000, // mask 0x0000FFFF, // size (64KB) this, // context (GraphicsSystem*) reinterpret_cast(ReadRegisterThunk), reinterpret_cast(WriteRegisterThunk)); // Guest vblank timer based on the configured guest video mode. system::X_VIDEO_MODE video_mode; kernel::xboxkrnl::VdQueryVideoMode(&video_mode); double refresh_rate_hz = std::max(1.0, double(float(video_mode.refresh_rate))); uint64_t guest_tick_frequency = chrono::Clock::guest_tick_frequency(); uint64_t vsync_interval_ticks = std::max(uint64_t(1), uint64_t(double(guest_tick_frequency) / refresh_rate_hz)); uint64_t no_vsync_interval_ticks = std::max(uint64_t(1), guest_tick_frequency / 1000); guest_vblank_interval_ticks_.store(vsync_interval_ticks, std::memory_order_release); vsync_worker_running_ = true; vsync_worker_thread_ = system::object_ref( new system::XHostThread(kernel_state_, 128 * 1024, 0, [this, vsync_interval_ticks, no_vsync_interval_ticks, guest_tick_frequency]() { uint64_t last_frame_time = chrono::Clock::QueryGuestTickCount(); while (vsync_worker_running_) { uint64_t current_time = chrono::Clock::QueryGuestTickCount(); uint64_t interval_ticks = REXCVAR_GET(guest_vblank_sync_to_refresh) ? vsync_interval_ticks : (REXCVAR_GET(vsync) ? vsync_interval_ticks : no_vsync_interval_ticks); double vblank_hz_override = GetGuestVblankHzOverride(); if (vblank_hz_override > 0.0) { interval_ticks = std::max( uint64_t(1), uint64_t(double(guest_tick_frequency) / vblank_hz_override)); } // Re-anchor when far behind so a shrinking interval (an override // switching from a paced rate to a much faster one) or a long stall // does not burst a backlog of MarkVblank calls in one wake. if (current_time - last_frame_time >= interval_ticks * 4) { last_frame_time = current_time - interval_ticks; } while (current_time - last_frame_time >= interval_ticks) { MarkVblank(); last_frame_time += interval_ticks; } rex::thread::Sleep(std::chrono::milliseconds(1)); } return 0; })); // TODO: set_can_debugger_suspend not yet ported // vsync_worker_thread_->set_can_debugger_suspend(true); vsync_worker_thread_->set_name("GPU VSync"); vsync_worker_thread_->Create(); if (REXCVAR_GET(trace_gpu_stream)) { BeginTracing(); } return X_STATUS_SUCCESS; } void GraphicsSystem::Shutdown() { if (command_processor_) { EndTracing(); command_processor_->Shutdown(); command_processor_.reset(); } if (vsync_worker_thread_) { vsync_worker_running_ = false; vsync_worker_thread_->Wait(0, 0, 0, nullptr); vsync_worker_thread_.reset(); } if (presenter_) { if (app_context_) { app_context_->CallInUIThreadSynchronous([this]() { presenter_.reset(); }); } // If there's no app context (thus the presenter is owned by the thread that // initialized the GraphicsSystem) or can't be queueing UI thread calls // anymore, shutdown anyway. presenter_.reset(); } provider_.reset(); } void GraphicsSystem::OnHostGpuLossFromAnyThread([[maybe_unused]] bool is_responsible) { // TODO(Triang3l): Somehow gain exclusive ownership of the Provider (may be // used by the command processor, the presenter, and possibly anything else, // it's considered free-threaded, except for lifetime management which will be // involved in this case) and reset it so a new host GPU API device is // created. Then ask the command processor to reset itself in its thread, and // ask the UI thread to reset the Presenter (the UI thread manages its // lifetime - but if there's no WindowedAppContext, either don't reset it as // in this case there's no user who needs uninterrupted gameplay, or somehow // protect it with a mutex so any thread can be considered a UI thread and // reset). if (host_gpu_loss_reported_.test_and_set(std::memory_order_relaxed)) { return; } rex::FatalError("Graphics device lost (probably due to an internal error)"); } uint32_t GraphicsSystem::ReadRegisterThunk(void* ppc_context, GraphicsSystem* gs, uint32_t addr) { return gs->ReadRegister(addr); } void GraphicsSystem::WriteRegisterThunk(void* ppc_context, GraphicsSystem* gs, uint32_t addr, uint32_t value) { gs->WriteRegister(addr, value); } uint32_t GraphicsSystem::ReadRegister(uint32_t addr) { uint32_t r = (addr & 0xFFFF) / 4; switch (r) { case 0x0F00: // RB_EDRAM_TIMING return 0x08100748; case 0x0F01: // RB_BC_CONTROL return 0x0000200E; case 0x194C: { // R500_D1MODE_V_COUNTER system::X_VIDEO_MODE video_mode; kernel::xboxkrnl::VdQueryVideoMode(&video_mode); return std::min(uint32_t(video_mode.display_height), uint32_t(0x0FFF)); } case 0x1951: // interrupt status return 1; // vblank case 0x1961: { // AVIVO_D1MODE_VIEWPORT_SIZE // Maximum [width(0x0FFF), height(0x0FFF)]. system::X_VIDEO_MODE video_mode; kernel::xboxkrnl::VdQueryVideoMode(&video_mode); uint32_t viewport_width = std::min(uint32_t(video_mode.display_width), uint32_t(0x0FFF)); uint32_t viewport_height = std::min(uint32_t(video_mode.display_height), uint32_t(0x0FFF)); return (viewport_width << 16) | viewport_height; } default: if (!register_file_.GetRegisterInfo(r)) { REXGPU_DEBUG("GPU: Read from unknown register ({:04X})", r); } } assert_true(r < RegisterFile::kRegisterCount); return register_file_.values[r]; } void GraphicsSystem::WriteRegister(uint32_t addr, uint32_t value) { uint32_t r = (addr & 0xFFFF) / 4; switch (r) { case 0x01C5: // CP_RB_WPTR command_processor_->UpdateWritePointer(value); break; case 0x1844: // AVIVO_D1GRPH_PRIMARY_SURFACE_ADDRESS break; default: REXGPU_WARN("Unknown GPU register {:04X} write: {:08X}", r, value); break; } assert_true(r < RegisterFile::kRegisterCount); register_file_.values[r] = value; } void GraphicsSystem::InitializeRingBuffer(uint32_t ptr, uint32_t size_log2) { command_processor_->InitializeRingBuffer(ptr, size_log2); } void GraphicsSystem::EnableReadPointerWriteBack(uint32_t ptr, uint32_t block_size_log2) { command_processor_->EnableReadPointerWriteBack(ptr, block_size_log2); } void GraphicsSystem::SetInterruptCallback(uint32_t callback, uint32_t user_data) { interrupt_callback_ = callback; interrupt_callback_data_ = user_data; REXGPU_INFO("SetInterruptCallback({:08X}, {:08X})", callback, user_data); } void GraphicsSystem::SetFrameBoundaryCallback(std::function callback) { frame_boundary_callback_ = std::move(callback); } bool GraphicsSystem::HandleVideoSwap(const system::GraphicsSwapSubmission& submission) { { std::lock_guard lock(last_swap_submission_mutex_); last_swap_submission_ = MergeSwapSubmission(last_swap_submission_, submission); ++last_swap_submission_sequence_; } if (frame_boundary_callback_) { frame_boundary_callback_(memory_); } return false; } bool GraphicsSystem::GetLastSwapSubmission(system::GraphicsSwapSubmission* out_submission, uint64_t* out_sequence) const { std::lock_guard lock(last_swap_submission_mutex_); if (out_submission) { *out_submission = last_swap_submission_; } if (out_sequence) { *out_sequence = last_swap_submission_sequence_; } return last_swap_submission_sequence_ != 0; } void GraphicsSystem::DispatchInterruptCallback(uint32_t source, uint32_t cpu) { if (!interrupt_callback_) { return; } auto thread = system::XThread::GetCurrentThread(); assert_not_null(thread); // Pick a CPU, if needed. We're going to guess 2. Because. if (cpu == 0xFFFFFFFF) { cpu = 2; } thread->SetActiveCpu(cpu); if (source == 0) { last_vblank_interrupt_guest_tick_.store(chrono::Clock::QueryGuestTickCount(), std::memory_order_release); } // REXGPU_INFO("Dispatching GPU interrupt at {:08X} w/ mode {} on cpu {}", // interrupt_callback_, source, cpu); uint64_t args[] = {source, interrupt_callback_data_}; function_dispatcher_->ExecuteInterrupt(thread->thread_state(), interrupt_callback_, args, rex::countof(args)); } namespace { std::atomic g_guest_vblank_hz_override{0.0}; } // namespace void GraphicsSystem::SetGuestVblankHzOverride(double hz) { g_guest_vblank_hz_override.store(hz, std::memory_order_relaxed); } double GraphicsSystem::GetGuestVblankHzOverride() { return g_guest_vblank_hz_override.load(std::memory_order_relaxed); } void GraphicsSystem::MarkVblank() { // TODO: Enable profiling once ported // SCOPE_profile_cpu_f("gpu"); // Increment vblank counter (so the game sees us making progress). if (command_processor_) { command_processor_->increment_counter(); } // TODO(benvanik): we shouldn't need to do the dispatch here, but there's // something wrong and the CP will block waiting for code that // needs to be run in the interrupt. DispatchInterruptCallback(0, 2); } void GraphicsSystem::ClearCaches() { command_processor_->CallInThread([&]() { command_processor_->ClearCaches(); }); } void GraphicsSystem::InvalidateGpuMemory() { command_processor_->CallInThread([&]() { command_processor_->InvalidateGpuMemory(); }); } void GraphicsSystem::InitializeShaderStorage(const std::filesystem::path& cache_root, uint32_t title_id, bool blocking) { if (!kStoreShaders) { return; } if (blocking) { if (command_processor_->is_paused()) { // Safe to run on any thread while the command processor is paused, no // race condition. command_processor_->InitializeShaderStorage(cache_root, title_id, true); } else { rex::thread::Fence fence; command_processor_->CallInThread([this, cache_root, title_id, &fence]() { command_processor_->InitializeShaderStorage(cache_root, title_id, true); fence.Signal(); }); fence.Wait(); } } else { command_processor_->CallInThread([this, cache_root, title_id]() { command_processor_->InitializeShaderStorage(cache_root, title_id, false); }); } } void GraphicsSystem::RequestFrameTrace() { command_processor_->RequestFrameTrace(REXCVAR_GET(trace_gpu_prefix)); } void GraphicsSystem::BeginTracing() { command_processor_->BeginTracing(REXCVAR_GET(trace_gpu_prefix)); } void GraphicsSystem::EndTracing() { command_processor_->EndTracing(); } void GraphicsSystem::Pause() { paused_ = true; command_processor_->Pause(); } void GraphicsSystem::Resume() { paused_ = false; command_processor_->Resume(); } bool GraphicsSystem::Save(::rex::stream::ByteStream* stream) { stream->Write(interrupt_callback_); stream->Write(interrupt_callback_data_); return command_processor_->Save(stream); } bool GraphicsSystem::Restore(::rex::stream::ByteStream* stream) { interrupt_callback_ = stream->Read(); interrupt_callback_data_ = stream->Read(); return command_processor_->Restore(stream); } } // namespace rex::graphics