mirror of
https://git.eden-emu.dev/eden-emu/eden
synced 2026-10-25 18:34:29 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e1b03d288 | |||
| ed4ae7e1bd | |||
| 97581e2aee | |||
| 98fe077f91 |
@@ -304,16 +304,6 @@ if (ARCHITECTURE_arm64 AND (ANDROID OR LINUX))
|
||||
add_compile_definitions(HAS_NCE=1)
|
||||
endif()
|
||||
|
||||
set(ENABLE_SCARLETNX_DEFAULT ON)
|
||||
if ((NOT LINUX AND NOT FREEBSD) OR (NOT CMAKE_C_COMPILER_ID MATCHES "Clang" OR NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang"))
|
||||
set(ENABLE_SCARLETNX_DEFAULT OFF)
|
||||
endif()
|
||||
option(ENABLE_SCARLETNX "Enable the ScarletNX recompiler CPU backend (requires Clang and LLVM)" "${ENABLE_SCARLETNX_DEFAULT}")
|
||||
if (ENABLE_SCARLETNX)
|
||||
set(HAS_SCARLETNX 1)
|
||||
add_compile_definitions(HAS_SCARLETNX=1)
|
||||
endif()
|
||||
|
||||
if (YUZU_ROOM)
|
||||
add_compile_definitions(YUZU_ROOM)
|
||||
endif()
|
||||
|
||||
@@ -243,27 +243,6 @@
|
||||
"repo": "crosire/reshade",
|
||||
"version": "v6.8.0"
|
||||
},
|
||||
"scarletnx": {
|
||||
"bundled": true,
|
||||
"hash": "93f7e6810644253d6396239092d5769c44b005fa7a17cf39c6d773050a19de74fca1f96bb6eec7cb40384eb158b2595c9e1171ad592d8eb8326fd31f5632f505",
|
||||
"options": [
|
||||
"SNX_BUILD_NX OFF",
|
||||
"SNX_BUILD_GNUABI OFF",
|
||||
"SNX_BUILD_3DS OFF",
|
||||
"SNX_BUILD_VITA OFF",
|
||||
"SNX_BUILD_AGB OFF",
|
||||
"SNX_DEPLOY OFF",
|
||||
"SNX_BUILD_TESTING OFF",
|
||||
"SNX_BUILD_GUI OFF",
|
||||
"SNX_MULTICALL OFF",
|
||||
"SNX_TRACY OFF",
|
||||
"SNX_RECOMPILER_SHARED OFF",
|
||||
"SNX_LLVM_SHARED OFF"
|
||||
],
|
||||
"package": "ScarletNX",
|
||||
"repo": "scarletnx/ScarletNX",
|
||||
"version": "f16810ce9c"
|
||||
},
|
||||
"sdl3": {
|
||||
"hash": "df5a323af7ac366661a3c0e887969c72584d232f3cc211419d59b0487b620b6b2859d4549c9e8df002ee489290062e466fcfddf7edc0872a37b1f2845e81c0f3",
|
||||
"min_version": "3.2.10",
|
||||
|
||||
@@ -78,9 +78,6 @@ The following options are desktop only.
|
||||
- `ENABLE_OPENGL` (ON) Enable the OpenGL graphics backend
|
||||
- Unavailable on Windows/ARM64 and on Android
|
||||
- You probably shouldn't turn this off.
|
||||
- `ENABLE_SCARLETNX` (OFF) Enable the ScarletNX recompiler
|
||||
- Requires Clang and LLVM's development files
|
||||
- Needs multicore and fastmem
|
||||
|
||||
### Qt
|
||||
|
||||
|
||||
Vendored
-8
@@ -249,14 +249,6 @@ if (YUZU_TESTS OR DYNARMIC_TESTS)
|
||||
AddJsonPackage(catch2)
|
||||
endif()
|
||||
|
||||
# ScarletNX
|
||||
if (ENABLE_SCARLETNX)
|
||||
AddJsonPackage(scarletnx)
|
||||
if (TARGET scarletnx-recompiler)
|
||||
set_target_properties(scarletnx-recompiler PROPERTIES SYSTEM ON)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# getopt
|
||||
if (MSVC)
|
||||
add_subdirectory(getopt)
|
||||
|
||||
@@ -256,9 +256,7 @@ struct Values {
|
||||
CpuBackend::Dynarmic,
|
||||
#endif
|
||||
CpuBackend::Dynarmic,
|
||||
#if defined(HAS_SCARLETNX)
|
||||
CpuBackend::ScarletNx,
|
||||
#elif defined(HAS_NCE)
|
||||
#ifdef HAS_NCE
|
||||
CpuBackend::Nce,
|
||||
#else
|
||||
CpuBackend::Dynarmic,
|
||||
|
||||
@@ -138,7 +138,7 @@ ENUM(RendererBackend, OpenGL_GLSL, Vulkan, Null, OpenGL_GLASM, OpenGL_SPIRV);
|
||||
ENUM(GpuAccuracy, Low, High);
|
||||
ENUM(DmaAccuracy, Default, Unsafe, Safe);
|
||||
ENUM(GpuFenceBehavior, Default, Immediate, Balanced, Accurate);
|
||||
ENUM(CpuBackend, Dynarmic, Nce, ScarletNx);
|
||||
ENUM(CpuBackend, Dynarmic, Nce);
|
||||
ENUM(CpuAccuracy, Auto, Accurate, Unsafe, Paranoid, Debugging);
|
||||
ENUM(CpuClock, Normal, Boost, Overclock)
|
||||
ENUM(MemoryLayout, Memory_4Gb, Memory_6Gb, Memory_8Gb, Memory_10Gb, Memory_12Gb);
|
||||
|
||||
@@ -1248,18 +1248,6 @@ if (HAS_NCE)
|
||||
target_link_libraries(core PRIVATE merry::oaknut)
|
||||
endif()
|
||||
|
||||
if (HAS_SCARLETNX)
|
||||
target_sources(core PRIVATE
|
||||
arm/scarletnx/arm_scarletnx_32.cpp
|
||||
arm/scarletnx/arm_scarletnx_32.h
|
||||
arm/scarletnx/arm_scarletnx_64.cpp
|
||||
arm/scarletnx/arm_scarletnx_64.h
|
||||
arm/scarletnx/scarletnx_api.h
|
||||
arm/scarletnx/scarletnx_process.cpp
|
||||
arm/scarletnx/scarletnx_process.h)
|
||||
target_link_libraries(core PRIVATE ScarletNX::recompiler)
|
||||
endif()
|
||||
|
||||
if (ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64 OR ARCHITECTURE_riscv64 OR ARCHITECTURE_loongarch64)
|
||||
target_sources(core PRIVATE
|
||||
arm/dynarmic/arm_dynarmic.h
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
#include <atomic>
|
||||
|
||||
#include "common/logging.h"
|
||||
#include "core/arm/scarletnx/arm_scarletnx_32.h"
|
||||
#include "core/core.h"
|
||||
#include "core/core_timing.h"
|
||||
#include "core/hle/kernel/k_process.h"
|
||||
|
||||
namespace Core {
|
||||
|
||||
namespace {
|
||||
|
||||
// The exit SignalInterrupt sets, which no block sets itself.
|
||||
constexpr u32 InterruptExit = 0x1000;
|
||||
constexpr u32 UserMode = 0x10;
|
||||
constexpr u32 ModeMask = 0x1F;
|
||||
|
||||
// FPSCR splits into FPSR and FPCR as in ArmDynarmic32.
|
||||
std::pair<u32, u32> FpscrToFpsrFpcr(u32 fpscr) {
|
||||
const u32 fpsr = (fpscr & 0xf8000000) | (fpscr & 0x80) | (fpscr & 0x1f);
|
||||
const u32 fpcr = (fpscr & 0x7ff8000) | (fpscr & 0x1f00);
|
||||
return {fpsr, fpcr};
|
||||
}
|
||||
|
||||
u32 FpsrFpcrToFpscr(u64 fpsr, u64 fpcr) {
|
||||
const auto [s, c] = FpscrToFpsrFpcr(u32(fpsr | fpcr));
|
||||
return s | c;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ArmScarletNx32::ArmScarletNx32(std::shared_ptr<ScarletNxProcess> process, std::size_t core_index)
|
||||
: ArmInterface{true}, m_process{std::move(process)}, m_core_index{core_index} {
|
||||
m_rt.coproc = &Coprocessor;
|
||||
m_cpu.parent = this;
|
||||
m_cpu.cpu.mem = m_process->m_arena;
|
||||
m_cpu.cpu.mem_size = 1ULL << 32;
|
||||
m_cpu.cpu.rt = &m_rt;
|
||||
|
||||
m_fault_scope.memory = &m_process->m_process.GetMemory();
|
||||
m_fault_scope.arena = m_process->m_arena;
|
||||
m_fault_scope.arena_size = ScarletNxProcess::ArenaSize;
|
||||
}
|
||||
|
||||
ArmScarletNx32::~ArmScarletNx32() {
|
||||
snx_recompiled32_free(m_module);
|
||||
}
|
||||
|
||||
int ArmScarletNx32::Svc(void* context, u32 imm, u64 number, const u64 arguments[6]) {
|
||||
auto& self = *static_cast<ArmScarletNx32*>(context);
|
||||
self.m_svc = imm;
|
||||
self.m_in_svc = true;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void ArmScarletNx32::Coprocessor(SnxArmCPU* cpu, u32 word) {
|
||||
auto& self = *reinterpret_cast<Cpu32*>(cpu)->parent;
|
||||
const u32 coproc = (word >> 8) & 0xF;
|
||||
const u32 crm = word & 0xF;
|
||||
|
||||
if (coproc == 15 && (word & 0x0F100010) == 0x0E000010) {
|
||||
// MCR p15
|
||||
const u32 opc1 = (word >> 21) & 7;
|
||||
const u32 crn = (word >> 16) & 0xF;
|
||||
const u32 opc2 = (word >> 5) & 7;
|
||||
if (opc1 == 0 && crn == 7 && crm == 5 && opc2 == 4) {
|
||||
// CP15_FLUSH_PREFETCH_BUFFER
|
||||
return;
|
||||
}
|
||||
if (opc1 == 0 && crn == 7 && crm == 10 && (opc2 == 4 || opc2 == 5)) {
|
||||
// CP15_DATA_SYNC_BARRIER, CP15_DATA_MEMORY_BARRIER
|
||||
std::atomic_thread_fence(std::memory_order_seq_cst);
|
||||
return;
|
||||
}
|
||||
} else if (coproc == 15 && (word & 0x0FF00000) == 0x0C500000) {
|
||||
// MRRC p15
|
||||
const u32 opc1 = (word >> 4) & 0xF;
|
||||
const u32 rt = (word >> 12) & 0xF;
|
||||
const u32 rt2 = (word >> 16) & 0xF;
|
||||
if (opc1 == 0 && crm == 14) {
|
||||
// CNTPCT
|
||||
const u64 ticks = self.m_process->m_system.CoreTiming().GetClockTicks();
|
||||
cpu->r[rt] = u32(ticks);
|
||||
cpu->r[rt2] = u32(ticks >> 32);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// TPIDRURW and TPIDRURO, and undefined for anything else.
|
||||
snx_arm_coproc_user(cpu, word);
|
||||
ASSERT(cpu->exit == SNX_ARM_OK && "unhandled coproc");
|
||||
}
|
||||
|
||||
HaltReason ArmScarletNx32::RunThread(Kernel::KThread* thread) {
|
||||
if (m_interrupt.exchange(false)) {
|
||||
return HaltReason::BreakLoop;
|
||||
}
|
||||
|
||||
// The block cache only covers the code present when it was made.
|
||||
const u64 generation = m_process->CodeGeneration();
|
||||
if (!m_module || generation != m_code_generation) {
|
||||
snx_recompiled32_free(m_module);
|
||||
m_module = m_process->MakeModule32();
|
||||
m_code_generation = generation;
|
||||
}
|
||||
|
||||
LoadCpu();
|
||||
m_in_svc = false;
|
||||
m_fault_scope.last_fault = ~u64{};
|
||||
SetScarletNxFaultScope(&m_fault_scope);
|
||||
if (sigsetjmp(m_fault_scope.env, 1) != 0) {
|
||||
SetScarletNxFaultScope(nullptr);
|
||||
LOG_CRITICAL(Core_ARM, "invalid memory access at {:#x}",
|
||||
m_fault_scope.fault_address);
|
||||
SaveCpu();
|
||||
return HaltReason::PrefetchAbort;
|
||||
}
|
||||
const int exit =
|
||||
m_module ? snx_recompiled32_run(m_module, &m_cpu.cpu, &Svc, this) : SNX_ARM_BAD_ENTRY;
|
||||
SetScarletNxFaultScope(nullptr);
|
||||
|
||||
SaveCpu();
|
||||
return Halt(exit);
|
||||
}
|
||||
|
||||
HaltReason ArmScarletNx32::Halt(int exit) {
|
||||
if (m_in_svc) {
|
||||
return HaltReason::SupervisorCall;
|
||||
}
|
||||
u32 const pc = m_cpu.cpu.r[15];
|
||||
u32 const syndrome = m_cpu.cpu.syndrome;
|
||||
switch (u32(exit)) {
|
||||
case InterruptExit:
|
||||
m_interrupt.store(false);
|
||||
return HaltReason::BreakLoop;
|
||||
case SNX_ARM_BREAKPOINT:
|
||||
// As Dynarmic does without a debugger: report it and go on.
|
||||
LOG_CRITICAL(Core_ARM, "BKPT #{:#x} at {:#x}", syndrome, pc);
|
||||
return HaltReason::BreakLoop;
|
||||
default:
|
||||
LOG_CRITICAL(Core_ARM, "A32 exit {} at {:#x} (syndrome {:#x})", exit, pc, syndrome);
|
||||
return HaltReason::PrefetchAbort;
|
||||
}
|
||||
}
|
||||
|
||||
HaltReason ArmScarletNx32::StepThread(Kernel::KThread* thread) {
|
||||
// Never reached: the debugger selects Dynarmic.
|
||||
LOG_ERROR(Core_ARM, "ScarletNX cannot single step");
|
||||
return HaltReason::BreakLoop;
|
||||
}
|
||||
|
||||
void ArmScarletNx32::LoadCpu() {
|
||||
auto& cpu = m_cpu.cpu;
|
||||
static_assert(sizeof(cpu.d) <= sizeof(m_ctx.v));
|
||||
for (std::size_t i = 0; i < 16; i++)
|
||||
cpu.r[i] = u32(m_ctx.r[i]);
|
||||
cpu.cpsr = (m_ctx.pstate & ~ModeMask) | UserMode;
|
||||
std::memcpy(cpu.d, m_ctx.v.data(), sizeof(cpu.d));
|
||||
cpu.fpscr = FpsrFpcrToFpscr(m_ctx.fpsr, m_ctx.fpcr);
|
||||
cpu.tpidrurw = u32(m_ctx.tpidr);
|
||||
cpu.tpidruro = m_tpidrro;
|
||||
cpu.exclusive_valid = 0;
|
||||
}
|
||||
|
||||
void ArmScarletNx32::SaveCpu() {
|
||||
const auto& cpu = m_cpu.cpu;
|
||||
for (std::size_t i = 0; i < 16; i++)
|
||||
m_ctx.r[i] = cpu.r[i];
|
||||
m_ctx.fp = cpu.r[11];
|
||||
m_ctx.sp = cpu.r[13];
|
||||
m_ctx.lr = cpu.r[14];
|
||||
m_ctx.pc = cpu.r[15];
|
||||
m_ctx.pstate = cpu.cpsr;
|
||||
std::memcpy(m_ctx.v.data(), cpu.d, sizeof(cpu.d));
|
||||
const auto [fpsr, fpcr] = FpscrToFpsrFpcr(cpu.fpscr);
|
||||
m_ctx.fpsr = fpsr;
|
||||
m_ctx.fpcr = fpcr;
|
||||
m_ctx.tpidr = cpu.tpidrurw;
|
||||
}
|
||||
|
||||
void ArmScarletNx32::GetContext(Kernel::Svc::ThreadContext& ctx) const {
|
||||
ctx = m_ctx;
|
||||
}
|
||||
|
||||
void ArmScarletNx32::SetContext(const Kernel::Svc::ThreadContext& ctx) {
|
||||
m_ctx = ctx;
|
||||
}
|
||||
|
||||
void ArmScarletNx32::SetTpidrroEl0(u64 value) {
|
||||
m_tpidrro = u32(value);
|
||||
}
|
||||
|
||||
void ArmScarletNx32::GetSvcArguments(std::span<uint64_t, 8> args) const {
|
||||
for (std::size_t i = 0; i < args.size(); i++)
|
||||
args[i] = u32(m_ctx.r[i]);
|
||||
}
|
||||
|
||||
void ArmScarletNx32::SetSvcArguments(std::span<const uint64_t, 8> args) {
|
||||
for (std::size_t i = 0; i < args.size(); i++)
|
||||
m_ctx.r[i] = u32(args[i]);
|
||||
}
|
||||
|
||||
u32 ArmScarletNx32::GetSvcNumber() const {
|
||||
return m_svc;
|
||||
}
|
||||
|
||||
void ArmScarletNx32::SignalInterrupt(Kernel::KThread* thread) {
|
||||
// The run loop checks the exit between blocks. A block's own exit takes precedence.
|
||||
m_interrupt.store(true);
|
||||
u32 expected = SNX_ARM_OK;
|
||||
__atomic_compare_exchange_n(&m_cpu.cpu.exit, &expected, InterruptExit, false, __ATOMIC_SEQ_CST, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
void ArmScarletNx32::ClearInstructionCache() {
|
||||
m_process->ClearInstructionCache();
|
||||
}
|
||||
|
||||
void ArmScarletNx32::InvalidateCacheRange(u64 addr, std::size_t size) {
|
||||
m_process->InvalidateCacheRange(addr, size);
|
||||
}
|
||||
|
||||
} // namespace Core
|
||||
@@ -1,81 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
#include "core/arm/arm_interface.h"
|
||||
#include "core/arm/scarletnx/scarletnx_api.h"
|
||||
#include "core/arm/scarletnx/scarletnx_process.h"
|
||||
|
||||
namespace Core {
|
||||
|
||||
/// Runs A32 and Thumb code through ScarletNX's block compiler. Blocks return to the run loop, so
|
||||
/// an SVC ends the run with r15 past it. The block cache is not thread-safe, so each core has its
|
||||
/// own, rebuilt when the process' code changes.
|
||||
class ArmScarletNx32 final : public ArmInterface {
|
||||
public:
|
||||
ArmScarletNx32(std::shared_ptr<ScarletNxProcess> process, std::size_t core_index);
|
||||
~ArmScarletNx32() override;
|
||||
|
||||
Architecture GetArchitecture() const override {
|
||||
return Architecture::AArch32;
|
||||
}
|
||||
|
||||
HaltReason RunThread(Kernel::KThread* thread) override;
|
||||
HaltReason StepThread(Kernel::KThread* thread) override;
|
||||
|
||||
void GetContext(Kernel::Svc::ThreadContext& ctx) const override;
|
||||
void SetContext(const Kernel::Svc::ThreadContext& ctx) override;
|
||||
void SetTpidrroEl0(u64 value) override;
|
||||
|
||||
void GetSvcArguments(std::span<uint64_t, 8> args) const override;
|
||||
void SetSvcArguments(std::span<const uint64_t, 8> args) override;
|
||||
u32 GetSvcNumber() const override;
|
||||
|
||||
void SignalInterrupt(Kernel::KThread* thread) override;
|
||||
void ClearInstructionCache() override;
|
||||
void InvalidateCacheRange(u64 addr, std::size_t size) override;
|
||||
|
||||
protected:
|
||||
const Kernel::DebugWatchpoint* HaltedWatchpoint() const override {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void RewindBreakpointInstruction() override {}
|
||||
|
||||
private:
|
||||
// The CPU first, so the coprocessor hook can find its interface.
|
||||
struct Cpu32 {
|
||||
SnxArmCPU cpu;
|
||||
ArmScarletNx32* parent;
|
||||
};
|
||||
|
||||
static int Svc(void* context, u32 imm, u64 number, const u64 arguments[6]);
|
||||
static void Coprocessor(SnxArmCPU* cpu, u32 word);
|
||||
HaltReason Halt(int exit);
|
||||
void LoadCpu();
|
||||
void SaveCpu();
|
||||
|
||||
std::shared_ptr<ScarletNxProcess> m_process;
|
||||
std::size_t m_core_index{};
|
||||
|
||||
snx_recompiled32* m_module{};
|
||||
u64 m_code_generation{~u64{}};
|
||||
|
||||
// The context the kernel reads and writes; m_cpu holds it while guest code runs.
|
||||
Kernel::Svc::ThreadContext m_ctx{};
|
||||
u32 m_tpidrro{};
|
||||
Cpu32 m_cpu{};
|
||||
SnxArmRt m_rt{};
|
||||
|
||||
bool m_in_svc{};
|
||||
u32 m_svc{};
|
||||
std::atomic<bool> m_interrupt{};
|
||||
|
||||
ScarletNxFaultScope m_fault_scope{};
|
||||
};
|
||||
|
||||
} // namespace Core
|
||||
@@ -1,261 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <sys/mman.h>
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/literals.h"
|
||||
#include "common/logging.h"
|
||||
#include "core/arm/scarletnx/arm_scarletnx_64.h"
|
||||
#include "core/hle/kernel/k_thread.h"
|
||||
#include "core/hle/kernel/k_process.h"
|
||||
|
||||
namespace Core {
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace Common::Literals;
|
||||
namespace ctx = boost::context::detail;
|
||||
|
||||
// Reserved, not committed: guest calls nest on it, and recompiling a region runs on it too.
|
||||
constexpr std::size_t StackSize = 32_MiB;
|
||||
constexpr std::size_t GuardSize = 64_KiB;
|
||||
|
||||
bool SameContext(const Kernel::Svc::ThreadContext a, const Kernel::Svc::ThreadContext b, bool in_svc) {
|
||||
// An SVC returns its results in x0-x7.
|
||||
for (std::size_t i = in_svc ? 8 : 0; i < a.r.size(); i++)
|
||||
if (a.r[i] != b.r[i])
|
||||
return false;
|
||||
return a.fp == b.fp && a.lr == b.lr && a.sp == b.sp && a.pc == b.pc
|
||||
&& (a.pstate >> 28) == (b.pstate >> 28) && a.v == b.v && a.fpcr == b.fpcr
|
||||
&& a.fpsr == b.fpsr && a.tpidr == b.tpidr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const RtAPI* ArmScarletNx64::Runtime() {
|
||||
// this incurs a cost each time we call Runtime() -- but we only ever call it once (ideally)
|
||||
static const RtAPI rt = [] {
|
||||
RtAPI api;
|
||||
snx_runtime_defaults(&api);
|
||||
api.svc = &Svc;
|
||||
api.undef = &Undefined;
|
||||
api.brk = &Breakpoint;
|
||||
api.poll = &Poll;
|
||||
api.miss = &Miss;
|
||||
api.code_changed = &CodeChanged;
|
||||
api.ticks = &ScarletNxTicks;
|
||||
return api;
|
||||
}();
|
||||
return &rt;
|
||||
}
|
||||
|
||||
ArmScarletNx64::ArmScarletNx64(std::shared_ptr<ScarletNxProcess> process, std::size_t core_index)
|
||||
: ArmInterface{true}
|
||||
, m_process{std::move(process)}, m_core_index{core_index}
|
||||
{
|
||||
// can't use virtualbuffer...
|
||||
void* const stack = mmap(nullptr, StackSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
ASSERT_MSG(stack != MAP_FAILED, "cannot allocate a guest stack");
|
||||
m_stack = static_cast<u8*>(stack);
|
||||
mprotect(m_stack, GuardSize, PROT_NONE);
|
||||
|
||||
m_cpu.mem = m_process->m_arena;
|
||||
m_cpu.rt = Runtime();
|
||||
m_cpu.thread = this;
|
||||
|
||||
m_fault_scope.memory = &m_process->m_process.GetMemory();
|
||||
m_fault_scope.arena = m_process->m_arena;
|
||||
m_fault_scope.arena_size = ScarletNxProcess::ArenaSize;
|
||||
}
|
||||
|
||||
ArmScarletNx64::~ArmScarletNx64() {
|
||||
munmap(m_stack, StackSize);
|
||||
}
|
||||
|
||||
void ArmScarletNx64::Svc(CPU* cpu, u32 imm) {
|
||||
auto& self = From(cpu);
|
||||
self.m_svc = imm;
|
||||
self.Yield(HaltReason::SupervisorCall, true, true);
|
||||
}
|
||||
|
||||
void ArmScarletNx64::Undefined(CPU* cpu, u64 pc, u32 word) {
|
||||
// A reserved encoding rejected from inside an instruction, which then goes on.
|
||||
if (cpu->pc != pc) {
|
||||
LOG_ERROR(Core_ARM, "reserved encoding {:08X} at {:#x}", word, pc);
|
||||
return;
|
||||
}
|
||||
LOG_CRITICAL(Core_ARM, "cannot execute {:08X} at {:#x}", word, pc);
|
||||
From(cpu).Yield(HaltReason::PrefetchAbort, false, false);
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
void ArmScarletNx64::Breakpoint(CPU* cpu, u64 pc, u32 imm) {
|
||||
// As Dynarmic does without a debugger: report it and go on.
|
||||
LOG_CRITICAL(Core_ARM, "BRK #{:#x} at {:#x}", imm, pc);
|
||||
cpu->pc = pc + 4;
|
||||
}
|
||||
|
||||
void ArmScarletNx64::Poll(CPU* cpu) {
|
||||
auto& self = From(cpu);
|
||||
__atomic_store_n(&cpu->poll, 0, __ATOMIC_RELAXED);
|
||||
if (self.m_interrupt.exchange(false))
|
||||
self.Yield(HaltReason::BreakLoop, true, false);
|
||||
}
|
||||
|
||||
void ArmScarletNx64::Miss(CPU* cpu, u64 pc) {
|
||||
auto& process = *From(cpu).m_process;
|
||||
const auto function = process.Lookup(pc);
|
||||
if (!function) {
|
||||
cpu->pc = pc;
|
||||
Undefined(cpu, pc, 0);
|
||||
return;
|
||||
}
|
||||
// The first region was recompiled by this lookup: dispatch through its table from now on.
|
||||
if (snx_recompiled* const module = cpu->disp ? nullptr : process.FindModule(pc))
|
||||
snx_recompiled_bind(module, cpu);
|
||||
function(cpu, pc);
|
||||
}
|
||||
|
||||
void ArmScarletNx64::CodeChanged(CPU* cpu, u64 addr) {
|
||||
constexpr u64 ICacheLineSize = 64;
|
||||
From(cpu).m_process->InvalidateCacheRange(addr & ~(ICacheLineSize - 1), ICacheLineSize);
|
||||
}
|
||||
|
||||
void ArmScarletNx64::Entry(ctx::transfer_t transfer) {
|
||||
auto* const self = static_cast<ArmScarletNx64*>(transfer.data);
|
||||
self->m_host = transfer.fctx;
|
||||
self->Execute();
|
||||
}
|
||||
|
||||
void ArmScarletNx64::Execute() {
|
||||
if (sigsetjmp(m_fault_scope.env, 1) != 0) {
|
||||
LOG_CRITICAL(Core_ARM, "invalid memory access at {:#x}", m_fault_scope.fault_address);
|
||||
Yield(HaltReason::PrefetchAbort, false, false);
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
// Every guest return to below the first frame comes back here.
|
||||
while (true)
|
||||
snx_lookup(&m_cpu, m_cpu.pc)(&m_cpu, m_cpu.pc);
|
||||
}
|
||||
|
||||
void ArmScarletNx64::Yield(HaltReason reason, bool resumable, bool in_svc) {
|
||||
m_halt = reason;
|
||||
m_resumable = resumable;
|
||||
m_suspended_in_svc = in_svc;
|
||||
m_host = ctx::jump_fcontext(m_host, nullptr).fctx;
|
||||
}
|
||||
|
||||
void ArmScarletNx64::ResetGuest() {
|
||||
m_guest = ctx::make_fcontext(m_stack + StackSize, StackSize - GuardSize, &Entry);
|
||||
if (snx_recompiled* const module = m_process->FindModule(m_cpu.pc))
|
||||
snx_recompiled_bind(module, &m_cpu);
|
||||
}
|
||||
|
||||
HaltReason ArmScarletNx64::RunThread(Kernel::KThread* thread) {
|
||||
if (m_interrupt.exchange(false))
|
||||
return HaltReason::BreakLoop;
|
||||
|
||||
bool const resume = m_resumable && m_suspended_thread_id == thread->GetThreadId() && SameContext(m_ctx, m_suspended_ctx, m_suspended_in_svc);
|
||||
LoadCpu();
|
||||
if (!resume)
|
||||
ResetGuest();
|
||||
|
||||
m_resumable = false;
|
||||
m_fault_scope.last_fault = ~u64{};
|
||||
SetScarletNxFaultScope(&m_fault_scope);
|
||||
m_guest = ctx::jump_fcontext(m_guest, this).fctx;
|
||||
SetScarletNxFaultScope(nullptr);
|
||||
|
||||
SaveCpu();
|
||||
if (m_resumable) {
|
||||
m_suspended_thread_id = thread->GetThreadId();
|
||||
m_suspended_ctx = m_ctx;
|
||||
}
|
||||
return m_halt;
|
||||
}
|
||||
|
||||
HaltReason ArmScarletNx64::StepThread(Kernel::KThread* thread) {
|
||||
// Never reached: the debugger selects Dynarmic.
|
||||
LOG_ERROR(Core_ARM, "ScarletNX cannot single step");
|
||||
return HaltReason::BreakLoop;
|
||||
}
|
||||
|
||||
void ArmScarletNx64::LoadCpu() {
|
||||
static_assert(sizeof(m_cpu.v) == sizeof(m_ctx.v));
|
||||
for (std::size_t i = 0; i < m_ctx.r.size(); i++)
|
||||
m_cpu.x[i] = m_ctx.r[i];
|
||||
m_cpu.x[29] = m_ctx.fp;
|
||||
m_cpu.x[30] = m_ctx.lr;
|
||||
m_cpu.x[31] = m_ctx.sp;
|
||||
m_cpu.pc = m_ctx.pc;
|
||||
set_nzcv(&m_cpu, m_ctx.pstate >> 28);
|
||||
std::memcpy(m_cpu.v, m_ctx.v.data(), sizeof(m_cpu.v));
|
||||
m_cpu.fpcr = m_ctx.fpcr;
|
||||
m_cpu.fpsr = m_ctx.fpsr;
|
||||
m_cpu.tpidr = m_ctx.tpidr;
|
||||
m_cpu.tpidrro = m_tpidrro;
|
||||
m_cpu.excl_on = 0;
|
||||
}
|
||||
|
||||
void ArmScarletNx64::SaveCpu() {
|
||||
for (std::size_t i = 0; i < m_ctx.r.size(); i++) {
|
||||
m_ctx.r[i] = m_cpu.x[i];
|
||||
}
|
||||
m_ctx.fp = m_cpu.x[29];
|
||||
m_ctx.lr = m_cpu.x[30];
|
||||
m_ctx.sp = m_cpu.x[31];
|
||||
m_ctx.pc = m_cpu.pc;
|
||||
m_ctx.pstate = get_nzcv(&m_cpu);
|
||||
std::memcpy(m_ctx.v.data(), m_cpu.v, sizeof(m_cpu.v));
|
||||
m_ctx.fpcr = m_cpu.fpcr;
|
||||
m_ctx.fpsr = m_cpu.fpsr;
|
||||
m_ctx.tpidr = m_cpu.tpidr;
|
||||
}
|
||||
|
||||
void ArmScarletNx64::GetContext(Kernel::Svc::ThreadContext& ctx) const {
|
||||
ctx = m_ctx;
|
||||
}
|
||||
|
||||
void ArmScarletNx64::SetContext(const Kernel::Svc::ThreadContext& ctx) {
|
||||
m_ctx = ctx;
|
||||
}
|
||||
|
||||
void ArmScarletNx64::SetTpidrroEl0(u64 value) {
|
||||
m_tpidrro = value;
|
||||
}
|
||||
|
||||
void ArmScarletNx64::GetSvcArguments(std::span<uint64_t, 8> args) const {
|
||||
for (std::size_t i = 0; i < args.size(); i++) {
|
||||
args[i] = m_ctx.r[i];
|
||||
}
|
||||
}
|
||||
|
||||
void ArmScarletNx64::SetSvcArguments(std::span<const uint64_t, 8> args) {
|
||||
for (std::size_t i = 0; i < args.size(); i++) {
|
||||
m_ctx.r[i] = args[i];
|
||||
}
|
||||
}
|
||||
|
||||
u32 ArmScarletNx64::GetSvcNumber() const {
|
||||
return m_svc;
|
||||
}
|
||||
|
||||
void ArmScarletNx64::SignalInterrupt(Kernel::KThread* thread) {
|
||||
// Generated code reads poll on backward branches and calls Poll, which yields.
|
||||
m_interrupt.store(true);
|
||||
__atomic_store_n(&m_cpu.poll, 1, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
void ArmScarletNx64::ClearInstructionCache() {
|
||||
m_process->ClearInstructionCache();
|
||||
}
|
||||
|
||||
void ArmScarletNx64::InvalidateCacheRange(u64 addr, std::size_t size) {
|
||||
m_process->InvalidateCacheRange(addr, size);
|
||||
}
|
||||
|
||||
} // namespace Core
|
||||
@@ -1,100 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
#include <boost/context/detail/fcontext.hpp>
|
||||
|
||||
#include "core/arm/arm_interface.h"
|
||||
#include "core/arm/scarletnx/scarletnx_api.h"
|
||||
#include "core/arm/scarletnx/scarletnx_process.h"
|
||||
|
||||
namespace Core {
|
||||
|
||||
/// Runs A64 code recompiled by ScarletNX.
|
||||
///
|
||||
/// A recompiled guest function is a host function that returns when the guest function returns,
|
||||
/// so guest calls nest on the host stack and an SVC cannot return from the generated code. Guest
|
||||
/// code therefore runs on a coroutine of this core, which the SVC and interrupt hooks suspend.
|
||||
/// When the kernel next runs the same thread with the same context, the coroutine resumes;
|
||||
/// otherwise its host frames are dropped and execution restarts at the new pc through dispatch.
|
||||
class ArmScarletNx64 final : public ArmInterface {
|
||||
public:
|
||||
ArmScarletNx64(std::shared_ptr<ScarletNxProcess> process, std::size_t core_index);
|
||||
~ArmScarletNx64() override;
|
||||
|
||||
Architecture GetArchitecture() const override {
|
||||
return Architecture::AArch64;
|
||||
}
|
||||
|
||||
HaltReason RunThread(Kernel::KThread* thread) override;
|
||||
HaltReason StepThread(Kernel::KThread* thread) override;
|
||||
|
||||
void GetContext(Kernel::Svc::ThreadContext& ctx) const override;
|
||||
void SetContext(const Kernel::Svc::ThreadContext& ctx) override;
|
||||
void SetTpidrroEl0(u64 value) override;
|
||||
|
||||
void GetSvcArguments(std::span<uint64_t, 8> args) const override;
|
||||
void SetSvcArguments(std::span<const uint64_t, 8> args) override;
|
||||
u32 GetSvcNumber() const override;
|
||||
|
||||
void SignalInterrupt(Kernel::KThread* thread) override;
|
||||
void ClearInstructionCache() override;
|
||||
void InvalidateCacheRange(u64 addr, std::size_t size) override;
|
||||
|
||||
protected:
|
||||
const Kernel::DebugWatchpoint* HaltedWatchpoint() const override {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void RewindBreakpointInstruction() override {}
|
||||
|
||||
private:
|
||||
static ArmScarletNx64& From(CPU* cpu) {
|
||||
return *static_cast<ArmScarletNx64*>(cpu->thread);
|
||||
}
|
||||
static const RtAPI* Runtime();
|
||||
|
||||
// Runtime hooks called by the generated code.
|
||||
static void Svc(CPU* cpu, u32 imm);
|
||||
static void Undefined(CPU* cpu, u64 pc, u32 word);
|
||||
static void Breakpoint(CPU* cpu, u64 pc, u32 imm);
|
||||
static void Poll(CPU* cpu);
|
||||
static void Miss(CPU* cpu, u64 pc);
|
||||
static void CodeChanged(CPU* cpu, u64 addr);
|
||||
|
||||
static void Entry(boost::context::detail::transfer_t transfer);
|
||||
[[noreturn]] void Execute();
|
||||
void Yield(HaltReason reason, bool resumable, bool in_svc);
|
||||
void ResetGuest();
|
||||
void LoadCpu();
|
||||
void SaveCpu();
|
||||
|
||||
std::shared_ptr<ScarletNxProcess> m_process;
|
||||
std::size_t m_core_index{};
|
||||
|
||||
// The context the kernel reads and writes; m_cpu holds it while guest code runs.
|
||||
Kernel::Svc::ThreadContext m_ctx{};
|
||||
u64 m_tpidrro{};
|
||||
CPU m_cpu{};
|
||||
|
||||
HaltReason m_halt{};
|
||||
u32 m_svc{};
|
||||
std::atomic<bool> m_interrupt{};
|
||||
|
||||
// The coroutine, and what it was suspended in, when it can be resumed.
|
||||
boost::context::detail::fcontext_t m_guest{};
|
||||
boost::context::detail::fcontext_t m_host{};
|
||||
u8* m_stack{};
|
||||
bool m_resumable{};
|
||||
bool m_suspended_in_svc{};
|
||||
u64 m_suspended_thread_id{};
|
||||
Kernel::Svc::ThreadContext m_suspended_ctx{};
|
||||
|
||||
ScarletNxFaultScope m_fault_scope{};
|
||||
};
|
||||
|
||||
} // namespace Core
|
||||
@@ -1,10 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
// snx_recompiler.h includes cpu.h and arm.h before its own extern "C" block, so the whole
|
||||
// header needs C linkage for the runtime library's functions to resolve.
|
||||
extern "C" {
|
||||
#include <scarletnx/snx_recompiler.h>
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <mutex>
|
||||
|
||||
#include <signal.h>
|
||||
|
||||
#include "common/fs/path_util.h"
|
||||
#include "common/logging.h"
|
||||
#include "common/settings.h"
|
||||
#include "core/arm/scarletnx/arm_scarletnx_32.h"
|
||||
#include "core/arm/scarletnx/arm_scarletnx_64.h"
|
||||
#include "core/arm/scarletnx/scarletnx_api.h"
|
||||
#include "core/arm/scarletnx/scarletnx_process.h"
|
||||
#include "core/core.h"
|
||||
#include "core/core_timing.h"
|
||||
#include "core/device_memory.h"
|
||||
#include "core/hle/kernel/k_process.h"
|
||||
#include "core/hle/kernel/kernel.h"
|
||||
#include "core/memory.h"
|
||||
|
||||
namespace Core {
|
||||
|
||||
namespace {
|
||||
|
||||
// ScarletNX refuses dispatch tables spanning more than 1 GiB of guest code.
|
||||
constexpr u64 MaxModuleSpan = 1ULL << 30;
|
||||
constexpr u64 A32Limit = 1ULL << 32;
|
||||
|
||||
std::atomic<Timing::CoreTiming*> g_core_timing{};
|
||||
|
||||
// Needs be TLS because SNX expects like, more than 4 threads (host == guest threads)
|
||||
// and well -- quite impervious to "emulated" threads... maybe one day this can be removed
|
||||
thread_local ScarletNxFaultScope* g_fault_scope{};
|
||||
static struct {
|
||||
struct sigaction old_sigsegv;
|
||||
struct sigaction old_sigbus;
|
||||
std::once_flag installed;
|
||||
} g_snx_data;
|
||||
|
||||
bool Overlaps(const ScarletNxProcess::CodeRange& range, u64 addr, u64 size) {
|
||||
return addr < range.addr + range.size && range.addr < addr + size;
|
||||
}
|
||||
|
||||
void ChainSignal(int sig, siginfo_t* info, void* raw_context) {
|
||||
const struct sigaction& old = sig == SIGSEGV ? g_snx_data.old_sigsegv : g_snx_data.old_sigbus;
|
||||
if (old.sa_flags & SA_SIGINFO) {
|
||||
old.sa_sigaction(sig, info, raw_context);
|
||||
} else if (old.sa_handler == SIG_DFL) {
|
||||
// The faulting instruction runs again and takes the default action.
|
||||
signal(sig, SIG_DFL);
|
||||
} else if (old.sa_handler != SIG_IGN) {
|
||||
old.sa_handler(sig);
|
||||
}
|
||||
}
|
||||
|
||||
void FaultHandler(int sig, siginfo_t* info, void* raw_context) {
|
||||
ScarletNxFaultScope* const scope = g_fault_scope;
|
||||
u8* const addr = static_cast<u8*>(info->si_addr);
|
||||
if (!scope || addr < scope->arena || addr >= scope->arena + scope->arena_size) {
|
||||
ChainSignal(sig, info, raw_context);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pages the rasterizer protects fault on access: invalidate them and retry. A second fault at
|
||||
// the same addr means retrying will not help.
|
||||
const u64 guest_address = u64(addr - scope->arena);
|
||||
if (guest_address != scope->last_fault) {
|
||||
scope->last_fault = guest_address;
|
||||
if (scope->memory->InvalidateNCE(guest_address & ~Memory::YUZU_PAGEMASK, Memory::YUZU_PAGESIZE)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
scope->fault_address = guest_address;
|
||||
siglongjmp(scope->env, 1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void InstallScarletNxFaultHandler() {
|
||||
std::call_once(g_snx_data.installed, [] {
|
||||
struct sigaction action {};
|
||||
action.sa_flags = SA_SIGINFO;
|
||||
action.sa_sigaction = &FaultHandler;
|
||||
sigemptyset(&action.sa_mask);
|
||||
sigaction(SIGSEGV, &action, &g_snx_data.old_sigsegv);
|
||||
sigaction(SIGBUS, &action, &g_snx_data.old_sigbus);
|
||||
});
|
||||
}
|
||||
|
||||
void SetScarletNxFaultScope(ScarletNxFaultScope* scope) {
|
||||
g_fault_scope = scope;
|
||||
}
|
||||
|
||||
u64 ScarletNxTicks() {
|
||||
return g_core_timing.load(std::memory_order_relaxed)->GetClockTicks();
|
||||
}
|
||||
|
||||
std::unique_ptr<ArmInterface> MakeScarletNxInterface(std::shared_ptr<ScarletNxProcess> process, bool is_64bit, std::size_t core_index) {
|
||||
if (is_64bit)
|
||||
return std::make_unique<ArmScarletNx64>(std::move(process), core_index);
|
||||
return std::make_unique<ArmScarletNx32>(std::move(process), core_index);
|
||||
}
|
||||
|
||||
ScarletNxProcess::ScarletNxProcess(System& system, Kernel::KProcess& process)
|
||||
: m_system{system}
|
||||
, m_process{process}
|
||||
, m_arena{system.DeviceMemory().buffer.VirtualBasePointer()}
|
||||
{
|
||||
InstallScarletNxFaultHandler();
|
||||
g_core_timing.store(&system.CoreTiming(), std::memory_order_relaxed);
|
||||
|
||||
m_recompiler = snx_recompiler_new(SNX_RECOMPILER_FLAG_LLVM);
|
||||
ASSERT(m_recompiler && "cannot create the recompiler");
|
||||
|
||||
// Linked modules are kept by a hash of their code, so later launches skip the recompile.
|
||||
const auto cache = Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "scarletnx";
|
||||
if (snx_recompiler_set_cache(m_recompiler, Common::FS::PathToUTF8String(cache).c_str()) != 0)
|
||||
LOG_WARNING(Core_ARM, "no code cache: {}", snx_recompiler_error(m_recompiler));
|
||||
}
|
||||
|
||||
ScarletNxProcess::~ScarletNxProcess() {
|
||||
for (auto& mod : m_modules)
|
||||
snx_recompiled_free(mod.mod);
|
||||
snx_recompiler_free(m_recompiler);
|
||||
}
|
||||
|
||||
bool ScarletNxProcess::ShouldUse(System& system, const Kernel::KProcess& process) {
|
||||
if (Settings::values.cpu_backend.GetValue() != Settings::CpuBackend::ScarletNx)
|
||||
return false;
|
||||
auto const f = [](auto& s) {
|
||||
LOG_WARNING(Core_ARM, "snx: {}", s);
|
||||
return false;
|
||||
};
|
||||
if (!process.IsApplication())
|
||||
return f("no qlaunch yet");
|
||||
if (!system.Kernel().IsMulticore())
|
||||
return f("single core mode needs cycle counting");
|
||||
if (system.DebuggerEnabled())
|
||||
return f("the debugger needs stepping and watchpoints");
|
||||
/// ScarletNX expects flat memory, so it will only work with fastmem
|
||||
if (!Settings::IsFastmemEnabled() || !system.DeviceMemory().buffer.VirtualBasePointer())
|
||||
return f("guest memory must be mapped by fastmem");
|
||||
if (snx_recompiler_abi() != SNX_ABI)
|
||||
return f("the library was built with a different ABI");
|
||||
if (!snx_recompiler_has(SNX_RECOMPILER_FLAG_LLVM))
|
||||
return f("LLVM cannot target this host");
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<ScarletNxProcess::CodeRange> ScarletNxProcess::GetCodeRanges() const {
|
||||
std::vector<CodeRange> r;
|
||||
const auto& page_table = m_process.GetPageTable().GetBasePageTable();
|
||||
u64 addr = GetInteger(page_table.GetAddressSpaceStart());
|
||||
while (true) {
|
||||
Kernel::KMemoryInfo info;
|
||||
Kernel::Svc::PageInfo page_info;
|
||||
if (page_table.QueryInfo(&info, &page_info, addr).IsError())
|
||||
break;
|
||||
if (True(info.GetPermission() & Kernel::KMemoryPermission::UserExecute)) {
|
||||
if (!r.empty() && r.back().addr + r.back().size == info.GetAddress()) {
|
||||
r.back().size += info.GetSize();
|
||||
} else {
|
||||
r.push_back({info.GetAddress(), info.GetSize()});
|
||||
}
|
||||
}
|
||||
// The block past the addr space ends at 0.
|
||||
if (info.GetEndAddress() <= addr)
|
||||
break;
|
||||
addr = info.GetEndAddress();
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
const ScarletNxProcess::Module* ScarletNxProcess::FindModuleLocked(u64 pc) const {
|
||||
for (const auto& mod : m_modules) {
|
||||
if (mod.retired || pc < mod.lo || pc >= mod.hi)
|
||||
continue;
|
||||
for (const auto& range : mod.ranges)
|
||||
if (pc >= range.addr && pc < range.addr + range.size)
|
||||
return &mod;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
snx_recompiled* ScarletNxProcess::RecompileRegionLocked(u64 pc) {
|
||||
const auto r = GetCodeRanges();
|
||||
const auto it = std::ranges::find_if(r, [pc](const CodeRange& range) {
|
||||
return pc >= range.addr && pc < range.addr + range.size;
|
||||
});
|
||||
if (it == r.end())
|
||||
return nullptr;
|
||||
|
||||
const auto is_covered = [this](const CodeRange& range) {
|
||||
return std::ranges::any_of(m_modules, [&](const Module& mod) {
|
||||
return !mod.retired && std::ranges::any_of(mod.ranges, [&](const CodeRange& cr) {
|
||||
return Overlaps(cr, range.addr, range.size);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Recompile as many neighbouring regions as one dispatch table can cover.
|
||||
auto first = size_t(it - r.begin());
|
||||
auto last = first;
|
||||
const auto end_of = [&](size_t i) { return r[i].addr + r[i].size; };
|
||||
while (first > 0 && !is_covered(r[first - 1]) && end_of(last) - r[first - 1].addr <= MaxModuleSpan)
|
||||
--first;
|
||||
while (last + 1 < r.size() && !is_covered(r[last + 1])
|
||||
&& end_of(last + 1) - r[first].addr <= MaxModuleSpan)
|
||||
++last;
|
||||
|
||||
Module mod{};
|
||||
mod.ranges.assign(r.begin() + first, r.begin() + last + 1);
|
||||
mod.lo = r[first].addr;
|
||||
mod.hi = end_of(last);
|
||||
|
||||
std::vector<snx_code_range> code;
|
||||
u64 code_size = 0;
|
||||
for (const auto& range : mod.ranges) {
|
||||
code.push_back({range.addr, m_arena + range.addr, range.size});
|
||||
code_size += range.size;
|
||||
}
|
||||
|
||||
LOG_INFO(Core_ARM, "recompiling {} KiB of code at {:#x}-{:#x}", code_size / 1024, mod.lo, mod.hi);
|
||||
mod.mod = snx_recompile_ranges(m_recompiler, code.data(), code.size(), &pc, 1);
|
||||
if (mod.mod) {
|
||||
LOG_INFO(Core_ARM, "recompiled {} functions", snx_recompiled_functions(mod.mod));
|
||||
} else {
|
||||
// Kept, so the region is not recompiled again on every miss.
|
||||
LOG_ERROR(Core_ARM, "recompilation failed: {}", snx_recompiler_error(m_recompiler));
|
||||
}
|
||||
|
||||
m_modules.push_back(std::move(mod));
|
||||
return m_modules.back().mod;
|
||||
}
|
||||
|
||||
ScarletNxProcess::SnxGuestFn ScarletNxProcess::Lookup(u64 pc) {
|
||||
snx_recompiled* mod;
|
||||
{
|
||||
std::scoped_lock lk{m_mutex};
|
||||
const Module* const known = FindModuleLocked(pc);
|
||||
mod = known ? known->mod : RecompileRegionLocked(pc);
|
||||
}
|
||||
if (!mod)
|
||||
return nullptr;
|
||||
// Waits for the mod's JIT when discovery missed the code at pc.
|
||||
return snx_recompiled_lookup(mod, pc);
|
||||
}
|
||||
|
||||
snx_recompiled* ScarletNxProcess::FindModule(u64 pc) {
|
||||
std::scoped_lock lk{m_mutex};
|
||||
const Module* const mod = FindModuleLocked(pc);
|
||||
return mod ? mod->mod : nullptr;
|
||||
}
|
||||
|
||||
snx_recompiled32* ScarletNxProcess::MakeModule32() {
|
||||
std::scoped_lock lk{m_mutex};
|
||||
std::vector<snx_code_range> code;
|
||||
for (const auto& range : GetCodeRanges()) {
|
||||
if (range.addr >= A32Limit) {
|
||||
continue;
|
||||
}
|
||||
code.push_back({range.addr, nullptr, std::min(range.size, A32Limit - range.addr)});
|
||||
}
|
||||
if (code.empty())
|
||||
return nullptr;
|
||||
snx_recompiled32* const mod = snx_recompile32_ranges(m_recompiler, code.data(), code.size());
|
||||
if (!mod)
|
||||
LOG_ERROR(Core_ARM, "cannot prepare A32 code: {}", snx_recompiler_error(m_recompiler));
|
||||
return mod;
|
||||
}
|
||||
|
||||
void ScarletNxProcess::InvalidateCacheRange(u64 addr, u64 size) {
|
||||
std::scoped_lock lk{m_mutex};
|
||||
for (auto& mod : m_modules) {
|
||||
if (mod.retired || !std::ranges::any_of(mod.ranges, [&](const CodeRange& range) {
|
||||
return Overlaps(range, addr, size);
|
||||
}))
|
||||
continue;
|
||||
// Running code diverts out of the retired functions, and the region is recompiled from
|
||||
// the new code when it is next reached.
|
||||
if (mod.mod)
|
||||
snx_recompiled_invalidate(mod.mod, addr, size, nullptr, nullptr);
|
||||
mod.retired = true;
|
||||
}
|
||||
m_code_generation.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
|
||||
void ScarletNxProcess::ClearInstructionCache() {
|
||||
std::scoped_lock lk{m_mutex};
|
||||
for (auto& mod : m_modules) {
|
||||
if (!mod.retired && mod.mod)
|
||||
snx_recompiled_invalidate(mod.mod, mod.lo, mod.hi - mod.lo, nullptr, nullptr);
|
||||
mod.retired = true;
|
||||
}
|
||||
m_code_generation.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
|
||||
} // namespace Core
|
||||
@@ -1,104 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <csetjmp>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include "common/common_types.h"
|
||||
|
||||
struct CPU;
|
||||
struct snx_recompiler;
|
||||
struct snx_recompiled;
|
||||
struct snx_recompiled32;
|
||||
|
||||
namespace Kernel {
|
||||
class KProcess;
|
||||
}
|
||||
|
||||
namespace Core::Memory {
|
||||
class Memory;
|
||||
}
|
||||
|
||||
namespace Core {
|
||||
|
||||
class ArmInterface;
|
||||
class System;
|
||||
|
||||
class ScarletNxProcess {
|
||||
public:
|
||||
using SnxGuestFn = void (*)(CPU*, u64);
|
||||
|
||||
// DeviceMemory reserves 39 bits of address space for the fastmem arena.
|
||||
static constexpr u64 ArenaSize = 1ULL << 39;
|
||||
|
||||
struct CodeRange {
|
||||
u64 addr;
|
||||
u64 size;
|
||||
};
|
||||
|
||||
ScarletNxProcess(System& system, Kernel::KProcess& process);
|
||||
~ScarletNxProcess();
|
||||
|
||||
static bool ShouldUse(System& system, const Kernel::KProcess& process);
|
||||
SnxGuestFn Lookup(u64 pc);
|
||||
snx_recompiled* FindModule(u64 pc);
|
||||
snx_recompiled32* MakeModule32();
|
||||
|
||||
void InvalidateCacheRange(u64 addr, u64 size);
|
||||
void ClearInstructionCache();
|
||||
|
||||
/// @brief general gate for when we are about to invalidate a given block aka. trigger regen
|
||||
u64 CodeGeneration() const {
|
||||
return m_code_generation.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
struct Module {
|
||||
std::vector<CodeRange> ranges;
|
||||
u64 lo;
|
||||
u64 hi;
|
||||
snx_recompiled* mod;
|
||||
bool retired;
|
||||
};
|
||||
|
||||
std::vector<CodeRange> GetCodeRanges() const;
|
||||
const Module* FindModuleLocked(u64 pc) const;
|
||||
snx_recompiled* RecompileRegionLocked(u64 pc);
|
||||
|
||||
std::vector<Module> m_modules;
|
||||
std::atomic<u64> m_code_generation{};
|
||||
|
||||
// TODO: remove parent ref?
|
||||
System& m_system;
|
||||
Kernel::KProcess& m_process;
|
||||
u8* m_arena{};
|
||||
snx_recompiler* m_recompiler{};
|
||||
|
||||
// ScarletNX's recompiler is used by one thread at a time.
|
||||
std::mutex m_mutex;
|
||||
};
|
||||
|
||||
/// Recovers from host faults in guest memory on the thread running guest code: a fault on a
|
||||
/// page the rasterizer protects is resolved and retried, any other jumps to env.
|
||||
struct ScarletNxFaultScope {
|
||||
sigjmp_buf env;
|
||||
Memory::Memory* memory{};
|
||||
u8* arena{};
|
||||
u64 arena_size{};
|
||||
u64 last_fault{~0ull};
|
||||
u64 fault_address{};
|
||||
};
|
||||
|
||||
void InstallScarletNxFaultHandler();
|
||||
void SetScarletNxFaultScope(ScarletNxFaultScope* scope);
|
||||
|
||||
/// CNTPCT_EL0 of the emulated system.
|
||||
u64 ScarletNxTicks();
|
||||
|
||||
std::unique_ptr<ArmInterface> MakeScarletNxInterface(std::shared_ptr<ScarletNxProcess> process, bool is_64bit, std::size_t core_index);
|
||||
|
||||
} // namespace Core
|
||||
@@ -23,9 +23,6 @@
|
||||
#ifdef HAS_NCE
|
||||
#include "core/arm/nce/arm_nce.h"
|
||||
#endif
|
||||
#ifdef HAS_SCARLETNX
|
||||
#include "core/arm/scarletnx/scarletnx_process.h"
|
||||
#endif
|
||||
|
||||
namespace Kernel {
|
||||
|
||||
@@ -1314,13 +1311,6 @@ void KProcess::InitializeInterfaces(KernelCore& kernel) {
|
||||
for (size_t i = 0; i < Core::Hardware::NUM_CPU_CORES; i++)
|
||||
m_arm_interfaces[i] = std::make_unique<Core::ArmNce>(kernel.System(), true, i);
|
||||
} else
|
||||
#endif
|
||||
#ifdef HAS_SCARLETNX
|
||||
if (Core::ScarletNxProcess::ShouldUse(kernel.System(), *this)) {
|
||||
auto scarletnx = std::make_shared<Core::ScarletNxProcess>(kernel.System(), *this);
|
||||
for (size_t i = 0; i < Core::Hardware::NUM_CPU_CORES; i++)
|
||||
m_arm_interfaces[i] = Core::MakeScarletNxInterface(scarletnx, this->Is64Bit(), i);
|
||||
} else
|
||||
#endif
|
||||
if (this->Is64Bit()) {
|
||||
for (size_t i = 0; i < Core::Hardware::NUM_CPU_CORES; i++) {
|
||||
|
||||
@@ -449,12 +449,7 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QObject* parent) {
|
||||
translations->insert({Settings::EnumMetadata<Settings::CpuBackend>::Index(),
|
||||
{
|
||||
PAIR(CpuBackend, Dynarmic, tr("Dynarmic")),
|
||||
#ifdef HAS_NCE
|
||||
PAIR(CpuBackend, Nce, tr("NCE")),
|
||||
#endif
|
||||
#ifdef HAS_SCARLETNX
|
||||
PAIR(CpuBackend, ScarletNx, tr("ScarletNX")),
|
||||
#endif
|
||||
}});
|
||||
translations->insert({Settings::EnumMetadata<Settings::FullscreenMode>::Index(),
|
||||
{
|
||||
|
||||
@@ -40,9 +40,10 @@ struct HandleKey {
|
||||
u32 sec_index, sec_offset, sec_shift_left;
|
||||
u32 count;
|
||||
bool has_secondary;
|
||||
u32 mask, sec_mask;
|
||||
constexpr bool operator==(const HandleKey& o) const noexcept {
|
||||
return std::tie(index, offset, shift_left, sec_index, sec_offset, sec_shift_left, count, has_secondary)
|
||||
== std::tie(o.index, o.offset, o.shift_left, o.sec_index, o.sec_offset, o.sec_shift_left, o.count, o.has_secondary);
|
||||
return std::tie(index, offset, shift_left, sec_index, sec_offset, sec_shift_left, count, has_secondary, mask, sec_mask)
|
||||
== std::tie(o.index, o.offset, o.shift_left, o.sec_index, o.sec_offset, o.sec_shift_left, o.count, o.has_secondary, o.mask, o.sec_mask);
|
||||
}
|
||||
};
|
||||
struct HandleKeyHash {
|
||||
@@ -52,6 +53,7 @@ struct HandleKeyHash {
|
||||
h ^= (size_t(k.sec_index) << 33) ^ (size_t(k.sec_offset) << 2);
|
||||
h ^= (size_t(k.sec_shift_left) << 3);
|
||||
h ^= (size_t(k.count) << 7);
|
||||
h ^= (size_t(k.mask) << 11) ^ (size_t(k.sec_mask) << 13);
|
||||
return h ^ (k.has_secondary ? 0x9e3779b97f4a7c15ULL : 0ULL);
|
||||
}
|
||||
};
|
||||
@@ -66,6 +68,14 @@ struct ConstBufferAddr {
|
||||
IR::U32 dynamic_offset;
|
||||
u32 count;
|
||||
bool has_secondary;
|
||||
/// The dynamic offset is a constant buffer value shifted left, the same for the whole draw
|
||||
bool has_index_cbuf;
|
||||
u32 index_cbuf_index;
|
||||
u32 index_cbuf_offset;
|
||||
u32 index_shift;
|
||||
/// Applied after the shifts
|
||||
u32 mask = 0xffffffff;
|
||||
u32 secondary_mask = 0xffffffff;
|
||||
};
|
||||
|
||||
class Environment {
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
@@ -41,9 +41,9 @@ union F2I {
|
||||
BitField<39, 2, Rounding> rounding;
|
||||
BitField<41, 1, u64> half;
|
||||
BitField<44, 1, u64> ftz;
|
||||
BitField<45, 1, u64> abs;
|
||||
BitField<45, 1, u64> neg;
|
||||
BitField<47, 1, u64> cc;
|
||||
BitField<49, 1, u64> neg;
|
||||
BitField<49, 1, u64> abs;
|
||||
};
|
||||
|
||||
size_t BitSize(DestFormat dest_format) {
|
||||
|
||||
@@ -43,7 +43,8 @@ u32 DynamicDescriptorSizeShift(const IR::U32& dynamic_offset) {
|
||||
if (!shift.IsImmediate())
|
||||
return DESCRIPTOR_SIZE_SHIFT;
|
||||
const u32 size_shift = shift.U32();
|
||||
return size_shift >= DESCRIPTOR_SIZE_SHIFT && size_shift < 31 ? size_shift : DESCRIPTOR_SIZE_SHIFT;
|
||||
// Separate texture and sampler handles are 4 bytes apart
|
||||
return size_shift >= 2 && size_shift < 31 ? size_shift : DESCRIPTOR_SIZE_SHIFT;
|
||||
}
|
||||
|
||||
u32 DynamicDescriptorCount(u32 base_offset, u32 size_shift, u32 max_descriptors) {
|
||||
@@ -59,6 +60,37 @@ u32 DynamicDescriptorCount(u32 base_offset, u32 size_shift, u32 max_descriptors)
|
||||
return std::min(descriptor_limit, available_count);
|
||||
}
|
||||
|
||||
struct UniformIndex {
|
||||
u32 cbuf_index;
|
||||
u32 cbuf_offset;
|
||||
u32 shift;
|
||||
};
|
||||
|
||||
/// Matches a dynamic handle offset that is a constant buffer value, optionally shifted left
|
||||
std::optional<UniformIndex> TrackUniformIndex(const IR::U32& dynamic_offset) {
|
||||
if (dynamic_offset.IsImmediate()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const IR::Inst* inst = dynamic_offset.InstRecursive();
|
||||
u32 shift = 0;
|
||||
if (inst->GetOpcode() == IR::Opcode::ShiftLeftLogical32) {
|
||||
if (!inst->Arg(1).IsImmediate() || inst->Arg(0).IsImmediate()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
shift = inst->Arg(1).U32();
|
||||
inst = inst->Arg(0).InstRecursive();
|
||||
}
|
||||
if (inst->GetOpcode() != IR::Opcode::GetCbufU32 || !inst->Arg(0).IsImmediate() ||
|
||||
!inst->Arg(1).IsImmediate() || shift >= 32) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return UniformIndex{
|
||||
.cbuf_index = inst->Arg(0).U32(),
|
||||
.cbuf_offset = inst->Arg(1).U32(),
|
||||
.shift = shift,
|
||||
};
|
||||
}
|
||||
|
||||
u32 SaturatingSub(u32 lhs, u32 rhs) {
|
||||
return lhs > rhs ? lhs - rhs : 0;
|
||||
}
|
||||
@@ -273,14 +305,15 @@ static inline u32 GetTextureHandleCached(Environment& env, const ConstBufferAddr
|
||||
// If has secondary, then it will be cbuf.secondary_{index|offset}, else its 0.
|
||||
// So we can just hand it out the raw variable without using sec_idx or sec_off
|
||||
// because comparing 0 against 0 will yield true.
|
||||
const HandleKey hk{cbuf.index, cbuf.offset, cbuf.shift_left, cbuf.secondary_index, cbuf.secondary_offset, cbuf.secondary_shift_left, cbuf.count, cbuf.has_secondary};
|
||||
const HandleKey hk{cbuf.index, cbuf.offset, cbuf.shift_left, cbuf.secondary_index, cbuf.secondary_offset, cbuf.secondary_shift_left, cbuf.count, cbuf.has_secondary, cbuf.mask, cbuf.secondary_mask};
|
||||
if (auto it = env.handle_cache.find(hk); it != env.handle_cache.end())
|
||||
return it->second;
|
||||
const u32 sec_idx = cbuf.has_secondary ? cbuf.secondary_index : cbuf.index;
|
||||
const u32 sec_off = cbuf.has_secondary ? cbuf.secondary_offset : cbuf.offset;
|
||||
const u32 lhs = ReadCbufCached(env, cbuf.index, cbuf.offset) << cbuf.shift_left;
|
||||
const u32 rhs = ReadCbufCached(env, sec_idx, sec_off) << cbuf.secondary_shift_left;
|
||||
const u32 handle = lhs | rhs;
|
||||
u32 handle = (ReadCbufCached(env, cbuf.index, cbuf.offset) << cbuf.shift_left) & cbuf.mask;
|
||||
if (cbuf.has_secondary) {
|
||||
handle |= (ReadCbufCached(env, cbuf.secondary_index, cbuf.secondary_offset)
|
||||
<< cbuf.secondary_shift_left) &
|
||||
cbuf.secondary_mask;
|
||||
}
|
||||
env.handle_cache.emplace(hk, handle);
|
||||
return handle;
|
||||
}
|
||||
@@ -352,10 +385,22 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
|
||||
if (lhs->has_secondary || rhs->has_secondary) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (lhs->count > 1 || rhs->count > 1) {
|
||||
if (lhs->count > 1 && rhs->count > 1) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (lhs->shift_left > 0 || lhs->index > rhs->index || lhs->offset > rhs->offset) {
|
||||
if (rhs->has_index_cbuf || lhs->has_index_cbuf) {
|
||||
if (rhs->has_index_cbuf && lhs->has_index_cbuf) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (rhs->has_index_cbuf) {
|
||||
std::swap(lhs, rhs);
|
||||
}
|
||||
} else if (rhs->count > 1) {
|
||||
// An indexed handle array combined with a single handle (e.g. texture indices with
|
||||
// a shared sampler), only the primary handle is indexed
|
||||
std::swap(lhs, rhs);
|
||||
} else if (lhs->count == 1 &&
|
||||
(lhs->shift_left > 0 || lhs->index > rhs->index || lhs->offset > rhs->offset)) {
|
||||
std::swap(lhs, rhs);
|
||||
}
|
||||
return ConstBufferAddr{
|
||||
@@ -365,9 +410,15 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
|
||||
.secondary_index = rhs->index,
|
||||
.secondary_offset = rhs->offset,
|
||||
.secondary_shift_left = rhs->shift_left,
|
||||
.dynamic_offset = {},
|
||||
.count = 1,
|
||||
.dynamic_offset = lhs->dynamic_offset,
|
||||
.count = lhs->count,
|
||||
.has_secondary = true,
|
||||
.has_index_cbuf = lhs->has_index_cbuf,
|
||||
.index_cbuf_index = lhs->index_cbuf_index,
|
||||
.index_cbuf_offset = lhs->index_cbuf_offset,
|
||||
.index_shift = lhs->index_shift,
|
||||
.mask = lhs->mask,
|
||||
.secondary_mask = rhs->mask,
|
||||
};
|
||||
}
|
||||
case IR::Opcode::ShiftLeftLogical32: {
|
||||
@@ -378,6 +429,8 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
|
||||
std::optional lhs{TrackCached(inst->Arg(0), env, host_info)};
|
||||
if (lhs) {
|
||||
lhs->shift_left = shift.U32();
|
||||
// A mask applied before the shift moves with the value
|
||||
lhs->mask <<= shift.U32();
|
||||
}
|
||||
return lhs;
|
||||
}
|
||||
@@ -405,7 +458,7 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
|
||||
}
|
||||
std::optional lhs{TrackCached(op1, env, host_info)};
|
||||
if (lhs) {
|
||||
lhs->shift_left = static_cast<u32>(std::countr_zero(op2.U32()));
|
||||
lhs->mask &= op2.U32();
|
||||
}
|
||||
return lhs;
|
||||
break;
|
||||
@@ -435,12 +488,12 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
|
||||
};
|
||||
}
|
||||
IR::Inst* const offset_inst{offset.InstRecursive()};
|
||||
if (offset_inst->GetOpcode() != IR::Opcode::IAdd32) {
|
||||
return std::nullopt;
|
||||
}
|
||||
u32 base_offset{};
|
||||
IR::U32 dynamic_offset;
|
||||
if (offset_inst->Arg(0).IsImmediate()) {
|
||||
if (offset_inst->GetOpcode() != IR::Opcode::IAdd32) {
|
||||
// The handle array starts at the beginning of the constant buffer
|
||||
dynamic_offset = IR::U32{offset};
|
||||
} else if (offset_inst->Arg(0).IsImmediate()) {
|
||||
base_offset = offset_inst->Arg(0).U32();
|
||||
dynamic_offset = IR::U32{offset_inst->Arg(1)};
|
||||
} else if (offset_inst->Arg(1).IsImmediate()) {
|
||||
@@ -450,6 +503,7 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
|
||||
return std::nullopt;
|
||||
}
|
||||
auto const size_shift = DynamicDescriptorSizeShift(dynamic_offset);
|
||||
const std::optional<UniformIndex> uniform_index{TrackUniformIndex(dynamic_offset)};
|
||||
auto const sampled_limit = (std::max)(1U, (std::min)(host_info.max_per_stage_descriptor_sampled_images,
|
||||
host_info.max_descriptor_set_sampled_images));
|
||||
auto const resource_limit = (std::max)(1U, host_info.max_per_stage_resources);
|
||||
@@ -463,6 +517,10 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
|
||||
.dynamic_offset = dynamic_offset,
|
||||
.count = DynamicDescriptorCount(base_offset, size_shift, (std::min)({DESCRIPTOR_MAX_COUNT, sampled_limit, resource_limit})),
|
||||
.has_secondary = false,
|
||||
.has_index_cbuf = uniform_index.has_value(),
|
||||
.index_cbuf_index = uniform_index ? uniform_index->cbuf_index : 0,
|
||||
.index_cbuf_offset = uniform_index ? uniform_index->cbuf_offset : 0,
|
||||
.index_shift = uniform_index ? uniform_index->shift : 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -497,12 +555,13 @@ TextureInst MakeInst(Environment& env, IR::Block* block, IR::Inst& inst, const H
|
||||
}
|
||||
|
||||
u32 GetTextureHandle(Environment& env, const ConstBufferAddr& cbuf) {
|
||||
const u32 secondary_index{cbuf.has_secondary ? cbuf.secondary_index : cbuf.index};
|
||||
const u32 secondary_offset{cbuf.has_secondary ? cbuf.secondary_offset : cbuf.offset};
|
||||
const u32 lhs_raw{env.ReadCbufValue(cbuf.index, cbuf.offset) << cbuf.shift_left};
|
||||
const u32 rhs_raw{env.ReadCbufValue(secondary_index, secondary_offset)
|
||||
<< cbuf.secondary_shift_left};
|
||||
return lhs_raw | rhs_raw;
|
||||
u32 handle{(env.ReadCbufValue(cbuf.index, cbuf.offset) << cbuf.shift_left) & cbuf.mask};
|
||||
if (cbuf.has_secondary) {
|
||||
handle |= (env.ReadCbufValue(cbuf.secondary_index, cbuf.secondary_offset)
|
||||
<< cbuf.secondary_shift_left) &
|
||||
cbuf.secondary_mask;
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
[[maybe_unused]] TextureType ReadTextureType(Environment& env, const ConstBufferAddr& cbuf) {
|
||||
@@ -536,7 +595,8 @@ public:
|
||||
desc.secondary_cbuf_offset == existing.secondary_cbuf_offset &&
|
||||
desc.secondary_shift_left == existing.secondary_shift_left &&
|
||||
desc.count == existing.count && desc.size_shift == existing.size_shift &&
|
||||
desc.has_secondary == existing.has_secondary;
|
||||
desc.has_secondary == existing.has_secondary && desc.mask == existing.mask &&
|
||||
desc.secondary_mask == existing.secondary_mask;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -562,7 +622,12 @@ public:
|
||||
desc.secondary_cbuf_index == existing.secondary_cbuf_index &&
|
||||
desc.secondary_cbuf_offset == existing.secondary_cbuf_offset &&
|
||||
desc.secondary_shift_left == existing.secondary_shift_left &&
|
||||
desc.count == existing.count && desc.size_shift == existing.size_shift;
|
||||
desc.count == existing.count && desc.size_shift == existing.size_shift &&
|
||||
desc.has_index_cbuf == existing.has_index_cbuf &&
|
||||
desc.index_cbuf_index == existing.index_cbuf_index &&
|
||||
desc.index_cbuf_offset == existing.index_cbuf_offset &&
|
||||
desc.index_shift == existing.index_shift && desc.mask == existing.mask &&
|
||||
desc.secondary_mask == existing.secondary_mask;
|
||||
})};
|
||||
// TODO: Read this from TIC
|
||||
texture_descriptors[index].is_multisample |= desc.is_multisample;
|
||||
@@ -808,9 +873,12 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
|
||||
.secondary_shift_left = cbuf.secondary_shift_left,
|
||||
.count = count,
|
||||
.size_shift = size_shift,
|
||||
.mask = cbuf.mask,
|
||||
.secondary_mask = cbuf.secondary_mask,
|
||||
});
|
||||
} else {
|
||||
count = std::min(count, sampled_dynamic_cap);
|
||||
// A uniform index is resolved when drawing, so a single descriptor is enough
|
||||
count = cbuf.has_index_cbuf ? 1 : std::min(count, sampled_dynamic_cap);
|
||||
const bool is_integer{is_texture_pixel_format_integer(cbuf)};
|
||||
index = descriptors.Add(TextureDescriptor{
|
||||
.type = flags.type,
|
||||
@@ -826,6 +894,12 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
|
||||
.secondary_shift_left = cbuf.secondary_shift_left,
|
||||
.count = count,
|
||||
.size_shift = size_shift,
|
||||
.has_index_cbuf = cbuf.has_index_cbuf,
|
||||
.index_cbuf_index = cbuf.index_cbuf_index,
|
||||
.index_cbuf_offset = cbuf.index_cbuf_offset,
|
||||
.index_shift = cbuf.index_shift,
|
||||
.mask = cbuf.mask,
|
||||
.secondary_mask = cbuf.secondary_mask,
|
||||
});
|
||||
flags.is_integer.Assign(is_integer ? 1 : 0);
|
||||
}
|
||||
|
||||
@@ -186,6 +186,8 @@ struct TextureBufferDescriptor {
|
||||
u32 secondary_shift_left;
|
||||
u32 count;
|
||||
u32 size_shift;
|
||||
u32 mask = 0xffffffff;
|
||||
u32 secondary_mask = 0xffffffff;
|
||||
|
||||
auto operator<=>(const TextureBufferDescriptor&) const = default;
|
||||
};
|
||||
@@ -219,6 +221,13 @@ struct TextureDescriptor {
|
||||
u32 secondary_shift_left;
|
||||
u32 count;
|
||||
u32 size_shift;
|
||||
/// The handle offset is moved by a constant buffer value read when drawing
|
||||
bool has_index_cbuf;
|
||||
u32 index_cbuf_index;
|
||||
u32 index_cbuf_offset;
|
||||
u32 index_shift;
|
||||
u32 mask = 0xffffffff;
|
||||
u32 secondary_mask = 0xffffffff;
|
||||
|
||||
auto operator<=>(const TextureDescriptor&) const = default;
|
||||
};
|
||||
|
||||
@@ -948,9 +948,14 @@ void BufferCache<P>::BindHostGraphicsUniformBuffer(size_t stage, u32 index, u32
|
||||
return alignment > 1 && (offset % alignment) != 0;
|
||||
}
|
||||
}();
|
||||
// Uniform data rewritten by the CPU between draws would need a transfer copy into the cached
|
||||
// buffer, which ends the current render pass. Read it from the stream buffer instead.
|
||||
static constexpr u32 MAX_STREAMED_UNIFORM_BYTES = static_cast<u32>(16_KiB);
|
||||
const bool use_fast_buffer = needs_alignment_stream
|
||||
|| (has_host_buffer && size <= channel_state->uniform_buffer_skip_cache_size
|
||||
&& !memory_tracker.IsRegionGpuModified(device_addr, size));
|
||||
|| (has_host_buffer && !memory_tracker.IsRegionGpuModified(device_addr, size)
|
||||
&& (size <= channel_state->uniform_buffer_skip_cache_size
|
||||
|| (size <= MAX_STREAMED_UNIFORM_BYTES
|
||||
&& memory_tracker.IsRegionCpuModified(device_addr, size))));
|
||||
if (use_fast_buffer) {
|
||||
if constexpr (IS_OPENGL) {
|
||||
if (runtime.HasFastBufferSubData()) {
|
||||
|
||||
@@ -259,6 +259,23 @@ public:
|
||||
|
||||
void BindHostComputeBuffers();
|
||||
|
||||
/// Calls func(buffer, offset, device_addr, size) for each storage buffer the current compute
|
||||
/// dispatch writes through a single host buffer
|
||||
template <typename Func>
|
||||
void ForEachWrittenComputeStorageBuffer(Func&& func) {
|
||||
const u32 written = channel_state->enabled_compute_storage_buffers &
|
||||
channel_state->written_compute_storage_buffers;
|
||||
ForEachEnabledBit(written, [&](u32 index) {
|
||||
const Binding& binding = channel_state->compute_storage_buffers[index];
|
||||
if (binding.buffer_id == NULL_BUFFER_ID || binding.segment_count != 0 ||
|
||||
binding.size == 0) {
|
||||
return;
|
||||
}
|
||||
Buffer& buffer = slot_buffers[binding.buffer_id];
|
||||
func(buffer, buffer.Offset(binding.device_addr), binding.device_addr, binding.size);
|
||||
});
|
||||
}
|
||||
|
||||
void SetUniformBuffersState(const std::array<u32, NUM_STAGES>& mask,
|
||||
const UniformBufferSizes* sizes);
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ set(SHADER_FILES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_present.vert
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_present_scaleforce_fp16.frag
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_present_scaleforce_fp32.frag
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_block_linear_unswizzle_2d_buffer.comp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_quad_indexed.comp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_turbo_mode.comp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_uint8.comp
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#version 460 core
|
||||
|
||||
// Copies a block linear 2D image from a buffer into a tightly packed linear buffer, 4 bytes at a
|
||||
// time. 16 byte chunks of a row are contiguous in a GOB, so any texel size works as long as the
|
||||
// row size is a multiple of 4 bytes.
|
||||
|
||||
layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout(std430, set = 0, binding = 0) readonly buffer InputBuffer {
|
||||
uint swizzled[];
|
||||
};
|
||||
|
||||
layout(std430, set = 0, binding = 1) writeonly buffer OutputBuffer {
|
||||
uint linear[];
|
||||
};
|
||||
|
||||
layout(push_constant) uniform PushConstants {
|
||||
uint src_word_offset;
|
||||
uint row_words;
|
||||
uint height;
|
||||
uint layer_stride;
|
||||
uint block_size;
|
||||
uint x_shift;
|
||||
uint block_height;
|
||||
uint block_height_mask;
|
||||
};
|
||||
|
||||
const uint GOB_SIZE_X_SHIFT = 6;
|
||||
const uint GOB_SIZE_Y_SHIFT = 3;
|
||||
const uint GOB_SIZE_SHIFT = GOB_SIZE_X_SHIFT + GOB_SIZE_Y_SHIFT;
|
||||
|
||||
uint SwizzleTable(uint pos) {
|
||||
const uint t[8] = uint[](
|
||||
0x12100200, 0x13110301, 0x16140604, 0x17150705,
|
||||
0x1a180a08, 0x1b190b09, 0x1e1c0e0c, 0x1f1d0f0d
|
||||
);
|
||||
const uint i = pos >> 4;
|
||||
const uint h = (t[i / 4] >> ((i % 4) * 8)) & 0xff;
|
||||
return (h << 4) | (pos & 0xf);
|
||||
}
|
||||
|
||||
void main() {
|
||||
const uint word = gl_GlobalInvocationID.x;
|
||||
const uint y = gl_GlobalInvocationID.y;
|
||||
const uint layer = gl_GlobalInvocationID.z;
|
||||
if (word >= row_words || y >= height) {
|
||||
return;
|
||||
}
|
||||
const uint x = word * 4;
|
||||
const uint block_y = y >> GOB_SIZE_Y_SHIFT;
|
||||
|
||||
uint offset = layer * layer_stride;
|
||||
offset += (block_y >> block_height) * block_size;
|
||||
offset += (block_y & block_height_mask) << GOB_SIZE_SHIFT;
|
||||
offset += (x >> GOB_SIZE_X_SHIFT) << x_shift;
|
||||
offset += SwizzleTable((y & 7) * 64 + (x & 63));
|
||||
|
||||
linear[(layer * height + y) * row_words + word] = swizzled[src_word_offset + offset / 4];
|
||||
}
|
||||
@@ -107,22 +107,36 @@ void ComputePipeline::Configure() {
|
||||
const auto read_handle{[&](const auto& desc, u32 index) {
|
||||
ASSERT(((qmd.const_buffer_enable_mask >> desc.cbuf_index) & 1) != 0);
|
||||
const u32 index_offset{index << desc.size_shift};
|
||||
const u32 offset{desc.cbuf_offset + index_offset};
|
||||
u32 offset{desc.cbuf_offset + index_offset};
|
||||
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) {
|
||||
if (desc.has_index_cbuf) {
|
||||
ASSERT(((qmd.const_buffer_enable_mask >> desc.index_cbuf_index) & 1) != 0);
|
||||
offset += gpu_memory->Read<u32>(cbufs[desc.index_cbuf_index].Address() +
|
||||
desc.index_cbuf_offset)
|
||||
<< desc.index_shift;
|
||||
}
|
||||
}
|
||||
const GPUVAddr addr{cbufs[desc.cbuf_index].Address() + offset};
|
||||
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
|
||||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
|
||||
if (desc.has_secondary) {
|
||||
ASSERT(((qmd.const_buffer_enable_mask >> desc.secondary_cbuf_index) & 1) != 0);
|
||||
const u32 secondary_offset{desc.secondary_cbuf_offset + index_offset};
|
||||
const u32 secondary_offset{desc.secondary_cbuf_offset};
|
||||
const GPUVAddr separate_addr{cbufs[desc.secondary_cbuf_index].Address() +
|
||||
secondary_offset};
|
||||
const u32 lhs_raw{gpu_memory->Read<u32>(addr) << desc.shift_left};
|
||||
const u32 rhs_raw{gpu_memory->Read<u32>(separate_addr)
|
||||
<< desc.secondary_shift_left};
|
||||
const u32 lhs_raw{(gpu_memory->Read<u32>(addr) << desc.shift_left) & desc.mask};
|
||||
const u32 rhs_raw{(gpu_memory->Read<u32>(separate_addr)
|
||||
<< desc.secondary_shift_left) &
|
||||
desc.secondary_mask};
|
||||
return TexturePair(lhs_raw | rhs_raw, via_header_index);
|
||||
}
|
||||
}
|
||||
return TexturePair(gpu_memory->Read<u32>(addr), via_header_index);
|
||||
u32 raw{gpu_memory->Read<u32>(addr)};
|
||||
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
|
||||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
|
||||
raw &= desc.mask;
|
||||
}
|
||||
return TexturePair(raw, via_header_index);
|
||||
}};
|
||||
const auto add_image{[&](const auto& desc, bool blacklist) {
|
||||
for (u32 index = 0; index < desc.count; ++index) {
|
||||
|
||||
@@ -308,23 +308,37 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) {
|
||||
const auto read_handle{[&](const auto& desc, u32 index) {
|
||||
ASSERT(cbufs[desc.cbuf_index].enabled);
|
||||
const u32 index_offset{index << desc.size_shift};
|
||||
const u32 offset{desc.cbuf_offset + index_offset};
|
||||
u32 offset{desc.cbuf_offset + index_offset};
|
||||
if constexpr (std::is_same_v<decltype(desc), const TextureDescriptor&>) {
|
||||
if (desc.has_index_cbuf) {
|
||||
ASSERT(cbufs[desc.index_cbuf_index].enabled);
|
||||
offset += gpu_memory->Read<u32>(cbufs[desc.index_cbuf_index].address +
|
||||
desc.index_cbuf_offset)
|
||||
<< desc.index_shift;
|
||||
}
|
||||
}
|
||||
const GPUVAddr addr{cbufs[desc.cbuf_index].address + offset};
|
||||
if constexpr (std::is_same_v<decltype(desc), const TextureDescriptor&> ||
|
||||
std::is_same_v<decltype(desc), const TextureBufferDescriptor&>) {
|
||||
if (desc.has_secondary) {
|
||||
ASSERT(cbufs[desc.secondary_cbuf_index].enabled);
|
||||
const u32 second_offset{desc.secondary_cbuf_offset + index_offset};
|
||||
const u32 second_offset{desc.secondary_cbuf_offset};
|
||||
const GPUVAddr separate_addr{cbufs[desc.secondary_cbuf_index].address +
|
||||
second_offset};
|
||||
const u32 lhs_raw{gpu_memory->Read<u32>(addr) << desc.shift_left};
|
||||
const u32 rhs_raw{gpu_memory->Read<u32>(separate_addr)
|
||||
<< desc.secondary_shift_left};
|
||||
const u32 lhs_raw{(gpu_memory->Read<u32>(addr) << desc.shift_left) & desc.mask};
|
||||
const u32 rhs_raw{(gpu_memory->Read<u32>(separate_addr)
|
||||
<< desc.secondary_shift_left) &
|
||||
desc.secondary_mask};
|
||||
const u32 raw{lhs_raw | rhs_raw};
|
||||
return TexturePair(raw, via_header_index);
|
||||
}
|
||||
}
|
||||
return TexturePair(gpu_memory->Read<u32>(addr), via_header_index);
|
||||
u32 raw{gpu_memory->Read<u32>(addr)};
|
||||
if constexpr (std::is_same_v<decltype(desc), const TextureDescriptor&> ||
|
||||
std::is_same_v<decltype(desc), const TextureBufferDescriptor&>) {
|
||||
raw &= desc.mask;
|
||||
}
|
||||
return TexturePair(raw, via_header_index);
|
||||
}};
|
||||
const auto add_image{[&](const auto& desc, bool blacklist) LAMBDA_FORCEINLINE {
|
||||
for (u32 index = 0; index < desc.count; ++index) {
|
||||
|
||||
@@ -129,7 +129,13 @@ void FixedPipelineState::Refresh(Tegra::Engines::Maxwell3D& maxwell3d, DynamicFe
|
||||
topology.Assign(can_collapse_topology_class
|
||||
? TOPOLOGY_CLASS_REPRESENTATIVE_LUT[static_cast<size_t>(topology_)]
|
||||
: topology_);
|
||||
msaa_mode.Assign(regs.anti_alias_samples_mode);
|
||||
// Target-independent rasterization rasterizes (and depth tests) at the raster sample count
|
||||
if (regs.tir_mode == Maxwell::TIRMode::RasterNTargetM &&
|
||||
regs.anti_alias_raster != Maxwell::AntiAliasRaster::Mode1x1) {
|
||||
msaa_mode.Assign(static_cast<Tegra::Texture::MsaaMode>(regs.anti_alias_raster));
|
||||
} else {
|
||||
msaa_mode.Assign(regs.anti_alias_samples_mode);
|
||||
}
|
||||
attachment0_dual_source_blend.Assign(ComputeAttachment0DualSourceBlend(regs) ? 1 : 0);
|
||||
|
||||
raw2 = 0;
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "video_core/host_shaders/queries_prefix_scan_sum_comp_spv.h"
|
||||
#include "video_core/host_shaders/queries_prefix_scan_sum_nosubgroups_comp_spv.h"
|
||||
#include "video_core/host_shaders/resolve_conditional_render_comp_spv.h"
|
||||
#include "video_core/host_shaders/vulkan_block_linear_unswizzle_2d_buffer_comp_spv.h"
|
||||
#include "video_core/host_shaders/vulkan_quad_indexed_comp_spv.h"
|
||||
#include "video_core/host_shaders/vulkan_uint8_comp_spv.h"
|
||||
#include "video_core/host_shaders/block_linear_unswizzle_3d_bcn_comp_spv.h"
|
||||
@@ -31,6 +32,7 @@
|
||||
#include "video_core/renderer_vulkan/vk_update_descriptor.h"
|
||||
#include "video_core/texture_cache/accelerated_swizzle.h"
|
||||
#include "video_core/texture_cache/types.h"
|
||||
#include "video_core/texture_cache/util.h"
|
||||
#include "video_core/textures/decoders.h"
|
||||
#include "video_core/vulkan_common/vulkan_device.h"
|
||||
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
||||
@@ -640,6 +642,143 @@ struct alignas(16) BlockLinearUnswizzle3DPushConstants {
|
||||
};
|
||||
static_assert(sizeof(BlockLinearUnswizzle3DPushConstants) <= 128);
|
||||
|
||||
struct BlockLinearUnswizzle2DBufferPushConstants {
|
||||
u32 src_word_offset;
|
||||
u32 row_words;
|
||||
u32 height;
|
||||
u32 layer_stride;
|
||||
u32 block_size;
|
||||
u32 x_shift;
|
||||
u32 block_height;
|
||||
u32 block_height_mask;
|
||||
};
|
||||
|
||||
BlockLinearUnswizzle2DBufferPass::BlockLinearUnswizzle2DBufferPass(
|
||||
const Device& device_, Scheduler& scheduler_, DescriptorPool& descriptor_pool_,
|
||||
StagingBufferPool& staging_buffer_pool_,
|
||||
ComputePassDescriptorQueue& compute_pass_descriptor_queue_)
|
||||
: ComputePass(device_, scheduler_, descriptor_pool_, INPUT_OUTPUT_DESCRIPTOR_SET_BINDINGS,
|
||||
INPUT_OUTPUT_DESCRIPTOR_UPDATE_TEMPLATE, INPUT_OUTPUT_BANK_INFO,
|
||||
COMPUTE_PUSH_CONSTANT_RANGE<sizeof(BlockLinearUnswizzle2DBufferPushConstants)>,
|
||||
VULKAN_BLOCK_LINEAR_UNSWIZZLE_2D_BUFFER_COMP_SPV),
|
||||
scheduler{scheduler_}, staging_buffer_pool{staging_buffer_pool_},
|
||||
compute_pass_descriptor_queue{compute_pass_descriptor_queue_} {}
|
||||
|
||||
BlockLinearUnswizzle2DBufferPass::~BlockLinearUnswizzle2DBufferPass() = default;
|
||||
|
||||
bool BlockLinearUnswizzle2DBufferPass::IsSupported(const Image& image) {
|
||||
const VideoCommon::ImageInfo& info = image.info;
|
||||
if (info.type != VideoCommon::ImageType::e2D || info.num_samples != 1 ||
|
||||
VideoCore::Surface::GetFormatType(info.format) != VideoCore::Surface::SurfaceType::ColorTexture ||
|
||||
VideoCore::Surface::DefaultBlockWidth(info.format) != 1 ||
|
||||
VideoCore::Surface::DefaultBlockHeight(info.format) != 1 ||
|
||||
True(image.flags & VideoCommon::ImageFlagBits::Converted) ||
|
||||
True(image.flags & VideoCommon::ImageFlagBits::Rescaled)) {
|
||||
return false;
|
||||
}
|
||||
const u32 bytes_per_block = VideoCore::Surface::BytesPerBlock(info.format);
|
||||
for (const VideoCommon::SwizzleParameters& swizzle : VideoCommon::FullUploadSwizzles(info)) {
|
||||
if ((swizzle.num_tiles.width * bytes_per_block) % 4 != 0 || swizzle.num_tiles.depth != 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::pair<VkBuffer, boost::container::small_vector<VkBufferImageCopy, 16>>
|
||||
BlockLinearUnswizzle2DBufferPass::Unswizzle(Image& image, VkBuffer src_buffer,
|
||||
VkDeviceSize src_offset) {
|
||||
const VideoCommon::ImageInfo& info = image.info;
|
||||
const auto swizzles = VideoCommon::FullUploadSwizzles(info);
|
||||
const u32 bytes_per_block = VideoCore::Surface::BytesPerBlock(info.format);
|
||||
const u32 layers = static_cast<u32>(info.resources.layers);
|
||||
const VkDeviceSize alignment = (std::max<VkDeviceSize>)(device.GetStorageBufferAlignment(), 4);
|
||||
|
||||
VkDeviceSize total_size = 0;
|
||||
for (const VideoCommon::SwizzleParameters& swizzle : swizzles) {
|
||||
total_size = Common::AlignUp(total_size, alignment);
|
||||
total_size += VkDeviceSize{swizzle.num_tiles.width} * bytes_per_block *
|
||||
swizzle.num_tiles.height * layers;
|
||||
}
|
||||
const StagingBufferRef linear = staging_buffer_pool.Request(total_size, MemoryUsage::DeviceLocal);
|
||||
|
||||
scheduler.RequestOutsideRenderPassOperationContext();
|
||||
scheduler.Record([](vk::CommandBuffer cmdbuf) {
|
||||
static constexpr VkMemoryBarrier READ_BARRIER{
|
||||
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
|
||||
.pNext = nullptr,
|
||||
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
|
||||
};
|
||||
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, READ_BARRIER);
|
||||
});
|
||||
|
||||
boost::container::small_vector<VkBufferImageCopy, 16> copies;
|
||||
VkDeviceSize linear_offset = 0;
|
||||
for (const VideoCommon::SwizzleParameters& swizzle : swizzles) {
|
||||
linear_offset = Common::AlignUp(linear_offset, alignment);
|
||||
const auto params = VideoCommon::Accelerated::MakeBlockLinearSwizzle2DParams(swizzle, info);
|
||||
const u32 row_words = swizzle.num_tiles.width * bytes_per_block / 4;
|
||||
const u32 height = swizzle.num_tiles.height;
|
||||
const VkDeviceSize level_size = VkDeviceSize{row_words} * 4 * height * layers;
|
||||
const VkDeviceSize level_src = src_offset + swizzle.buffer_offset;
|
||||
const VkDeviceSize aligned_src = level_src & ~(alignment - 1);
|
||||
const VkDeviceSize src_size = image.guest_size_bytes - swizzle.buffer_offset +
|
||||
(level_src - aligned_src);
|
||||
|
||||
compute_pass_descriptor_queue.Acquire(scheduler, 2);
|
||||
compute_pass_descriptor_queue.AddBuffer(src_buffer, aligned_src, src_size);
|
||||
compute_pass_descriptor_queue.AddBuffer(linear.buffer, linear.offset + linear_offset,
|
||||
level_size);
|
||||
const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()};
|
||||
const BlockLinearUnswizzle2DBufferPushConstants push{
|
||||
.src_word_offset = static_cast<u32>((level_src - aligned_src) / 4),
|
||||
.row_words = row_words,
|
||||
.height = height,
|
||||
.layer_stride = params.layer_stride,
|
||||
.block_size = params.block_size,
|
||||
.x_shift = params.x_shift,
|
||||
.block_height = params.block_height,
|
||||
.block_height_mask = params.block_height_mask,
|
||||
};
|
||||
scheduler.Record([this, descriptor_data, push, layers](vk::CommandBuffer cmdbuf) {
|
||||
static constexpr u32 WORKGROUP_SIZE_X = 64;
|
||||
const VkDescriptorSet set = descriptor_allocator.Commit();
|
||||
device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, descriptor_data);
|
||||
cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *pipeline);
|
||||
cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {});
|
||||
cmdbuf.PushConstants(*layout, VK_SHADER_STAGE_COMPUTE_BIT, push);
|
||||
cmdbuf.Dispatch(Common::DivCeil(push.row_words, WORKGROUP_SIZE_X), push.height, layers);
|
||||
});
|
||||
copies.push_back(VkBufferImageCopy{
|
||||
.bufferOffset = linear.offset + linear_offset,
|
||||
.bufferRowLength = 0,
|
||||
.bufferImageHeight = 0,
|
||||
.imageSubresource{
|
||||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
.mipLevel = static_cast<u32>(swizzle.level),
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = layers,
|
||||
},
|
||||
.imageOffset{0, 0, 0},
|
||||
.imageExtent{swizzle.num_tiles.width, height, 1},
|
||||
});
|
||||
linear_offset += level_size;
|
||||
}
|
||||
scheduler.Record([](vk::CommandBuffer cmdbuf) {
|
||||
static constexpr VkMemoryBarrier WRITE_BARRIER{
|
||||
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
|
||||
.pNext = nullptr,
|
||||
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
|
||||
.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT,
|
||||
};
|
||||
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, WRITE_BARRIER);
|
||||
});
|
||||
return {linear.buffer, std::move(copies)};
|
||||
}
|
||||
|
||||
BlockLinearUnswizzle3DPass::BlockLinearUnswizzle3DPass(
|
||||
const Device& device_, Scheduler& scheduler_,
|
||||
DescriptorPool& descriptor_pool_,
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include <span>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/container/small_vector.hpp>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "video_core/engines/maxwell_3d.h"
|
||||
#include "video_core/renderer_vulkan/vk_descriptor_pool.h"
|
||||
@@ -137,6 +139,28 @@ private:
|
||||
MemoryAllocator& memory_allocator;
|
||||
};
|
||||
|
||||
class BlockLinearUnswizzle2DBufferPass final : public ComputePass {
|
||||
public:
|
||||
explicit BlockLinearUnswizzle2DBufferPass(
|
||||
const Device& device_, Scheduler& scheduler_, DescriptorPool& descriptor_pool_,
|
||||
StagingBufferPool& staging_buffer_pool_,
|
||||
ComputePassDescriptorQueue& compute_pass_descriptor_queue_);
|
||||
~BlockLinearUnswizzle2DBufferPass();
|
||||
|
||||
/// Returns true when the guest layout of the image can be unswizzled by this pass
|
||||
[[nodiscard]] static bool IsSupported(const Image& image);
|
||||
|
||||
/// Unswizzles the block linear guest data of the image, read from a GPU buffer, into a linear
|
||||
/// buffer. Returns the linear buffer and the copies that upload it into the image.
|
||||
std::pair<VkBuffer, boost::container::small_vector<VkBufferImageCopy, 16>> Unswizzle(
|
||||
Image& image, VkBuffer src_buffer, VkDeviceSize src_offset);
|
||||
|
||||
private:
|
||||
Scheduler& scheduler;
|
||||
StagingBufferPool& staging_buffer_pool;
|
||||
ComputePassDescriptorQueue& compute_pass_descriptor_queue;
|
||||
};
|
||||
|
||||
class BlockLinearUnswizzle3DPass final : public ComputePass {
|
||||
public:
|
||||
explicit BlockLinearUnswizzle3DPass(const Device& device_, Scheduler& scheduler_,
|
||||
|
||||
@@ -174,21 +174,36 @@ bool ComputePipeline::Configure(Tegra::Engines::KeplerCompute& kepler_compute,
|
||||
const auto read_handle{[&](const auto& desc, u32 index) {
|
||||
ASSERT(((qmd.const_buffer_enable_mask >> desc.cbuf_index) & 1) != 0);
|
||||
const u32 index_offset{index << desc.size_shift};
|
||||
const u32 offset{desc.cbuf_offset + index_offset};
|
||||
u32 offset{desc.cbuf_offset + index_offset};
|
||||
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) {
|
||||
if (desc.has_index_cbuf) {
|
||||
ASSERT(((qmd.const_buffer_enable_mask >> desc.index_cbuf_index) & 1) != 0);
|
||||
offset += gpu_memory.Read<u32>(cbufs[desc.index_cbuf_index].Address() +
|
||||
desc.index_cbuf_offset)
|
||||
<< desc.index_shift;
|
||||
}
|
||||
}
|
||||
const GPUVAddr addr{cbufs[desc.cbuf_index].Address() + offset};
|
||||
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
|
||||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
|
||||
if (desc.has_secondary) {
|
||||
ASSERT(((qmd.const_buffer_enable_mask >> desc.secondary_cbuf_index) & 1) != 0);
|
||||
const u32 secondary_offset{desc.secondary_cbuf_offset + index_offset};
|
||||
const u32 secondary_offset{desc.secondary_cbuf_offset};
|
||||
const GPUVAddr separate_addr{cbufs[desc.secondary_cbuf_index].Address() +
|
||||
secondary_offset};
|
||||
const u32 lhs_raw{gpu_memory.Read<u32>(addr) << desc.shift_left};
|
||||
const u32 rhs_raw{gpu_memory.Read<u32>(separate_addr) << desc.secondary_shift_left};
|
||||
const u32 lhs_raw{(gpu_memory.Read<u32>(addr) << desc.shift_left) & desc.mask};
|
||||
const u32 rhs_raw{(gpu_memory.Read<u32>(separate_addr)
|
||||
<< desc.secondary_shift_left) &
|
||||
desc.secondary_mask};
|
||||
return TexturePair(lhs_raw | rhs_raw, via_header_index);
|
||||
}
|
||||
}
|
||||
return TexturePair(gpu_memory.Read<u32>(addr), via_header_index);
|
||||
u32 raw{gpu_memory.Read<u32>(addr)};
|
||||
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
|
||||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
|
||||
raw &= desc.mask;
|
||||
}
|
||||
return TexturePair(raw, via_header_index);
|
||||
}};
|
||||
const auto add_image{[&](const auto& desc, bool blacklist) {
|
||||
for (u32 index = 0; index < desc.count; ++index) {
|
||||
|
||||
@@ -377,23 +377,37 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) {
|
||||
const auto read_handle{[&](const auto& desc, u32 index) {
|
||||
ASSERT(cbufs[desc.cbuf_index].enabled);
|
||||
const u32 index_offset{index << desc.size_shift};
|
||||
const u32 offset{desc.cbuf_offset + index_offset};
|
||||
u32 offset{desc.cbuf_offset + index_offset};
|
||||
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) {
|
||||
if (desc.has_index_cbuf) {
|
||||
ASSERT(cbufs[desc.index_cbuf_index].enabled);
|
||||
offset += gpu_memory->Read<u32>(cbufs[desc.index_cbuf_index].address +
|
||||
desc.index_cbuf_offset)
|
||||
<< desc.index_shift;
|
||||
}
|
||||
}
|
||||
const GPUVAddr addr{cbufs[desc.cbuf_index].address + offset};
|
||||
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
|
||||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
|
||||
if (desc.has_secondary) {
|
||||
ASSERT(cbufs[desc.secondary_cbuf_index].enabled);
|
||||
const u32 second_offset{desc.secondary_cbuf_offset + index_offset};
|
||||
const u32 second_offset{desc.secondary_cbuf_offset};
|
||||
const GPUVAddr separate_addr{cbufs[desc.secondary_cbuf_index].address +
|
||||
second_offset};
|
||||
const u32 lhs_raw{gpu_memory->Read<u32>(addr) << desc.shift_left};
|
||||
const u32 rhs_raw{gpu_memory->Read<u32>(separate_addr)
|
||||
<< desc.secondary_shift_left};
|
||||
const u32 lhs_raw{(gpu_memory->Read<u32>(addr) << desc.shift_left) & desc.mask};
|
||||
const u32 rhs_raw{(gpu_memory->Read<u32>(separate_addr)
|
||||
<< desc.secondary_shift_left) &
|
||||
desc.secondary_mask};
|
||||
const u32 raw{lhs_raw | rhs_raw};
|
||||
return TexturePair(raw, via_header_index);
|
||||
}
|
||||
}
|
||||
return TexturePair(gpu_memory->Read<u32>(addr), via_header_index);
|
||||
u32 raw{gpu_memory->Read<u32>(addr)};
|
||||
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
|
||||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
|
||||
raw &= desc.mask;
|
||||
}
|
||||
return TexturePair(raw, via_header_index);
|
||||
}};
|
||||
const auto add_image{[&](const auto& desc, bool blacklist) LAMBDA_FORCEINLINE {
|
||||
for (u32 index = 0; index < desc.count; ++index) {
|
||||
|
||||
@@ -596,6 +596,12 @@ void RasterizerVulkan::DispatchCompute() {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto update_images_written_by_buffers = [this] {
|
||||
buffer_cache.ForEachWrittenComputeStorageBuffer(
|
||||
[this](auto& buffer, u32 offset, DAddr device_addr, u32 size) {
|
||||
texture_cache.UpdateImagesFromBuffer(device_addr, size, buffer.Handle(), offset);
|
||||
});
|
||||
};
|
||||
const auto& qmd{kepler_compute->launch_description};
|
||||
auto indirect_address = kepler_compute->GetIndirectComputeAddress();
|
||||
if (indirect_address) {
|
||||
@@ -612,6 +618,7 @@ void RasterizerVulkan::DispatchCompute() {
|
||||
}
|
||||
cmdbuf.DispatchIndirect(indirect_buffer, indirect_offset);
|
||||
});
|
||||
update_images_written_by_buffers();
|
||||
return;
|
||||
}
|
||||
const std::array<u32, 3> dim{qmd.grid_dim_x, qmd.grid_dim_y, qmd.grid_dim_z};
|
||||
@@ -634,6 +641,7 @@ void RasterizerVulkan::DispatchCompute() {
|
||||
}
|
||||
cmdbuf.Dispatch(dim[0], dim[1], dim[2]);
|
||||
});
|
||||
update_images_written_by_buffers();
|
||||
|
||||
// Log compute dispatch
|
||||
if (GPU::Logging::IsActive() &&
|
||||
|
||||
@@ -946,6 +946,8 @@ TextureCacheRuntime::TextureCacheRuntime(const Device& device_, Scheduler& sched
|
||||
astc_decoder_pass.emplace(device, scheduler, descriptor_pool, staging_buffer_pool,
|
||||
compute_pass_descriptor_queue, memory_allocator);
|
||||
}
|
||||
bl2d_buffer_unswizzle_pass.emplace(device, scheduler, descriptor_pool, staging_buffer_pool,
|
||||
compute_pass_descriptor_queue);
|
||||
if (!device.IsKhrImageFormatListSupported()) {
|
||||
return;
|
||||
}
|
||||
@@ -1089,6 +1091,58 @@ VkImageView TextureCacheRuntime::GetOrCreateResolveShadow(VkImage msaa_image, Vk
|
||||
return *shadow.view;
|
||||
}
|
||||
|
||||
VkImageView TextureCacheRuntime::GetOrCreateTirColorScratch(VkImage color_image, VkFormat format,
|
||||
VkExtent2D extent, u32 layers,
|
||||
VkSampleCountFlagBits samples) {
|
||||
TirColorScratch& scratch = tir_color_scratch[color_image];
|
||||
if (scratch.image && scratch.format == format && scratch.extent.width == extent.width &&
|
||||
scratch.extent.height == extent.height && scratch.layers == layers &&
|
||||
scratch.samples == samples) {
|
||||
return *scratch.view;
|
||||
}
|
||||
if (scratch.image) {
|
||||
scheduler.Finish();
|
||||
}
|
||||
scratch.image = memory_allocator.CreateImage(VkImageCreateInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
.imageType = VK_IMAGE_TYPE_2D,
|
||||
.format = format,
|
||||
.extent = {extent.width, extent.height, 1},
|
||||
.mipLevels = 1,
|
||||
.arrayLayers = layers,
|
||||
.samples = samples,
|
||||
.tiling = VK_IMAGE_TILING_OPTIMAL,
|
||||
.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
|
||||
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
|
||||
.queueFamilyIndexCount = 0,
|
||||
.pQueueFamilyIndices = nullptr,
|
||||
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
});
|
||||
scratch.view = device.GetLogical().CreateImageView(VkImageViewCreateInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
.image = *scratch.image,
|
||||
.viewType = layers > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D,
|
||||
.format = format,
|
||||
.components{},
|
||||
.subresourceRange{
|
||||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
.baseMipLevel = 0,
|
||||
.levelCount = 1,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = layers,
|
||||
},
|
||||
});
|
||||
scratch.format = format;
|
||||
scratch.extent = extent;
|
||||
scratch.layers = layers;
|
||||
scratch.samples = samples;
|
||||
return *scratch.view;
|
||||
}
|
||||
|
||||
const TextureCacheRuntime::ResolveShadow* TextureCacheRuntime::GetValidResolveShadow(
|
||||
VkImage msaa_image) const {
|
||||
const auto it = resolve_shadows.find(msaa_image);
|
||||
@@ -2068,6 +2122,27 @@ void Image::UploadMemory(VkBuffer buffer, VkDeviceSize offset,
|
||||
}
|
||||
}
|
||||
|
||||
bool Image::UploadSwizzledFromBuffer(VkBuffer buffer, VkDeviceSize offset) {
|
||||
if (runtime == nullptr || !runtime->bl2d_buffer_unswizzle_pass || offset % 4 != 0 ||
|
||||
guest_size_bytes > runtime->device.GetMaxStorageBufferRange() ||
|
||||
!BlockLinearUnswizzle2DBufferPass::IsSupported(*this)) {
|
||||
return false;
|
||||
}
|
||||
if (ENABLE_MSAA_RESOLVE_CONSUME) {
|
||||
runtime->InvalidateResolveShadow(Handle());
|
||||
}
|
||||
auto [linear_buffer, copies] =
|
||||
runtime->bl2d_buffer_unswizzle_pass->Unswizzle(*this, buffer, offset);
|
||||
const VkImage vk_image = *original_image;
|
||||
const VkImageAspectFlags vk_aspect_mask = aspect_mask;
|
||||
const bool was_initialized = std::exchange(initialized, true);
|
||||
scheduler->Record([linear_buffer, vk_image, vk_aspect_mask, was_initialized,
|
||||
copies = std::move(copies)](vk::CommandBuffer cmdbuf) {
|
||||
CopyBufferToImage(cmdbuf, linear_buffer, vk_image, vk_aspect_mask, was_initialized, copies);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void Image::UploadMemory(const StagingBufferRef& map, std::span<const BufferImageCopy> copies) {
|
||||
UploadMemory(map.buffer, map.offset, copies);
|
||||
}
|
||||
@@ -2970,6 +3045,26 @@ Framebuffer::Framebuffer(TextureCacheRuntime& runtime, ImageView* color_buffer,
|
||||
|
||||
Framebuffer::~Framebuffer() = default;
|
||||
|
||||
/// Single-sampled color targets with a multisampled depth buffer, as used by target-independent
|
||||
/// rasterization. Vulkan requires matching sample counts for these attachments.
|
||||
static bool IsMixedSamplesTarget(std::span<ImageView*, NUM_RT> color_buffers,
|
||||
const ImageView* depth_buffer) {
|
||||
if (depth_buffer == nullptr || depth_buffer->Samples() == VK_SAMPLE_COUNT_1_BIT) {
|
||||
return false;
|
||||
}
|
||||
bool has_color = false;
|
||||
for (const ImageView* const color_buffer : color_buffers) {
|
||||
if (color_buffer == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if (color_buffer->Samples() != VK_SAMPLE_COUNT_1_BIT) {
|
||||
return false;
|
||||
}
|
||||
has_color = true;
|
||||
}
|
||||
return has_color;
|
||||
}
|
||||
|
||||
void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime,
|
||||
std::span<ImageView*, NUM_RT> color_buffers,
|
||||
ImageView* depth_buffer, bool is_rescaled_) {
|
||||
@@ -3024,6 +3119,13 @@ void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime,
|
||||
} else {
|
||||
renderpass_key.depth_format = PixelFormat::Invalid;
|
||||
}
|
||||
if (IsMixedSamplesTarget(color_buffers, depth_buffer)) {
|
||||
const VkExtent2D extent{(std::min)(render_area.width, width),
|
||||
(std::min)(render_area.height, height)};
|
||||
CreateMixedSamplesFramebuffer(runtime, color_buffers, attachments, renderpass_key,
|
||||
static_cast<u32>((std::max)(num_layers, 1)), extent);
|
||||
return;
|
||||
}
|
||||
renderpass_key.samples = samples;
|
||||
const bool do_resolve_color = ENABLE_MSAA_TILER_RESOLVE &&
|
||||
samples != VK_SAMPLE_COUNT_1_BIT && num_colors > 0 && runtime.device.IsTiler();
|
||||
@@ -3040,10 +3142,7 @@ void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime,
|
||||
discard_msaa_depth_stencil = ENABLE_MSAA_RESOLVE_CONSUME &&
|
||||
ENABLE_MSAA_DEPTH_STENCIL_DISCARD && do_resolve_depth_stencil;
|
||||
|
||||
renderpass = runtime.render_pass_cache.Get(renderpass_key);
|
||||
render_pass_key = renderpass_key;
|
||||
render_pass_cache = &runtime.render_pass_cache;
|
||||
runtime_ptr = &runtime;
|
||||
UseRenderPass(runtime, renderpass_key);
|
||||
render_area.width = (std::min)(render_area.width, width);
|
||||
render_area.height = (std::min)(render_area.height, height);
|
||||
|
||||
@@ -3075,6 +3174,53 @@ void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime,
|
||||
}
|
||||
|
||||
num_color_buffers = static_cast<u32>(num_colors);
|
||||
CreateVkFramebuffer(runtime, attachments, static_cast<u32>((std::max)(num_layers, 1)));
|
||||
}
|
||||
|
||||
void Framebuffer::CreateMixedSamplesFramebuffer(TextureCacheRuntime& runtime,
|
||||
std::span<ImageView*, NUM_RT> color_buffers,
|
||||
std::span<const VkImageView> attachments,
|
||||
RenderPassKey renderpass_key, u32 layers,
|
||||
VkExtent2D extent) {
|
||||
// attachments holds the real color targets followed by the depth buffer
|
||||
const size_t num_colors = attachments.size() - 1;
|
||||
boost::container::small_vector<VkImageView, NUM_RT * 2 + 2> mixed_attachments;
|
||||
for (const ImageView* const color_buffer : color_buffers) {
|
||||
if (!color_buffer) {
|
||||
continue;
|
||||
}
|
||||
const VkFormat vk_format = MaxwellToVK::SurfaceFormat(runtime.device, FormatType::Optimal,
|
||||
true, color_buffer->format)
|
||||
.format;
|
||||
mixed_attachments.push_back(runtime.GetOrCreateTirColorScratch(
|
||||
color_buffer->ImageHandle(), vk_format, extent, layers, samples));
|
||||
}
|
||||
mixed_attachments.push_back(attachments[num_colors]);
|
||||
for (size_t index = 0; index < num_colors; ++index) {
|
||||
mixed_attachments.push_back(attachments[index]);
|
||||
}
|
||||
// Multisampled scratch color + depth, resolving color into the real single-sampled targets
|
||||
renderpass_key.samples = samples;
|
||||
renderpass_key.resolve_color = true;
|
||||
renderpass_key.resolve_depth_stencil = false;
|
||||
discard_msaa_color = false;
|
||||
discard_msaa_depth_stencil = false;
|
||||
UseRenderPass(runtime, renderpass_key);
|
||||
render_area = extent;
|
||||
num_color_buffers = static_cast<u32>(num_colors);
|
||||
CreateVkFramebuffer(runtime, mixed_attachments, layers);
|
||||
}
|
||||
|
||||
void Framebuffer::UseRenderPass(TextureCacheRuntime& runtime,
|
||||
const RenderPassKey& renderpass_key) {
|
||||
renderpass = runtime.render_pass_cache.Get(renderpass_key);
|
||||
render_pass_key = renderpass_key;
|
||||
render_pass_cache = &runtime.render_pass_cache;
|
||||
runtime_ptr = &runtime;
|
||||
}
|
||||
|
||||
void Framebuffer::CreateVkFramebuffer(TextureCacheRuntime& runtime,
|
||||
std::span<const VkImageView> attachments, u32 layers) {
|
||||
framebuffer = runtime.device.GetLogical().CreateFramebuffer({
|
||||
.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
@@ -3084,7 +3230,7 @@ void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime,
|
||||
.pAttachments = attachments.data(),
|
||||
.width = render_area.width,
|
||||
.height = render_area.height,
|
||||
.layers = static_cast<u32>((std::max)(num_layers, 1)),
|
||||
.layers = layers,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -133,6 +133,11 @@ public:
|
||||
|
||||
[[nodiscard]] const ResolveShadow* GetValidResolveShadow(VkImage msaa_image) const;
|
||||
|
||||
/// Multisampled stand-in for a single-sampled color target rendered with a TIR depth buffer
|
||||
[[nodiscard]] VkImageView GetOrCreateTirColorScratch(VkImage color_image, VkFormat format,
|
||||
VkExtent2D extent, u32 layers,
|
||||
VkSampleCountFlagBits samples);
|
||||
|
||||
void InvalidateResolveShadow(VkImage msaa_image);
|
||||
|
||||
void MarkResolveShadowUpToDate(VkImage msaa_image);
|
||||
@@ -159,6 +164,7 @@ public:
|
||||
std::optional<ASTCDecoderPass> astc_decoder_pass;
|
||||
|
||||
std::optional<BlockLinearUnswizzle3DPass> bl3d_unswizzle_pass;
|
||||
std::optional<BlockLinearUnswizzle2DBufferPass> bl2d_buffer_unswizzle_pass;
|
||||
const Settings::ResolutionScalingInfo& resolution;
|
||||
std::array<std::vector<VkFormat>, VideoCore::Surface::MaxPixelFormat> view_formats;
|
||||
|
||||
@@ -187,6 +193,15 @@ public:
|
||||
|
||||
std::vector<MsaaScratchImage> msaa_scratch_images;
|
||||
::Common::unordered_map<VkImage, ResolveShadow> resolve_shadows;
|
||||
struct TirColorScratch {
|
||||
vk::Image image;
|
||||
vk::ImageView view;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkExtent2D extent{};
|
||||
u32 layers = 0;
|
||||
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
};
|
||||
::Common::unordered_map<VkImage, TirColorScratch> tir_color_scratch;
|
||||
std::vector<std::pair<u64, ResolveShadow>> pending_resolve_shadows;
|
||||
};
|
||||
|
||||
@@ -279,6 +294,22 @@ public:
|
||||
void MarkResolveShadowsUpToDate() const;
|
||||
|
||||
private:
|
||||
/// Builds a framebuffer for single-sampled color targets drawn with a multisampled depth
|
||||
/// buffer (target-independent rasterization). Color is rendered into multisampled scratch
|
||||
/// images that resolve into the real targets at the end of the pass.
|
||||
void CreateMixedSamplesFramebuffer(TextureCacheRuntime& runtime,
|
||||
std::span<ImageView*, NUM_RT> color_buffers,
|
||||
std::span<const VkImageView> attachments,
|
||||
RenderPassKey renderpass_key, u32 layers,
|
||||
VkExtent2D extent);
|
||||
|
||||
/// Fetches the render pass for the key from the cache and remembers it for variants.
|
||||
void UseRenderPass(TextureCacheRuntime& runtime, const RenderPassKey& renderpass_key);
|
||||
|
||||
/// Creates the Vulkan framebuffer for the current render pass and render area.
|
||||
void CreateVkFramebuffer(TextureCacheRuntime& runtime,
|
||||
std::span<const VkImageView> attachments, u32 layers);
|
||||
|
||||
static constexpr size_t NUM_MEMOIZED_RENDER_PASS_VARIANTS = 8;
|
||||
|
||||
vk::Framebuffer framebuffer;
|
||||
@@ -325,6 +356,10 @@ public:
|
||||
void UploadMemory(const StagingBufferRef& map,
|
||||
std::span<const VideoCommon::BufferImageCopy> copies);
|
||||
|
||||
/// Replaces the image contents with its block linear guest data held in a GPU buffer.
|
||||
/// Returns false when the image layout is not supported.
|
||||
bool UploadSwizzledFromBuffer(VkBuffer buffer, VkDeviceSize offset);
|
||||
|
||||
void DownloadMemory(VkBuffer buffer, size_t offset,
|
||||
std::span<const VideoCommon::BufferImageCopy> copies);
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -6,4 +9,6 @@
|
||||
|
||||
namespace VideoCommon {
|
||||
template class VideoCommon::TextureCache<Vulkan::TextureCacheParams>;
|
||||
template void VideoCommon::TextureCache<Vulkan::TextureCacheParams>::UpdateImagesFromBuffer<VkBuffer>(
|
||||
DAddr, size_t, VkBuffer, u64);
|
||||
}
|
||||
|
||||
@@ -595,6 +595,36 @@ void TextureCache<P>::WriteMemory(DAddr cpu_addr, size_t size) {
|
||||
});
|
||||
}
|
||||
|
||||
template <class P>
|
||||
template <typename BufferHandle>
|
||||
void TextureCache<P>::UpdateImagesFromBuffer(DAddr cpu_addr, size_t size, BufferHandle buffer,
|
||||
u64 buffer_offset) {
|
||||
if constexpr (requires(Image& image) { image.UploadSwizzledFromBuffer(buffer, buffer_offset); }) {
|
||||
// The guest can write a render target through a storage buffer bound over its memory
|
||||
// (e.g. a compute clear). Only images the binding matches exactly are replaced, a larger
|
||||
// binding may hold unrelated live images in the parts the shader does not write.
|
||||
static constexpr size_t MAX_BINDING_PADDING = 64 * 1024;
|
||||
ForEachImageInRegion(cpu_addr, size, [&](ImageId image_id, Image& image) {
|
||||
if (image.cpu_addr != cpu_addr || size < image.guest_size_bytes ||
|
||||
size - image.guest_size_bytes > MAX_BINDING_PADDING ||
|
||||
True(image.flags & ImageFlagBits::Remapped) ||
|
||||
True(image.flags & ImageFlagBits::Sparse)) {
|
||||
return;
|
||||
}
|
||||
if (!image.UploadSwizzledFromBuffer(buffer, buffer_offset)) {
|
||||
return;
|
||||
}
|
||||
if (True(image.flags & ImageFlagBits::CpuModified)) {
|
||||
image.flags &= ~ImageFlagBits::CpuModified;
|
||||
if (False(image.flags & ImageFlagBits::Tracked)) {
|
||||
TrackImage(image, image_id);
|
||||
}
|
||||
}
|
||||
MarkModification(image);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
template <class P>
|
||||
bool TextureCache<P>::IsDownloadable(const ImageBase& image) const noexcept {
|
||||
if (!image.IsSafeGpuCopy()) {
|
||||
@@ -1928,7 +1958,26 @@ ImageViewId TextureCache<P>::FindDepthBuffer() {
|
||||
if (gpu_addr == 0) {
|
||||
return ImageViewId{};
|
||||
}
|
||||
const ImageInfo info(regs.zeta, regs.zeta_size, regs.anti_alias_samples_mode);
|
||||
// Target-independent rasterization: the depth buffer keeps the raster sample count while
|
||||
// the color targets use anti_alias_samples_mode.
|
||||
Tegra::Texture::MsaaMode zeta_msaa_mode = regs.anti_alias_samples_mode;
|
||||
if (regs.anti_alias_raster != Tegra::Engines::Maxwell3D::Regs::AntiAliasRaster::Mode1x1) {
|
||||
const auto raster_mode = static_cast<Tegra::Texture::MsaaMode>(regs.anti_alias_raster);
|
||||
bool use_raster = regs.tir_mode == Tegra::Engines::Maxwell3D::Regs::TIRMode::RasterNTargetM;
|
||||
if (!use_raster && regs.anti_alias_samples_mode == Tegra::Texture::MsaaMode::Msaa1x1) {
|
||||
// Clears can be issued before tir_mode is enabled. Only a depth buffer sized for the
|
||||
// raster samples relative to the color target is treated as multisampled, otherwise
|
||||
// it ping-pongs between a 1x and a multisampled image every frame.
|
||||
const auto& rt0 = regs.rt[0];
|
||||
use_raster = rt0.Address() != 0 && rt0.format != Tegra::RenderTargetFormat::NONE &&
|
||||
regs.zeta_size.width == rt0.width * NumSamplesX(raster_mode) &&
|
||||
regs.zeta_size.height == rt0.height * NumSamplesY(raster_mode);
|
||||
}
|
||||
if (use_raster) {
|
||||
zeta_msaa_mode = raster_mode;
|
||||
}
|
||||
}
|
||||
const ImageInfo info(regs.zeta, regs.zeta_size, zeta_msaa_mode);
|
||||
return FindRenderTargetView(info, gpu_addr);
|
||||
}
|
||||
|
||||
|
||||
@@ -203,6 +203,12 @@ public:
|
||||
/// Mark images in a range as modified from the CPU
|
||||
void WriteMemory(DAddr cpu_addr, size_t size);
|
||||
|
||||
/// Replaces the contents of the images fully inside a range that a GPU buffer wrote, with the
|
||||
/// data of that buffer
|
||||
template <typename BufferHandle>
|
||||
void UpdateImagesFromBuffer(DAddr cpu_addr, size_t size, BufferHandle buffer,
|
||||
u64 buffer_offset);
|
||||
|
||||
/// Download contents of host images to guest memory in a region
|
||||
void DownloadMemory(DAddr cpu_addr, size_t size);
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ ConfigureCpu::ConfigureCpu(const Core::System& system_,
|
||||
connect(backend_combobox, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||
&ConfigureCpu::UpdateGroup);
|
||||
|
||||
#if defined(HAS_NCE) || defined(HAS_SCARLETNX)
|
||||
#ifdef HAS_NCE
|
||||
ui->backend_group->setVisible(true);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -3661,12 +3661,12 @@ void MainWindow::OnToggleGraphicsAPI() {
|
||||
api = Settings::RendererBackend::OpenGL_GLSL;
|
||||
break;
|
||||
case Settings::RendererBackend::OpenGL_GLSL:
|
||||
api = Settings::RendererBackend::OpenGL_GLSL;
|
||||
break;
|
||||
case Settings::RendererBackend::OpenGL_SPIRV:
|
||||
api = Settings::RendererBackend::OpenGL_GLASM;
|
||||
break;
|
||||
case Settings::RendererBackend::OpenGL_GLASM:
|
||||
api = Settings::RendererBackend::OpenGL_SPIRV;
|
||||
break;
|
||||
case Settings::RendererBackend::OpenGL_SPIRV:
|
||||
api = Settings::RendererBackend::Null;
|
||||
break;
|
||||
#else
|
||||
|
||||
Reference in New Issue
Block a user