From f4f8d5bbce747fefdc583d15948fd9ccd9ca6977 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 07:53:49 +0300 Subject: [PATCH 01/40] feat(relinker): exclude single sce_module files, tolerate a zero code tail - --exclude-sce-module (repeatable) drops one title module from guest conversion, e.g. a title libc.prx, while the rest of sce_module is still relinked as guest modules - the instruction scanner stops at zero padding that ends a code segment with an odd byte instead of failing on a truncated `add [rax], al` Co-Authored-By: Claude Opus 5.5 --- core/relinker/cli/include/Cli.hpp | 2 ++ core/relinker/cli/src/CliArgs.cpp | 9 ++++++++- core/relinker/codegen/src/InstructionScanner.cpp | 2 ++ .../relinker/codegen/tests/Amd64OnlyConverterTests.cpp | 10 ++++++++++ core/relinker/main.cpp | 3 ++- .../relinker/include/relinker/guest/GuestImage.hpp | 4 +++- .../relinker/relinker/src/guest/GuestModuleBuilder.cpp | 5 ++++- 7 files changed, 31 insertions(+), 4 deletions(-) diff --git a/core/relinker/cli/include/Cli.hpp b/core/relinker/cli/include/Cli.hpp index 7d5dc0db3..15005aa2a 100644 --- a/core/relinker/cli/include/Cli.hpp +++ b/core/relinker/cli/include/Cli.hpp @@ -1,6 +1,7 @@ #ifndef CORE_RELINKER_CLI_INCLUDE_CLI_HPP #define CORE_RELINKER_CLI_INCLUDE_CLI_HPP +#include #include #include @@ -20,6 +21,7 @@ struct Args { std::string inputPath; std::string outputPath; std::string runPath = "$ORIGIN/libs"; + std::set excludedSceModules; }; Args ParseArgs(int argc, char* argv[]); diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index 2514a24c7..212d084d8 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -15,6 +15,10 @@ Args ParseArgs(int argc, char* argv[]) { args.skipSyscallCheck = true; } else if (arg == "--skip-sce-module") { args.skipSceModule = true; + } else if (arg == "--exclude-sce-module") { + if (i + 1 >= argc) + throw std::runtime_error("--exclude-sce-module requires a file name"); + args.excludedSceModules.insert(argv[++i]); } else if (arg == "--to-intel") { args.toIntel = true; } else if (arg.rfind("unused-filter=", 0) == 0) { @@ -50,6 +54,9 @@ Args ParseArgs(int argc, char* argv[]) { } } + if (args.skipSceModule && !args.excludedSceModules.empty()) + throw std::runtime_error("--exclude-sce-module conflicts with --skip-sce-module"); + if (args.windowsDiagnostics && !args.toWindows) throw std::runtime_error("--windows-diagnostics requires --windows"); @@ -58,7 +65,7 @@ Args ParseArgs(int argc, char* argv[]) { if (args.inputPath.empty() || args.outputPath.empty()) throw std::runtime_error( - "Usage: relinker [--windows] [--windows-diagnostics] [--windows-gui] [--skip-syscall-check] [--skip-sce-module] [--to-intel] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" + "Usage: relinker [--windows] [--windows-diagnostics] [--windows-gui] [--skip-syscall-check] [--skip-sce-module] [--exclude-sce-module ]... [--to-intel] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" "Example: relinker input.elf output.elf" ); diff --git a/core/relinker/codegen/src/InstructionScanner.cpp b/core/relinker/codegen/src/InstructionScanner.cpp index f76cfeb6f..94addfff7 100644 --- a/core/relinker/codegen/src/InstructionScanner.cpp +++ b/core/relinker/codegen/src/InstructionScanner.cpp @@ -35,6 +35,8 @@ std::vector InstructionScanner::ScanCodeSection( try { length = decoder.Decode(cursor, available); } catch (const CodegenException& e) { + // Zero padding at the segment tail decodes as `add [rax], al` pairs; an odd tail leaves one truncated byte. + if (std::all_of(cursor, cursor + available, [](const std::uint8_t b) { return b == 0; })) break; throw CodegenException(std::string("Cannot decode instruction: ") + e.what(), codeSectionOffset + i); } diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 280e82980..3d2c49ccd 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -392,6 +393,14 @@ void registerFormExecution() { void registerFormExecution() {} #endif +void scannerZeroTail() { + const auto scanner = Codegen::MakeInstructionScanner(); + const Bytes code{0xC3, 0x00, 0x00, 0x00}; + require(scanner->ScanCodeSection(code, 0, code.size()).size() == 2, "Odd zero padding at segment tail must end the scan"); + const Bytes truncated{0xC3, 0x0F}; + requireFailure([&] { (void)scanner->ScanCodeSection(truncated, 0, truncated.size()); }, "Truncated non-zero tail must still fail"); +} + int main() { try { decoderLengths(); @@ -402,6 +411,7 @@ int main() { converterSegment(); converterFailureOffsets(); linuxPlacement(); + scannerZeroTail(); std::cout << "AMD64-only converter tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 705c3f6b0..0802223bf 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -71,6 +71,7 @@ int main(const int argc, char* argv[]) { std::cout << "System: " << (args.toWindows ? "Windows" : "Linux") << "; unused-filter=" << args.unusedFilterLevel << "\n"; std::cout << "sce_module/sce_modules processing: " << (args.skipSceModule ? "disabled (--skip-sce-module)" : "enabled") << '\n'; + for (const auto& name : args.excludedSceModules) std::cout << "sce_module excluded: " << name << '\n'; auto result = pipeline->Relink(sourceBytes); for (const auto& patch : result.Patches) { if (patch.Offset > sourceBytes.size() || patch.Bytes.size() > sourceBytes.size() - patch.Offset) @@ -80,7 +81,7 @@ int main(const int argc, char* argv[]) { std::vector guestArtifacts; if (!args.skipSceModule) { - guestArtifacts = Relinker::GuestModuleBuilder().Build(args.inputPath, absPath, result.DynamicSection, args.toWindows, args.toIntel, *syscallScanner, args.lazyBinding, args.runPath); + guestArtifacts = Relinker::GuestModuleBuilder().Build(args.inputPath, absPath, result.DynamicSection, args.toWindows, args.toIntel, *syscallScanner, args.lazyBinding, args.runPath, args.excludedSceModules); } if (args.writeRegistry) { diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index 71cca90fa..f85029698 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include namespace Relinker { @@ -44,7 +46,7 @@ struct GuestArtifact { class GuestModuleBuilder { public: - std::vector Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, bool windows, bool toIntel, ISyscallScanner& syscallScanner, bool lazyBinding, const std::string& runPath) const; + std::vector Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, bool windows, bool toIntel, ISyscallScanner& syscallScanner, bool lazyBinding, const std::string& runPath, const std::set& excludedModules) const; }; } diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index a8655e1e4..aa68ea39d 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -11,7 +11,7 @@ namespace Relinker { -std::vector GuestModuleBuilder::Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, const bool windows, const bool toIntel, ISyscallScanner& syscallScanner, const bool lazyBinding, const std::string& runPath) const { +std::vector GuestModuleBuilder::Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, const bool windows, const bool toIntel, ISyscallScanner& syscallScanner, const bool lazyBinding, const std::string& runPath, const std::set& excludedModules) const { const auto root = std::filesystem::absolute(inputPath).parent_path(); const auto singular = root / "sce_module"; const auto plural = root / "sce_modules"; @@ -22,8 +22,10 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const auto directory = hasSingular ? singular : plural; if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + directory.string()); std::vector paths; + std::set unmatchedExclusions = excludedModules; for (const auto& entry : std::filesystem::directory_iterator(directory)) { if (entry.path().filename().string().ends_with(GuestModuleSuffix)) continue; + if (unmatchedExclusions.erase(entry.path().filename().string()) != 0) continue; if (!entry.is_regular_file()) continue; std::ifstream stream(entry.path(), std::ios::binary); if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + entry.path().string()); @@ -32,6 +34,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + entry.path().string()); if (stream.gcount() == 4 && static_cast(magic[0]) == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F') paths.push_back(entry.path()); } + if (!unmatchedExclusions.empty()) throw Domain::RelinkerException("Excluded sce_module file not found: " + *unmatchedExclusions.begin()); std::sort(paths.begin(), paths.end()); if (paths.empty()) return {}; if (lazyBinding) throw Domain::RelinkerException("Guest modules require eager binding; --lazy-binding is incompatible"); From 8e6614500a7eb1a3d6f1d1acf7be0b73acfef034 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 07:53:49 +0300 Subject: [PATCH 02/40] feat(shader): MIMG UNORM, _a sample aliases, LOD clamp, VOP3 carry-in - MIMG word0 bit 12 (UNORM) is accepted on every op without a sampler (compute image stores set it, as the ISA asks); sample and gather still reject it, and rejections name the instruction words. - The image_sample_*_a opcodes keep the plain address layout, as in Kyty. - The _cl LOD clamp becomes the MinLod image operand (or clamps an explicit level) on devices with shaderResourceMinLod; a gather with a clamp fails loudly. - v_add_co_ci_u32, v_sub_co_ci_u32 and v_subrev_co_ci_u32 in VOP3 form take their carry-in from the SGPR pair of their third source, not VCC: a glyph upload's reciprocal division corrected its quotient on the wrong lanes and UI text came out stippled. - Pixel exports follow each MRT slot's component mapping and write only the components of their export format (32_R, 32_GR, 32_AR). Co-Authored-By: Claude Opus 5.5 --- .../RdnaDecoder/src/RdnaImageOpDecoder.cpp | 20 ++++++++++++------ .../SpirvBackend/SpirvEmitterState.hpp | 2 ++ .../SpirvBackend/src/SpirvImageEmitter.cpp | 21 +++++++++++++++++++ .../SpirvBackend/src/SpirvModuleEmitter.cpp | 9 ++++++-- .../src/ControlFlowInstructions.cpp | 5 +++-- 5 files changed, 47 insertions(+), 10 deletions(-) diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp index b9a95735e..efa90d7b2 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp @@ -1,5 +1,6 @@ #include "RdnaDecoder/RdnaImageOpDecoder.hpp" #include +#include #include #include @@ -82,9 +83,11 @@ void validateFlags(std::uint32_t flags) { if ((flags & ~known) != 0u) { throw std::runtime_error("unknown image address flags"); } - if ((flags & (RdnaImageSampleFlagLodClamp | RdnaImageSampleFlagCd | RdnaImageSampleFlagAdjust)) != 0u) { - throw std::runtime_error("unsupported image clamp, coarse derivative or adjustment address layout"); + if ((flags & RdnaImageSampleFlagCd) != 0u) { + throw std::runtime_error("unsupported image coarse derivative (_cd) address layout"); } + // The _a sample aliases (OPM set) keep the plain address layout: the adjustment only touches reserved + // bits of the sampler's dword 3, which the resource tracker canonicalizes (as in Kyty). const auto lodModes = flags & (RdnaImageSampleFlagLod | RdnaImageSampleFlagBias | RdnaImageSampleFlagDerivative | RdnaImageSampleFlagLevelZero); if (std::popcount(lodModes) > 1) { throw std::runtime_error("conflicting image LOD modes"); @@ -225,9 +228,15 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span> 18u) & 0x7Fu) | ((word0 & 1u) << 7u); - const auto reservedWord0 = opcode == 0xe6u ? 0x000340C0u : 0x000350C0u; + const auto& info = lookupOpcode(opcode); + // Word0 bit 12 is UNORM, unnormalized addressing, which only a sampler reads: loads, stores and + // atomics address texels by integer anyway (the ISA wants it set on stores and atomics, and the + // RTIP ray queries require it). On a sample or gather it is not implemented. + const auto reservedWord0 = info.sample || info.gather ? 0x000350C0u : 0x000340C0u; if ((word0 & reservedWord0) != 0u || (word1 & 0x3C000000u) != 0u) { - throw std::runtime_error("unsupported or reserved MIMG control bits"); + char message[96]; + std::snprintf(message, sizeof(message), "unsupported or reserved MIMG control bits (words %08x %08x)", word0, word1); + throw std::runtime_error(message); } const auto nsa = (word0 >> 1u) & 3u; const auto wordCount = 2u + nsa; @@ -237,7 +246,6 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span std::numeric_limits::max() - (wordCount * 4u - 1u)) { throw std::runtime_error("invalid MIMG program counter"); } - const auto& info = lookupOpcode(opcode); const bool a16 = (word1 & 0x40000000u) != 0u; const bool d16 = (word1 & 0x80000000u) != 0u; const auto flags = info.flags | (a16 ? RdnaImageSampleFlagA16 : 0u); @@ -259,7 +267,7 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span #include #include #include @@ -132,6 +133,9 @@ ImageSampleLayout Layout(const MemoryInfo& mem, RdnaImageDimension dimension) { if (HasFlag(mem, RdnaImageSampleFlagLod)) { layout.lod = cursor++; } + if (HasFlag(mem, RdnaImageSampleFlagLodClamp)) { + layout.clamp = cursor++; + } return layout; } @@ -647,6 +651,9 @@ void EmitGatherOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con auto& state = ctx.state; const auto& mem = access.mem; const auto dimension = access.image.dimension; + if (setup.layout.clamp != NoImageComponent) { + ctx.Fail(access.inst, "is a gather with an LOD clamp, which is not implemented"); + } if (dimension == RdnaImageDimension::Dim1D) { if (setup.dref || !HasFlag(mem, RdnaImageSampleFlagLevelZero) || HasFlag(mem, RdnaImageSampleFlagOffset) || HasFlag(mem, RdnaImageSampleFlagGatherHorizontal)) { ctx.Fail(access.inst, "has an unsupported 1D gather variant"); @@ -765,6 +772,20 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con operandMask |= spv::ImageOperandsBiasMask; operands.push_back(AddressF32(ctx, access, setup.layout.bias)); } + if (setup.layout.clamp != NoImageComponent) { + const auto& capabilities = state.requirements.capabilities; + if (std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityMinLod)) == capabilities.end()) ctx.Fail(access.inst, "clamps its LOD, which needs the device's shaderResourceMinLod"); + const auto clamp = AddressF32(ctx, access, setup.layout.clamp); + if ((operandMask & spv::ImageOperandsLodMask) != 0u) { + // An explicit level takes no MinLod operand: the level itself is clamped. + const auto clamped = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450FMax, operands.back(), clamp); + operands.back() = clamped; + } else { + operandMask |= spv::ImageOperandsMinLodMask; + operands.push_back(clamp); + } + } const auto sampled = MakeSampledImage(state, mem.resource, mem.sampler, access.slot); const auto sample = state.module.AllocateId(); std::vector words = {opcode, resultType, sample, sampled, setup.coord}; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index fae0a76bd..1f6127396 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -193,7 +193,9 @@ std::uint32_t ExportRawComponent(SpirvValueEmitContext& ctx, std::uint32_t vecto std::uint32_t ExportVector(SpirvValueEmitContext& ctx, std::uint32_t data, const ExportInfo& exp, bool uintOutput) { auto& state = ctx.state; if (exp.compr && !uintOutput) { - const auto unpack = MrtOutputMode(state, exp) == 5u ? GLSLstd450UnpackUnorm2x16 : GLSLstd450UnpackHalf2x16; + // SPI_SHADER_COL_FORMAT: 5 UNORM16_ABGR, 6 SNORM16_ABGR, otherwise FP16_ABGR. + const auto mode = MrtOutputMode(state, exp); + const auto unpack = mode == 5u ? GLSLstd450UnpackUnorm2x16 : mode == 6u ? GLSLstd450UnpackSnorm2x16 : GLSLstd450UnpackHalf2x16; std::array f32 {ConstantF32(state, 0u), ConstantF32(state, 0u), ConstantF32(state, 0u), ConstantF32(state, 0x3f800000u)}; for (std::uint32_t pair = 0; pair < 2u; pair++) { if ((exp.en & (3u << (pair * 2u))) == 0u) { @@ -221,6 +223,9 @@ std::uint32_t ExportVector(SpirvValueEmitContext& ctx, std::uint32_t data, const ConstantU32(state, 0u), ConstantU32(state, uintOutput ? 1u : 0x3f800000u), }; + // SPI_SHADER_COL_FORMAT 32_R, 32_GR and 32_AR export only those components; the rest read as 0, alpha 1. + const auto mode = MrtOutputMode(state, exp); + const auto exported = exp.en & (mode == 1u ? 0x1u : mode == 2u ? 0x3u : mode == 3u ? 0x9u : 0xfu); if (exp.compr) { for (std::uint32_t pair = 0; pair < 2u; pair++) { if ((exp.en & (3u << (pair * 2u))) == 0u) { @@ -238,7 +243,7 @@ std::uint32_t ExportVector(SpirvValueEmitContext& ctx, std::uint32_t data, const } } else { for (std::uint32_t component = 0; component < 4u; component++) { - if (((exp.en >> component) & 1u) != 0u) { + if (((exported >> component) & 1u) != 0u) { raw.at(component) = ExportRawComponent(ctx, data, component); } } diff --git a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp index 7835f97a0..9080ff69b 100644 --- a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp @@ -77,7 +77,8 @@ void TranslationContext::addU32(const RdnaInstruction& inst, bool vector, bool u ir.SetScc(carryOut.Value()); return; } - const IrU1 carryIn = vector ? IrU1(ir.GetVcc()) : IrU1(ir.GetScc()); + // A vector carry-in is the mask the third source names: VCC for VOP2, any SGPR pair for VOP3. + const IrU1 carryIn = !vector ? IrU1(ir.GetScc()) : inst.sourceCount >= 3u ? readMask(sourceAt(inst, 2u)) : IrU1(ir.GetVcc()); const IrU32 carryInU32(ir.Select(carryIn.Value(), ir.Constant(1u), ir.Constant(0u))); IrValue& secondAdd = ir.Emit(IrOpcode::IAddCarry32, IrType::U32x2, {&sum.Value(), &carryInU32.Value()}); const IrU32 result(ir.Emit(IrOpcode::CompositeExtractU32x2, IrType::U32, {&secondAdd, &ir.Constant(0u)})); @@ -111,7 +112,7 @@ void TranslationContext::subbU32(const RdnaInstruction& inst, bool vector, bool const IrU32 second = readU32(sourceAt(inst, 1u)); const IrU32& lhs = reverse ? second : first; const IrU32& rhs = reverse ? first : second; - const IrU1 borrowIn = vector ? IrU1(ir.GetVcc()) : IrU1(ir.GetScc()); + const IrU1 borrowIn = !vector ? IrU1(ir.GetScc()) : inst.sourceCount >= 3u ? readMask(sourceAt(inst, 2u)) : IrU1(ir.GetVcc()); const IrU32 borrowInU32(ir.Select(borrowIn.Value(), ir.Constant(1u), ir.Constant(0u))); const IrU32 partial(ir.ISub(lhs.Value(), rhs.Value())); const IrU1 firstBorrow(ir.ULessThan(lhs.Value(), rhs.Value())); From c206f7049a8ec8378090f662fa5f97749d332b90 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 07:53:50 +0300 Subject: [PATCH 03/40] feat(kernel, libc): memory, files and threads for retail title boot libkernel: - direct memory keeps its contents per physical page across unmap and remap; a page mapped at two live addresses at once throws - sceKernelDirectMemoryQuery answers from tracked allocations - SCE protections read back as set, GPU and AMPR bits included - APR prefix path resolve, file size and stat by id, positional reads, fsync/pread/pwrite; directories open as descriptors on Windows - sceKernelCheckReachability, getpeername, shutdown - Dinkumware C11 threads (_Mtx_*, _Cnd_*, _Thrd_*) in the title libc layout; any pthread stack size, rounded at thread creation libc: - unaddressed guest mappings go to the system-managed area from 0x2_0000_0000, leaving the application area to fixed mappings - the process starts in /app0 - guest sscanf on Windows; a null %s prints as (null) as on FreeBSD - localeconv, _FNan, _FDtest, _FExp, __udivti3, wcsncpy_s, aligned operator new, _Throw_Cpp_error Tests: direct memory following physical pages, guest formatting. Co-Authored-By: Claude Opus 5.5 --- core/libs/CMakeLists.txt | 9 + core/libs/prx/libc/include/GuestArena.hpp | 2 +- .../prx/libc/include/WindowsFormatting.hpp | 5 +- core/libs/prx/libc/src/Formatting.cpp | 227 ++++++++++-------- core/libs/prx/libc/src/General.cpp | 3 +- core/libs/prx/libc/src/GuestAllocations.cpp | 16 +- core/libs/prx/libc/src/GuestArena.cpp | 9 +- core/libs/prx/libc/src/Math.cpp | 30 +++ core/libs/prx/libc/src/RuntimeSupport.cpp | 10 + core/libs/prx/libc/src/StdioExtras.cpp | 26 ++ core/libs/prx/libc/src/Strings.cpp | 19 ++ .../AppMetadata/include/AppMetadata.hpp | 2 + .../AppMetadata/include/ParamJsonParser.hpp | 3 + .../libkernel/AppMetadata/src/AppMetadata.cpp | 7 + .../AppMetadata/src/ParamJsonParser.cpp | 4 + core/libs/prx/libkernel/Apr/src/Apr.cpp | 54 +++++ core/libs/prx/libkernel/CMakeLists.txt | 2 + .../libkernel/DirectMemory/DirectMemory.cpp | 151 +++++++++++- .../libkernel/DirectMemory/DirectMemory.hpp | 13 +- .../prx/libkernel/DirectMemory/Export.cpp | 27 +-- .../prx/libkernel/DirectMemory/MemoryPool.cpp | 45 ++-- .../prx/libkernel/DirectMemory/MemoryPool.hpp | 8 +- .../File/include/DirectoryDescriptor.hpp | 20 ++ .../File/src/DirectoryDescriptor.cpp | 91 +++++++ .../prx/libkernel/File/src/NativeStat.cpp | 2 + core/libs/prx/libkernel/File/src/Open.cpp | 10 + core/libs/prx/libkernel/File/src/Stdio.cpp | 105 ++++---- .../prx/libkernel/Pthread/Posix/Xthreads.cpp | 157 ++++++++++++ core/libs/prx/libkernel/Pthread/src/Attr.cpp | 7 +- .../libs/prx/libkernel/Pthread/src/Thread.cpp | 5 +- core/libs/prx/libkernel/Socket/src/Udp.cpp | 16 ++ core/libs/prx/libkernel/Unimplemented.cpp | 40 --- core/libs/tests/Formatting.cpp | 1 + core/libs/tests/GuestMemory.cpp | 42 ++++ 34 files changed, 913 insertions(+), 255 deletions(-) create mode 100644 core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp create mode 100644 core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp create mode 100644 core/libs/prx/libkernel/Pthread/Posix/Xthreads.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index a3e7b1519..3aeac5508 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -458,6 +458,15 @@ if(BUILD_TESTING) set_tests_properties(mspace PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_executable(guest_formatting_tests tests/Formatting.cpp) + target_include_directories(guest_formatting_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_formatting_tests PRIVATE libc) + configure_windows_unwind(guest_formatting_tests) + add_test(NAME guest_formatting COMMAND guest_formatting_tests) + if(WIN32) + set_tests_properties(guest_formatting PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_memory_tests tests/GuestMemory.cpp) target_include_directories(guest_memory_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_memory_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index 0311cbd7e..d747e6f01 100644 --- a/core/libs/prx/libc/include/GuestArena.hpp +++ b/core/libs/prx/libc/include/GuestArena.hpp @@ -5,7 +5,7 @@ #include // Guest virtual memory is placed inside one reserved arena below the PS5 application map limit -// (0xFC_0000_0000), in ascending first-fit order like the PS5 kernel. Guest code indexes tables by +// (0xFC_0000_0000), in ascending first-fit order from the system-managed area like the PS5 kernel. Guest code indexes tables by // absolute address and breaks on host addresses outside that range. namespace GuestArena { diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index 2b2775270..6e6d0b18a 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -202,12 +202,13 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo AppendUtf8(utf8, static_cast(args.Next())); output.Value(spec + 's', utf8.c_str()); } else if (conversion == 's' && (length.empty() || length == "h")) { + // FreeBSD's printf, the console's, prints a null string as "(null)". const char* value = args.Next(); - if (!value) throw std::invalid_argument("Null formatted string"); + if (!value) value = "(null)"; output.Value(spec + 's', value); } else if ((conversion == 's' && length == "l") || (conversion == 'S' && length.empty())) { const char16_t* value = args.Next(); - if (!value) throw std::invalid_argument("Null formatted wide string"); + if (!value) value = u"(null)"; std::string utf8; while (*value) { char32_t code = *value++; diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 30395fd3e..59d445d6c 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -16,6 +16,116 @@ #include "prx/libc/include/WindowsFormatting.hpp" #endif +namespace { + +// Guest long is 64-bit; MinGW's is 32-bit, so integer "l" conversions need "ll" on the host. +std::string HostLength(const std::string& length, char conversion) { +#ifdef _WIN32 + if (length == "l" && std::strchr("diouxXn", conversion) != nullptr) return "ll"; +#else + (void)conversion; +#endif + return length; +} + +// Guest scanf, one directive at a time through the host sscanf, so the guest arguments can come from any +// va_list flavor. secure adds the _s rules: %s, %c and %[ take a buffer capacity after the pointer. +template +int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer nextPointer, NextCapacity nextCapacity) { + if (buffer == nullptr || format == nullptr) return EOF; + const char* input = buffer; + int assigned = 0; + bool converted = false; + const char* cursor = format; + const auto finish = [](int result) { return result; }; + while (*cursor != '\0') { + if (std::isspace(static_cast(*cursor))) { + while (std::isspace(static_cast(*cursor))) ++cursor; + while (std::isspace(static_cast(*input))) ++input; + continue; + } + if (*cursor != '%' || cursor[1] == '%') { + const char expected = *cursor == '%' ? '%' : *cursor; + cursor += *cursor == '%' ? 2 : 1; + if (expected == '%') while (std::isspace(static_cast(*input))) ++input; + if (*input != expected) return finish(!converted && *input == '\0' ? EOF : assigned); + ++input; + continue; + } + ++cursor; + const bool suppress = *cursor == '*'; + if (suppress) ++cursor; + unsigned long width = 0; + while (std::isdigit(static_cast(*cursor))) width = width * 10 + static_cast(*cursor++ - '0'); + const char* lengthStart = cursor; + while (std::strchr("hljztL", *cursor) != nullptr && *cursor != '\0') ++cursor; + const std::string length(lengthStart, cursor); + const char conversion = *cursor; + if (conversion == '\0') return finish(assigned); + std::string specifier(1, conversion); + if (conversion == '[') { + const char* setStart = cursor++; + if (*cursor == '^') ++cursor; + if (*cursor == ']') ++cursor; + while (*cursor != '\0' && *cursor != ']') ++cursor; + if (*cursor != ']') return finish(assigned); + specifier.assign(setStart, cursor + 1); + } + ++cursor; + if (conversion == 'n') { + if (!suppress) *static_cast(nextPointer()) = static_cast(input - buffer); + continue; + } + if (conversion != 'c' && conversion != '[') { + const char* probe = input; + while (std::isspace(static_cast(*probe))) ++probe; + if (*probe == '\0') return finish(converted ? assigned : EOF); + } + const bool sized = secure && (conversion == 's' || conversion == 'c' || conversion == '['); + std::string directive = "%"; + void* target = nullptr; + if (suppress) directive += '*'; + else target = nextPointer(); + if (sized && !suppress) { + const auto capacity = static_cast(nextCapacity()); + const unsigned long limit = conversion == 'c' ? capacity : (capacity == 0 ? 0 : capacity - 1); + const unsigned long wanted = conversion == 'c' ? (width == 0 ? 1 : width) : width; + if (limit == 0 || (conversion == 'c' && wanted > limit)) { + if (capacity != 0) static_cast(target)[0] = '\0'; + return finish(assigned); + } + width = wanted == 0 ? limit : std::min(wanted, limit); + } + if (width != 0) directive += std::to_string(width); + directive += HostLength(length, conversion); + directive += specifier; + directive += "%n"; + int consumed = -1; + const int matched = suppress ? std::sscanf(input, directive.c_str(), &consumed) : std::sscanf(input, directive.c_str(), target, &consumed); + if (consumed < 0 || (!suppress && matched != 1)) return finish(!converted && *input == '\0' ? EOF : assigned); + if (sized && !suppress && conversion != 'c') { + const char next = input[consumed]; + bool overflow = false; + if (conversion == 's') overflow = next != '\0' && !std::isspace(static_cast(next)); + else if (next != '\0') { + char probe[2]; + const std::string test = "%1" + specifier; + overflow = std::sscanf(input + consumed, test.c_str(), probe) == 1; + } + if (overflow) { + static_cast(target)[0] = '\0'; + return finish(assigned); + } + } + input += consumed; + converted = true; + if (!suppress) ++assigned; + } + return finish(assigned); +} + +} + extern "C" { int APS5_VABI vfprintf_nid_postfix(FileStream* stream, const char* format, VaList* args) { @@ -135,6 +245,8 @@ int APS5_VABI sprintf_nid_postfix(VA_ARGS) { #endif +#ifndef _WIN32 + int APS5_VABI sscanf_nid_postfix(VA_ARGS) { LibcDetail::RegSaveArea regs; LibcDetail::FillRegSaveArea(regs, rdx, rcx, r8, r9, 0, 0, @@ -149,113 +261,34 @@ int APS5_VABI sscanf_nid_postfix(VA_ARGS) { ); } +#endif + #ifdef _WIN32 +int APS5_VABI sscanf_nid_postfix(const char* buffer, const char* format, ...) { + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); + const int result = ScanGuest(buffer, format, false, [&] { return __builtin_va_arg(args, void*); }, [&] { return __builtin_va_arg(args, unsigned int); }); + __builtin_sysv_va_end(args); + return result; +} + int APS5_VABI sscanf_s_nid_postfix(const char* buffer, const char* format, ...) { - (void)buffer; - (void)format; - NotImplemented_nid_no_patch(__func__); - return 0; + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); + const int result = ScanGuest(buffer, format, true, [&] { return __builtin_va_arg(args, void*); }, [&] { return __builtin_va_arg(args, unsigned int); }); + __builtin_sysv_va_end(args); + return result; } #else int APS5_VABI sscanf_s_nid_postfix(const char* buffer, const char* format, ...) { - if (buffer == nullptr || format == nullptr) return EOF; std::va_list args; va_start(args, format); - const char* input = buffer; - int assigned = 0; - bool converted = false; - const char* cursor = format; - const auto finish = [&](int result) { - va_end(args); - return result; - }; - while (*cursor != '\0') { - if (std::isspace(static_cast(*cursor))) { - while (std::isspace(static_cast(*cursor))) ++cursor; - while (std::isspace(static_cast(*input))) ++input; - continue; - } - if (*cursor != '%' || cursor[1] == '%') { - const char expected = *cursor == '%' ? '%' : *cursor; - cursor += *cursor == '%' ? 2 : 1; - if (expected == '%') while (std::isspace(static_cast(*input))) ++input; - if (*input != expected) return finish(!converted && *input == '\0' ? EOF : assigned); - ++input; - continue; - } - ++cursor; - const bool suppress = *cursor == '*'; - if (suppress) ++cursor; - unsigned long width = 0; - while (std::isdigit(static_cast(*cursor))) width = width * 10 + static_cast(*cursor++ - '0'); - const char* lengthStart = cursor; - while (std::strchr("hljztL", *cursor) != nullptr && *cursor != '\0') ++cursor; - const std::string length(lengthStart, cursor); - const char conversion = *cursor; - if (conversion == '\0') return finish(assigned); - std::string specifier(1, conversion); - if (conversion == '[') { - const char* setStart = cursor++; - if (*cursor == '^') ++cursor; - if (*cursor == ']') ++cursor; - while (*cursor != '\0' && *cursor != ']') ++cursor; - if (*cursor != ']') return finish(assigned); - specifier.assign(setStart, cursor + 1); - } - ++cursor; - if (conversion == 'n') { - if (!suppress) *va_arg(args, int*) = static_cast(input - buffer); - continue; - } - if (conversion != 'c' && conversion != '[') { - const char* probe = input; - while (std::isspace(static_cast(*probe))) ++probe; - if (*probe == '\0') return finish(converted ? assigned : EOF); - } - const bool sized = conversion == 's' || conversion == 'c' || conversion == '['; - std::string directive = "%"; - void* target = nullptr; - if (suppress) directive += '*'; - else target = va_arg(args, void*); - if (sized && !suppress) { - const auto capacity = static_cast(va_arg(args, unsigned int)); - const unsigned long limit = conversion == 'c' ? capacity : (capacity == 0 ? 0 : capacity - 1); - const unsigned long wanted = conversion == 'c' ? (width == 0 ? 1 : width) : width; - if (limit == 0 || (conversion == 'c' && wanted > limit)) { - if (capacity != 0) static_cast(target)[0] = '\0'; - return finish(assigned); - } - width = wanted == 0 ? limit : std::min(wanted, limit); - } - if (width != 0) directive += std::to_string(width); - directive += length; - directive += specifier; - directive += "%n"; - int consumed = -1; - const int matched = suppress ? std::sscanf(input, directive.c_str(), &consumed) : std::sscanf(input, directive.c_str(), target, &consumed); - if (consumed < 0 || (!suppress && matched != 1)) return finish(!converted && *input == '\0' ? EOF : assigned); - if (sized && !suppress && conversion != 'c') { - const char next = input[consumed]; - bool overflow = false; - if (conversion == 's') overflow = next != '\0' && !std::isspace(static_cast(next)); - else if (next != '\0') { - char probe[2]; - const std::string test = "%1" + specifier; - overflow = std::sscanf(input + consumed, test.c_str(), probe) == 1; - } - if (overflow) { - static_cast(target)[0] = '\0'; - return finish(assigned); - } - } - input += consumed; - converted = true; - if (!suppress) ++assigned; - } - return finish(assigned); + const int result = ScanGuest(buffer, format, true, [&] { return va_arg(args, void*); }, [&] { return va_arg(args, unsigned int); }); + va_end(args); + return result; } #endif diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 5a3364b95..8730c57f8 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -53,7 +53,8 @@ std::optional ResolveAlias(const std::string& guestPath) struct WorkingDirectory { std::mutex mutex; const std::filesystem::path root = std::filesystem::canonical(std::filesystem::current_path()); - std::filesystem::path current = root; + // A PS5 process starts in /app0, so titles open game files by relative path. + std::filesystem::path current = std::filesystem::is_directory(root / "app0") ? root / "app0" : root; }; WorkingDirectory& Directories() { static WorkingDirectory state; return state; } std::filesystem::path Resolve(WorkingDirectory& state, const char* path) { diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index b0f022aaf..f3d87781e 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -181,6 +181,12 @@ void GuestAllocationsAdd_nid_postfix(void* mutation, void* pointer, std::size_t ranges.emplace(address, std::make_shared(Range{address, bytes, readable, writable, address, bytes})); } +[[noreturn]] void PinnedFailure(std::uintptr_t address, std::size_t bytes, const char* why) { + char message[160]; + std::snprintf(message, sizeof(message), "guest allocation 0x%llx+0x%llx is owned by an active GPU command (%s)", static_cast(address), static_cast(bytes), why); + throw std::runtime_error(message); +} + void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* pointer, std::size_t bytes) { const auto address = reinterpret_cast(pointer); require(bytes <= std::numeric_limits::max() - address, "guest allocation range overflow"); @@ -192,12 +198,11 @@ void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* poi // this registry, so waiting with the lock held would deadlock them. The scan restarts after the // lock is retaken, since another mutation may have run meanwhile. Without a waiter the lease is // dropped by another thread (a synchronous draw or dispatch), so yielding suffices. A waiter - // round that finished nothing (the holder is not recorded GPU work) counts as a spin, so a lease - // nobody releases fails as fast as it did without a waiter; rounds that did finish work are - // bounded by the deadline only, as each one is a GPU wait. + // round that finished nothing (the holder is not recorded GPU work) yields: the holder may be a + // driver thread inside a long Vulkan call (importing host memory takes a lease for its whole + // length), so every wait is bounded by the deadline only. auto* state = static_cast(mutation); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60); - int idleRounds = 0; int syncedRounds = 0; for (;;) { bool pinned = false; @@ -212,7 +217,7 @@ void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* poi } } if (!pinned) return; - require(std::chrono::steady_clock::now() < deadline, "guest allocation is owned by an active GPU command"); + if (std::chrono::steady_clock::now() >= deadline) PinnedFailure(address, bytes, "waited 60 s for the lease"); const auto waiter = pinWaiter.load(std::memory_order_acquire); bool progressed = false; if (waiter != nullptr && state != nullptr && state->lock.owns_lock()) { @@ -229,7 +234,6 @@ void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* poi if (++syncedRounds == 8) std::fprintf(stderr, "[gpu] leased allocation 0x%llx+0x%llx still pinned after %d recorder syncs\n", static_cast(address), static_cast(bytes), syncedRounds); continue; } - require(++idleRounds < 1000000, "guest allocation is owned by an active GPU command"); } } diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 4baedeeaf..19066a785 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -15,7 +15,13 @@ namespace GuestArena { namespace { -constexpr std::uintptr_t PreferredBase = 0x0000001000000000ull; +// Mappings without an address go to the system-managed area from 0x2_0000_0000 first, as in Kyty and +// shadPS4; the application area from 0x10_0000_0000 is left to fixed mappings until that fills (titles +// map their virtual memory pages at a fixed window from 0x10_0000_0000). The system-reserved range +// between the two is never handed out. +constexpr std::uintptr_t PreferredBase = 0x0000000200000000ull; +constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull; +constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull; constexpr std::size_t MaximumSize = 0x0000007000000000ull; constexpr std::size_t MinimumSize = 0x0000001000000000ull; constexpr std::uintptr_t MapAreaEnd = 0x000000FC00000000ull; @@ -99,6 +105,7 @@ private: if (!reserved) continue; _base = reinterpret_cast(reserved); _end = _base + size; + if (_base < SystemReservedEnd && SystemReservedStart < _end) _used.emplace(std::max(_base, SystemReservedStart), std::min(_end, SystemReservedEnd)); return; } } diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 2aca8a4e5..0556f6e34 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -97,4 +97,34 @@ void APS5_VABI srand_nid_postfix(unsigned int seed) { g_randState = seed; } + +// Dinkumware float classification codes returned by _FDtest and _FExp. +constexpr short FloatDenorm = -2; +constexpr short FloatFinite = -1; +constexpr short FloatInfinite = 1; +constexpr short FloatNan = 2; + +std::uint32_t _FNan_nid_postfix[4] = {0x7FC00000u, 0, 0, 0}; + +short APS5_VABI _FDtest_nid_postfix(const float* x) { + switch (std::fpclassify(*x)) { + case FP_NAN: return FloatNan; + case FP_INFINITE: return FloatInfinite; + case FP_ZERO: return 0; + case FP_SUBNORMAL: return FloatDenorm; + default: return FloatFinite; + } +} + +// Stores y * e^(*x) * 2^eoff in *x and classifies the result as 0 (zero or underflow), infinite, or finite. +short APS5_VABI _FExp_nid_postfix(float* x, float y, short eoff) { + *x = static_cast(std::ldexp(static_cast(y) * std::exp(static_cast(*x)), eoff)); + if (*x == 0.0f) return 0; + return std::isinf(*x) ? FloatInfinite : FloatFinite; +} + +unsigned __int128 APS5_VABI __udivti3_nid_postfix(unsigned __int128 dividend, unsigned __int128 divisor) { + return dividend / divisor; +} + } diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index bf1ada3eb..0f4fc3d5c 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -100,6 +100,16 @@ unsigned int APS5_VABI _ZSt14_Random_devicev_nid_postfix() { throw std::runtime_error("_Throw_C_error: C11 thread error " + std::to_string(code)); } +[[noreturn]] void APS5_VABI _ZSt16_Throw_Cpp_errori_nid_postfix(int code) { + throw std::runtime_error("_Throw_Cpp_error: C++ thread error " + std::to_string(code)); +} + +// The title's std::error_category object carries its own Dinkumware vtable and string ABI. +const void* APS5_VABI _ZSt16generic_categoryv_nid_postfix() { + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + void APS5_VABI _Lockfilelock_nid_postfix(FileStream* stream) { if (stream == nullptr) throw std::invalid_argument("_Lockfilelock: null stream"); #ifdef _WIN32 diff --git a/core/libs/prx/libc/src/StdioExtras.cpp b/core/libs/prx/libc/src/StdioExtras.cpp index d2aabc81d..a1b8d21d6 100644 --- a/core/libs/prx/libc/src/StdioExtras.cpp +++ b/core/libs/prx/libc/src/StdioExtras.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -50,6 +51,27 @@ int APS5_VABI sprintf_s_nid_postfix(char* buffer, size_t size, const char* forma std::abort(); } +// Dinkumware lconv as the title's libc lays it out: monetary strings, CHAR_MAX monetary flags, then numeric and message strings. +struct GuestLconv { + const char* monetary[7]; + char monetaryFlags[14]; + const char* decimalPoint; + const char* numeric[4]; + const char* falseName; + const char* trueName; + const char* no; + const char* yes; +}; +static_assert(sizeof(GuestLconv) == 0x90); + +const GuestLconv* APS5_VABI localeconv_nid_postfix() { + static const GuestLconv lconv{ + {"", "", "", "", "", "", ""}, + {CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX}, + ".", {"", "", "", ""}, "false", "true", "", ""}; + return &lconv; +} + // Only the "C" locale exists. const char* APS5_VABI setlocale_nid_postfix(int category, const char* locale) { (void)category; @@ -101,6 +123,10 @@ void APS5_VABI _ZdaPvm_nid_postfix(void* pointer, std::size_t) { if (pointer != nullptr) ApplicationHeapFree_nid_no_patch(pointer); } +void* APS5_VABI _ZnwmSt11align_val_t_nid_postfix(std::size_t size, std::size_t alignment) { + return GuestHeap::GuestHeapAlign_nid_postfix(alignment, size); +} + void APS5_VABI _ZdlPvSt11align_val_t_nid_postfix(void* pointer, std::size_t alignment) { (void)alignment; GuestHeap::GuestHeapFree_nid_postfix(pointer); diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 1f70ddc44..3ba35d3c0 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -188,6 +188,25 @@ int APS5_VABI strncpy_s_nid_postfix(char* dest, size_t destsz, const char* src, return 0; } +int APS5_VABI wcsncpy_s_nid_postfix(wchar_t* dest, size_t destsz, const wchar_t* src, size_t count) { + constexpr int GuestEinval = 22; + constexpr int GuestErange = 34; + if (!dest || destsz == 0) return GuestEinval; + if (!src) { + dest[0] = L'\0'; + return GuestEinval; + } + size_t length = 0; + while (length < count && src[length] != L'\0') ++length; + if (length >= destsz) { + dest[0] = L'\0'; + return GuestErange; + } + std::wmemcpy(dest, src, length); + dest[length] = L'\0'; + return 0; +} + int APS5_VABI strcpy_s_nid_postfix(char* dest, size_t destsz, const char* src) { return strncpy_s_nid_postfix(dest, destsz, src, static_cast(-1)); } diff --git a/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp b/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp index a9eab7266..93a0ebdc8 100644 --- a/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp +++ b/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp @@ -22,6 +22,8 @@ AppTitle GetAppTitle_nid_postfix(); AppTitleId GetAppTitleId_nid_postfix(); bool HasAppIcon_nid_postfix(); AppIconData GetAppIconData_nid_postfix(); +// The download data quota param.json declares, in MiB (0 when none). +std::uint64_t GetAppDownloadDataSizeMiB_nid_postfix(); } diff --git a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp index dd60d00c5..08b60e434 100644 --- a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp +++ b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp @@ -1,12 +1,15 @@ #ifndef CORE_LIBS_PRX_LIBKERNEL_APPMETADATA_INCLUDE_PARAMJSONPARSER_HPP #define CORE_LIBS_PRX_LIBKERNEL_APPMETADATA_INCLUDE_PARAMJSONPARSER_HPP +#include #include #include struct ParsedParamJson { std::string title; std::string titleId; + // The download data quota in MiB; 0 when the title declares none. + std::uint64_t downloadDataSizeMiB = 0; }; ParsedParamJson parseParamJson(const std::filesystem::path& paramJsonPath); diff --git a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp index d727008d7..b99d5aa7f 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp @@ -17,6 +17,7 @@ bool g_iconAttempted = false; bool g_iconLoaded = false; char g_title[128] = {}; char g_titleId[12] = {}; +std::uint64_t g_downloadDataSizeMiB = 0; std::vector g_iconBytes; constexpr const char* AppMetadataParamJsonGuestPath = "/app0/sce_sys/param.json"; @@ -35,6 +36,7 @@ void ensureTitleLoaded() { const auto parsed = parseParamJson(resolvedPath); copyToFixedBuffer(g_title, sizeof(g_title), parsed.title); copyToFixedBuffer(g_titleId, sizeof(g_titleId), parsed.titleId); + g_downloadDataSizeMiB = parsed.downloadDataSizeMiB; g_titleLoaded = true; } @@ -68,6 +70,11 @@ AppTitle GetAppTitle_nid_postfix() { return result; } +std::uint64_t GetAppDownloadDataSizeMiB_nid_postfix() { + ensureTitleLoaded(); + return g_downloadDataSizeMiB; +} + AppTitleId GetAppTitleId_nid_postfix() { ensureTitleLoaded(); AppTitleId result{}; diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index 30756344f..89b53c3f2 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -259,5 +259,9 @@ ParsedParamJson parseParamJson(const std::filesystem::path& paramJsonPath) { ParsedParamJson result; result.title = title; result.titleId = titleId; + if (const JsonValue* downloadData = findObjectMember(root, "downloadDataSize")) { + if (downloadData->type != JsonType::Number || downloadData->numberValue < 0) throw std::runtime_error("param.json downloadDataSize is not a size"); + result.downloadDataSizeMiB = static_cast(downloadData->numberValue); + } return result; } diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 8d25f8f30..8510e7b63 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -3,6 +3,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" +#include "prx/libkernel/File/include/NativeStat.hpp" #include #include #include @@ -186,6 +187,35 @@ int APS5_VABI sceKernelAprResolveFilepathsToIdsAndFileSizes(const char** paths, return 0; } +// The prefix is prepended to every path; titles pass "" with absolute /app0 paths. +int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizes(const char* prefix, const char** paths, uint32_t count, uint32_t* ids, uint64_t* sizes, uint32_t* error_index) { + if (!prefix || !paths || !ids) return _fail(GUEST_EINVAL); + for (uint32_t index = 0; index < count; ++index) { + const std::string path = paths[index] ? std::string(prefix) + paths[index] : std::string(); + if (path.empty() || !_resolve(path.c_str(), &ids[index], sizes ? &sizes[index] : nullptr)) { + if (error_index) *error_index = index; + return _fail(GUEST_ENOENT); + } + } + return 0; +} + +int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIds(const char* prefix, const char** paths, uint32_t count, uint32_t* ids, uint32_t* error_index) { + return sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizes(prefix, paths, count, ids, nullptr, error_index); +} + +int APS5_VABI sceKernelAprGetFileSize(uint32_t id, uint64_t* size) { + if (!size) return _fail(GUEST_EINVAL); + *size = _file(id).size; + return 0; +} + +int APS5_VABI sceKernelAprGetFileStat(uint32_t id, FileStat* stat) { + if (!stat) return _fail(GUEST_EINVAL); + File::FillFileStat(_file(id).path, stat); + return 0; +} + int APS5_VABI sceKernelAprSubmitCommandBuffer(const Apr::CommandBufferObject* buffer, uint32_t priority) { (void)priority; if (!buffer || buffer->type != Apr::BufferType::Apr) return _fail(GUEST_EINVAL); @@ -193,4 +223,28 @@ int APS5_VABI sceKernelAprSubmitCommandBuffer(const Apr::CommandBufferObject* bu return 0; } +// Command buffers run synchronously, so a submission is complete (result 0) before its id is returned +// and waiting on it has nothing left to do. +int APS5_VABI sceKernelAprSubmitCommandBufferAndGetId(const Apr::CommandBufferObject* buffer, uint32_t priority, uint32_t* id) { + if (!id) return _fail(GUEST_EINVAL); + const int result = sceKernelAprSubmitCommandBuffer(buffer, priority); + if (result != 0) return result; + static std::atomic nextId{1}; + *id = nextId.fetch_add(1); + return 0; +} + +int APS5_VABI sceKernelAprSubmitCommandBufferAndGetResult(const Apr::CommandBufferObject* buffer, uint32_t priority, uint32_t* result, uint32_t* id) { + if (!result) return _fail(GUEST_EINVAL); + const int submitted = sceKernelAprSubmitCommandBufferAndGetId(buffer, priority, id); + if (submitted != 0) return submitted; + *result = 0; + return 0; +} + +int APS5_VABI sceKernelAprWaitCommandBuffer(uint32_t id) { + (void)id; + return 0; +} + } diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index aff9e0a7b..6585bc086 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Apr/src/Apr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/Equeue.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/EventAccessors.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/File/src/DirectoryDescriptor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/NativeStat.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Open.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Stdio.cpp @@ -28,6 +29,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Once.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Sem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Xthreads.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Socket.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Udp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/AddressConversion.cpp diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index c003b3564..812c8b3de 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -9,9 +9,12 @@ #include #include #include +#include +#include #include #include #include +#include #if defined(__linux__) #include @@ -215,6 +218,73 @@ void Trace(const char* format, ...) { va_end(args); } +// Direct memory keeps its contents across mappings: titles unmap physical pages and map them again at +// another address (titles move their pools that way) and read back what they wrote through the old one. +// ponytail: the contents move by copying at unmap and map, so a physical page mapped at two live +// addresses throws; a shared section with views (as in Kyty) lifts that but loses write watching. +struct DirectMapping { + std::uintptr_t end; + std::uint64_t phys; +}; +std::mutex g_directLock; +std::map g_directMappings; +// Contents of unmapped direct memory by physical page; all-zero pages are left out. +std::map> g_physPages; + +void ProtectOrThrow(std::uintptr_t address, std::size_t len, int nativeProt) { + if (mprotect(reinterpret_cast(address), len, nativeProt) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); +} + +void SaveContents(std::uintptr_t address, std::size_t len, std::uint64_t phys) { + static const unsigned char zero[PS5_PAGE_SIZE] = {}; + ProtectOrThrow(address, len, PROT_READ); + for (std::size_t offset = 0; offset < len; offset += PS5_PAGE_SIZE) { + const auto* page = reinterpret_cast(address + offset); + if (std::memcmp(page, zero, PS5_PAGE_SIZE) == 0) continue; + auto& copy = g_physPages[phys + offset]; + copy.reset(new unsigned char[PS5_PAGE_SIZE]); + std::memcpy(copy.get(), page, PS5_PAGE_SIZE); + } +} + +// Takes [start, end) out of the direct mappings; with save its bytes stay as the contents of its pages. +void EraseMappings(std::uintptr_t start, std::uintptr_t end, bool save) { + auto it = g_directMappings.lower_bound(start); + if (it != g_directMappings.begin() && std::prev(it)->second.end > start) --it; + while (it != g_directMappings.end() && it->first < end) { + const auto base = it->first; + const auto mapping = it->second; + it = g_directMappings.erase(it); + const auto cutStart = std::max(base, start); + if (save) SaveContents(cutStart, std::min(mapping.end, end) - cutStart, mapping.phys + (cutStart - base)); + if (base < start) g_directMappings.emplace(base, DirectMapping{start, mapping.phys}); + if (mapping.end > end) it = g_directMappings.emplace(end, DirectMapping{mapping.end, mapping.phys + (end - base)}).first; + } +} + +void RequireUnmapped(std::uint64_t phys, std::size_t len, std::uintptr_t address) { + for (const auto& [base, mapping] : g_directMappings) { + if (mapping.phys < phys + len && phys < mapping.phys + (mapping.end - base)) { + char message[192]; + std::snprintf(message, sizeof(message), "direct memory 0x%llx+0x%zx is already mapped at 0x%llx; mapping it at 0x%llx too is not implemented", static_cast(phys), len, + static_cast(base), static_cast(address)); + throw std::runtime_error(message); + } + } +} + +// Records a fresh mapping of phys at address (zero-filled, already protected nativeProt) and brings its contents back. +void AddMapping(std::uintptr_t address, std::size_t len, std::uint64_t phys, int nativeProt) { + RequireUnmapped(phys, len, address); + g_directMappings.emplace(address, DirectMapping{address + len, phys}); + auto it = g_physPages.lower_bound(phys); + const auto last = g_physPages.lower_bound(phys + len); + if (it == last) return; + ProtectOrThrow(address, len, PROT_READ | PROT_WRITE); + for (; it != last; it = g_physPages.erase(it)) std::memcpy(reinterpret_cast(address + (it->first - phys)), it->second.get(), PS5_PAGE_SIZE); + ProtectOrThrow(address, len, nativeProt); +} + bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr, size_t len, int prot, int flags, int64_t physStart = -1) { constexpr int GuestMapFixed = 0x10; constexpr int GuestMapNoOverwrite = 0x80; @@ -223,10 +293,18 @@ bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr, Trace("remap fixed %p+0x%zx prot=0x%x phys=0x%llx", addr, len, prot, static_cast(physStart)); const auto nativeProtection = LinuxProtFromSce(prot); mutation.Protect(addr, len, (prot & 3) != 0, (prot & 2) != 0, [&] { + const auto address = reinterpret_cast(addr); + std::lock_guard lock(g_directLock); + EraseMappings(address, address + len, true); + // The range shows the physical pages' contents now, not what was mapped there before. #ifdef _WIN32 + if (physStart >= 0 && !VirtualFree(addr, len, MEM_DECOMMIT)) throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualFree decommit failed"); if (!VirtualAlloc(addr, len, MEM_COMMIT, WinProtFromPosix(nativeProtection))) throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualAlloc commit failed"); +#else + if (physStart >= 0 && madvise(addr, len, MADV_DONTNEED) != 0) throw std::system_error(errno, std::generic_category(), "madvise failed"); #endif if (mprotect(addr, len, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); + if (physStart >= 0) AddMapping(address, len, static_cast(physStart), nativeProtection); }); return true; } @@ -306,6 +384,34 @@ void* MapAligned(void* addr, size_t len, int prot, int flags, size_t alignment) return aligned; } +// SCE protections as the title set them: the host page protection keeps only the CPU bits, but titles +// read the GPU and AMPR bits back (a streamer reads straight into memory the APR may write). +struct ProtectedRange { + std::uintptr_t end; + int prot; +}; +std::mutex g_protectionLock; +std::map g_protections; + +void EraseProtections(std::uintptr_t start, std::uintptr_t end) { + auto it = g_protections.lower_bound(start); + if (it != g_protections.begin() && std::prev(it)->second.end > start) --it; + while (it != g_protections.end() && it->first < end) { + const auto rangeStart = it->first; + const auto range = it->second; + it = g_protections.erase(it); + if (rangeStart < start) g_protections.emplace(rangeStart, ProtectedRange{start, range.prot}); + if (range.end > end) it = g_protections.emplace(end, ProtectedRange{range.end, range.prot}).first; + } +} + +void RecordProtection(const void* addr, size_t len, int prot) { + const auto start = reinterpret_cast(addr); + std::lock_guard lock(g_protectionLock); + EraseProtections(start, start + len); + if (prot >= 0) g_protections.emplace(start, ProtectedRange{start + len, prot}); +} + void ValidateOutput(void** addr) { if (!addr) { // return SCE_KERNEL_ERROR_EINVAL; @@ -322,16 +428,23 @@ int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, return SCE_KERNEL_ERROR_EINVAL; } GuestAllocations::Mutation mutation; - if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags)) return 0; + if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags, physStart)) { + RecordProtection(*addr, len, prot); + return 0; + } if (*addr != nullptr && (flags & GuestMapFixedFlag) != 0) mutation.RequireAvailable(*addr, len); + std::lock_guard lock(g_directLock); + RequireUnmapped(static_cast(physStart), len, reinterpret_cast(*addr)); void* mapped = MapAligned(*addr, len, LinuxProtFromSce(prot), flags, alignment); try { mutation.Add(mapped, len, (prot & 3) != 0, (prot & 2) != 0); + AddMapping(reinterpret_cast(mapped), len, static_cast(physStart), LinuxProtFromSce(prot)); } catch (...) { Unmap(mapped, len); throw; } *addr = mapped; + RecordProtection(mapped, len, prot); Trace("map direct %p+0x%zx phys=0x%llx prot=0x%x flags=0x%x align=0x%zx", mapped, len, static_cast(physStart), prot, flags, alignment); return 0; } @@ -340,7 +453,10 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags) { ValidateOutput(addr); if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; GuestAllocations::Mutation mutation; - if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags)) return 0; + if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags)) { + RecordProtection(*addr, len, prot); + return 0; + } if (*addr != nullptr && (flags & GuestMapFixedFlag) != 0) mutation.RequireAvailable(*addr, len); void* mapped = MapAligned(*addr, len, LinuxProtFromSce(prot), flags, PS5_PAGE_SIZE); try { @@ -350,6 +466,7 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags) { throw; } *addr = mapped; + RecordProtection(mapped, len, prot); Trace("map anon %p+0x%zx prot=0x%x flags=0x%x", mapped, len, prot, flags); return 0; } @@ -379,6 +496,7 @@ int DoMprotect(const void* addr, size_t len, int prot) { mutation.Protect(pointer, bytes, (prot & 3) != 0, (prot & 2) != 0, [&] { if (mprotect(const_cast(pointer), bytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); }); + RecordProtection(pointer, bytes, prot); return 0; } @@ -387,6 +505,11 @@ int DoMunmap(void* addr, size_t len) { if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0 || !addr) return SCE_KERNEL_ERROR_EINVAL; GuestAllocations::Mutation mutation; mutation.Unmap(addr, len, [&](const void* allocation, bool last) { + { + const auto address = reinterpret_cast(addr); + std::lock_guard lock(g_directLock); + EraseMappings(address, address + len, true); + } #if defined(__linux__) Unmap(addr, len); #else @@ -395,6 +518,7 @@ int DoMunmap(void* addr, size_t len) { else munmap(addr, len); #endif }); + RecordProtection(addr, len, -1); return 0; } @@ -413,3 +537,26 @@ int DoReserveVirtual(void** addr, size_t len, size_t alignment) { Trace("reserve %p+0x%zx align=0x%zx", mapped, len, alignment); return 0; } + +void ForgetDirectMemory(int64_t start, size_t len) { + const auto first = static_cast(start); + const auto end = first + len; + std::lock_guard lock(g_directLock); + g_physPages.erase(g_physPages.lower_bound(first), g_physPages.lower_bound(end)); + std::vector> released; + for (const auto& [base, mapping] : g_directMappings) { + const auto mappedEnd = mapping.phys + (mapping.end - base); + if (mapping.phys < end && first < mappedEnd) released.emplace_back(base + (std::max(first, mapping.phys) - mapping.phys), base + (std::min(end, mappedEnd) - mapping.phys)); + } + for (const auto& [from, to] : released) EraseMappings(from, to, false); +} + +bool GuestProtection(uintptr_t addr, int* prot) { + std::lock_guard lock(g_protectionLock); + auto next = g_protections.upper_bound(addr); + if (next == g_protections.begin()) return false; + const auto containing = std::prev(next); + if (addr >= containing->second.end) return false; + *prot = containing->second.prot; + return true; +} diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index 53289beff..a71ffc684 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -9,20 +9,19 @@ static constexpr size_t DIRECT_MEMORY_SIZE = 13824ULL * 1024 * 1024; static constexpr size_t PS5_PAGE_SIZE = 0x4000; -struct DirectMemoryBlock { - uint64_t start; - uint64_t end; - int memoryType; -}; - int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut); void DirectMemoryFree(int64_t start, size_t len); -bool DirectMemoryQueryBlock(uint64_t offset, DirectMemoryBlock* block); +// Drops what the released physical range held and stops its live mappings from saving into it. +void ForgetDirectMemory(int64_t start, size_t len); +// Finds the allocation containing offset, or with findNext the first one above it; false when there is none. +bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType); size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment); int DoMapAnon(void** addr, size_t len, int prot, int flags); int DoMprotect(const void* addr, size_t len, int prot); int DoMunmap(void* addr, size_t len); int DoReserveVirtual(void** addr, size_t len, size_t alignment); +// The SCE protection the title last mapped or protected addr with (GPU and AMPR bits included); false when unknown. +bool GuestProtection(uintptr_t addr, int* prot); #endif \ No newline at end of file diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index c7922f496..0f7fabf6c 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -134,21 +134,12 @@ int APS5_VABI sceKernelAvailableDirectMemorySize(int64_t search_start, int64_t s } int APS5_VABI sceKernelDirectMemoryQuery(int64_t offset, int flags, void* info, size_t info_size) { - (void)flags; + constexpr int SCE_KERNEL_DMQ_FIND_NEXT = 1; if (!info || offset < 0) return SCE_KERNEL_ERROR_EINVAL; struct DirectMemoryQueryInfo { int64_t start; int64_t end; int memory_type; }; if (info_size < sizeof(DirectMemoryQueryInfo)) return SCE_KERNEL_ERROR_EINVAL; auto* q = static_cast(info); - DirectMemoryBlock block{}; - if (DirectMemoryQueryBlock(static_cast(offset), &block)) { - q->start = static_cast(block.start); - q->end = static_cast(block.end); - q->memory_type = block.memoryType; - } else { - q->start = offset & ~static_cast(PS5_PAGE_SIZE - 1); - q->end = q->start + PS5_PAGE_SIZE; - q->memory_type = -1; - } + if (!DirectMemoryFind(offset, (flags & SCE_KERNEL_DMQ_FIND_NEXT) != 0, &q->start, &q->end, &q->memory_type)) return SCE_KERNEL_ERROR_EACCES; return 0; } @@ -225,6 +216,8 @@ int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInf info->start = best->allocationAddress; info->end = best->allocationAddress + best->allocationBytes; info->protection = (best->readable ? 1 : 0) | (best->writable ? 2 : 0) | (!best->releasable ? 4 : 0); + int recorded = 0; + if (GuestProtection(std::max(address, info->start), &recorded)) info->protection = recorded; info->is_direct = best->releasable ? 1u : 0u; info->is_committed = 1; ApplyRangeName(std::max(address, info->start), info); @@ -286,12 +279,14 @@ int APS5_VABI sceKernelMtypeprotect(const void* addr, size_t len, int type, int return 0; } +// The mapping that contains addr, as sceKernelVirtualQuery reports it without FIND_NEXT. int APS5_VABI sceKernelQueryMemoryProtection(void* addr, void** start, void** end, int* prot) { - (void)addr; - (void)start; - (void)end; - (void)prot; - NotImplemented_nid_no_patch(__func__); + VirtualQueryInfo info{}; + const int result = sceKernelVirtualQuery(addr, 0, &info, sizeof(info)); + if (result != 0) return result; + if (start) *start = reinterpret_cast(info.start); + if (end) *end = reinterpret_cast(info.end); + if (prot) *prot = info.protection; return 0; } diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index 5d4f7e01e..2bf37398b 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -21,7 +21,7 @@ struct PhysicalMemoryPool { while (cur + len <= end && cur + len <= DIRECT_MEMORY_SIZE) { if (_isFree(cur, len)) { _mark(cur, len, true); - _ranges[cur] = {cur, cur + len, memoryType}; + _blocks[cur] = {cur + len, memoryType}; *physOut = static_cast(cur); return 0; } @@ -33,23 +33,26 @@ struct PhysicalMemoryPool { void Free(uint64_t start, size_t len) { std::lock_guard lock(_mutex); _mark(start, len, false); - auto it = _ranges.upper_bound(start); - if (it == _ranges.begin()) return; - --it; - if (start + len > it->second.end) return; - const DirectMemoryBlock block = it->second; - _ranges.erase(it); - if (block.start < start) _ranges[block.start] = {block.start, start, block.memoryType}; - if (start + len < block.end) _ranges[start + len] = {start + len, block.end, block.memoryType}; + const uint64_t end = start + len; + auto it = _blocks.upper_bound(start); + if (it != _blocks.begin() && std::prev(it)->second.end > start) --it; + while (it != _blocks.end() && it->first < end) { + const uint64_t blockStart = it->first; + const Block block = it->second; + it = _blocks.erase(it); + if (blockStart < start) _blocks[blockStart] = {start, block.type}; + if (block.end > end) it = _blocks.emplace(end, Block{block.end, block.type}).first; + } } - bool Query(uint64_t offset, DirectMemoryBlock* block) { + bool Find(uint64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { std::lock_guard lock(_mutex); - auto it = _ranges.upper_bound(offset); - if (it == _ranges.begin()) return false; - --it; - if (offset >= it->second.end) return false; - *block = it->second; + auto it = _blocks.upper_bound(offset); + if (it != _blocks.begin() && std::prev(it)->second.end > offset) --it; + else if (!findNext || it == _blocks.end()) return false; + *start = static_cast(it->first); + *end = static_cast(it->second.end); + *memoryType = it->second.type; return true; } @@ -81,9 +84,14 @@ private: _used[first + i] = used; } + struct Block { + uint64_t end; + int type; + }; + std::mutex _mutex; bool _used[NUM_PAGES] = {}; - std::map _ranges; + std::map _blocks; }; int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut) { @@ -91,11 +99,12 @@ int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t } void DirectMemoryFree(int64_t start, size_t len) { + ForgetDirectMemory(start, len); PhysicalMemoryPool::Instance().Free(static_cast(start), len); } -bool DirectMemoryQueryBlock(uint64_t offset, DirectMemoryBlock* block) { - return PhysicalMemoryPool::Instance().Query(offset, block); +bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { + return PhysicalMemoryPool::Instance().Find(static_cast(offset), findNext, start, end, memoryType); } size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit) { diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp index 6bf75c51b..28ab864c9 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp @@ -1,6 +1,12 @@ #ifndef CORE_LIBS_PRX_LIBKERNEL_DIRECTMEMORY_MEMORYPOOL_HPP #define CORE_LIBS_PRX_LINKERNEL_DIRECTMEMORY_MEMORYPOOL_HPP -#include "DirectMemory.hpp" +#include +#include + +int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut); +void DirectMemoryFree(int64_t start, size_t len); +// Finds the allocation containing offset, or with findNext the first one above it; false when there is none. +bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType); #endif diff --git a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp new file mode 100644 index 000000000..1bbb73f17 --- /dev/null +++ b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp @@ -0,0 +1,20 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_DIRECTORYDESCRIPTOR_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_DIRECTORYDESCRIPTOR_HPP + +#include +#include + +// Windows cannot open a directory as a file descriptor, so a guest open() of a directory gets a +// descriptor for NUL that is tracked here with its path and a getdents cursor. +namespace File { + +int OpenDirectoryDescriptor(const std::filesystem::path& path); +std::optional DirectoryDescriptorPath(int fd); +void ForgetDirectoryDescriptor(int fd); +// Fills FreeBSD dirent records { u32 fileno; u16 reclen; u8 type; u8 namlen; char name[]; } (4-byte aligned). +// Returns the bytes written, or -1 when fd is not a directory descriptor. +int ReadDirectoryDescriptor(int fd, char* buf, int nbytes); + +} + +#endif diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp new file mode 100644 index 000000000..9bd7ac5ed --- /dev/null +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -0,0 +1,91 @@ +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" + +#ifdef _WIN32 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint8_t GuestDirectoryType = 4; +constexpr std::uint8_t GuestRegularType = 8; + +struct DirectoryState { + std::filesystem::path path; + std::vector> entries; + std::size_t cursor = 0; + bool loaded = false; +}; + +std::mutex g_mutex; +std::map g_directories; + +} + +namespace File { + +int OpenDirectoryDescriptor(const std::filesystem::path& path) { + const int fd = ::_open("NUL", _O_RDONLY | _O_BINARY); + if (fd < 0) return -1; + std::lock_guard lock(g_mutex); + g_directories[fd] = DirectoryState{path}; + return fd; +} + +std::optional DirectoryDescriptorPath(int fd) { + std::lock_guard lock(g_mutex); + const auto found = g_directories.find(fd); + if (found == g_directories.end()) return std::nullopt; + return found->second.path; +} + +void ForgetDirectoryDescriptor(int fd) { + std::lock_guard lock(g_mutex); + g_directories.erase(fd); +} + +int ReadDirectoryDescriptor(int fd, char* buf, int nbytes) { + std::lock_guard lock(g_mutex); + const auto found = g_directories.find(fd); + if (found == g_directories.end()) return -1; + auto& state = found->second; + if (!state.loaded) { + state.loaded = true; + state.entries.emplace_back(".", GuestDirectoryType); + state.entries.emplace_back("..", GuestDirectoryType); + std::error_code error; + for (const auto& entry : std::filesystem::directory_iterator(state.path, error)) { + std::error_code typeError; + state.entries.emplace_back(entry.path().filename().string(), entry.is_directory(typeError) ? GuestDirectoryType : GuestRegularType); + } + } + std::size_t used = 0; + while (state.cursor < state.entries.size()) { + const auto& [name, type] = state.entries[state.cursor]; + const std::size_t record = (8 + name.size() + 1 + 3) & ~std::size_t{3}; + if (used + record > static_cast(nbytes)) break; + char* out = buf + used; + std::memset(out, 0, record); + const auto fileNumber = static_cast(state.cursor + 1); + const auto recordLength = static_cast(record); + std::memcpy(out, &fileNumber, sizeof(fileNumber)); + std::memcpy(out + 4, &recordLength, sizeof(recordLength)); + out[6] = static_cast(type); + out[7] = static_cast(name.size()); + std::memcpy(out + 8, name.c_str(), name.size() + 1); + used += record; + ++state.cursor; + } + return static_cast(used); +} + +} + +#endif diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index f9f195b81..cc1c0af37 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -1,4 +1,5 @@ #include "prx/libkernel/File/include/NativeStat.hpp" +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include #include @@ -11,6 +12,7 @@ static int DoStat(const std::filesystem::path& p, NativeStat* st) { return _wstat64(p.wstring().c_str(), st); } static int DoFstat(int fd, NativeStat* st) { + if (const auto directory = File::DirectoryDescriptorPath(fd)) return DoStat(*directory, st); return _fstat64(fd, st); } #else diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index fd45c02fe..469c4ad12 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -2,6 +2,7 @@ #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "SceTypes.hpp" @@ -98,6 +99,12 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { APS5_LOG_OUT("path=%s flags=0x%X nativeFlags=0x%X mode=0%o", path, flags, MapFlags(flags), mode); auto native = ResolvePath_nid_no_patch(path); int fd = NativeOpen(native, MapFlags(flags), mode); +#ifdef _WIN32 + if (fd < 0 && errno != ENOENT) { + std::error_code error; + if (std::filesystem::is_directory(native, error)) fd = File::OpenDirectoryDescriptor(native); + } +#endif if (fd < 0) { return SceErrorFromErrno(errno); } @@ -105,6 +112,9 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { } int APS5_VABI sceKernelClose(int d) { +#ifdef _WIN32 + File::ForgetDirectoryDescriptor(d); +#endif if (NativeClose(d) != 0) { throw std::runtime_error(std::string(__func__) + ": close failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); } diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index ece3fae6d..e0dbf878e 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -6,6 +6,8 @@ #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/File/include/NativeStat.hpp" +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include #include @@ -45,35 +47,36 @@ static int NativeFlock(int descriptor, int operation) { if (operation & 4) flags |= LOCKFILE_FAIL_IMMEDIATELY; return ::LockFileEx(handle, flags, 0, MAXDWORD, MAXDWORD, &overlapped) ? 0 : -1; } +// Positioned I/O through OVERLAPPED is atomic, unlike seek + read, which races when streaming threads share a +// descriptor. ponytail: on a synchronous handle it still moves the file pointer to offset + n, which POSIX pread +// does not; a mixed read()/pread() user on one descriptor would notice. +static std::int64_t NativePositioned(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset, bool write) { + if (nbytes > static_cast(std::numeric_limits::max())) { + throw std::runtime_error("NativePositioned: nbytes exceeds platform limit"); + } + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + if (handle == INVALID_HANDLE_VALUE) { + errno = EBADF; + return -1; + } + OVERLAPPED overlapped{}; + overlapped.Offset = static_cast(offset); + overlapped.OffsetHigh = static_cast(static_cast(offset) >> 32u); + DWORD done = 0; + const BOOL ok = write ? ::WriteFile(handle, buf, static_cast(nbytes), &done, &overlapped) + : ::ReadFile(handle, buf, static_cast(nbytes), &done, &overlapped); + if (!ok) { + if (!write && ::GetLastError() == ERROR_HANDLE_EOF) return 0; + errno = EIO; + return -1; + } + return done; +} static std::int64_t NativePread(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset) { - if (nbytes > static_cast(std::numeric_limits::max())) { - throw std::runtime_error("NativePread: nbytes exceeds platform limit"); - } - std::int64_t saved = ::_lseeki64(descriptor, 0, SEEK_CUR); - if (saved < 0) { - return -1; - } - if (::_lseeki64(descriptor, offset, SEEK_SET) < 0) { - return -1; - } - int n = ::_read(descriptor, buf, static_cast(nbytes)); - ::_lseeki64(descriptor, saved, SEEK_SET); - return n; + return NativePositioned(descriptor, buf, nbytes, offset, false); } static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nbytes, std::int64_t offset) { - if (nbytes > static_cast(std::numeric_limits::max())) { - throw std::runtime_error("NativePwrite: nbytes exceeds platform limit"); - } - std::int64_t saved = ::_lseeki64(descriptor, 0, SEEK_CUR); - if (saved < 0) { - return -1; - } - if (::_lseeki64(descriptor, offset, SEEK_SET) < 0) { - return -1; - } - int n = ::_write(descriptor, buf, static_cast(nbytes)); - ::_lseeki64(descriptor, saved, SEEK_SET); - return n; + return NativePositioned(descriptor, const_cast(buf), nbytes, offset, true); } #else #include @@ -273,9 +276,10 @@ int APS5_VABI unlink_nid_postfix(const char* path) { } int APS5_VABI sceKernelCheckReachability(const char* path) { - (void)path; - NotImplemented_nid_no_patch(__func__); - return 0; + if (path == nullptr) return SCE_KERNEL_ERROR_EINVAL; + if (std::strlen(path) > 255) return SCE_KERNEL_ERROR_ENAMETOOLONG; + std::error_code error; + return std::filesystem::exists(ResolvePath_nid_no_patch(path), error) ? 0 : SCE_KERNEL_ERROR_ENOENT; } int APS5_VABI sceKernelFstat(int d, FileStat* sb) { @@ -285,28 +289,25 @@ int APS5_VABI sceKernelFstat(int d, FileStat* sb) { } int APS5_VABI sceKernelFsync(int fd) { - (void)fd; - NotImplemented_nid_no_patch(__func__); - return 0; +#ifdef _WIN32 + return ::_commit(fd); +#else + return ::fsync(fd); +#endif } #ifdef _WIN32 -int APS5_VABI sceKernelGetdents(int fd, char* buf, int nbytes) { - (void)fd; - (void)buf; - (void)nbytes; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep) { + if (buf == nullptr) return SceErrorFromErrno(GUEST_EFAULT); + if (nbytes <= 0) return SceErrorFromErrno(GUEST_EINVAL); + if (basep != nullptr) *basep = 0; + const int written = File::ReadDirectoryDescriptor(fd, buf, nbytes); + return written < 0 ? SceErrorFromErrno(GUEST_ENOTDIR) : written; } -int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep) { - (void)fd; - (void)buf; - (void)nbytes; - (void)basep; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceKernelGetdents(int fd, char* buf, int nbytes) { + return sceKernelGetdirentries(fd, buf, nbytes, nullptr); } #else @@ -371,21 +372,11 @@ int APS5_VABI sceKernelMkdir(const char* path, uint16_t mode) { #ifdef _WIN32 int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset) { - (void)d; - (void)buf; - (void)nbytes; - (void)offset; - NotImplemented_nid_no_patch(__func__); - return 0; + return pread_nid_postfix(d, buf, nbytes, offset); } int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t offset) { - (void)d; - (void)buf; - (void)nbytes; - (void)offset; - NotImplemented_nid_no_patch(__func__); - return 0; + return pwrite_nid_disambig1_nid_postfix(d, buf, nbytes, offset); } #else diff --git a/core/libs/prx/libkernel/Pthread/Posix/Xthreads.cpp b/core/libs/prx/libkernel/Pthread/Posix/Xthreads.cpp new file mode 100644 index 000000000..5dd4ab78a --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/Posix/Xthreads.cpp @@ -0,0 +1,157 @@ +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include "../include/Pthread.hpp" +#include "../include/Mutex.hpp" +#include "../include/Cond.hpp" +#include "../../Time/include/Time.hpp" +#include "Common.hpp" + +// Dinkumware C11 thread support as the title's own libc.prx implements it: each _Mtx_t/_Cnd_t is a +// pointer-sized slot holding the sce object, and results are _Thrd_* codes. + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +Pthread APS5_VABI scePthreadSelf(); +} + +namespace { + +constexpr int ThrdSuccess = 0; +constexpr int ThrdNomem = 1; +constexpr int ThrdTimedout = 2; +constexpr int ThrdBusy = 3; +constexpr int ThrdError = 4; +constexpr int MtxPlain = 0x1; +constexpr int MtxRecursive = 0x100; +constexpr int SceMutexRecursive = 2; +constexpr int SceEdeadlk = static_cast(0x8002000Bu); +constexpr int SceEnomem = static_cast(0x8002000Cu); +constexpr int SceEtimedout = static_cast(0x8002003Cu); +constexpr int GuestRealtimeClock = 0; + +int initResult(int result) { + return result == 0 ? ThrdSuccess : result == SceEnomem ? ThrdNomem : ThrdError; +} + +// std::_Pad as laid out by the title's libc; _Go() is the first vtable slot. +struct GuestPad { + unsigned (APS5_VABI* const* vtable)(GuestPad*); + PthreadCond cond; + PthreadMutex mutex; + bool started; +}; +static_assert(offsetof(GuestPad, cond) == 0x8 && offsetof(GuestPad, mutex) == 0x10 && offsetof(GuestPad, started) == 0x18); + +void* APS5_VABI padEntry(void* arg) { + auto* pad = static_cast(arg); + pad->vtable[0](pad); + return nullptr; +} + +void require(int result, const char* what) { + if (result != ThrdSuccess) throw std::runtime_error(what); +} + +} + +extern "C" { + +int APS5_VABI _Mtx_init_nid_postfix(PthreadMutex* mutex, int type) { + *mutex = nullptr; + PthreadMutexattr attr{}; + const int attrResult = scePthreadMutexattrInit(&attr); + if (attrResult != 0) return initResult(attrResult); + if ((type & MtxRecursive) != 0 && scePthreadMutexattrSettype(&attr, SceMutexRecursive) != 0) { + scePthreadMutexattrDestroy(&attr); + return ThrdError; + } + const int result = scePthreadMutexInit(mutex, &attr, nullptr); + scePthreadMutexattrDestroy(&attr); + return initResult(result); +} + +void APS5_VABI _Mtx_destroy_nid_postfix(PthreadMutex* mutex) { + scePthreadMutexDestroy(mutex); +} + +int APS5_VABI _Mtx_lock_nid_postfix(PthreadMutex* mutex) { + const int result = scePthreadMutexLock(mutex); + return result == 0 ? ThrdSuccess : result == SceEdeadlk ? ThrdBusy : ThrdError; +} + +int APS5_VABI _Mtx_unlock_nid_postfix(PthreadMutex* mutex) { + scePthreadMutexUnlock(mutex); + return ThrdSuccess; +} + +int APS5_VABI _Cnd_init_nid_postfix(PthreadCond* cond) { + return initResult(scePthreadCondInit(cond, nullptr, nullptr)); +} + +void APS5_VABI _Cnd_destroy_nid_postfix(PthreadCond* cond) { + scePthreadCondDestroy(cond); +} + +int APS5_VABI _Cnd_broadcast_nid_postfix(PthreadCond* cond) { + scePthreadCondBroadcast(cond); + return ThrdSuccess; +} + +// xtime has the KernelTimespec layout and is an absolute gettimeofday deadline. +int APS5_VABI _Cnd_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* deadline) { + KernelUseconds usec = 0; + if (!PosixThread::RelativeMicroseconds(GuestRealtimeClock, deadline, &usec)) throw std::runtime_error("_Cnd_timedwait: invalid xtime"); + const int result = scePthreadCondTimedwait(cond, mutex, usec); + return result == 0 ? ThrdSuccess : result == SceEtimedout ? ThrdTimedout : ThrdError; +} + +int APS5_VABI _Thrd_join_nid_postfix(Pthread thread, int* result) { + void* value = nullptr; + if (scePthreadJoin(thread, &value) != 0) return ThrdError; + if (result) *result = static_cast(reinterpret_cast(value)); + return ThrdSuccess; +} + +Pthread APS5_VABI _Thrd_id_nid_postfix() { + return scePthreadSelf(); +} + +// Microseconds since the epoch, not the 100 ns ticks of other Dinkumware ports. +std::int64_t APS5_VABI _Xtime_get_ticks_nid_postfix() { + KernelTimeval now{}; + gettimeofday_nid_postfix(&now, nullptr); + return now.tv_sec * 1000000 + now.tv_usec; +} + +void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(GuestPad* pad) { + require(_Cnd_init_nid_postfix(&pad->cond), "std::_Pad: condition variable init failed"); + require(_Mtx_init_nid_postfix(&pad->mutex, MtxPlain), "std::_Pad: mutex init failed"); + pad->started = false; + require(_Mtx_lock_nid_postfix(&pad->mutex), "std::_Pad: mutex lock failed"); +} + +void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(GuestPad* pad) { + _Mtx_unlock_nid_postfix(&pad->mutex); + _Mtx_destroy_nid_postfix(&pad->mutex); + _Cnd_destroy_nid_postfix(&pad->cond); +} + +void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, Pthread* thread) { + if (scePthreadCreate(thread, nullptr, padEntry, pad, nullptr) != 0) throw std::runtime_error("std::_Pad: thread start failed"); + while (!pad->started) { + if (scePthreadCondWait(&pad->cond, &pad->mutex) != 0) throw std::runtime_error("std::_Pad: launch wait failed"); + } +} + +void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad) { + require(_Mtx_lock_nid_postfix(&pad->mutex), "std::_Pad: mutex lock failed"); + pad->started = true; + scePthreadCondSignal(&pad->cond); + _Mtx_unlock_nid_postfix(&pad->mutex); +} + +} diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index c91c1038a..909bd5a44 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -57,10 +57,9 @@ int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stacksiz if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); if (stacksize < 16384) throw std::runtime_error(std::string(__func__) + ": too small"); #ifdef _WIN32 - SYSTEM_INFO system{}; - GetSystemInfo(&system); - if (stacksize % system.dwPageSize != 0 || stacksize > std::numeric_limits::max()) - throw std::runtime_error(std::string(__func__) + ": invalid Windows stack size"); + // Like libthr, any size is kept as given; thread creation rounds it up to whole pages. + if (stacksize > std::numeric_limits::max() - 0xffffu) + throw std::runtime_error(std::string(__func__) + ": stack size exceeds the Windows limit"); #endif (*attr)->_stacksize = stacksize; return SCE_OK; diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 27b84bba6..98d952d68 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -210,11 +210,12 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread #ifdef _WIN32 SYSTEM_INFO system{}; GetSystemInfo(&system); - if (p->stackSize < 16384 || p->stackSize % system.dwPageSize != 0 || p->stackSize > std::numeric_limits::max()) + const std::size_t nativeStack = (p->stackSize + system.dwPageSize - 1) / system.dwPageSize * system.dwPageSize; + if (p->stackSize < 16384 || nativeStack > std::numeric_limits::max()) throw std::runtime_error("scePthreadCreate: invalid Windows stack size"); auto native = std::make_unique(NativeThreadArgs{std::move(args), start.get_future(), {}}); auto initialized = native->initialized.get_future(); - const auto handle = _beginthreadex(nullptr, static_cast(p->stackSize), StartNativeThread, native.get(), 0, nullptr); + const auto handle = _beginthreadex(nullptr, static_cast(nativeStack), StartNativeThread, native.get(), 0, nullptr); if (handle == 0) throw std::system_error(errno, std::generic_category(), "Creating guest thread"); p->nativeHandle = reinterpret_cast(handle); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 386c95ed5..575feb15a 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -225,6 +225,12 @@ int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_ if (!Address(address, length, native, size)) return -1; return ::bind(socket->value, reinterpret_cast(&native), size) ? Fail(NativeError()) : 0; } +int APS5_VABI shutdown_nid_postfix(int descriptor, int how) { + const auto socket = Lookup(descriptor); + if (!socket) return Fail(9); + if (how < 0 || how > 2) return Fail(22); // SHUT_RD/SHUT_WR/SHUT_RDWR share values with the host constants + return ::shutdown(socket->value, how) ? Fail(NativeError()) : 0; +} int APS5_VABI getsockname_nid_postfix(int descriptor, void* address, std::uint32_t* length) { const auto socket = Lookup(descriptor); if (!socket) return -1; @@ -235,6 +241,16 @@ int APS5_VABI getsockname_nid_postfix(int descriptor, void* address, std::uint32 GuestAddress(native, address, length); return 0; } +int APS5_VABI getpeername_nid_postfix(int descriptor, void* address, std::uint32_t* length) { + const auto socket = Lookup(descriptor); + if (!socket) return -1; + if (!address || !length) return Fail(14); + sockaddr_storage native{}; + socklen_t size = sizeof(native); + if (::getpeername(socket->value, reinterpret_cast(&native), &size)) return Fail(NativeError()); + GuestAddress(native, address, length); + return 0; +} int APS5_VABI ioctl_nid_postfix(int descriptor, std::uint64_t request, void* argument) { const auto socket = Lookup(descriptor); if (!socket) return -1; diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index 107359bc9..f5401a80b 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -15,16 +15,6 @@ int APS5_VABI sceCoredumpWriteUserData() { return 0; } -int APS5_VABI sceKernelAprGetFileSize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceKernelAprGetFileStat() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelAprResolveFilepathsToIdsAndFileSizesForEach() { NotImplemented_nid_no_patch(__func__); return 0; @@ -35,16 +25,6 @@ int APS5_VABI sceKernelAprResolveFilepathsToIdsForEach() { return 0; } -int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIds() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizes() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizesForEach() { NotImplemented_nid_no_patch(__func__); return 0; @@ -55,29 +35,9 @@ int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIdsForEach() { return 0; } -int APS5_VABI sceKernelAprSubmitCommandBufferAndGetId() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceKernelAprSubmitCommandBufferAndGetResult() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceKernelAprWaitCommandBuffer() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelFtruncate() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI shutdown_nid_postfix() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 58dd3dfe3..7550e68a9 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -68,6 +68,7 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(snprintf_nid_postfix(buffer, 5, "%s", "abcdef") == 6); Require(std::strcmp(buffer, "abcd") == 0 && buffer[5] == '!'); Require(snprintf_nid_postfix(nullptr, 0, "%s:%d", "abcdef", 123) == 10); + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "[%s]", static_cast(nullptr)) == 8 && std::strcmp(buffer, "[(null)]") == 0); buffer[0] = 'x'; Require(snprintf_nid_postfix(buffer, 1, "%d", 123) == 3 && buffer[0] == 0); Require(sprintf_nid_postfix(buffer, "%hhd %hhu %hd %hu %%", 255, 257, 65535, 65537) == 11); diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index a1953e7b0..c8a27e885 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -18,6 +18,10 @@ int APS5_VABI sceKernelMunmap(void*, std::size_t); int APS5_VABI sceKernelVirtualQuery(const void*, int, VirtualQueryInfo*, std::uint64_t); int APS5_VABI sceKernelSetVirtualRangeName(const void*, std::uint64_t, const char*); int APS5_VABI sceKernelClearVirtualRangeName(const void*, std::uint64_t); +int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); +int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); +int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); +int APS5_VABI sceKernelReserveVirtualRange(void**, std::size_t, int, std::size_t); } static void Require(bool condition) { @@ -58,8 +62,46 @@ static void CheckNamedAndHintedMappings() { Require(sceKernelMunmap(first, length) == 0); } +// Direct memory contents follow the physical pages: unmapped and mapped again elsewhere, they read back. +static void CheckDirectMemoryFollowsPhysicalPages() { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0); + void* first = nullptr; + Require(sceKernelMapDirectMemory(&first, page * 2, 3, 0, phys, 0) == 0); + static_cast(first)[0] = 11; + static_cast(first)[page + 5] = 22; + bool aliased = false; + void* alias = nullptr; + try { sceKernelMapDirectMemory(&alias, page, 3, 0, phys + page, 0); } catch (const std::exception&) { aliased = true; } + Require(aliased); + Require(sceKernelMunmap(first, page * 2) == 0); + void* filler = nullptr; + Require(sceKernelMapFlexibleMemory(&filler, page * 2, 3, 0) == 0); + void* second = nullptr; + Require(sceKernelMapDirectMemory(&second, page, 3, 0, phys + page, 0) == 0); + Require(second != first && static_cast(second)[5] == 22); + void* reserved = nullptr; + Require(sceKernelReserveVirtualRange(&reserved, page, 0, 0) == 0); + void* fixed = reserved; + Require(sceKernelMapDirectMemory(&fixed, page, 1, 0x10, phys, 0) == 0); + Require(fixed == reserved && static_cast(fixed)[0] == 11); + Require(sceKernelMunmap(fixed, page) == 0); + Require(sceKernelMunmap(second, page) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0); + std::int64_t again = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &again) == 0 && again == phys); + void* fresh = nullptr; + Require(sceKernelMapDirectMemory(&fresh, page * 2, 3, 0, again, 0) == 0); + Require(static_cast(fresh)[0] == 0 && static_cast(fresh)[page + 5] == 0); + Require(sceKernelMunmap(fresh, page * 2) == 0); + Require(sceKernelMunmap(filler, page * 2) == 0); + Require(sceKernelReleaseDirectMemory(again, page * 2) == 0); +} + int main() { CheckNamedAndHintedMappings(); + CheckDirectMemoryFollowsPhysicalPages(); constexpr std::size_t page = 0x4000; const auto failed = reinterpret_cast(static_cast(-1)); const auto reject = [&](std::size_t length, int protection, int flags, int fd, From fd4700b909d81bc23cffc6d5097fe688f9333c97 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 07:53:50 +0300 Subject: [PATCH 04/40] feat(agc): packet sizes, register writes, GS/HS fusion, jump patches - packet sizes for the existing builders and the tessellation ring config - direct register writes and end-of-pipe action data, type and cache patches - GS/HS shader halves fused by merging their registers, after Kyty - jump target patches; reserved jumps are allowed Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgc/Cb/src/Registers.cpp | 8 +- core/libs/prx/libSceAgc/Cb/src/Sync.cpp | 4 +- .../prx/libSceAgc/Command/src/Control.cpp | 4 +- .../prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp | 3 +- .../prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp | 6 +- core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp | 4 +- .../prx/libSceAgc/DcbState/src/Registers.cpp | 24 ++--- .../prx/libSceAgc/Misc/src/ShaderFusion.cpp | 101 ++++++++++++++++-- core/libs/prx/libSceAgc/Patch/src/Jump.cpp | 31 +++++- .../Patch/src/QueueEndOfPipeAction.cpp | 36 ++++--- core/libs/prx/libSceAgc/Patch/src/Rewind.cpp | 32 +++--- core/libs/prx/libSceAgc/Unimplemented.cpp | 6 -- 12 files changed, 178 insertions(+), 81 deletions(-) diff --git a/core/libs/prx/libSceAgc/Cb/src/Registers.cpp b/core/libs/prx/libSceAgc/Cb/src/Registers.cpp index 62857c1c2..f8ed2f09c 100644 --- a/core/libs/prx/libSceAgc/Cb/src/Registers.cpp +++ b/core/libs/prx/libSceAgc/Cb/src/Registers.cpp @@ -17,15 +17,11 @@ std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer* buf, st } uint32_t APS5_VABI sceAgcCbSetShRegisterRangeDirectGetSize(uint32_t num_values) { - (void)num_values; - NotImplemented_nid_no_patch(__func__); - return 0; + return (num_values + 2u) * 4u; // header + offset + values } std::uint32_t APS5_VABI sceAgcCbSetUcRegisterRangeDirectGetSize(std::uint32_t numValues) { - (void)numValues; - NotImplemented_nid_no_patch(__func__); - return 0; + return (numValues + 2u) * 4u; // header + offset + values } std::uint32_t* APS5_VABI sceAgcCbSetShRegistersDirect(CommandBuffer* buf, const volatile ShaderRegister* regs, std::uint32_t numRegs) { diff --git a/core/libs/prx/libSceAgc/Cb/src/Sync.cpp b/core/libs/prx/libSceAgc/Cb/src/Sync.cpp index 473c39dae..05d5395f8 100644 --- a/core/libs/prx/libSceAgc/Cb/src/Sync.cpp +++ b/core/libs/prx/libSceAgc/Cb/src/Sync.cpp @@ -15,9 +15,7 @@ std::uint32_t* APS5_VABI sceAgcCbNop_nid_postfix(CommandBuffer* buf, std::uint32 } uint32_t APS5_VABI sceAgcCbNopGetSize(uint32_t size_in_dwords) { - (void)size_in_dwords; - NotImplemented_nid_no_patch(__func__); - return 0; + return size_in_dwords * 4u; // a NOP packet is exactly size_in_dwords dwords including its header } std::uint32_t APS5_VABI sceAgcCbQueueEndOfPipeActionGetSize() { diff --git a/core/libs/prx/libSceAgc/Command/src/Control.cpp b/core/libs/prx/libSceAgc/Command/src/Control.cpp index 88f8d637d..9906cd432 100644 --- a/core/libs/prx/libSceAgc/Command/src/Control.cpp +++ b/core/libs/prx/libSceAgc/Command/src/Control.cpp @@ -7,7 +7,9 @@ std::uint32_t* WriteJump(CommandBuffer* buffer, std::uint8_t chain, std::uint8_t CheckBits(chain, 1, function); CheckBits(cachePolicy, 3, function); CheckBits(sizeInDwords, 0xfffffu, function); - CheckAddress(guestAddress, 4, function); + // Titles reserve a jump with a null target and write the address into the packet later. + Require((guestAddress & 3u) == 0, function, "misaligned jump target"); + // INDIRECT_BUFFER: address, then size in bits 19:0, chain (no return) in bit 20, cache policy in 29:28. return Emit(buffer, 0x3fu, {static_cast(guestAddress) & ~3u, static_cast(guestAddress >> 32u), 0x0f200000u | (static_cast(cachePolicy) << 28u) | (static_cast(chain) << 20u) | sizeInDwords}, function); } diff --git a/core/libs/prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp b/core/libs/prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp index 1578158dc..6129e7174 100644 --- a/core/libs/prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp +++ b/core/libs/prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp @@ -32,8 +32,7 @@ std::uint32_t* APS5_VABI sceAgcDcbDrawIndexOffset(CommandBuffer* buf, std::uint3 } uint32_t APS5_VABI sceAgcDcbDrawIndexOffsetGetSize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 20; // DRAW_INDEX_OFFSET_2: header + 4 dwords } uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirect(CommandBuffer* buf, uint32_t data_offset_in_bytes, uint64_t modifier) { diff --git a/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp b/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp index bdc1ac7c4..09e477f28 100644 --- a/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp +++ b/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp @@ -14,8 +14,7 @@ std::uint32_t* APS5_VABI sceAgcDcbSetIndexBuffer(CommandBuffer* buf, std::uint64 } std::uint32_t APS5_VABI sceAgcDcbSetIndexBufferGetSize() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 12; // INDEX_BASE: header + 2 dwords } std::uint32_t* APS5_VABI sceAgcDcbSetIndexCount(CommandBuffer* buf, std::uint32_t indexCount) { @@ -33,8 +32,7 @@ std::uint32_t* APS5_VABI sceAgcDcbSetIndexSize(CommandBuffer* buf, std::uint8_t } std::uint32_t APS5_VABI sceAgcDcbSetIndexSizeGetSize() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 12; // SET_UCONFIG_REG_INDEX: header + 2 dwords } std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint32_t dataOffsetInBytes) { diff --git a/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp b/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp index 9e63c69d3..2e196b43d 100644 --- a/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp +++ b/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp @@ -42,9 +42,7 @@ std::uint32_t* APS5_VABI sceAgcDcbEventWrite(CommandBuffer* buf, std::uint8_t ev } std::uint64_t APS5_VABI sceAgcDcbEventWriteGetSize(std::uint8_t eventType) { - (void)eventType; - NotImplemented_nid_no_patch(__func__); - return 0; + return (eventType & 0xfeu) == 0x38u ? 16 : 8; // EVENT_WRITE, plus the address for the 0x38/0x39 events } std::uint32_t* APS5_VABI sceAgcDcbStallCommandBufferParser(CommandBuffer* buf) { diff --git a/core/libs/prx/libSceAgc/DcbState/src/Registers.cpp b/core/libs/prx/libSceAgc/DcbState/src/Registers.cpp index cf78cf4c8..36c25d7f5 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Registers.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Registers.cpp @@ -9,15 +9,11 @@ extern "C" { uint32_t* APS5_VABI sceAgcDcbSetCxRegisterDirect(CommandBuffer* buf, ShaderRegister reg) { - (void)buf; - (void)reg; - NotImplemented_nid_no_patch(__func__); - return nullptr; + return Agc::Command::WriteRegisterRange(buf, 0x69u, reg.offset, ®.value, 1u, __func__); } uint32_t APS5_VABI sceAgcDcbSetCxRegisterDirectGetSize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 12; } std::uint32_t* APS5_VABI sceAgcDcbSetCxRegistersIndirect(CommandBuffer* buf, const volatile ShaderRegister* regs, std::uint32_t numRegs) { @@ -31,15 +27,11 @@ std::uint32_t APS5_VABI sceAgcDcbSetCxRegistersIndirectGetSize(std::uint32_t num } uint32_t* APS5_VABI sceAgcDcbSetShRegisterDirect(CommandBuffer* buf, ShaderRegister reg) { - (void)buf; - (void)reg; - NotImplemented_nid_no_patch(__func__); - return nullptr; + return Agc::Command::WriteRegisterRange(buf, 0x76u, reg.offset, ®.value, 1u, __func__); } std::uint32_t APS5_VABI sceAgcDcbSetShRegisterDirectGetSize() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 12; } std::uint32_t* APS5_VABI sceAgcDcbSetShRegistersIndirect(CommandBuffer* buf, const volatile ShaderRegister* regs, std::uint32_t numRegs) { @@ -52,15 +44,11 @@ std::uint32_t APS5_VABI sceAgcDcbSetShRegistersIndirectGetSize(std::uint32_t num } uint32_t* APS5_VABI sceAgcDcbSetUcRegisterDirect(CommandBuffer* buf, ShaderRegister reg) { - (void)buf; - (void)reg; - NotImplemented_nid_no_patch(__func__); - return nullptr; + return Agc::Command::WriteRegisterRange(buf, 0x79u, reg.offset, ®.value, 1u, __func__); } std::uint32_t APS5_VABI sceAgcDcbSetUcRegisterDirectGetSize() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 12; } std::uint32_t* APS5_VABI sceAgcDcbSetUcRegistersIndirect(CommandBuffer* buf, const volatile ShaderRegister* regs, std::uint32_t numRegs) { diff --git a/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp b/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp index 519e720bd..0f3cc6356 100644 --- a/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp @@ -1,33 +1,116 @@ #include "prx/libSceAgc/Misc/include/ShaderFusion.hpp" +#include +#include +#include +#include #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" struct SizeAlign { uint64_t m_size; size_t m_align; }; +namespace { + +using namespace ShaderRegs; + +constexpr int GRAPHICS5_ERROR_INVALID_SHADER_HALVES = static_cast(0x8a6c0008u); +constexpr std::uint32_t SPI_SHADER_PGM_CHKSUM_GS = 0x080u; +constexpr std::uint32_t SPI_SHADER_PGM_RSRC1_GS = 0x08Au; +constexpr std::uint32_t SPI_SHADER_PGM_RSRC2_GS = 0x08Bu; +constexpr std::uint32_t SPI_SHADER_PGM_CHKSUM_HS = 0x100u; +constexpr std::uint32_t SPI_SHADER_PGM_RSRC1_HS = 0x10Au; +constexpr std::uint32_t SPI_SHADER_PGM_RSRC2_HS = 0x10Bu; + +bool ValidHalves(const Shader* front, const Shader* back) { + const auto frontType = static_cast(front->type); + const auto backType = static_cast(back->type); + return (frontType == ShaderBinaryType::GsFront && backType == ShaderBinaryType::GsBack) || + (frontType == ShaderBinaryType::HsFront && backType == ShaderBinaryType::HsBack); +} + +ShaderRegister& FindRegister(ShaderRegister* regs, std::uint32_t count, std::uint32_t offset, std::uint32_t occurrence = 0) { + for (std::uint32_t i = 0; regs != nullptr && i < count; ++i) { + if (regs[i].offset == offset && occurrence-- == 0) return regs[i]; + } + throw std::runtime_error("sceAgcUnknownFuseShaderHalves: shader half lacks register " + std::to_string(offset)); +} + +void MergeMax(ShaderRegister& dst, const ShaderRegister& src, std::uint32_t shift, std::uint32_t mask) { + const auto field = std::max((dst.value >> shift) & mask, (src.value >> shift) & mask); + dst.value = (dst.value & ~(mask << shift)) | (field << shift); +} + +} + extern "C" { APS5_EXPORT("fd5Bp5tGTgo", sceAgcUnknownFuseShaderHalves); int APS5_VABI sceAgcUnknownFuseShaderHalves(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem) { - (void)fused_result; - (void)front; - (void)back; - (void)scratch_mem; - NotImplemented_nid_no_patch(__func__); + if (fused_result == nullptr || front == nullptr || back == nullptr) APS5_INVALID_ARG_EX; + if (!ValidHalves(front, back)) return GRAPHICS5_ERROR_INVALID_SHADER_HALVES; + const bool isGs = static_cast(front->type) == ShaderBinaryType::GsFront; + const std::uint32_t stageBit = isGs ? (1u << 22u) : (1u << 21u); + if (((front->specials->vgt_shader_stages_en.value ^ back->specials->vgt_shader_stages_en.value) & stageBit) != 0) { + return GRAPHICS5_ERROR_INVALID_SHADER_HALVES; + } + *fused_result = *back; + fused_result->type = static_cast(isGs ? ShaderBinaryType::Gs : ShaderBinaryType::Hs); + if (scratch_mem != nullptr) { + auto* regs = static_cast(scratch_mem); + std::memcpy(regs, back->sh_registers, std::size_t{back->num_sh_registers} * sizeof(ShaderRegister)); + fused_result->sh_registers = regs; + } + ShaderRegister* fused = fused_result->sh_registers; + const std::uint32_t fusedCount = fused_result->num_sh_registers; + const std::uint32_t frontCount = front->num_sh_registers; + const std::uint32_t checksum = isGs ? SPI_SHADER_PGM_CHKSUM_GS : SPI_SHADER_PGM_CHKSUM_HS; + for (std::uint32_t occurrence = 0; occurrence < 2; ++occurrence) { + FindRegister(fused, fusedCount, checksum, occurrence).value = FindRegister(front->sh_registers, frontCount, checksum, occurrence).value; + } + const auto& frontRsrc1 = FindRegister(front->sh_registers, frontCount, isGs ? SPI_SHADER_PGM_RSRC1_GS : SPI_SHADER_PGM_RSRC1_HS); + const auto& frontRsrc2 = FindRegister(front->sh_registers, frontCount, isGs ? SPI_SHADER_PGM_RSRC2_GS : SPI_SHADER_PGM_RSRC2_HS); + auto& fusedRsrc1 = FindRegister(fused, fusedCount, isGs ? SPI_SHADER_PGM_RSRC1_GS : SPI_SHADER_PGM_RSRC1_HS); + auto& fusedRsrc2 = FindRegister(fused, fusedCount, isGs ? SPI_SHADER_PGM_RSRC2_GS : SPI_SHADER_PGM_RSRC2_HS); + // Both halves run in one wave, so VGPRs are the larger of the two and shared VGPRs are reallocated (after Kyty). + const std::uint32_t frontVgprs = ((frontRsrc1.value & 0x3fu) + 1u) * 4u; + const std::uint32_t backVgprs = ((fusedRsrc1.value & 0x3fu) + 1u) * 4u; + const std::uint32_t frontTotal = frontVgprs + (frontRsrc2.value >> 28u) * 8u; + const std::uint32_t backTotal = backVgprs + (fusedRsrc2.value >> 28u) * 8u; + const std::uint32_t maxTotal = std::max(frontTotal, backTotal); + const std::uint32_t shared = std::max(frontVgprs, backVgprs) >= maxTotal ? 0u : (maxTotal - std::min(frontTotal, backTotal) + 7u) / 64u; + fusedRsrc2.value = (fusedRsrc2.value & 0x0fffffffu) | ((shared & 0xfu) << 28u); + MergeMax(fusedRsrc1, frontRsrc1, 0, 0x3fu); + if (isGs) { + MergeMax(fusedRsrc1, frontRsrc1, 29, 0x3u); + MergeMax(fusedRsrc2, frontRsrc2, 16, 0x3u); + fusedRsrc2.value = (fusedRsrc2.value & 0xfffbffffu) | (frontRsrc2.value & 0x00040000u); + } else { + MergeMax(fusedRsrc1, frontRsrc1, 28, 0x3u); + } + fusedRsrc2.value = (fusedRsrc2.value & 0xf7ffffc1u) | (frontRsrc2.value & 0x0800003eu); + // The fused program starts at the front half; its address goes in the ES/LS program registers. + const std::uint32_t lo = isGs ? SPI_SHADER_PGM_LO_ES : SPI_SHADER_PGM_LO_LS; + auto& loRegister = FindRegister(fused, fusedCount, lo); + auto& hiRegister = FindRegister(fused, fusedCount, lo + 1u); + const auto address = reinterpret_cast(front->code); + loRegister.value = static_cast(address >> 8u); + hiRegister.value = (hiRegister.value & 0xffffff00u) | static_cast((address >> 40u) & 0xffu); + fused_result->user_data = nullptr; return 0; } APS5_EXPORT("dolOmWH+huQ", sceAgcUnknownGetFusedShaderSize); int APS5_VABI sceAgcUnknownGetFusedShaderSize(SizeAlign* dst, const Shader* front, const Shader* back) { - (void)dst; - (void)front; - (void)back; - NotImplemented_nid_no_patch(__func__); + if (dst == nullptr || front == nullptr || back == nullptr) APS5_INVALID_ARG_EX; + if (!ValidHalves(front, back)) return GRAPHICS5_ERROR_INVALID_SHADER_HALVES; + dst->m_size = std::uint64_t{back->num_sh_registers} * sizeof(ShaderRegister); + dst->m_align = 4; return 0; } diff --git a/core/libs/prx/libSceAgc/Patch/src/Jump.cpp b/core/libs/prx/libSceAgc/Patch/src/Jump.cpp index da33b9147..435489880 100644 --- a/core/libs/prx/libSceAgc/Patch/src/Jump.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/Jump.cpp @@ -7,14 +7,35 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +// INDIRECT_BUFFER as sceAgcDcbJump writes it: [1..2] target, [3] size in 19:0, chain in 20, cache policy in 29:28. +void PatchJump(std::uint32_t* cmd, const volatile std::uint32_t* target, std::uint32_t sizeInDwords, const char* function) { + Agc::Command::ValidatePacket(cmd, 0x3fu, 4, function); + const auto address = reinterpret_cast(target); + Agc::Command::CheckAddress(address, 4, function); + Agc::Command::CheckBits(sizeInDwords, 0xfffffu, function); + cmd[1] = static_cast(address); + cmd[2] = static_cast(address >> 32u); + cmd[3] = (cmd[3] & ~0xfffffu) | sizeInDwords; +} + +} + extern "C" { int APS5_VABI sceAgcJumpPatchSetTarget(uint32_t* cmd, const volatile uint32_t* target, uint32_t size_in_dwords) { - (void)cmd; - (void)target; - (void)size_in_dwords; - NotImplemented_nid_no_patch(__func__); - return 0; + PatchJump(cmd, target, size_in_dwords, __func__); + return 0; +} + +// Named after Kyty's reading: a jump patch that also sets the cache policy. +APS5_EXPORT("Ikfdt-rIqCE", sceAgcUnknown_Ikfdt_MrIqCE); +int APS5_VABI sceAgcUnknown_Ikfdt_MrIqCE(uint32_t* cmd, uint64_t cache_policy, const volatile uint32_t* target, uint32_t size_in_dwords) { + Agc::Command::CheckBits(cache_policy, 3u, __func__); + PatchJump(cmd, target, size_in_dwords, __func__); + cmd[3] = (cmd[3] & ~(3u << 28u)) | (static_cast(cache_policy) << 28u); + return 0; } } diff --git a/core/libs/prx/libSceAgc/Patch/src/QueueEndOfPipeAction.cpp b/core/libs/prx/libSceAgc/Patch/src/QueueEndOfPipeAction.cpp index 53b2e91ed..c7be7ba5f 100644 --- a/core/libs/prx/libSceAgc/Patch/src/QueueEndOfPipeAction.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/QueueEndOfPipeAction.cpp @@ -31,25 +31,37 @@ int APS5_VABI sceAgcQueueEndOfPipeActionPatchAddress(std::uint32_t* cmd, const v } int APS5_VABI sceAgcQueueEndOfPipeActionPatchData(uint32_t* cmd, uint32_t context_id, uint32_t data_sel, uint64_t data) { - (void)cmd; - (void)context_id; - (void)data_sel; - (void)data; - NotImplemented_nid_no_patch(__func__); - return 0; + // RELEASE_MEM as built by sceAgcCbReleaseMem: [2] dst | interrupt << 24 | data select << 29, [5..6] data, [7] interrupt context. + Agc::Command::ValidatePacket(cmd, 0x49u, 8, __func__); + Agc::Command::Require(data_sel <= 3, __func__, "invalid release data selector"); + Agc::Command::CheckBits(context_id, 0x7ffffffu, __func__); + if (data_sel == 1) { + Agc::Command::CheckBits(data, 0xffffffffu, __func__); + } + Agc::Command::Require(((cmd[2] >> 24u) & 7u) != 4u || data_sel == 0, __func__, "interrupt-only release cannot write data"); + cmd[2] = (cmd[2] & 0x1fffffffu) | (data_sel << 29u); + cmd[5] = static_cast(data); + cmd[6] = static_cast(data >> 32u); + cmd[7] = context_id; + return 0; } int APS5_VABI sceAgcQueueEndOfPipeActionPatchGcrCntl(std::uint32_t* cmd, std::uint16_t gcrControl) { - (void)cmd; - (void)gcrControl; - NotImplemented_nid_no_patch(__func__); + Agc::Command::ValidatePacket(cmd, 0x49u, 8, __func__); + Agc::Command::CheckBits(gcrControl, 0xfffu, __func__); + std::uint32_t control = gcrControl; + if ((control & 0x300u) == 0x100u) { + control |= 0x200u; + } + cmd[1] = (cmd[1] & ~(0xfffu << 12u)) | (control << 12u); return 0; } int APS5_VABI sceAgcQueueEndOfPipeActionPatchType(std::uint32_t* cmd, std::uint8_t action) { - (void)cmd; - (void)action; - NotImplemented_nid_no_patch(__func__); + Agc::Command::ValidatePacket(cmd, 0x49u, 8, __func__); + Agc::Command::CheckBits(action, 0x3fu, __func__); + const std::uint32_t eventIndex = action >= 0x2fu ? 6u : 5u; + cmd[1] = (cmd[1] & ~0xf3fu) | action | (eventIndex << 8u); return 0; } diff --git a/core/libs/prx/libSceAgc/Patch/src/Rewind.cpp b/core/libs/prx/libSceAgc/Patch/src/Rewind.cpp index fed73340b..26eabbc17 100644 --- a/core/libs/prx/libSceAgc/Patch/src/Rewind.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/Rewind.cpp @@ -2,25 +2,33 @@ #include "prx/libSceAgc/Command/include/Memory.hpp" #include "prx/libSceAgc/Command/include/Packet.hpp" +#include #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -extern "C" { +namespace { -int APS5_VABI sceAgcRewindPatchSetRewindState(uint32_t* cmd, uint8_t state) { - (void)cmd; - (void)state; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcAsyncRewindPatchSetRewindState(std::uint32_t* cmd, std::uint8_t state) { - (void)cmd; - (void)state; - NotImplemented_nid_no_patch(__func__); +// The valid bit (31) releases a queue stalled on the REWIND; the release store publishes the commands +// the title wrote behind it before setting the bit. +int SetRewindState(std::uint32_t* cmd, std::uint8_t state, const char* function) { + Agc::Command::ValidatePacket(cmd, 0x59u, 2, function); + Agc::Command::CheckBits(state, 1, function); + std::atomic_ref(cmd[1]).store(static_cast(state) << 31u, std::memory_order_release); return 0; } } + +extern "C" { + +int APS5_VABI sceAgcRewindPatchSetRewindState(uint32_t* cmd, uint8_t state) { + return SetRewindState(cmd, state, __func__); +} + +int APS5_VABI sceAgcAsyncRewindPatchSetRewindState(std::uint32_t* cmd, std::uint8_t state) { + return SetRewindState(cmd, state, __func__); +} + +} diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index b4b94f6a4..3cb827709 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -121,12 +121,6 @@ int APS5_VABI sceAgcUnknown_ICkECTBxrMw() { return 0; } -APS5_EXPORT("Ikfdt-rIqCE", sceAgcUnknown_Ikfdt_MrIqCE); -int APS5_VABI sceAgcUnknown_Ikfdt_MrIqCE() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - APS5_EXPORT("RTpj-tIlvZc", sceAgcUnknown_RTpj_MtIlvZc); int APS5_VABI sceAgcUnknown_RTpj_MtIlvZc() { NotImplemented_nid_no_patch(__func__); From 2476bfda5645a1b970be951de5cab35c6d29b460 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 07:54:04 +0300 Subject: [PATCH 05/40] feat(agc-driver): depth/stencil and mip-chain targets, more color formats - Depth/stencil: draws that test depth or stencil against a bound surface render with a GPU-only depth/stencil attachment (DepthSurface), cleared to DB_DEPTH_CLEAR/DB_STENCIL_CLEAR when made. Z test/write/function and per face the stencil function, operations and masks (BACKFACE_ENABLE); replacements by another value where Vulkan's single reference can carry them. Clear and copy draws, add/sub other than 1, array or mip views and MSAA throw, and texture reads of a surface's memory are refused. Scaleform menus mask with the stencil (REPLACE 0, ADD_CLAMP, EQUAL); their masked content was skipped before. - Mipmapped color targets render through the resident image of the whole chain with a view of the mip, so mips in the packed tail are drawn too; CB_COLOR_VIEW's mip level is read from bits 26-29 (GFX10). - Color: SNORM targets, UNORM16/SNORM16 exports, one-component formats honouring COMP_SWAP (A8), 32_R/32_GR/32_AR exports, linear rows padded to 256 bytes, BLEND_BYPASS. - INDIRECT_BUFFER jumps are followed when copying a submission; thin 3D textures in display and rotated XOR swizzles; only the bytes the GPU changed are written back; resource registration. - VideoOut: a flip stalls the GPU queue while the flip queue is full and holds the submitter back. - APS5_DUMP_FRAMES_EVERY dumps every k-th presented frame. - The graphics test target builds again (missing sources, a hand-aligned 64 KiB buffer, stale expectations updated) and gains DepthStencilTests. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 + .../Execution/include/VideoOutput.hpp | 3 + .../libSceAgcDriver/Execution/src/Driver.cpp | 155 ++++++++++--- .../Execution/src/GuestMemory.cpp | 15 +- .../prx/libSceAgcDriver/Execution/src/Pm4.cpp | 15 +- .../Execution/src/VulkanDevice.cpp | 17 +- .../Graphics/include/DepthSurface.hpp | 23 ++ .../Graphics/include/Pipeline.hpp | 9 +- .../Graphics/include/ShaderInputState.hpp | 2 +- .../Graphics/include/State.hpp | 45 +++- .../Graphics/include/Texture.hpp | 4 +- .../Graphics/src/ColorTargetLayout.cpp | 7 +- .../Graphics/src/ColorTargetTransfer.cpp | 4 +- .../Graphics/src/DepthSurface.cpp | 130 +++++++++++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 33 ++- .../Graphics/src/GuestTextureResource.cpp | 7 + .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 37 +++- .../Graphics/src/ShaderInputState.cpp | 9 +- .../libSceAgcDriver/Graphics/src/State.cpp | 206 +++++++++++++++--- .../libSceAgcDriver/Graphics/src/Texture.cpp | 9 +- .../Graphics/src/TextureTiling.cpp | 16 +- .../Resource/src/Lifecycle.cpp | 7 +- .../Resource/src/Registration.cpp | 1 - .../State/src/Configuration.cpp | 8 +- .../tests/ColorTargetLayout.cpp | 12 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 85 +++++++- core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 13 ++ .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 31 +-- 28 files changed, 781 insertions(+), 128 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e97930867..fb36e364d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -59,6 +59,7 @@ add_library(libSceAgcDriver SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/AspectFit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Draw.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DepthSurface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetLayout.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetTransfer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Pipeline.cpp @@ -219,6 +220,7 @@ add_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL tests/GuestSamplerResource.cpp tests/TextureDetiler.cpp Graphics/src/Draw.cpp + Graphics/src/DepthSurface.cpp Graphics/src/Pipeline.cpp Graphics/src/State.cpp Graphics/src/ColorTargetLayout.cpp @@ -239,8 +241,11 @@ add_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL Graphics/src/Texture.cpp Graphics/src/UnitShadow.cpp Graphics/src/Sampler.cpp + Graphics/src/Recorder.cpp + Graphics/src/ShaderInputState.cpp ${agcTextureDetileShaderHeader} Execution/src/GuestMemory.cpp + Execution/src/Pm4.cpp ) target_include_directories(agc_driver_graphics_tests PRIVATE @@ -327,6 +332,7 @@ if(AGC_BUILD_VISUAL_TEST) Execution/src/GuestMemory.cpp Execution/src/BdaFeatures.cpp Graphics/src/Draw.cpp + Graphics/src/DepthSurface.cpp Graphics/src/ColorTargetLayout.cpp Graphics/src/ColorTargetTransfer.cpp Graphics/src/Pipeline.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp index 79f96d8c1..8669b4859 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp @@ -40,6 +40,9 @@ class IVideoOutput { public: virtual ~IVideoOutput() = default; virtual std::shared_ptr Reserve(const FlipInfo& info) = 0; + // Blocks a submission carrying a flip until the output's flip queue has room, as the system + // holds back a submitter that runs ahead of the display. Called without the driver's lock. + virtual void WaitForFlipRoom() {} virtual std::shared_ptr CaptureRenderingWait(std::uint32_t index) { throw std::runtime_error("VideoOut: rendering waits are unsupported by this output"); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp index c34a57bfa..55bd2b36e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp @@ -203,6 +203,10 @@ struct Submission { // submission trusts only labels the recorder noted with a newer stamp (see Recorder::NoteLabel). std::uint64_t received = 0; std::chrono::steady_clock::time_point enqueuedAt{}; + // The guest commands behind a REWIND (commands stop at it): the title writes them after submitting + // and marks the REWIND valid, so they are read only then (Driver::executeRewindTail). + const std::uint32_t* rewindTail = nullptr; + std::size_t rewindWords = 0; }; std::uint32_t readRegister(const Registers& registers, std::uint32_t offset) { @@ -524,6 +528,117 @@ private: public: + // Copies a submission's commands, stopping after the first REWIND: what follows it is read when the + // REWIND becomes valid. INDIRECT_BUFFER packets are followed as the CP follows them: a chained one + // (bit 20 of its size dword) replaces the rest of its buffer with the target, a call inserts the + // target and continues after the packet. + static void copyCommands(Submission& submission, const std::uint32_t* guest, std::size_t words) { + submission.commands.clear(); + std::size_t budget = std::size_t{1} << 26u; + copySegment(submission, guest, words, budget); + } + + // Appends one command segment; true when a REWIND ended the copy. + static bool copySegment(Submission& submission, const std::uint32_t* guest, std::size_t words, std::size_t& budget) { + require(words <= budget, "command buffer jumps exceed the copy limit (a jump loop?)"); + budget -= words; + for (std::size_t cursor = 0; cursor < words;) { + const auto header = guest[cursor]; + if (Pm4::FillerPacket(header)) { submission.commands.push_back(header); ++cursor; continue; } + const auto count = (header & 0xc0000000u) == 0xc0000000u ? Pm4::PacketWords(header) : words - cursor; + if ((header & 0xc0000000u) != 0xc0000000u || count > words - cursor) { + // Malformed: copied as is for validate to report with its context. + submission.commands.insert(submission.commands.end(), guest + cursor, guest + words); + return false; + } + const auto opcode = (header >> 8u) & 0xffu; + if (opcode == 0x3fu) { + require(count == 4, "invalid INDIRECT_BUFFER size"); + const auto* target = reinterpret_cast(static_cast(guest[cursor + 1] & ~3u) | (static_cast(guest[cursor + 2] & 0xffffu) << 32u)); + const std::size_t targetWords = guest[cursor + 3] & 0xfffffu; + const bool chain = (guest[cursor + 3] & (1u << 20u)) != 0; + GuestMemory::CheckRange(target, targetWords * sizeof(std::uint32_t), alignof(std::uint32_t)); + if (copySegment(submission, target, targetWords, budget)) return true; + if (chain) return false; + cursor += count; + continue; + } + submission.commands.insert(submission.commands.end(), guest + cursor, guest + cursor + count); + cursor += count; + if (opcode == 0x59u) { + submission.rewindTail = guest + cursor; + submission.rewindWords = words - cursor; + return true; + } + } + return false; + } + + // Holds the submitting thread while an output named by a flip in the submission has a full flip + // queue (mutex not held: the presenter needs the driver to finish flips). + void waitForFlipRoom(const Submission& submission) { + for (std::size_t cursor = 0; cursor < submission.commands.size(); cursor += Pm4::PacketWords(submission.commands[cursor])) { + if (submission.commands[cursor] != FlipPacketHeader) continue; + std::shared_ptr output; + { + std::lock_guard lock(mutex); + const auto found = outputs.find(submission.commands[cursor + 1]); + require(found != outputs.end(), "flip references an unregistered video output"); + output = found->second; + } + output->WaitForFlipRoom(); + } + } + + // Takes the flip reservations and rendering waits the submission's packets name (mutex held). + void reserveOutputs(Submission& submission) { + for (std::size_t cursor = 0; cursor < submission.commands.size();) { + const auto* words = submission.commands.data() + cursor; + if (words[0] == RenderingWaitPacketHeader) { + const auto output = outputs.find(words[1]); + require(output != outputs.end(), "rendering wait references an unregistered video output"); + auto wait = output->second->CaptureRenderingWait(words[2]); + require(wait != nullptr, "video output returned a null rendering wait"); + submission.renderingWaits.emplace(cursor, std::move(wait)); + } + if (words[0] == FlipPacketHeader) { + const auto output = outputs.find(words[1]); + require(output != outputs.end(), "flip references an unregistered video output"); + const FlipInfo info{words[1], std::bit_cast(words[2]), words[3], std::bit_cast(static_cast(words[4]) | (static_cast(words[5]) << 32u))}; + auto request = output->second->Reserve(info); + require(request != nullptr, "video output returned a null flip reservation"); + submission.flips.emplace(cursor, std::move(request)); + } + cursor += Pm4::PacketWords(words[0]); + } + } + + // REWIND stalls the queue until the CPU marks it valid (sceAgcRewindPatchSetRewindState); the + // commands behind it are copied from guest memory only then and run as a continuation. + void executeRewindTail(const Submission& stalled) { + std::atomic_ref control(*const_cast(stalled.rewindTail - 1)); + while ((control.load(std::memory_order_acquire) & 0x80000000u) == 0) { + CheckFailure(); + checkStopping(); + PollSleep(); + } + Submission tail{}; + tail.queue = stalled.queue; + copyCommands(tail, stalled.rewindTail, stalled.rewindWords); + validate(tail.commands, tail.queue, stalled.rewindTail); + waitForFlipRoom(tail); + { + std::lock_guard lock(mutex); + rethrowFailure(); + checkStopping(); + reserveOutputs(tail); + tail.shaders = shaders; + tail.serial = stalled.serial; + tail.received = ++eventSerial; + } + execute(tail); + } + void Submit(const Packet* packet, std::uint32_t queue) { CheckFailure(); require(queue == 0 || (queue >= 0x20 && queue < 0x58), "unsupported compute queue"); @@ -538,10 +653,11 @@ public: if (descriptor.dw_num != 0) { require(descriptor.dw_num <= std::numeric_limits::max() / sizeof(std::uint32_t), "command size overflow"); GuestMemory::CheckRange(descriptor.addr, static_cast(descriptor.dw_num) * sizeof(std::uint32_t), alignof(std::uint32_t)); - submission.commands.assign(descriptor.addr, descriptor.addr + descriptor.dw_num); + copyCommands(submission, descriptor.addr, descriptor.dw_num); } const auto copied = profile ? std::chrono::steady_clock::now() : start; validate(submission.commands, queue, descriptor.addr); + waitForFlipRoom(submission); static const bool trace = std::getenv("APS5_TRACE_GPU") != nullptr; if (trace) std::fprintf(stderr, "[gpu] %.1f submit queue=0x%x dwords=%zu at %p\n", TraceMs(), queue, submission.commands.size(), static_cast(descriptor.addr)); const auto validated = profile ? std::chrono::steady_clock::now() : start; @@ -550,25 +666,7 @@ public: rethrowFailure(); checkStopping(); require(accepted != std::numeric_limits::max(), "submission serial overflow"); - for (std::size_t cursor = 0; cursor < submission.commands.size();) { - const auto* words = submission.commands.data() + cursor; - if (words[0] == RenderingWaitPacketHeader) { - const auto output = outputs.find(words[1]); - require(output != outputs.end(), "rendering wait references an unregistered video output"); - auto wait = output->second->CaptureRenderingWait(words[2]); - require(wait != nullptr, "video output returned a null rendering wait"); - submission.renderingWaits.emplace(cursor, std::move(wait)); - } - if (words[0] == FlipPacketHeader) { - const auto output = outputs.find(words[1]); - require(output != outputs.end(), "flip references an unregistered video output"); - const FlipInfo info{words[1], std::bit_cast(words[2]), words[3], std::bit_cast(static_cast(words[4]) | (static_cast(words[5]) << 32u))}; - auto request = output->second->Reserve(info); - require(request != nullptr, "video output returned a null flip reservation"); - submission.flips.emplace(cursor, std::move(request)); - } - cursor += Pm4::PacketWords(words[0]); - } + reserveOutputs(submission); submission.shaders = shaders; submission.serial = accepted + 1; // Every CPU store the game made before this call precedes the stamp; labels recorded @@ -3889,13 +3987,19 @@ private: if (!drawParameters.indirect && !drawParameters.indexed && (drawParameters.indexCount == 0 || drawParameters.instanceCount == 0)) return DrawVerdict::Nothing; static const bool traceIndirect = std::getenv("APS5_TRACE_INDIRECT_DRAWS") != nullptr; if (traceIndirect && drawParameters.indirect) std::fprintf(stderr, "[draw] indirect packet %s args 0x%llx count %u reached\n", Pm4::Name(packet[0]).c_str(), static_cast(drawParameters.indirect->arguments), drawParameters.indirect->count); - // Depth/stencil-only passes (no color writes, no pixel shader) have no effect without depth - // targets, which are not emulated. + // A draw without color writes or a pixel shader draws nothing, unless it tests depth or + // stencil against a bound surface: such depth-only draws need a pipeline without a + // fragment stage, which is not implemented. { const auto targetMask = queue.context.find(0x8e); const auto shaderMask = queue.context.find(0x8f); const bool colorWrites = targetMask != queue.context.end() && shaderMask != queue.context.end() && (targetMask->second & shaderMask->second) != 0; - if (!colorWrites && !queue.shader.contains(0x8)) return DrawVerdict::Nothing; + if (!colorWrites && !queue.shader.contains(0x8)) { + const auto word = [&](std::uint32_t offset) { const auto it = queue.context.find(offset); return it == queue.context.end() ? 0u : it->second; }; + if ((word(0x200) & 3u) == 0 || ((word(0x010) & 3u) == 0 && (word(0x011) & 1u) == 0)) return DrawVerdict::Nothing; + rejected = "AGC graphics: depth/stencil-only draws without a pixel shader are not implemented"; + return DrawVerdict::Rejected; + } } if (drawParameters.indexed) { const auto restart = queue.userConfig.find(0x24b); @@ -4057,7 +4161,7 @@ private: } append(0x008, 1, Stage::Fragment, 0x00b, 0x00c, Role::Fragment); programs.back().firstUserSgpr = 0; - product->pixel = Graphics::DecodePixelStageInfo(queue.context, graphics.hasColorTarget, graphics.color.componentMapping); + product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(graphics)); return product; }; if (decode == nullptr || VerifyDrawRecipe()) { @@ -6090,7 +6194,7 @@ private: static const bool submitAtEnd = std::getenv("APS5_SUBMIT_AT_END") != nullptr; if (!deferredLabels().labels.empty() || Graphics::Recorder::PendingLabelSince().has_value() || Graphics::Recorder::RecordedWorkSinceSubmit() != 0) { auto& costs = submissionCosts(submission.queue); - if (!submitAtEnd && submission.queue == 0 && workerQueued() != nullptr && workerQueued()->load(std::memory_order_acquire) != 0) { + if (!submitAtEnd && submission.rewindTail == nullptr && submission.queue == 0 && workerQueued() != nullptr && workerQueued()->load(std::memory_order_acquire) != 0) { ++costs.endSkipped; return; } @@ -6102,6 +6206,7 @@ private: recordDeferredLabels(localDevice.get(), submission.queue); if (localDevice != nullptr) localDevice->SubmitRecorded(submission.queue == 0); } + if (submission.rewindTail != nullptr) executeRewindTail(submission); } // The calling worker's pending count (QueueWorker::queued), set by run(); null off a worker. diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 561ef3b50..c6f588ffe 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1365,9 +1365,18 @@ void WriteChanged(std::uint64_t address, std::span current, std std::size_t lastChanged = 0; for (std::size_t at = 0; at < size; at += block) { if (!differs(at)) continue; - std::memcpy(destination + at, current.data() + at, std::min(block, size - at)); - firstChanged = std::min(firstChanged, at); - lastChanged = std::min(at + block, size); + // Only the bytes the GPU changed: the rest of the block holds the snapshot, and storing it + // would roll back what the CPU wrote there since (a title lost an object's vtable pointer). + const auto blockEnd = std::min(at + block, size); + for (std::size_t run = at; run < blockEnd;) { + if (current[run] == original[run]) { ++run; continue; } + auto runEnd = run + 1; + while (runEnd < blockEnd && current[runEnd] != original[runEnd]) ++runEnd; + std::memcpy(destination + run, current.data() + run, runEnd - run); + firstChanged = std::min(firstChanged, run); + lastChanged = std::max(lastChanged, runEnd); + run = runEnd; + } } // Stamped like a GPU write: a collect memoized for this packet would not see the page fault. if (firstChanged < lastChanged) MarkWritten(address + firstChanged, lastChanged - firstChanged); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index ff230342a..ea62413b1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -30,7 +30,11 @@ std::uint64_t address(std::uint32_t low, std::uint32_t high) { std::uint32_t registerOffset(std::uint32_t value) { require(value != 0xffffffffu, "indirect register sentinel semantics are not implemented"); const auto offset = value & ~0x70000000u; - require(offset <= 0xffffu, "extended register semantics are not implemented"); + if (offset > 0xffffu) { + char what[80]; + std::snprintf(what, sizeof(what), "extended register semantics are not implemented (offset dword 0x%08x)", value); + throw std::runtime_error(what); + } return offset; } @@ -175,8 +179,9 @@ std::string_view UnsupportedReason(std::uint32_t header) { case 0x22: return "conditional command execution and conditional flip reservation are not implemented"; case 0x33: case 0x3f: return "nested command buffers, branching and nested flip reservation are not implemented"; case 0x3c: case 0x93: return {}; - case 0x39: case 0x59: + case 0x39: return "cooperative command-queue waits are not implemented"; + case 0x59: return {}; case 0x84: case 0x85: case 0x86: case 0x88: return "separate CE/DE execution and counter synchronization are not implemented"; case 0x49: return {}; @@ -341,6 +346,11 @@ void Validate(std::span packet, std::uint32_t queue) { require(packet.size() - 2 <= 0x10000u - offset, "register range overflow"); break; } + case 0x59: + // REWIND: bit 31 is the valid flag the CPU sets; the driver waits for it (see Driver). + size(2); + require((packet[1] & 0x7fffffffu) == 0, "unsupported REWIND payload bits"); + break; case 0x3c: case 0x93: { size(opcode == 0x3c ? 7 : 9); require((packet[1] & 0x10u) != 0, "register-space WAIT_REG_MEM is not implemented"); @@ -688,6 +698,7 @@ void Execute(std::span packet, QueueState& queue) { } return; } + case 0x59: break; case 0x3c: case 0x93: { // The waited-on value is written by the CPU or another queue; poll it like the CP would. const auto start = std::chrono::steady_clock::now(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 83bd0b373..9aa13dfb5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" @@ -489,6 +490,8 @@ struct VulkanDevice::State { // Cached graphics pipelines (with their framebuffers, modules, render passes and layouts) // belong to this device and must be destroyed while it lives. Graphics::ClearCachedPipelines(device); + // Their framebuffers were the last users of the depth surfaces' views. + Graphics::ClearDepthSurfaces(device); { std::lock_guard pipelines(computePipelinesMutex); computePipelines.clear(); @@ -779,6 +782,9 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u // Gathers with non-constant offsets (ImageGatherExtended). enabled.shaderImageGatherExtended = available.shaderImageGatherExtended; if (enabled.shaderImageGatherExtended) state->capabilities.push_back(spv::CapabilityImageGatherExtended); + // Sampling with an LOD clamp (image_sample_*_cl) as the MinLod image operand. + enabled.shaderResourceMinLod = available.shaderResourceMinLod; + if (enabled.shaderResourceMinLod) state->capabilities.push_back(spv::CapabilityMinLod); if (enabled.shaderStorageImageWriteWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageWriteWithoutFormat); if (enabled.shaderStorageImageReadWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageReadWithoutFormat); // Bindless image tables index an image array with a wave-uniform runtime slot. @@ -1688,17 +1694,24 @@ bool NoResidentPresent() { // Debug aid: APS5_DUMP_FRAMES= saves the first n presented display buffers as frame_.bmp, // read back by the blit's own submission and written after its fence (RetireSlot), or with // APS5_NO_GPU_DUMP=1 decoded from the tiled guest buffer on the CPU, as before. +// APS5_DUMP_FRAMES_EVERY= dumps every k-th presented frame (up to APS5_DUMP_FRAMES of them) +// instead of the first ones, to reach screens far into a run. struct FrameDumps { int limit; bool cpu; + int every = 1; int dumped = 0; + std::uint64_t presents = 0; }; FrameDumps& Dumps() { static FrameDumps dumps{[] { const char* value = std::getenv("APS5_DUMP_FRAMES"); return value ? std::atoi(value) : 0; - }(), std::getenv("APS5_NO_GPU_DUMP") != nullptr}; + }(), std::getenv("APS5_NO_GPU_DUMP") != nullptr, [] { + const char* value = std::getenv("APS5_DUMP_FRAMES_EVERY"); + return value ? std::max(std::atoi(value), 1) : 1; + }()}; return dumps; } @@ -1742,7 +1755,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { } auto& dumps = Dumps(); bool dumpFrame = false; - if (dumps.dumped < dumps.limit) { + if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { if (dumps.cpu) { const auto full = ReadDisplayBuffer(buffer); WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, full, DumpScale()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp new file mode 100644 index 000000000..424a80d85 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -0,0 +1,23 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DEPTHSURFACE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DEPTHSURFACE_HPP + +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include + +namespace AgcDriver::Graphics { + +// The attachment view of the device's image for a depth/stencil target, made on first use. The +// image lives on the GPU only, always in the general layout: its guest surface (tiled, HTILE +// compressed) is never read or written, so a new image starts at the target's clear values, and +// texture reads of the surface are refused (DepthSurfaceAt). Images stay until the device goes +// (ClearDepthSurfaces), so their views are stable for cached framebuffers. +// ponytail: never evicted, one image per distinct surface; evict (and keep recorded ones alive) if +// titles churn through many. +VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); +void ClearDepthSurfaces(VkDevice device); +// Whether the depth or stencil plane of a surface drawn with starts at `address`. +bool DepthSurfaceAt(std::uint64_t address); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index efa0a9514..ef98ccc0f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -40,9 +40,11 @@ public: Pipeline(const Pipeline&) = delete; Pipeline& operator=(const Pipeline&) = delete; VkPipelineLayout Layout() const; - // The framebuffer of these attachment views. A resident target's view (`owners[i]` set) is stable - // while its StorageTexture lives, so framebuffers made of resident views are kept in the pipeline - // and reused when every owner is still the same object; any other set is built per call. + // The framebuffer of these attachment views: the color views, then the depth surface's when the + // pipeline has one. A resident target's view (`owners[i]` set) is stable while its + // StorageTexture lives, and a depth surface's while its device does, so framebuffers made of + // resident views are kept in the pipeline and reused when every owner is still the same + // object; any other set is built per call. std::shared_ptr AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent); // Begins the render pass on the framebuffer, binds the pipeline and sets viewport and scissor. void Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, VkExtent2D extent, const VkViewport& viewport, const VkRect2D& scissor) const; @@ -69,6 +71,7 @@ private: VkRenderPass renderPass = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE; std::size_t attachments = 0; + std::size_t colorAttachments = 0; std::vector framebuffers; }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp index 540d73af7..df16329a9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp @@ -10,7 +10,7 @@ namespace AgcDriver::Graphics { -ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, bool hasColorTarget, std::uint8_t colorComponentMapping); +ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array& exportMappings); ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& shader); // A guest range DecodeVertexStageInfo read (an attribute word, a vertex V#) with the bytes as read: // the draw cache validates them by value with the stage's capture (design_cpu_final rule RD). diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index fd90957c3..c4e32973a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -40,10 +40,40 @@ struct ColorTarget { // DCC metadata of a compressed target (CB_COLOR_INFO DCC_ENABLE), or 0 (see DccMetadata.hpp). std::uint64_t dccAddress = 0; bool dccAlphaOnMsb = false; + // A view of one mip of a mipmapped surface (CB_COLOR_VIEW MIP_LEVEL, CB_COLOR_ATTRIB2 MAX_MIP): + // the surface's base, mip 0 extent and mip count, the rendered mip, and whether it lies in the + // packed mip tail (which shares one block with the smaller mips, so it is only rendered through + // the resident image of the whole chain). `address`, `extent` and `bytes` are the mip's. + std::uint64_t surfaceAddress = 0; + VkExtent2D surfaceExtent{}; + std::uint32_t mipCount = 1; + std::uint32_t mip = 0; + bool mipTail = false; +}; + +// The depth/stencil surface a draw with depth or stencil tests renders with (DB_Z_INFO, +// DB_STENCIL_INFO and their bases). Its image lives on the GPU only (DepthSurface.hpp). +struct DepthTarget { + std::uint64_t address; + // The stencil plane, 0 without one. + std::uint64_t stencilAddress; + VkExtent2D extent; + VkFormat format; + // DB_DEPTH_CLEAR / DB_STENCIL_CLEAR: what a new image starts as. + float clearDepth; + std::uint8_t clearStencil; }; struct State { ShaderStages stages; + // Set when the draw tests depth or stencil against a bound surface; the tests below are off otherwise. + std::optional depth; + bool depthTest = false; + bool depthWrite = false; + VkCompareOp depthCompare = VK_COMPARE_OP_ALWAYS; + bool stencilTest = false; + VkStencilOpState stencilFront{}; + VkStencilOpState stencilBack{}; // MRT slot 0; `colors`/`blends` hold every written slot, attachment i being slot i. ColorTarget color; std::vector colors; @@ -64,6 +94,8 @@ struct State { ShaderStages DecodeShaderStages(const QueueState& queue); State DecodeState(const QueueState& queue); +// The component mapping of each MRT slot's export (identity past the written slots). +std::array ExportMappings(const State& state); // The message DecodeState (or the pixel stage decode after it) would throw for the register rules // this precheck covers, evaluated without exceptions before the draw is decoded; empty when they // pass (DecodeState still checks everything). A register a rule needs that is absent is no verdict. @@ -99,11 +131,14 @@ struct DrawKeyRange { std::uint32_t first; std::uint32_t count; }; -inline constexpr std::array DrawKeyRegisters{{ - // PA_SC_SCREEN_SCISSOR, the window offset/scissor and clip rect, the edge rule, the hardware - // screen offset, CB_TARGET_MASK/CB_SHADER_MASK, the generic and viewport 0 scissors, the - // viewport 0 depth clamp, the blend constants, the viewport 0 transform. - {RegisterBank::Context, 0x00c, 2}, {RegisterBank::Context, 0x080, 4}, {RegisterBank::Context, 0x08c, 4}, {RegisterBank::Context, 0x090, 2}, {RegisterBank::Context, 0x094, 2}, {RegisterBank::Context, 0x0b4, 2}, {RegisterBank::Context, 0x105, 4}, {RegisterBank::Context, 0x10f, 6}, +inline constexpr std::array DrawKeyRegisters{{ + // DB_RENDER_CONTROL, DB_DEPTH_VIEW, DB_DEPTH_SIZE_XY, DB_STENCIL_CLEAR/DB_DEPTH_CLEAR with + // PA_SC_SCREEN_SCISSOR, DB_Z_INFO .. DB_STENCIL_WRITE_BASE and their high words; the window + // offset/scissor and clip rect, the edge rule, the hardware screen offset, CB_TARGET_MASK/ + // CB_SHADER_MASK, the generic and viewport 0 scissors, the viewport 0 depth clamp, the blend + // constants, DB_STENCIL_CONTROL/DB_STENCILREFMASK(_BF), the viewport 0 transform. + {RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 1}, {RegisterBank::Context, 0x007, 1}, {RegisterBank::Context, 0x00a, 4}, {RegisterBank::Context, 0x010, 6}, {RegisterBank::Context, 0x01a, 4}, + {RegisterBank::Context, 0x080, 4}, {RegisterBank::Context, 0x08c, 4}, {RegisterBank::Context, 0x090, 2}, {RegisterBank::Context, 0x094, 2}, {RegisterBank::Context, 0x0b4, 2}, {RegisterBank::Context, 0x105, 4}, {RegisterBank::Context, 0x10b, 3}, {RegisterBank::Context, 0x10f, 6}, // SPI_PS_INPUT_CNTL_0..31, SPI_PS_INPUT_ENA/ADDR, SPI_PS_IN_CONTROL, SPI_SHADER_POS/Z/COL_FORMAT, // CB_BLEND0..7_CONTROL, GE_MAX_OUTPUT_PER_SUBGROUP. {RegisterBank::Context, 0x191, 32}, {RegisterBank::Context, 0x1b3, 2}, {RegisterBank::Context, 0x1b6, 1}, {RegisterBank::Context, 0x1c3, 3}, {RegisterBank::Context, 0x1e0, 8}, {RegisterBank::Context, 0x1ff, 1}, diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 3e42c0c42..ff073f026 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -111,7 +111,7 @@ public: // Render targets live in the same images: draws attach mip 0 through a view of the color // buffer's format and mark the image dirty like a storage write. bool Attachable() const { return attachable; } - VkImageView AttachmentView(VkFormat format); + VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0); void WriteBack(); // Deferred write-back (APS5_EAGER_WRITEBACK=1 stores at once instead). void MarkDirty(); @@ -403,7 +403,7 @@ private: std::uint32_t defaultMip = 0; std::map extraViews; bool attachable = false; - std::map attachmentViews; + std::map, VkImageView> attachmentViews; VkFormat storageFormat = VK_FORMAT_UNDEFINED; // Results are on the GPU only (guarded by the pending-write registry lock). bool dirty = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp index f9d4c41e9..1af888ca6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp @@ -32,9 +32,12 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, require(std::has_single_bit(bytesPerElement) && bytesPerElement <= 16u, "AGC graphics: unsupported color element size"); std::uint32_t paddedHeight = height; switch (mode) { - case ColorTileMode::Linear: - require(width * bytesPerElement % 256u == 0, "AGC graphics: linear surface pitch requires a width aligned to 256 bytes"); + case ColorTileMode::Linear: { + // Rows are padded to 256 bytes, addrlib's linear pitch alignment (linear textures use the same). + const auto pitchAlignment = 256u / bytesPerElement; + pitch = (width + pitchAlignment - 1u) / pitchAlignment * pitchAlignment; break; + } case ColorTileMode::RenderTarget: { // SW_64KB_R_X: 64 KiB blocks of 2^(16 - log2(bpe)) elements, wider than tall for odd powers. const auto log2Elements = 16u - static_cast(std::countr_zero(bytesPerElement)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetTransfer.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetTransfer.cpp index 9d9d7d346..36690adc4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetTransfer.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetTransfer.cpp @@ -9,7 +9,7 @@ void ReadColorTarget(const ColorTarget& target, std::span destination PerformanceTimer timing("ColorTarget.Read"); const ColorTargetLayout layout(target.extent.width, target.extent.height, target.tileMode, target.elementBytes); Require(target.bytes == layout.Bytes() && destination.size() == layout.LinearBytes(), "color target transfer size mismatch"); - if (target.tileMode == ColorTileMode::Linear) { + if (target.tileMode == ColorTileMode::Linear && layout.Bytes() == layout.LinearBytes()) { GuestMemory::Read(target.address, destination, layout.Alignment()); return; } @@ -25,7 +25,7 @@ void WriteColorTarget(const ColorTarget& target, std::span sour PerformanceTimer timing("ColorTarget.Write"); const ColorTargetLayout layout(target.extent.width, target.extent.height, target.tileMode, target.elementBytes); Require(target.bytes == layout.Bytes() && source.size() == layout.LinearBytes(), "color target transfer size mismatch"); - if (target.tileMode == ColorTileMode::Linear) { + if (target.tileMode == ColorTileMode::Linear && layout.Bytes() == layout.LinearBytes()) { GuestMemory::Write(target.address, source, layout.Alignment()); return; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp new file mode 100644 index 000000000..ddb073359 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -0,0 +1,130 @@ +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include +#include +#include +#include + +namespace AgcDriver::Graphics { +namespace { + +class DepthSurface { +public: + DepthSurface(const Context& context, const DepthTarget& target) : context(context), target(target) { + this->context.bufferPool.reset(); + VkFormatProperties properties{}; + context.formatProperties(context.physical, target.format, &properties); + Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0, "depth/stencil format " + std::to_string(target.format) + " cannot be an attachment on this device"); + Require(target.extent.width <= context.limits.maxFramebufferWidth && target.extent.height <= context.limits.maxFramebufferHeight, "depth target exceeds framebuffer limits"); + const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (target.stencilAddress != 0 ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); + try { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = target.format; + info.extent = {target.extent.width, target.extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = VK_SAMPLE_COUNT_1_BIT; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage depth"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory depth target"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory depth"); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = target.format; + viewInfo.subresourceRange = {aspects, 0, 1, 0, 1}; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth"); + // Cleared in the general layout it keeps from then on, recorded ahead of the draw (a draw's + // pass begins behind a barrier from every earlier transfer write). + auto* recorder = Recorder::Active(); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toGeneral.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; + toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.image = image; + toGeneral.subresourceRange = viewInfo.subresourceRange; + const auto barrier = context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier"); + barrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + const VkClearDepthStencilValue clear{target.clearDepth, target.clearStencil}; + context.Function("vkCmdClearDepthStencilImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &toGeneral.subresourceRange); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT); + if (batch) batch->SubmitAndWait(); + else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + } catch (...) { + release(); + throw; + } + } + ~DepthSurface() { release(); } + DepthSurface(const DepthSurface&) = delete; + DepthSurface& operator=(const DepthSurface&) = delete; + + const Context context; + const DepthTarget target; + VkImage image = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + +private: + void release() noexcept { + if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + view = VK_NULL_HANDLE; + image = VK_NULL_HANDLE; + memory = VK_NULL_HANDLE; + } +}; + +bool sameSurface(const DepthTarget& a, const DepthTarget& b) { + return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format; +} + +std::mutex& surfacesMutex() { + static std::mutex mutex; + return mutex; +} + +// Never destroyed with the statics: the images belong to a device that may be gone by then. +std::vector>& surfaces() { + static auto* list = new std::vector>(); + return *list; +} + +} + +VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (surface->context.device == context.device && sameSurface(surface->target, target)) return surface->view; + } + surfaces().push_back(std::make_unique(context, target)); + return surfaces().back()->view; +} + +void ClearDepthSurfaces(VkDevice device) { + std::lock_guard lock(surfacesMutex()); + std::erase_if(surfaces(), [&](const auto& surface) { return surface->context.device == device; }); +} + +bool DepthSurfaceAt(std::uint64_t address) { + std::lock_guard lock(surfacesMutex()); + return std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& surface) { return surface->target.address == address || surface->target.stencilAddress == address; }); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 73e9dfa33..e6b9ae5c5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" @@ -41,15 +42,17 @@ std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) { // The color buffer as a single-mip 2D surface descriptor (tile mode SW_64KB_R_X). GuestTextureResource SurfaceForTarget(const ColorTarget& color) { Require(color.tileMode == ColorTileMode::RenderTarget, "only 64 KiB tiled color targets are resident"); + // A mipmapped target is its whole chain; the attachment view picks the mip. + const bool chain = color.mipCount > 1; GuestTextureResource surface{}; - surface.baseAddress = color.address; - surface.width = color.extent.width; - surface.height = color.extent.height; + surface.baseAddress = chain ? color.surfaceAddress : color.address; + surface.width = chain ? color.surfaceExtent.width : color.extent.width; + surface.height = chain ? color.surfaceExtent.height : color.extent.height; surface.depthOrLastArray = 0; surface.baseArray = 0; - surface.mipCount = 1; + surface.mipCount = color.mipCount; surface.baseLevel = 0; - surface.lastLevel = 0; + surface.lastLevel = color.mipCount - 1; surface.tileMode = TextureTileMode::kR64KBX; surface.dimension = TextureDimension::k2D; surface.format = GuestFormatFor(color.format, color.elementBytes); @@ -1135,8 +1138,10 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam if (continued) { record.pipeline->Continue(commands, state.viewport, state.scissor); } else { - const VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT}; - context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, 0, 1, &before, 0, nullptr, 0, nullptr); + // Depth surfaces stay in the general layout too; an earlier pass's depth writes (the recorder's + // trailing barrier after a pass does not name them) are made visible here. + const VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT}; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, 0, 1, &before, 0, nullptr, 0, nullptr); countBarrier(1); APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded"); if (gpuIndirect) rewritten = recordIndirectArguments(context, commands, recorder, true, *record.indirect, scratch, argumentBuffer, argumentOffset, countBarrier); @@ -1281,15 +1286,16 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& binding.resident = refreshResidentTarget(state, color, outcome, profile, [&] { auto resident = CachedStorageSurface(context, SurfaceForTarget(color)); Require(resident->Attachable(), "storage format cannot be a color attachment"); - Require(resident->GuestBytes() == colorLayout.Bytes(), "resident image layout differs from the color layout"); + Require(color.mipCount > 1 || resident->GuestBytes() == colorLayout.Bytes(), "resident image layout differs from the color layout"); return resident; }); } if (binding.resident != nullptr) { timer.phase(PhaseReadTarget); - targetViews.push_back(binding.resident->AttachmentView(color.format)); + targetViews.push_back(binding.resident->AttachmentView(color.format, color.mip)); continue; } + Require(!color.mipTail, "rendering into a packed mip tail needs the resident image of its surface"); if (binding.gpuTiling) { binding.mip = ColorTargetMip(color, colorLayout); binding.tiled = std::make_unique(context, colorLayout.Bytes(), copies); @@ -1319,6 +1325,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& binding.target = std::make_unique(context, color, state.blends[index].blendEnable != 0); targetViews.push_back(binding.target->View()); } + // The depth surface is the last attachment (Pipeline::AcquireFramebuffer). + if (state.depth) targetViews.push_back(DepthSurfaceView(context, *state.depth)); timer.phase(PhasePrepare); const bool recordDraws = RecordDraws(); auto* recorder = Recorder::Active(); @@ -1463,7 +1471,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // only a template the resource cache serves under this content key (reusable: no lease, // no copied writes, every direct region import- or mirror-served) of a direct draw, so a // hit's proof is the template's ProveCurrent and nothing needs completion work. - if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable()) { + // ponytail: none for depth draws (the recipe replays color targets only); add the depth view when they need the hit path. + if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !state.depth) { auto recipe = std::make_shared(); recipe->device = context.device; recipe->templateRef = resources; @@ -1536,6 +1545,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& upload.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, 0, 1, &upload, 0, nullptr, 0, nullptr); countBarrier(); + if (state.depth) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT); + countBarrier(); + } APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded"); if (gpuIndirect) rewritten = recordIndirectArguments(context, commands, recorder, recorded, indirect, scratch, argumentBuffer, argumentOffset, countBarrier); for (auto& binding : targets) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 3cd364eae..cf4db9175 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -1,6 +1,7 @@ #include #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include #include #include @@ -46,6 +47,12 @@ GuestTextureResource DecodeTextureResource(std::span words) const auto base40 = (static_cast(words[0]) | (static_cast(words[1]) << 32u)) & 0xffffffffffull; const auto baseAddress = base40 << 8u; Require(baseAddress != 0, "guest texture descriptor has a null base address"); + // A depth surface's contents live on the GPU only; its guest memory is stale. + if (DepthSurfaceAt(baseAddress)) { + char text[96]; + std::snprintf(text, sizeof(text), "AGC graphics: sampling depth/stencil surface 0x%llx is not implemented", static_cast(baseAddress)); + throw std::runtime_error(text); + } const auto minLod = (words[1] >> 8u) & 0xfffu; const auto format = (words[1] >> 20u) & 0x1ffu; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index af75014dd..c06f4471b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -41,7 +41,7 @@ void ValidateViewport(const Context& context, const VkViewport& viewport) { Require(std::min(viewport.y, viewport.y + viewport.height) >= context.limits.viewportBoundsRange[0] && std::max(viewport.y, viewport.y + viewport.height) <= context.limits.viewportBoundsRange[1], "viewport Y exceeds device bounds"); } -Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size()) { +Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()) { // A cached pipeline may outlive its device's teardown (see ClearCachedPipelines); it must not keep // the buffer pool, which is reset with the device, alive past it. this->context.bufferPool.reset(); @@ -105,6 +105,21 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = static_cast(references.size()); subpass.pColorAttachments = references.empty() ? nullptr : references.data(); + // The depth surface stays in the general layout (DepthSurface.hpp). + const VkAttachmentReference depthReference{static_cast(colors.size()), VK_IMAGE_LAYOUT_GENERAL}; + if (state.depth) { + VkAttachmentDescription depth{}; + depth.format = state.depth->format; + depth.samples = VK_SAMPLE_COUNT_1_BIT; + depth.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + depth.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + depth.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + depth.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; + depth.initialLayout = VK_IMAGE_LAYOUT_GENERAL; + depth.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + colors.push_back(depth); + subpass.pDepthStencilAttachment = &depthReference; + } VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; passInfo.attachmentCount = static_cast(colors.size()); passInfo.pAttachments = colors.empty() ? nullptr : colors.data(); @@ -138,6 +153,13 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput raster.lineWidth = 1; VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; + depthStencil.depthTestEnable = state.depthTest; + depthStencil.depthWriteEnable = state.depthWrite; + depthStencil.depthCompareOp = state.depthCompare; + depthStencil.stencilTestEnable = state.stencilTest; + depthStencil.front = state.stencilFront; + depthStencil.back = state.stencilBack; VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; blend.attachmentCount = static_cast(state.blends.size()); blend.pAttachments = state.blends.empty() ? nullptr : state.blends.data(); @@ -155,6 +177,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput pipelineInfo.pViewportState = &viewports; pipelineInfo.pRasterizationState = &raster; pipelineInfo.pMultisampleState = &samples; + pipelineInfo.pDepthStencilState = state.depth ? &depthStencil : nullptr; pipelineInfo.pColorBlendState = &blend; pipelineInfo.pDynamicState = &dynamic; pipelineInfo.layout = layout; @@ -202,7 +225,7 @@ VkPipelineLayout Pipeline::Layout() const { } std::shared_ptr Pipeline::AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent) { - Require(targets.size() == attachments && targets.size() == owners.size(), "render targets do not match the pipeline's color attachments"); + Require(targets.size() == attachments && owners.size() == colorAttachments, "render targets do not match the pipeline's attachments"); const bool resident = std::all_of(owners.begin(), owners.end(), [](const auto& owner) { return owner != nullptr; }); if (resident) { // Entries whose views are gone can never match again and go as soon as no recorded draw holds @@ -322,6 +345,16 @@ std::vector pipelineKey(const Context& context, const State& state, c for (const auto value : state.blendConstants) append(key, value); append(key, state.colors.size()); for (const auto& color : state.colors) append(key, color.format); + append(key, state.depth.has_value()); + if (state.depth) { + append(key, state.depth->format); + append(key, state.depthTest); + append(key, state.depthWrite); + append(key, state.depthCompare); + append(key, state.stencilTest); + append(key, state.stencilFront); + append(key, state.stencilBack); + } append(key, state.stages.mesh.has_value()); if (state.stages.mesh) { const auto& mesh = *state.stages.mesh; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index e69fe0fb8..a45d6ee4b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -84,7 +84,7 @@ ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& }; } -ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, bool hasColorTarget, std::uint8_t colorComponentMapping) { +ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array& exportMappings) { const auto inControl = read(context, spiPsInControl, RegisterBank::Context); const auto inputNum = inControl & 0x3Fu; if (inputNum > 32u) { @@ -117,11 +117,6 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con const bool depthExportEnable = (shaderControl & 0x1u) != 0; const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0; const auto zOrder = (shaderControl >> 4u) & 0x3u; - std::array targetExportMapping{}; - targetExportMapping.fill(0xe4u); - if (hasColorTarget) { - targetExportMapping[0] = colorComponentMapping; - } return ShaderRecompiler::ShaderPixelStageInfo{ inputNum, interpolatorSettings, @@ -142,7 +137,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable, ((shaderControl >> 10u) & 0x1u) != 0, targetOutputMode, - targetExportMapping + exportMappings }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index dcc5a608e..9c771a7ec 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -79,8 +79,117 @@ bool IgnoreDepthTest() { return ignore; } -// DB_DEPTH_CONTROL: depth and stencil tests whose function is ALWAYS and that write no depth cannot -// change color output, so they are accepted without a depth target (stencil writes are dropped). +// Whether DB_Z_INFO or DB_STENCIL_INFO names a plane (an absent register names none). +bool depthSurfaceBound(const Registers& cx) { + const auto z = find(cx, 0x010); + const auto stencil = find(cx, 0x011); + return (z != cx.end() && (z->second & 3u) != 0) || (stencil != cx.end() && (stencil->second & 1u) != 0); +} + +// One face of the stencil state (the compare function, DB_STENCIL_CONTROL's three operations and +// DB_STENCILREFMASK) as Vulkan's. AMD's compare functions are in Vulkan's order. Vulkan has one +// reference per face for the test and every replacement, so a replacement by another value +// (REPLACE_OP writes STENCILOPVAL, ONES 0xff) is taken where the bits it writes are not also compared. +VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint32_t refMask, bool readOnly) { + VkStencilOpState face{}; + face.compareOp = static_cast(compare); + face.compareMask = (refMask >> 8u) & 0xffu; + face.writeMask = readOnly ? 0u : (refMask >> 16u) & 0xffu; + const auto test = refMask & 0xffu; + const auto opValue = refMask >> 24u; + auto reference = test; + // The reference bits already spoken for: the compared ones (none for NEVER and ALWAYS), then + // the replaced ones. + auto fixed = compare == 0 || compare == 7 ? 0u : face.compareMask; + const auto convert = [&](std::uint32_t op) { + if (face.writeMask == 0) return VK_STENCIL_OP_KEEP; + char detail[96]; + switch (op) { + case 0: return VK_STENCIL_OP_KEEP; + case 1: return VK_STENCIL_OP_ZERO; + case 2: + case 3: + case 4: { + const auto value = op == 2 ? 0xffu : op == 3 ? test : opValue; + if (((reference ^ value) & fixed & face.writeMask) != 0) { + std::snprintf(detail, sizeof(detail), "AGC graphics: stencil replacement 0x%02x against test value 0x%02x (masks 0x%02x/0x%02x) is unsupported", value, test, face.compareMask, face.writeMask); + throw std::runtime_error(detail); + } + reference = (reference & ~face.writeMask) | (value & face.writeMask); + fixed |= face.writeMask; + return VK_STENCIL_OP_REPLACE; + } + case 5: + case 6: + case 8: + case 9: + // ADD/SUB add STENCILOPVAL; Vulkan's increments add 1. + if (opValue != 1) { + std::snprintf(detail, sizeof(detail), "AGC graphics: stencil add/subtract of 0x%02x is unsupported", opValue); + throw std::runtime_error(detail); + } + return op == 5 ? VK_STENCIL_OP_INCREMENT_AND_CLAMP : op == 6 ? VK_STENCIL_OP_DECREMENT_AND_CLAMP : op == 8 ? VK_STENCIL_OP_INCREMENT_AND_WRAP : VK_STENCIL_OP_DECREMENT_AND_WRAP; + case 7: return VK_STENCIL_OP_INVERT; + default: + std::snprintf(detail, sizeof(detail), "AGC graphics: stencil operation %u is unsupported", op); + throw std::runtime_error(detail); + } + }; + face.failOp = convert(ops & 0xfu); + face.passOp = convert((ops >> 4u) & 0xfu); + face.depthFailOp = convert((ops >> 8u) & 0xfu); + face.reference = reference; + return face; +} + +// The depth/stencil surface and tests of a draw that tests depth or stencil with a plane bound. +void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) { + // DEPTH/STENCIL_COMPRESS_DISABLE (bits 5, 6) and the ordering hints from bit 13 on do not change + // what is drawn. + zero(cx, 0x000, 0x00001f9fu, "depth/stencil clear, copy, resummarize or decompress draws (DB_RENDER_CONTROL)"); + const auto view = read(cx, 0x002); + // Z_READ_ONLY and STENCIL_READ_ONLY (bits 24, 25) turn the writes off. + zero(cx, 0x002, ~0x03000000u, "depth array slices or mips (DB_DEPTH_VIEW)"); + // NUM_SAMPLES, PARTIALLY_RESIDENT, MAXMIP; the rest is tiling and compression the host image has none of. + zero(cx, 0x010, 0x000f100cu, "multisampled, partially resident or mipmapped depth (DB_Z_INFO)"); + zero(cx, 0x011, 0x00001000u, "partially resident stencil (DB_STENCIL_INFO)"); + const auto zFormat = read(cx, 0x010) & 3u; + const bool stencil = (read(cx, 0x011) & 1u) != 0; + Require(zFormat != 2, "Z_24 depth is unsupported"); + Require(zFormat != 0 || (depthControl & 2u) == 0, "depth test without a depth plane"); + Require(stencil || (depthControl & 1u) == 0, "stencil test without a stencil plane"); + const auto base = [&](std::uint32_t low, std::uint32_t highOffset) { + const auto high = find(cx, highOffset); + return (high == cx.end() ? 0ull : static_cast(high->second & 0xffu) << 40u) | (static_cast(read(cx, low)) << 8u); + }; + const bool depthReadOnly = (view & 0x01000000u) != 0; + const bool stencilReadOnly = (view & 0x02000000u) != 0; + DepthTarget depth{}; + depth.address = zFormat != 0 ? base(0x012, 0x01a) : 0; + depth.stencilAddress = stencil ? base(0x013, 0x01b) : 0; + Require(zFormat == 0 || depthReadOnly || base(0x014, 0x01c) == depth.address, "depth read and written at different addresses is unsupported"); + Require(!stencil || stencilReadOnly || base(0x015, 0x01d) == depth.stencilAddress, "stencil read and written at different addresses is unsupported"); + const auto size = read(cx, 0x007); + depth.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u}; + // A stencil-only surface gets an unused depth plane. + depth.format = zFormat == 1 ? (stencil ? VK_FORMAT_D16_UNORM_S8_UINT : VK_FORMAT_D16_UNORM) : (stencil ? VK_FORMAT_D32_SFLOAT_S8_UINT : VK_FORMAT_D32_SFLOAT); + depth.clearDepth = readFloat(cx, 0x00b); + depth.clearStencil = static_cast(read(cx, 0x00a) & 0xffu); + result.depth = depth; + result.depthTest = (depthControl & 2u) != 0; + result.depthWrite = result.depthTest && (depthControl & 4u) != 0 && !depthReadOnly; + result.depthCompare = static_cast((depthControl >> 4u) & 7u); + result.stencilTest = (depthControl & 1u) != 0; + if (result.stencilTest) { + const auto ops = read(cx, 0x10b); + result.stencilFront = stencilFace((depthControl >> 8u) & 7u, ops, read(cx, 0x10c), stencilReadOnly); + // BACKFACE_ENABLE gives back faces their own function, operations and masks. + result.stencilBack = (depthControl & 0x80u) != 0 ? stencilFace((depthControl >> 20u) & 7u, ops >> 12u, read(cx, 0x10d), stencilReadOnly) : result.stencilFront; + } +} + +// DB_DEPTH_CONTROL without a bound surface: depth and stencil tests whose function is ALWAYS and +// that write no depth cannot change color output, so they are accepted (stencil writes go nowhere). bool depthPassThrough(std::uint32_t depthControl) { const bool stencil = (depthControl & 1u) != 0; const bool depth = (depthControl & 2u) != 0; @@ -129,44 +238,50 @@ VkBlendOp blendOp(std::uint32_t value) { struct DecodedColorFormat { VkFormat format; std::uint32_t elementBytes; + // The exported component each attachment component stores, two bits each (0xe4 is identity). + std::uint8_t componentMapping = 0xe4u; }; // CB_COLOR_INFO FORMAT / NUMBER_TYPE / COMP_SWAP to a Vulkan attachment format. AMD formats list // components from the least significant bits, as Vulkan's non-packed formats do. DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, std::uint32_t swap) { - constexpr std::uint32_t unorm = 0, sint = 5, srgb = 6, floating = 7; + constexpr std::uint32_t unorm = 0, snorm = 1, sint = 5, srgb = 6, floating = 7; constexpr std::uint32_t uint = 4; const auto fail = [&]() -> DecodedColorFormat { throw std::runtime_error("AGC graphics: unsupported color format " + std::to_string(format) + " number type " + std::to_string(number) + " component swap " + std::to_string(swap)); }; const bool alternate = swap == 1; - if (swap > 1) return fail(); + // A one-component format stores the exported component COMP_SWAP names: R, G, B or A (an A8 + // target is COLOR_8 with SWAP_ALT_REV). + const auto single = [&](VkFormat vkFormat, std::uint32_t bytes) { return DecodedColorFormat{vkFormat, bytes, static_cast((0xe4u & ~3u) | swap)}; }; + if (swap > 1 && format != 1 && format != 2 && format != 4) return fail(); switch (format) { case 1: - if (swap != 0) return fail(); - if (number == unorm) return {VK_FORMAT_R8_UNORM, 1}; - if (number == uint) return {VK_FORMAT_R8_UINT, 1}; + if (number == unorm) return single(VK_FORMAT_R8_UNORM, 1); + if (number == snorm) return single(VK_FORMAT_R8_SNORM, 1); + if (number == uint) return single(VK_FORMAT_R8_UINT, 1); return fail(); case 2: - if (swap != 0) return fail(); - if (number == unorm) return {VK_FORMAT_R16_UNORM, 2}; - if (number == floating) return {VK_FORMAT_R16_SFLOAT, 2}; - if (number == uint) return {VK_FORMAT_R16_UINT, 2}; + if (number == unorm) return single(VK_FORMAT_R16_UNORM, 2); + if (number == snorm) return single(VK_FORMAT_R16_SNORM, 2); + if (number == floating) return single(VK_FORMAT_R16_SFLOAT, 2); + if (number == uint) return single(VK_FORMAT_R16_UINT, 2); return fail(); case 3: if (swap != 0) return fail(); if (number == unorm) return {VK_FORMAT_R8G8_UNORM, 2}; + if (number == snorm) return {VK_FORMAT_R8G8_SNORM, 2}; return fail(); case 4: - if (swap != 0) return fail(); - if (number == floating) return {VK_FORMAT_R32_SFLOAT, 4}; - if (number == uint) return {VK_FORMAT_R32_UINT, 4}; - if (number == sint) return {VK_FORMAT_R32_SINT, 4}; + if (number == floating) return single(VK_FORMAT_R32_SFLOAT, 4); + if (number == uint) return single(VK_FORMAT_R32_UINT, 4); + if (number == sint) return single(VK_FORMAT_R32_SINT, 4); return fail(); case 5: if (swap != 0) return fail(); if (number == floating) return {VK_FORMAT_R16G16_SFLOAT, 4}; if (number == unorm) return {VK_FORMAT_R16G16_UNORM, 4}; + if (number == snorm) return {VK_FORMAT_R16G16_SNORM, 4}; if (number == uint) return {VK_FORMAT_R16G16_UINT, 4}; return fail(); case 6: @@ -179,6 +294,7 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, return {alternate ? VK_FORMAT_A2R10G10B10_UNORM_PACK32 : VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4}; case 10: if (number == unorm) return {alternate ? VK_FORMAT_B8G8R8A8_UNORM : VK_FORMAT_R8G8B8A8_UNORM, 4}; + if (number == snorm) return {alternate ? VK_FORMAT_B8G8R8A8_SNORM : VK_FORMAT_R8G8B8A8_SNORM, 4}; if (number == srgb) return {alternate ? VK_FORMAT_B8G8R8A8_SRGB : VK_FORMAT_R8G8B8A8_SRGB, 4}; return fail(); case 11: @@ -190,6 +306,7 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, if (swap != 0) return fail(); if (number == floating) return {VK_FORMAT_R16G16B16A16_SFLOAT, 8}; if (number == unorm) return {VK_FORMAT_R16G16B16A16_UNORM, 8}; + if (number == snorm) return {VK_FORMAT_R16G16B16A16_SNORM, 8}; if (number == uint) return {VK_FORMAT_R16G16B16A16_UINT, 8}; return fail(); case 14: @@ -304,7 +421,9 @@ State DecodeState(const QueueState& queue) { zero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports"); { const auto depthControl = read(cx, 0x200); - if (depthPassThrough(depthControl)) { + if ((depthControl & 3u) != 0 && depthSurfaceBound(cx)) { + decodeDepth(cx, depthControl, result); + } else if (depthPassThrough(depthControl)) { static bool reported = false; if (!reported) { reported = true; @@ -378,7 +497,9 @@ State DecodeState(const QueueState& queue) { for (std::uint32_t slot = 0; slot < slotCount; ++slot) { // Export formats only matter for the targets the draw writes. const auto slotExport = (exportFormat >> (4u * slot)) & 0xfu; - Require(slotExport == 4 || slotExport == 9, "only FP16_ABGR or 32_ABGR color export is supported"); + // FP16, UNORM16 and SNORM16 exports are unpacked to floats by the shader; 32_R, 32_GR, 32_AR and + // 32_ABGR are written as is, the components a format leaves out reading as 0 (alpha 1). + if (slotExport == 0 || slotExport == 7 || slotExport == 8 || slotExport > 9) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " is unsupported"); const auto stride = slot * 0xfu; ColorTarget color{}; const auto info = read(cx, 0x31c + stride); @@ -388,13 +509,15 @@ State DecodeState(const QueueState& queue) { const auto format = (info >> 2u) & 0x1fu; const auto decoded = DecodeColorFormat(format, number, swap); // ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written - // uncompressed and only its fast-clear keys matter (see DccMetadata.hpp). - Require((info & ~(0x00029f7cu | 0x00040000u | 0x10000000u)) == 0, "color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported"); - Require((info & 0x8000u) != 0 || number == 7, "unclamped normalized color is unsupported"); - // CB_COLOR_VIEW: MIP_LEVEL (bits 24-27) selects the rendered mip; array slices are not modeled. + // uncompressed and only its fast-clear keys matter (see DccMetadata.hpp). BLEND_BYPASS (bit 16) is + // checked against enabled blending below. + if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")"); + Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported"); + // CB_COLOR_VIEW (GFX10: SLICE_START 0-12, SLICE_MAX 13-25, MIP_LEVEL 26-29): MIP_LEVEL selects + // the rendered mip; array slices are not modeled. const auto view = read(cx, 0x31b + stride); - Require((view & ~0x0f000000u) == 0, "color array views are unsupported"); - const auto viewMip = (view >> 24u) & 0xfu; + Require((view & ~0x3c000000u) == 0, "color array views are unsupported"); + const auto viewMip = (view >> 26u) & 0xfu; zero(cx, 0x31d + stride, ~0u, "color samples, fragments or destination alpha override"); const auto attrib2 = read(cx, 0x3b0 + slot); const auto maxMip = attrib2 >> 28u; @@ -404,22 +527,26 @@ State DecodeState(const QueueState& queue) { color.extent = {((attrib2 >> 14u) & 0x3fffu) + 1u, (attrib2 & 0x3fffu) + 1u}; color.elementBytes = decoded.elementBytes; std::uint64_t mipOffset = 0; + color.surfaceExtent = color.extent; + color.mipCount = maxMip + 1u; + color.mip = viewMip; if (maxMip != 0) { // A mipmapped surface is addressed like a texture; the view renders into one mip of it. const auto mips = ComputeElementMipLayout(color.tileMode == ColorTileMode::Linear ? TextureTileMode::kLinear : TextureTileMode::kR64KBX, color.elementBytes, color.extent.width, color.extent.height, maxMip + 1u); const auto& mip = mips.at(viewMip); - Require(!mip.tail, "rendering into a packed mip tail is unsupported"); mipOffset = mip.tiledOffset; + color.mipTail = mip.tail; color.extent = {mip.width, mip.height}; } const ColorTargetLayout colorLayout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes); const auto high = read(cx, 0x390 + slot); Require((high & ~0xffu) == 0, "invalid color address extension"); - color.address = ((static_cast(high) << 40u) | (static_cast(read(cx, 0x318 + stride)) << 8u)) + mipOffset; + color.surfaceAddress = (static_cast(high) << 40u) | (static_cast(read(cx, 0x318 + stride)) << 8u); + color.address = color.surfaceAddress + mipOffset; color.bytes = colorLayout.Bytes(); GuestMemory::CheckRange(reinterpret_cast(color.address), color.bytes, colorLayout.Alignment(), true); color.format = decoded.format; - color.componentMapping = 0xe4u; + color.componentMapping = decoded.componentMapping; if ((info & 0x10000000u) != 0) { if (maxMip == 0) { const auto dccHigh = find(cx, 0x3a8 + slot); @@ -443,6 +570,9 @@ State DecodeState(const QueueState& queue) { } if (result.hasColorTarget) { result.color = result.colors.front(); + if (result.depth) result.renderExtent = {std::min(result.renderExtent.width, result.depth->extent.width), std::min(result.renderExtent.height, result.depth->extent.height)}; + } else if (result.depth) { + result.renderExtent = result.depth->extent; } else { const auto screenBottomRight = read(cx, 0xd); APS5_LOG_OUT_DEBUG("No color target, screen BR register=0x%x", screenBottomRight); @@ -478,7 +608,13 @@ State DecodeState(const QueueState& queue) { APS5_LOG_OUT_DEBUG("Blend %u control=0x%x", slot, blend); Require((blend & 0x0000e000u) == 0, "reserved blend control bits"); VkPipelineColorBlendAttachmentState state{}; - state.colorWriteMask = (targetMask >> (4u * slot)) & 0xfu; + // CB_TARGET_MASK names exported components; attachment component c stores exported component + // mapping[c], so its write bit and, when it stores alpha, its blend come from that component. + const auto mapping = result.colors[slot].componentMapping; + const auto exportedMask = (targetMask >> (4u * slot)) & 0xfu; + for (std::uint32_t component = 0; component < 4; ++component) { + if (((exportedMask >> ((mapping >> (2u * component)) & 3u)) & 1u) != 0) state.colorWriteMask |= 1u << component; + } state.blendEnable = (blend >> 30u) & 1u; if (state.blendEnable) { Require((read(cx, 0x31c + slot * 0xfu) & 0x10000u) == 0, "blend bypass conflicts with enabled blending"); @@ -489,6 +625,11 @@ State DecodeState(const QueueState& queue) { state.srcAlphaBlendFactor = blendFactor(alpha & 0x1fu); state.dstAlphaBlendFactor = blendFactor((alpha >> 8u) & 0x1fu); state.alphaBlendOp = blendOp((alpha >> 5u) & 7u); + if ((mapping & 3u) == 3u) { + state.srcColorBlendFactor = state.srcAlphaBlendFactor; + state.dstColorBlendFactor = state.dstAlphaBlendFactor; + state.colorBlendOp = state.alphaBlendOp; + } for (std::uint32_t i = 0; i < 4; ++i) result.blendConstants[i] = readFloat(cx, 0x105 + i); } result.blends.push_back(state); @@ -498,6 +639,13 @@ State DecodeState(const QueueState& queue) { return result; } +std::array ExportMappings(const State& state) { + std::array mappings{}; + mappings.fill(0xe4u); + for (std::size_t slot = 0; slot < state.colors.size() && slot < mappings.size(); ++slot) mappings[slot] = state.colors[slot].componentMapping; + return mappings; +} + std::string DrawRejection(const QueueState& queue, bool indexed) { const auto& cx = queue.context; // A register a rule needs that is absent gives no verdict here: DecodeState reports it. @@ -517,7 +665,9 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (auto reason = indexed ? nonzero(queue.userConfig, 0x24b, ~0u, "primitive restart (GE_MULTI_PRIM_IB_RESET_EN)") : std::string(); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; if (value(cx, 0x200, word)) { - if (!depthPassThrough(word) && !IgnoreDepthTest() && (word & DepthControlMask) != 0) return zeroMessage(0x200, word, "depth, stencil or conditional color writes"); + // Tests against a bound surface are decoded (and their limits checked) by DecodeState. + const bool surface = (word & 3u) != 0 && depthSurfaceBound(cx); + if (!surface && !depthPassThrough(word) && !IgnoreDepthTest() && (word & DepthControlMask) != 0) return zeroMessage(0x200, word, "depth, stencil or conditional color writes"); if (auto reason = require((word & 0xc0000008u) == 0, "depth bounds or depth-conditional color writes are unsupported"); !reason.empty()) return reason; } if (auto reason = nonzero(cx, 0x203, ShaderControlMask, "depth export, shader coverage or ordered fragment execution"); !reason.empty()) return reason; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 74f55c1a8..7f5008dd6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -866,9 +866,10 @@ VkImageView StorageTexture::createView(std::uint32_t mip) const { return created; } -VkImageView StorageTexture::AttachmentView(VkFormat format) { +VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip) { Require(attachable, "storage image cannot be a color attachment"); - const auto found = attachmentViews.find(format); + Require(mip < descriptor.mipCount, "attachment mip exceeds the storage image"); + const auto found = attachmentViews.find({format, mip}); if (found != attachmentViews.end()) return found->second; VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; usage.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; @@ -878,10 +879,10 @@ VkImageView StorageTexture::AttachmentView(VkFormat format) { viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; viewInfo.format = format; viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; - viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1u, descriptor.baseArray, 1u}; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 1u, descriptor.baseArray, 1u}; VkImageView created = VK_NULL_HANDLE; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &created), "vkCreateImageView attachment"); - attachmentViews.emplace(format, created); + attachmentViews.emplace(std::pair{format, mip}, created); return created; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index 49af94ea1..48506c345 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include #include #include #include @@ -245,7 +246,7 @@ std::array ThickBlockExtent(TextureTileMode tileMode, std::uin switch (tileMode) { case TextureTileMode::kStandard4KB: return {1u << thick4KB[index][0], 1u << thick4KB[index][1], 1u << thick4KB[index][2]}; case TextureTileMode::kStandard64KB: return {1u << thick64KB[index][0], 1u << thick64KB[index][1], 1u << thick64KB[index][2]}; - default: throw std::runtime_error("AGC graphics: 3D textures are only supported linear or in SW_4KB_S / SW_64KB_S"); + default: throw std::runtime_error("AGC graphics: 3D textures are only supported linear or in SW_4KB_S / SW_64KB_S, not in XOR swizzle " + std::to_string(XorSwizzleMode(tileMode)) + " at " + std::to_string(bytesPerElement) + " bytes per element"); } } @@ -289,6 +290,19 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) { SurfaceGeometry geometry; + if (descriptor.dimension == TextureDimension::k3D && (descriptor.tileMode == TextureTileMode::kD64KBX || descriptor.tileMode == TextureTileMode::kR64KBX)) { + // addrlib keeps display and rotated swizzles thin for 3D (only Z and standard ones are thick): + // each depth slice is laid out as an array slice, its index XORed in through the equation. + Require(descriptor.mipCount == 1, "mipmapped 3D textures are not implemented"); + const auto depth = descriptor.depthOrLastArray + 1u; + geometry.mips = ComputeMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, 1u); + geometry.layers = depth; + geometry.imageDepth = depth; + geometry.guestBytes = ComputeSurfaceSize(geometry.mips, depth); + geometry.layerBytes = geometry.guestBytes / depth; + for (const auto& mip : geometry.mips) geometry.sliceLinearBytes = std::max(geometry.sliceLinearBytes, mip.linearOffset + mip.linearSize); + return geometry; + } if (descriptor.dimension == TextureDimension::k3D) { Require(descriptor.mipCount == 1, "mipmapped 3D textures are not implemented"); const auto depth = descriptor.depthOrLastArray + 1u; diff --git a/core/libs/prx/libSceAgcDriver/Resource/src/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Resource/src/Lifecycle.cpp index 8ff5e49b4..78757ea71 100644 --- a/core/libs/prx/libSceAgcDriver/Resource/src/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Resource/src/Lifecycle.cpp @@ -5,16 +5,17 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +static constexpr uint32_t AGC_EINVAL = 0x80020065u; + extern "C" { uint32_t APS5_VABI sceAgcDriverInitResourceRegistration(void) { - NotImplemented_nid_no_patch(__func__); return 0; } uint32_t APS5_VABI sceAgcDriverQueryResourceRegistrationUserMemoryRequirements(uint64_t* size_in_bytes) { - (void)size_in_bytes; - NotImplemented_nid_no_patch(__func__); + if (!size_in_bytes) return AGC_EINVAL; + *size_in_bytes = 0x4000ULL; return 0; } diff --git a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp index 4de64c2dd..7aec5262e 100644 --- a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp +++ b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp @@ -30,7 +30,6 @@ int APS5_VABI sceAgcDriverRegisterResource(uint32_t* resource_handle, uint32_t o int APS5_VABI sceAgcDriverRegisterWorkloadStream(uint32_t stream_id, const void* stream) { (void)stream_id; (void)stream; - NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp b/core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp index fa1ae944e..08c81ffb4 100644 --- a/core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp +++ b/core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp @@ -2,23 +2,23 @@ #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { +// Tessellation runs as Vulkan tessellation stages, which keep hull-shader outputs and tessellation +// factors on the host GPU: the off-chip hull memory and the tessellation-factor ring are never read. int APS5_VABI sceAgcDriverSetHsOffchipParam(uint64_t value0, uint64_t value1, uint64_t value2) { (void)value0; (void)value1; (void)value2; - NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceAgcDriverSetTFRing(const volatile void* base, uint32_t size) { - (void)base; - (void)size; - NotImplemented_nid_no_patch(__func__); + if (base == nullptr || size == 0) throw std::invalid_argument("sceAgcDriverSetTFRing: empty ring"); return 0; } diff --git a/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp index 1c96fbee9..5b3c912d5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp @@ -1,7 +1,10 @@ #include "BdaTests.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp" +#include #include +#include #include +#include #include namespace { @@ -25,7 +28,8 @@ void RunColorTargetLayoutTests() { reject([] { DecodeColorTileMode(0xcdc6c000); }); reject([] { DecodeColorTileMode(0x09014000); }); reject([] { ColorTargetLayout(0, 1, ColorTileMode::RenderTarget); }); - reject([] { ColorTargetLayout(63, 1, ColorTileMode::Linear); }); + const ColorTargetLayout padded(63, 2, ColorTileMode::Linear); + Require(padded.Bytes() == 512 && padded.LinearBytes() == 504 && padded.Offset(0, 1) == 256, "linear rows are not padded to 256 bytes"); const ColorTargetLayout screen(3840, 2160, ColorTileMode::RenderTarget); Require(screen.Bytes() == 33423360 && screen.LinearBytes() == 33177600 && screen.Alignment() == 65536, "4K color backing layout is incorrect"); const ColorTargetLayout layout(257, 129, ColorTileMode::RenderTarget); @@ -55,8 +59,10 @@ void RunColorTargetLayoutTests() { } reject([&] { layout.Detile(std::span(tiled).first(4), restored); }); reject([&] { layout.Tile(std::span(linear).first(4), tiled); }); - alignas(65536) static std::array guest{}; - guest.fill(std::byte{0x6b}); + // Aligned by hand: a PE section cannot ask for 64 KiB alignment. + static std::vector storage(2 * 65536); + const std::span guest(reinterpret_cast((reinterpret_cast(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}), 65536); + std::fill(guest.begin(), guest.end(), std::byte{0x6b}); ColorTarget target{reinterpret_cast(guest.data()), {2, 2}, VK_FORMAT_R8G8B8A8_UNORM, guest.size(), 0xe4, ColorTileMode::RenderTarget}; std::array pixels{}; pixels.fill(std::byte{0x32}); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index dacf921c2..d636c3d97 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -99,8 +99,10 @@ void stateTests() { queue.context[0x91] = 0x30020; state = AgcDriver::Graphics::DecodeState(queue); Require(state.scissor.offset.x == 3 && state.scissor.offset.y == 1 && state.scissor.extent.width == 29 && state.scissor.extent.height == 2, "scissor intersection changed"); + // DCC_ENABLE: the target is written uncompressed, its keys at CB_COLOR0_DCC_BASE. queue.context[0x31c] |= 0x10000000; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DCC"); + queue.context[0x325] = 0x1234; + Require(AgcDriver::Graphics::DecodeState(queue).color.dccAddress == 0x123400, "DCC key address decode changed"); queue = makeState(); queue.context.erase(0x3b8); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); @@ -108,11 +110,14 @@ void stateTests() { queue.context[0x3b8] |= 5u << 14u; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported color tile mode"); queue = makeState(); + // A linear width off the 256-byte pitch alignment is padded (rows of 64 texels here). queue.context[0x3b0] = (62u << 14u) | 3u; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "pitch"); + Require(AgcDriver::Graphics::DecodeState(queue).color.bytes == 64u * 4u * 4u, "padded linear pitch changed"); queue = makeState(); + // Slot 1 written by the shader needs an export format. queue.context[0x8e] = 0xff; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "target zero"); + queue.context[0x8f] = 0xff; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "color export format 0"); queue = makeState(); queue.context[0x200] = 2; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "depth"); @@ -128,7 +133,7 @@ void stateTests() { queue.context[0x1b4] = 2; state = AgcDriver::Graphics::DecodeState(queue); Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "precheck rejected the reference state"); - static_cast(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, state.hasColorTarget, state.color.componentMapping)); + static_cast(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, AgcDriver::Graphics::ExportMappings(state))); AgcDriver::Graphics::RegisterReadLog() = nullptr; Require(!log.empty(), "the register facade recorded nothing"); for (const auto read : log) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a register the decoders read: " + std::string(AgcDriver::Graphics::RegisterBankName(read.bank)) + " " + std::to_string(read.offset)); @@ -233,6 +238,74 @@ void DisabledColorTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); } +// Depth/stencil tests against a bound surface: Scaleform's mask pass (stencil ALWAYS, add 1) and +// content pass (stencil EQUAL), as a Scaleform menu draws them. +void DepthStencilTests() { + auto queue = makeState(); + queue.context[0x000] = 0; + queue.context[0x002] = 0; + queue.context[0x007] = (1u << 16u) | 3u; + queue.context[0x00a] = 7; + queue.context[0x00b] = 0; + queue.context[0x010] = 0x22900983; + queue.context[0x011] = 0x20000181; + for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100; + for (const auto offset : {0x013u, 0x015u}) queue.context[offset] = 0x200; + queue.context[0x10b] = 0x00050050; + queue.context[0x10c] = 0x01ffff01; + queue.context[0x10d] = 0x01000001; + queue.context[0x200] = 0x00700711; + std::vector log; + AgcDriver::Graphics::RegisterReadLog() = &log; + auto state = AgcDriver::Graphics::DecodeState(queue); + AgcDriver::Graphics::RegisterReadLog() = nullptr; + for (const auto read : log) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a depth register the decoder reads: " + std::to_string(read.offset)); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); + Require(rejection.empty(), "precheck rejected a stencil draw with a surface: " + rejection); + Require(state.depth && state.depth->address == 0x10000 && state.depth->stencilAddress == 0x20000 && state.depth->format == VK_FORMAT_D32_SFLOAT_S8_UINT && state.depth->clearStencil == 7, "depth surface decode changed"); + Require(state.renderExtent.width == 4 && state.renderExtent.height == 2, "render extent ignores the depth surface"); + Require(!state.depthTest && !state.depthWrite && state.stencilTest, "depth/stencil enables changed"); + const auto& front = state.stencilFront; + Require(front.compareOp == VK_COMPARE_OP_ALWAYS && front.passOp == VK_STENCIL_OP_INCREMENT_AND_CLAMP && front.failOp == VK_STENCIL_OP_KEEP && front.reference == 1 && front.writeMask == 0xff, "stencil mask pass changed"); + Require(std::memcmp(&state.stencilBack, &front, sizeof(front)) == 0, "back faces without BACKFACE_ENABLE must use the front state"); + // The content pass: EQUAL to 2, nothing written. + queue.context[0x10b] = 0; + queue.context[0x10c] = 0x01ffff02; + queue.context[0x200] = 0x00200211; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.compareOp == VK_COMPARE_OP_EQUAL && state.stencilFront.reference == 2 && state.stencilFront.passOp == VK_STENCIL_OP_KEEP, "stencil content pass changed"); + // Clearing to 0 with REPLACE_TEST under ALWAYS. + queue.context[0x10b] = 0x00030030; + queue.context[0x10c] = 0x01ffff00; + queue.context[0x200] = 0x00700771; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.passOp == VK_STENCIL_OP_REPLACE && state.stencilFront.reference == 0 && std::memcmp(&state.stencilBack, &state.stencilFront, sizeof(state.stencilFront)) == 0, "stencil clear pass changed"); + // BACKFACE_ENABLE: back faces get STENCILFUNC_BF, the _BF operations and DB_STENCILREFMASK_BF. + queue.context[0x200] = 0x007007f1; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilBack.compareOp == VK_COMPARE_OP_ALWAYS && state.stencilBack.passOp == VK_STENCIL_OP_KEEP && state.stencilBack.writeMask == 0 && state.stencilBack.reference == 1, "back-face stencil state changed"); + // REPLACE_OP writing a value the test compares differently has no single Vulkan reference. + queue.context[0x10b] = 0x40; + queue.context[0x10c] = 0x05ffff02; + queue.context[0x200] = 0x00200211; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement"); + // Adding anything but 1 has no Vulkan operation. + queue.context[0x10b] = 0x50; + queue.context[0x200] = 0x00700711; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil add/subtract"); + queue.context[0x10b] = 0; + queue.context[0x000] = 1; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DB_RENDER_CONTROL"); + // CB_COLOR_VIEW's mip level is bits 26-29. + queue = makeState(); + queue.context[0x31b] = 1u << 26u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "mip exceeds"); + queue.context[0x31b] = 1u << 13u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "array views"); +} + void DepthClipTests() { auto queue = makeState(); const auto direct = AgcDriver::Graphics::DecodeState(queue); @@ -261,7 +334,8 @@ void DepthClipTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "inverted viewport depth clamp"); queue.context[0xb4] = 0; for (std::uint32_t bit = 0; bit < 32; ++bit) { - if (bit == 19) continue; + // DX_CLIP_SPACE_DEF (19) and ZCLIP_NEAR/FAR_DISABLE (26, 27, depth clamping) are decoded. + if (bit == 19 || bit == 26 || bit == 27) continue; queue.context[0x204] = 1u << bit; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "PA_CL_CLIP_CNTL"); } @@ -1168,6 +1242,7 @@ int main() { stateTests(); hardwareScreenOffsetTests(); DepthClipTests(); + DepthStencilTests(); DisabledColorTests(); ShaderStageTests(); InitialContextTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 34198c47b..5723d6ec8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -342,6 +342,18 @@ void testMemorySynchronization() { check(memory.destination == 42, "failed synchronization changed the destination"); } +void testWriteChangedKeepsUntouchedBytes() { + // Write-back stores only the bytes the GPU changed: its other bytes are the snapshot, and the + // CPU may have written them since. + alignas(256) static std::uint8_t guest[256]; + std::memset(guest, 0, sizeof(guest)); + std::vector original(sizeof(guest)), current(sizeof(guest)); + current[3] = std::byte{7}; + guest[100] = 0x55; + AgcDriver::GuestMemory::WriteChanged(reinterpret_cast(guest), current, original); + check(guest[3] == 7 && guest[100] == 0x55, "write-back rolled back a byte the GPU did not change"); +} + void testEventWrite() { for (const auto eventType : {0x07u, 0x0fu, 0x10u}) { AgcDriver::Pm4::Validate(makePacket(0x46, {0x400u | eventType}), 0); @@ -481,6 +493,7 @@ int main(int argc, char** argv) { return 0; } testCatalog(); + testWriteChangedKeepsUntouchedBytes(); testRegisters(); testContextAndBases(); testIndexedDraw(); diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 26d2bdb67..45104221d 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -59,6 +59,16 @@ public: return std::make_shared(cfg, index, cfg->bufferReuse[index].Capture()); } + void WaitForFlipRoom() override { + std::unique_lock queueLock(queue->mutex); + const bool room = queue->changed.wait_for(queueLock, std::chrono::seconds(60), [&] { + return queue->failure || queue->stopping || cfg->shutdownToken.stop_requested() || queue->reservations.load() < VIDEO_OUT_FLIP_QUEUE_CAPACITY; + }); + if (queue->failure) std::rethrow_exception(queue->failure); + if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; + require(room, "flip queue stayed full for 60 s: the presenter is not completing flips"); + } + std::shared_ptr Reserve(const AgcDriver::FlipInfo& info) override { // All flip modes are presented at the next vsync. require(info.mode >= VIDEO_OUT_FLIP_MODE_VSYNC && info.mode <= 6, "unsupported flip mode"); @@ -70,23 +80,13 @@ public: request->outputHandle = info.handle; request->flipMode = static_cast(info.mode); request->flipArg = info.argument; - // Debug aid: APS5_FLIP_QUEUE_WAIT=1 waits for room instead of failing the flip, so a - // presenter stall keeps the process alive for a debugger (reported every 5 s). - static const bool waitWhenFull = std::getenv("APS5_FLIP_QUEUE_WAIT") != nullptr; - if (waitWhenFull) { - int waited = 0; - while (queue->reservations.load() >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) { - if (cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - if (++waited % 500 == 0) std::fprintf(stderr, "[flip] queue full for %d s; waiting (APS5_FLIP_QUEUE_WAIT)\n", waited / 100); - } - } std::lock_guard queueLock(queue->mutex); if (queue->failure) std::rethrow_exception(queue->failure); if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; std::lock_guard lock(cfg->mutex); checkConfig(*cfg); - require(queue->reservations.load() < VIDEO_OUT_FLIP_QUEUE_CAPACITY, "flip queue full"); + // The limit was waited for by WaitForFlipRoom before the driver took its lock; concurrent + // submitters may overshoot it by their own flips. if (info.index >= 0) { request->buffer = cfg->buffers[info.index]; require(request->buffer.Occupied(), "flip buffer is not registered"); @@ -479,6 +479,7 @@ void VideoOutDriver::presentLoop(std::stop_token token) { flipQueue->requests.pop_front(); } } + SDL_Event event; while (SDL_PollEvent(&event)) { if (event.type == SDL_QUIT) { @@ -504,7 +505,11 @@ void VideoOutDriver::presentLoop(std::stop_token token) { } current->timing->Print(current->outputHandle, current->index, current->flipArg, finished, interval); } - current.reset(); + if (current) { + current.reset(); + // processFlip released the flip's reservation: a submitter may be waiting for room. + flipQueue->changed.notify_all(); + } } } catch (const ProcessShutdown&) { } catch (const std::exception& error) { From 028d465e98ea93edd4df1cfee96acebf70c9f9e0 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 07:54:05 +0300 Subject: [PATCH 06/40] feat(audio): AJM MP3 through FFmpeg, 16-bit and ambisonic AudioOut2, voice - AJM codec 0 (MP3) decodes with FFmpeg, prebuilt through the ffmpeg-core submodule as in Kyty; sceAjmBatchJobDecode and the Run job; the PCM encoding follows the job flags; other codecs throw - AudioOut2 ports of 16-bit integer samples, and the ambisonics speaker array decode for the reported layout - the voice library for a console with no headset, with a frame size for voice ports Co-Authored-By: Claude Opus 5.5 --- .gitmodules | 3 + 3rdparty/ffmpeg-core | 1 + CMakeLists.txt | 5 + core/libs/prx/libSceAjm.native/CMakeLists.txt | 2 +- core/libs/prx/libSceAjm.native/Export.cpp | 26 -- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 309 ++++++++++++++---- core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 10 + .../libSceAudioOut/src/AudioOut2Internal.hpp | 12 +- .../prx/libSceAudioOut/src/AudioOut2Port.cpp | 23 +- .../src/AudioOut2SpeakerArray.cpp | 104 +++++- core/libs/prx/libSceVoice/Export.cpp | 285 ++++++++++------ 11 files changed, 564 insertions(+), 216 deletions(-) create mode 160000 3rdparty/ffmpeg-core diff --git a/.gitmodules b/.gitmodules index 995853108..0a5f83fb7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,6 +19,9 @@ [submodule "3rdparty/LibAtrac9"] path = 3rdparty/LibAtrac9 url = https://github.com/shadps4-emu/ext-LibAtrac9.git +[submodule "3rdparty/ffmpeg-core"] + path = 3rdparty/ffmpeg-core + url = https://github.com/KytyPS5/ext-ffmpeg-core.git [submodule "3rdparty/freetype"] path = 3rdparty/freetype url = https://github.com/freetype/freetype.git diff --git a/3rdparty/ffmpeg-core b/3rdparty/ffmpeg-core new file mode 160000 index 000000000..9ac4cfd19 --- /dev/null +++ b/3rdparty/ffmpeg-core @@ -0,0 +1 @@ +Subproject commit 9ac4cfd195f192ed8b08566f49c28dbb48d08341 diff --git a/CMakeLists.txt b/CMakeLists.txt index 3751edef8..9236f55fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,11 @@ set(ANYPS5_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") set(CMAKE_EXE_LINKER_FLAGS "") add_subdirectory(3rdparty/SDL2) set(CMAKE_EXE_LINKER_FLAGS "${ANYPS5_EXE_LINKER_FLAGS}") +# Prebuilt FFmpeg (AJM's MP3 decoder), downloaded for the submodule's commit as in Kyty. +set(ARCHITECTURE x86_64) +add_subdirectory(3rdparty/ffmpeg-core) +# The system libraries its pkg-config files name, which ffmpeg-core adds only for MSVC. +target_link_libraries(ffmpeg INTERFACE bcrypt ole32 user32) set(FT_DISABLE_ZLIB ON CACHE BOOL "" FORCE) set(FT_DISABLE_BZIP2 ON CACHE BOOL "" FORCE) diff --git a/core/libs/prx/libSceAjm.native/CMakeLists.txt b/core/libs/prx/libSceAjm.native/CMakeLists.txt index 83bd7327e..6623b3914 100644 --- a/core/libs/prx/libSceAjm.native/CMakeLists.txt +++ b/core/libs/prx/libSceAjm.native/CMakeLists.txt @@ -27,5 +27,5 @@ target_include_directories(libSceAjm.native PRIVATE ${LIBS_INCLUDE_DIR} ${LIBATRAC9_DIR} ) -target_link_libraries(libSceAjm.native PRIVATE atrac9 libc libkernel) +target_link_libraries(libSceAjm.native PRIVATE atrac9 ffmpeg libc libkernel) configure_windows_unwind(libSceAjm.native) diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index c2150a64a..ff03fa3b4 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -33,18 +33,6 @@ int APS5_VABI sceAjmBatchJobControl(AjmBatchInfo* info, uint32_t instance, uint6 return 0; } -int APS5_VABI sceAjmBatchJobDecode(AjmBatchInfo* info, uint32_t instance, const void* bitstream_input, size_t bitstream_input_size, void* pcm_output, size_t pcm_output_size, void* result) { - (void)info; - (void)instance; - (void)bitstream_input; - (void)bitstream_input_size; - (void)pcm_output; - (void)pcm_output_size; - (void)result; - AjmStub(__func__); - return 0; -} - int APS5_VABI sceAjmBatchJobDecodeSingle(AjmBatchInfo* info, uint32_t instance, const void* bitstream_input, size_t bitstream_input_size, void* pcm_output, size_t pcm_output_size, void* result) { (void)info; (void)instance; @@ -114,20 +102,6 @@ int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo* info, uint32_t instanc return 0; } -int APS5_VABI sceAjmBatchJobRun(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const void* data_input, size_t data_input_size, void* data_output, size_t data_output_size, void* sideband_output, size_t sideband_output_size) { - (void)info; - (void)instance; - (void)flags; - (void)data_input; - (void)data_input_size; - (void)data_output; - (void)data_output_size; - (void)sideband_output; - (void)sideband_output_size; - AjmStub(__func__); - return 0; -} - int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo* info, uint32_t instance, float ratio, uint32_t flags, void* result) { (void)info; (void)instance; diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 1bfc8f760..13c823926 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -3,14 +3,21 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "libatrac9.h" +extern "C" { +#include +#include +} #include #include +#include #include #include #include #include #include #include +#include +#include #include // The batch buffer and its job records are private to this library: the guest only reserves the @@ -29,6 +36,7 @@ constexpr std::int32_t AJM_RESULT_PARTIAL_INPUT = 0x00000008; constexpr std::int32_t AJM_RESULT_NOT_ENOUGH_ROOM = 0x00000010; constexpr std::int32_t AJM_RESULT_CODEC_ERROR = 0x40000000; +constexpr std::uint32_t CODEC_MP3 = 0; constexpr std::uint32_t CODEC_AT9 = 1; // APS5_TRACE_AJM=1 logs every job the title submits and what the library writes back. @@ -89,9 +97,15 @@ struct Instance { std::uint64_t totalDecodedSamples = 0; SidebandGaplessDecode gapless{}; bool flagsReported = false; + // MP3: the FFmpeg decoder (it keeps the bit reservoir between frames) and the last frame's shape. + AVCodecContext* mp3 = nullptr; + std::uint32_t mp3Channels = 0; + std::uint32_t mp3SampleRate = 0; + std::uint32_t mp3Bitrate = 0; ~Instance() { if (decoder) Atrac9ReleaseHandle(decoder); + if (mp3) avcodec_free_context(&mp3); } }; @@ -223,21 +237,99 @@ bool consumedFirstRun(Instance& instance) { return true; } -// Decodes AT9 frames from the concatenated inputs into the concatenated outputs in the instance's PCM -// encoding. -void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { +std::vector JoinInputs(const JobHeader& job, const AjmBuffer* inputs) { std::vector input; for (std::uint32_t index = 0; index < job.inputCount; ++index) { const auto* data = static_cast(inputs[index].ptr); input.insert(input.end(), data, data + inputs[index].size); } - std::size_t outputCapacity = 0; - for (std::uint32_t index = 0; index < job.outputCount; ++index) outputCapacity += outputs[index].size; + return input; +} + +// The job's output buffers, filled in order. +struct PcmOutputs { + const AjmBuffer* buffers; + std::uint32_t count; + std::size_t capacity = 0; + std::size_t produced = 0; + std::uint32_t index = 0; + std::size_t offset = 0; + + PcmOutputs(const AjmBuffer* buffers, std::uint32_t count) : buffers(buffers), count(count) { + for (std::uint32_t i = 0; i < count; ++i) capacity += buffers[i].size; + } + std::size_t Room() const { return capacity - produced; } + void Emit(const std::uint8_t* source, std::size_t bytes) { + while (bytes > 0 && index < count) { + const std::size_t chunk = std::min(buffers[index].size - offset, bytes); + std::memcpy(static_cast(buffers[index].ptr) + offset, source, chunk); + source += chunk; + bytes -= chunk; + produced += chunk; + offset += chunk; + if (offset == buffers[index].size) { + ++index; + offset = 0; + } + } + } +}; + +// Instance flags bits 7..9 select the PCM encoding: 0 16-bit, 1 32-bit integer, 2 float. +std::uint32_t PcmEncoding(const Instance& instance) { + return static_cast((instance.flags >> 7u) & 7u); +} + +// Emits one decoded frame of interleaved PCM after the instance's gapless skip and up to its sample limit. +void EmitFrame(Instance& instance, PcmOutputs& outputs, const std::uint8_t* pcm, std::size_t samples, std::size_t channels, std::size_t sampleBytes) { + std::size_t first = 0; + std::size_t count = samples; + if (instance.gapless.skippedSamples < instance.gapless.skipSamples) { + const std::size_t skip = std::min(count, instance.gapless.skipSamples - instance.gapless.skippedSamples); + instance.gapless.skippedSamples = static_cast(instance.gapless.skippedSamples + skip); + first += skip; + count -= skip; + } + if (instance.gapless.totalSamples != 0) { + const std::uint64_t remaining = instance.gapless.totalSamples > instance.totalDecodedSamples ? instance.gapless.totalSamples - instance.totalDecodedSamples : 0; + count = static_cast(std::min(count, remaining)); + } + outputs.Emit(pcm + first * channels * sampleBytes, count * channels * sampleBytes); + instance.totalDecodedSamples += count; +} + +// The sidebands a run job asked for, in the SDK's order after its result. +void WriteRunSideband(const JobHeader& job, const Instance& instance, std::int32_t result, std::size_t consumed, std::size_t produced, std::uint32_t frames, const SidebandFormat& format) { + auto* sideband = static_cast(job.sideband); + std::size_t offset = 0; + const auto write = [&](const void* value, std::size_t size) { + if (!sideband || offset + size > job.sidebandSize) return; + std::memcpy(sideband + offset, value, size); + offset += size; + }; + const SidebandResult status{result, 0}; + write(&status, sizeof(status)); + if (job.flags & SIDEBAND_STREAM) { + const SidebandStream stream{static_cast(consumed), static_cast(produced), instance.totalDecodedSamples}; + write(&stream, sizeof(stream)); + } + if (job.flags & SIDEBAND_FORMAT) write(&format, sizeof(format)); + if (job.flags & SIDEBAND_GAPLESS_DECODE) write(&instance.gapless, sizeof(instance.gapless)); + if (job.flags & RUN_MULTIPLE_FRAMES) { + const SidebandMultipleFrames multiple{frames, 0}; + write(&multiple, sizeof(multiple)); + } +} + +// Decodes AT9 frames from the concatenated inputs into the concatenated outputs in the instance's PCM +// encoding. +void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { + const auto input = JoinInputs(job, inputs); + PcmOutputs pcmOutputs(outputs, job.outputCount); const auto channels = static_cast(instance.info.channels); const auto frameSamples = static_cast(instance.info.frameSamples); - // Instance flags bits 7..9 select the PCM encoding: 0 16-bit, 1 32-bit integer, 2 float. - const auto encoding = static_cast((instance.flags >> 7u) & 7u); + const auto encoding = PcmEncoding(instance); const std::size_t sampleBytes = encoding == 0 ? sizeof(std::int16_t) : sizeof(std::int32_t); const std::size_t frameBytes = frameSamples * channels * sampleBytes; std::vector pcm(frameBytes); @@ -247,25 +339,7 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c std::int32_t result = 0; int decodeStatus = 0; std::size_t consumed = 0; - std::size_t produced = 0; std::uint32_t frames = 0; - std::uint32_t outputIndex = 0; - std::size_t outputOffset = 0; - const auto emit = [&](const std::uint8_t* source, std::size_t bytes) { - while (bytes > 0 && outputIndex < job.outputCount) { - const std::size_t room = outputs[outputIndex].size - outputOffset; - const std::size_t chunk = std::min(room, bytes); - std::memcpy(static_cast(outputs[outputIndex].ptr) + outputOffset, source, chunk); - source += chunk; - bytes -= chunk; - produced += chunk; - outputOffset += chunk; - if (outputOffset == outputs[outputIndex].size) { - ++outputIndex; - outputOffset = 0; - } - } - }; // A superframe is a fixed superframeSize bytes holding framesInSuperframe frames packed back to back, // each byte aligned but of its own size (the encoder moves bits between the frames of a superframe), @@ -282,7 +356,7 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c if (input.size() != consumed) result |= AJM_RESULT_PARTIAL_INPUT; break; } - if (outputCapacity - produced < frameBytes) { + if (pcmOutputs.Room() < frameBytes) { if (frames == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } @@ -310,54 +384,137 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c } ++frames; - std::size_t first = 0; - std::size_t count = frameSamples; - if (instance.gapless.skippedSamples < instance.gapless.skipSamples) { - const std::size_t skip = std::min(count, instance.gapless.skipSamples - instance.gapless.skippedSamples); - instance.gapless.skippedSamples = static_cast(instance.gapless.skippedSamples + skip); - first += skip; - count -= skip; - } - if (instance.gapless.totalSamples != 0) { - const std::uint64_t remaining = instance.gapless.totalSamples > instance.totalDecodedSamples ? instance.gapless.totalSamples - instance.totalDecodedSamples : 0; - count = static_cast(std::min(count, remaining)); - } - emit(pcm.data() + first * channels * sampleBytes, count * channels * sampleBytes); - instance.totalDecodedSamples += count; + EmitFrame(instance, pcmOutputs, pcm.data(), frameSamples, channels, sampleBytes); if ((job.flags & RUN_MULTIPLE_FRAMES) == 0) break; } if (TraceEnabled()) { std::fprintf(stderr, "[ajm] instance %u run flags 0x%llx: %zu input bytes in %u buffers, %zu output bytes in %u buffers, sideband %llu bytes; superframe %d (%d frames), %d channels, %d samples/frame, %d Hz, config %02x %02x %02x %02x -> result 0x%x, decoder status 0x%x, %u frames, consumed %zu, produced %zu, total samples %llu, gapless total %u skip %u skipped %u, input", - job.instance, static_cast(job.flags), input.size(), job.inputCount, outputCapacity, job.outputCount, static_cast(job.sidebandSize), instance.info.superframeSize, instance.info.framesInSuperframe, instance.info.channels, instance.info.frameSamples, instance.info.samplingRate, - instance.info.configData[0], instance.info.configData[1], instance.info.configData[2], instance.info.configData[3], static_cast(result), static_cast(decodeStatus), frames, consumed, produced, static_cast(instance.totalDecodedSamples), instance.gapless.totalSamples, instance.gapless.skipSamples, instance.gapless.skippedSamples); + job.instance, static_cast(job.flags), input.size(), job.inputCount, pcmOutputs.capacity, job.outputCount, static_cast(job.sidebandSize), instance.info.superframeSize, instance.info.framesInSuperframe, instance.info.channels, instance.info.frameSamples, instance.info.samplingRate, + instance.info.configData[0], instance.info.configData[1], instance.info.configData[2], instance.info.configData[3], static_cast(result), static_cast(decodeStatus), frames, consumed, pcmOutputs.produced, static_cast(instance.totalDecodedSamples), instance.gapless.totalSamples, instance.gapless.skipSamples, instance.gapless.skippedSamples); for (std::size_t i = 0; i < input.size() && i < 16; ++i) std::fprintf(stderr, " %02x", input[i]); std::fprintf(stderr, "\n"); } - auto* sideband = static_cast(job.sideband); - std::size_t offset = 0; - const auto write = [&](const void* value, std::size_t size) { - if (!sideband || offset + size > job.sidebandSize) return; - std::memcpy(sideband + offset, value, size); - offset += size; - }; - const SidebandResult status{result, 0}; - write(&status, sizeof(status)); - if (job.flags & SIDEBAND_STREAM) { - const SidebandStream stream{static_cast(consumed), static_cast(produced), instance.totalDecodedSamples}; - write(&stream, sizeof(stream)); - } - if (job.flags & SIDEBAND_FORMAT) { - const SidebandFormat format{static_cast(channels), ChannelMask(channels), static_cast(instance.info.samplingRate), encoding, 0, 0}; - write(&format, sizeof(format)); - } - if (job.flags & SIDEBAND_GAPLESS_DECODE) write(&instance.gapless, sizeof(instance.gapless)); - if (job.flags & RUN_MULTIPLE_FRAMES) { - const SidebandMultipleFrames multiple{frames, 0}; - write(&multiple, sizeof(multiple)); - } + WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, SidebandFormat{static_cast(channels), ChannelMask(channels), static_cast(instance.info.samplingRate), encoding, 0, 0}); } +// An MPEG audio Layer III frame header: the frame's bytes and PCM shape; false when the bytes do not +// start one (free-format bitrates included). +struct Mp3Frame { + std::size_t bytes; + std::uint32_t channels; + std::uint32_t samples; + std::uint32_t sampleRate; + std::uint32_t bitrate; +}; + +bool ParseMp3Frame(const std::uint8_t* data, std::size_t size, Mp3Frame& frame) { + if (size < 4) return false; + const std::uint32_t header = (std::uint32_t{data[0]} << 24u) | (std::uint32_t{data[1]} << 16u) | (std::uint32_t{data[2]} << 8u) | data[3]; + const auto version = (header >> 19u) & 3u; + const auto layer = (header >> 17u) & 3u; + const auto bitrateIndex = (header >> 12u) & 15u; + const auto rateIndex = (header >> 10u) & 3u; + if ((header & 0xFFE00000u) != 0xFFE00000u || version == 1 || layer != 1 || bitrateIndex == 0 || bitrateIndex == 15 || rateIndex == 3) return false; + static constexpr std::uint32_t rates[4][3] = {{11025, 12000, 8000}, {0, 0, 0}, {22050, 24000, 16000}, {44100, 48000, 32000}}; + static constexpr std::uint32_t mpeg1Kbps[16] = {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0}; + static constexpr std::uint32_t mpeg2Kbps[16] = {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0}; + const bool mpeg1 = version == 3; + frame.sampleRate = rates[version][rateIndex]; + frame.bitrate = (mpeg1 ? mpeg1Kbps : mpeg2Kbps)[bitrateIndex] * 1000u; + frame.samples = mpeg1 ? 1152u : 576u; + frame.channels = ((header >> 6u) & 3u) == 3u ? 1u : 2u; + frame.bytes = (mpeg1 ? 144u : 72u) * frame.bitrate / frame.sampleRate + ((header >> 9u) & 1u); + return true; +} + +void OpenMp3(Instance& instance) { + static const bool quiet = (av_log_set_level(TraceEnabled() ? AV_LOG_WARNING : AV_LOG_QUIET), true); + (void)quiet; + const AVCodec* codec = avcodec_find_decoder(AV_CODEC_ID_MP3); + if (instance.mp3) avcodec_free_context(&instance.mp3); + instance.mp3 = codec ? avcodec_alloc_context3(codec) : nullptr; + if (!instance.mp3 || avcodec_open2(instance.mp3, codec, nullptr) < 0) throw std::runtime_error("AJM: cannot open the FFmpeg MP3 decoder"); +} + +// Decodes whole MP3 frames from the concatenated inputs. Like AT9 superframes, a frame is decoded only +// once all its bytes are present: a trailing partial frame stays unconsumed as partial input. +void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { + const auto input = JoinInputs(job, inputs); + PcmOutputs pcmOutputs(outputs, job.outputCount); + const auto encoding = PcmEncoding(instance); + if (encoding > 2) throw std::runtime_error("AJM MP3: PCM encoding " + std::to_string(encoding) + " is not implemented"); + const std::size_t sampleBytes = encoding == 0 ? sizeof(std::int16_t) : sizeof(std::int32_t); + std::unique_ptr packet(av_packet_alloc(), [](AVPacket* p) { av_packet_free(&p); }); + std::unique_ptr decoded(av_frame_alloc(), [](AVFrame* f) { av_frame_free(&f); }); + if (!packet || !decoded) throw std::bad_alloc(); + std::vector pcm; + + std::int32_t result = 0; + std::size_t consumed = 0; + std::uint32_t frames = 0; + while (consumed < input.size()) { + Mp3Frame frame{}; + if (!ParseMp3Frame(input.data() + consumed, input.size() - consumed, frame)) { + result |= AJM_RESULT_INVALID_DATA; + break; + } + if (input.size() - consumed < frame.bytes) { + result |= AJM_RESULT_PARTIAL_INPUT; + break; + } + if (pcmOutputs.Room() < frame.samples * frame.channels * sampleBytes) { + if (frames == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; + break; + } + // Not reference counted: the decoder copies the frame into a padded buffer of its own. + packet->data = const_cast(input.data() + consumed); + packet->size = static_cast(frame.bytes); + if (avcodec_send_packet(instance.mp3, packet.get()) < 0) { + result |= AJM_RESULT_INVALID_DATA; + break; + } + consumed += frame.bytes; + ++frames; + instance.mp3Channels = frame.channels; + instance.mp3SampleRate = frame.sampleRate; + instance.mp3Bitrate = frame.bitrate; + int status = 0; + while ((status = avcodec_receive_frame(instance.mp3, decoded.get())) == 0) { + if (decoded->format != AV_SAMPLE_FMT_FLTP) throw std::runtime_error("AJM MP3: FFmpeg sample format " + std::to_string(decoded->format) + " is not converted"); + const auto channels = static_cast(decoded->ch_layout.nb_channels); + const auto samples = static_cast(decoded->nb_samples); + pcm.resize(samples * channels * sampleBytes); + for (std::size_t sample = 0; sample < samples; ++sample) { + for (std::size_t channel = 0; channel < channels; ++channel) { + const float value = reinterpret_cast(decoded->extended_data[channel])[sample]; + auto* out = pcm.data() + (sample * channels + channel) * sampleBytes; + if (encoding == 0) { + const auto converted = static_cast(std::clamp(std::lrint(value * 32768.0), -32768L, 32767L)); + std::memcpy(out, &converted, sizeof(converted)); + } else if (encoding == 1) { + const auto converted = static_cast(std::clamp(std::llrint(value * 2147483648.0), -2147483648LL, 2147483647LL)); + std::memcpy(out, &converted, sizeof(converted)); + } else { + std::memcpy(out, &value, sizeof(value)); + } + } + } + EmitFrame(instance, pcmOutputs, pcm.data(), samples, channels, sampleBytes); + av_frame_unref(decoded.get()); + } + if (status != AVERROR(EAGAIN)) { + result |= AJM_RESULT_INVALID_DATA; + break; + } + if ((job.flags & RUN_MULTIPLE_FRAMES) == 0) break; + } + if (consumed == input.size() && frames == 0 && result == 0) result |= AJM_RESULT_PARTIAL_INPUT; + + AJM_TRACE("[ajm] instance %u mp3 run flags 0x%llx: %zu input bytes, %zu output bytes -> result 0x%x, %u frames, consumed %zu, produced %zu, %u channels, %u Hz, %u bps, total samples %llu\n", job.instance, static_cast(job.flags), input.size(), pcmOutputs.capacity, static_cast(result), frames, consumed, pcmOutputs.produced, + instance.mp3Channels, instance.mp3SampleRate, instance.mp3Bitrate, static_cast(instance.totalDecodedSamples)); + WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, SidebandFormat{instance.mp3Channels, ChannelMask(instance.mp3Channels), instance.mp3SampleRate, encoding, instance.mp3Bitrate, 0}); +} void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { if (job.kind == JobKind::GetStatistics) { AJM_TRACE("[ajm] statistics job: sideband %llu bytes\n", static_cast(job.sidebandSize)); @@ -383,6 +540,7 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out instance->totalDecodedSamples = 0; instance->gapless.skippedSamples = 0; if (instance->decoder && instance->initialized) ResetDecoder(*instance); + if (instance->mp3) avcodec_flush_buffers(instance->mp3); WriteResult(job.sideband, job.sidebandSize, 0); break; case JobKind::SetGaplessDecode: { @@ -401,9 +559,10 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out WriteResult(job.sideband, job.sidebandSize, AJM_RESULT_NOT_INITIALIZED); } else if (instance->codec == CODEC_AT9) { RunAt9(*instance, job, inputs, outputs); + } else if (instance->codec == CODEC_MP3) { + RunMp3(*instance, job, inputs, outputs); } else { - AJM_TRACE("[ajm] instance %u run on codec %u without a decoder\n", job.instance, instance->codec); - WriteResult(job.sideband, job.sidebandSize, AJM_RESULT_CODEC_ERROR); + throw std::runtime_error("AJM: decoding codec " + std::to_string(instance->codec) + " is not implemented"); } break; default: @@ -458,12 +617,14 @@ int APS5_VABI sceAjmMemoryUnregister(uint32_t context, void* ptr) { int APS5_VABI sceAjmInstanceCreate(uint32_t context, uint32_t codec, uint64_t flags, uint32_t* instance) { (void)context; if (!instance) return SCE_AJM_ERROR_INVALID_PARAMETER; - static std::atomic reportedCodecs{0}; - if (codec != CODEC_AT9 && codec < 32 && (reportedCodecs.fetch_or(1u << codec) & (1u << codec)) == 0) - std::fprintf(stderr, "[ajm] codec %u has no decoder; its jobs report codec errors\n", codec); auto created = std::make_unique(); created->codec = codec; created->flags = flags; + // MP3 has no codec parameters: the stream's frame headers carry them, so it runs without an initialize job. + if (codec == CODEC_MP3) { + OpenMp3(*created); + created->initialized = true; + } const std::uint32_t id = (codec << 14) | (g_nextInstance.fetch_add(1, std::memory_order_relaxed) & 0x3FFF); std::lock_guard lock(g_lock); g_instances[id] = std::move(created); @@ -532,6 +693,16 @@ int APS5_VABI sceAjmBatchJobRunSplit(AjmBatchInfo* info, uint32_t instance, uint return Append(info, header, input_buffers, output_buffers); } +int APS5_VABI sceAjmBatchJobRun(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const void* data_input, size_t data_input_size, void* data_output, size_t data_output_size, void* sideband_output, size_t sideband_output_size) { + const AjmBuffer input{const_cast(data_input), data_input_size}; + const AjmBuffer output{data_output, data_output_size}; + return sceAjmBatchJobRunSplit(info, instance, flags, &input, 1, &output, 1, sideband_output, sideband_output_size); +} + +int APS5_VABI sceAjmBatchJobDecode(AjmBatchInfo* info, uint32_t instance, const void* bitstream_input, size_t bitstream_input_size, void* pcm_output, size_t pcm_output_size, void* result) { + return sceAjmBatchJobRun(info, instance, SIDEBAND_STREAM, bitstream_input, bitstream_input_size, pcm_output, pcm_output_size, result, sizeof(SidebandResult) + sizeof(SidebandStream)); +} + int APS5_VABI sceAjmBatchJobGetStatistics(AjmBatchInfo* info, float interval, void* result) { (void)interval; return Append(info, MakeHeader(JobKind::GetStatistics, 0, result, 24), nullptr, nullptr); diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 16d7901f5..c1fccfd6e 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include #include @@ -329,6 +331,9 @@ int APS5_VABI sceAudioOutOpen(int userId, int type, int index, std::uint32_t len if (type != PORT_TYPE_VIBRATION) { openDevice(port); } + // APS5_TRACE_AUDIOOUT=1 reports the ports a title opens. + static const bool trace = std::getenv("APS5_TRACE_AUDIOOUT") != nullptr; + if (trace) std::fprintf(stderr, "[audioout] port %d: type %d, %u samples at %u Hz, format %d, device %u\n", i + 1, type, len, freq, static_cast(format), static_cast(port.device)); return i + 1; } } @@ -490,4 +495,9 @@ int APS5_VABI sceAudioOutGetPortState(int handle, AudioOutPortState* state) { return 0; } +int APS5_VABI sceAudioOutSetMixLevelPadSpk(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp index 611d3e726..86e9524cb 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp @@ -46,13 +46,13 @@ struct AudioOut2Port { std::uint32_t dataFormat = 0; std::uint32_t samplingFreq = 0; std::uint32_t flags = 0; - // Channel count decoded from dataFormat; 0 when the format is not understood (the port is then - // not rendered). Samples are float. + // Channel count and sample type decoded from dataFormat: float samples, or 16-bit integers. std::uint32_t channels = 0; - // The PCM buffer (one grain, float, interleaved) the title last handed over through the data - // attribute. It is guest memory the title rewrites every tick, so it is read when the context - // mixes, not when it is set. - const float* data = nullptr; + bool int16 = false; + // The PCM buffer (one grain, interleaved) the title last handed over through the data attribute. + // It is guest memory the title rewrites every tick, so it is read when the context mixes, not + // when it is set. + const void* data = nullptr; float volume[AUDIO_OUT2_PORT_CHANNELS_MAX] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; std::uint64_t dataSets = 0; std::uint64_t attributeTraces = 0; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp index 546ac704e..ea2f8a169 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp @@ -1,7 +1,10 @@ #include #include #include +#include #include +#include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -19,11 +22,12 @@ static constexpr std::uint64_t ATTRIBUTE_TRACE_EVERY = 2000; static constexpr std::uint32_t ATTRIBUTE_DATA = 0; static constexpr std::uint32_t ATTRIBUTE_VOLUME = 1; -// data_format bits 8..11 carry the channel count: the title opens 0x100 (mono object ports), -// 0x200 (stereo) and 0x880 (7.1 bed). The buffers behind them are float, as their spacing shows -// (1024 bytes per 256-sample mono grain). The low byte's meaning is not known. +// data_format bits 8..11 carry the channel count, bits 0..6 the sample type (0 float, 1 16-bit +// integer) and bit 7 the standard 8-channel order, as Kyty decodes it: the titles open 0x100 (mono +// object ports), 0x200 (stereo), 0x880 (7.1 bed) and 0x201 (a Bink movie's sound). static constexpr std::uint32_t FORMAT_CHANNELS_SHIFT = 8; static constexpr std::uint32_t FORMAT_CHANNELS_MASK = 0xFu; +static constexpr std::uint32_t FORMAT_TYPE_MASK = 0x7Fu; static std::mutex g_portsLock; // Grows on demand: the title opens its bed ports plus max_object_ports object ports at once. @@ -42,8 +46,12 @@ static constexpr float DOWNMIX_GAIN = 0.7071f; static void AccumulatePort(const AudioOut2Port& port, float* out, std::uint32_t frames) { const auto ch = port.channels; const float* volume = port.volume; + float in[AUDIO_OUT2_PORT_CHANNELS_MAX]; for (std::uint32_t frame = 0; frame < frames; frame++) { - const float* in = port.data + static_cast(frame) * ch; + const auto first = static_cast(frame) * ch; + for (std::uint32_t c = 0; c < ch; c++) { + in[c] = port.int16 ? static_cast(port.data)[first + c] / 32768.0f : static_cast(port.data)[first + c]; + } float left = 0.0f; float right = 0.0f; if (ch == 1) { @@ -116,7 +124,12 @@ int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut2 entry.samplingFreq = params->sampling_freq; entry.flags = params->flags; entry.channels = (params->data_format >> FORMAT_CHANNELS_SHIFT) & FORMAT_CHANNELS_MASK; - if (entry.channels > AUDIO_OUT2_PORT_CHANNELS_MAX) entry.channels = 0; + const auto sampleType = params->data_format & FORMAT_TYPE_MASK; + if (entry.channels == 0 || entry.channels > AUDIO_OUT2_PORT_CHANNELS_MAX || sampleType > 1) { + entry = AudioOut2Port{}; + throw std::runtime_error("sceAudioOut2PortCreate: data format 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%x", params->data_format); return std::string(text); }() + " is not implemented"); + } + entry.int16 = sampleType == 1; *port = static_cast(index) + 1; AUDIOOUT2_TRACE("t=%.3f PortCreate ctx=%llx -> port %llu: type=0x%x data_format=0x%x (%u float ch) sampling_freq=%u flags=0x%x user=%llx\n", AudioOut2TraceSeconds(), static_cast(ctx), static_cast(*port), params->port_type, params->data_format, diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp index 5f9850726..a8da0320c 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp @@ -1,30 +1,107 @@ +#include +#include #include #include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +// Layout of the params titles pass to sceAudioOut2SpeakerArrayCreate. +struct GuestVbapParams { + const AudioOut2Position* positions; + std::uint32_t numSpeakers; + std::uint8_t is3d; + void* memory; + std::size_t memorySize; + std::uint32_t reserved; +}; + +constexpr std::uint32_t MaxSpeakers = 32; +constexpr std::uint32_t AmbisonicsChannelFlag = 0x40; +constexpr std::uint32_t MaxAmbisonicsOrder = 5; + +// Lives in the memory the title allocates from sceAudioOut2GetSpeakerArrayMemorySize; the handle points at it. +struct SpeakerArray { + std::uint32_t magic; + std::uint32_t numSpeakers; + AudioOut2Position directions[MaxSpeakers]; +}; +constexpr std::uint32_t SpeakerArrayMagic = 0x53504b41; + +SpeakerArray& Array(AudioOut2SpeakerArrayHandle handle) { + auto* array = static_cast(handle); + if (array == nullptr || array->magic != SpeakerArrayMagic) throw std::invalid_argument("invalid speaker array handle"); + return *array; +} + +// Real SN3D spherical harmonic for ACN index `acn` (no Condon-Shortley phase), frame x front, y left, z up. +float Sn3d(std::uint32_t acn, const AudioOut2Position& direction) { + const int n = static_cast(std::sqrt(static_cast(acn))); + const int m = static_cast(acn) - n * n - n; + const int am = std::abs(m); + const double length = std::sqrt(double(direction.x) * direction.x + double(direction.y) * direction.y + double(direction.z) * direction.z); + const double sinEl = length > 0 ? direction.z / length : 0; + const double cosEl = std::sqrt(std::max(0.0, 1 - sinEl * sinEl)); + const double azimuth = std::atan2(direction.y, direction.x); + double legendre = 1; + for (int i = 1; i <= am; ++i) legendre *= (2 * i - 1) * cosEl; + if (n > am) { + double previous = legendre; + double current = sinEl * (2 * am + 1) * legendre; + for (int l = am + 2; l <= n; ++l) { + const double next = (sinEl * (2 * l - 1) * current - (l + am - 1) * previous) / (l - am); + previous = current; + current = next; + } + legendre = current; + } + double ratio = 1; + for (int i = n - am + 1; i <= n + am; ++i) ratio /= i; + const double norm = std::sqrt((m == 0 ? 1.0 : 2.0) * ratio); + return static_cast(norm * legendre * (m < 0 ? std::sin(am * azimuth) : std::cos(am * azimuth))); +} + +} + extern "C" { int APS5_VABI sceAudioOut2SpeakerArrayCreate(AudioOut2SpeakerArrayHandle* handle, const void* vbap_params, const void* ambi_params) { - (void)handle; - (void)vbap_params; - (void)ambi_params; - NotImplemented_nid_no_patch(__func__); + const auto* params = static_cast(vbap_params); + if (handle == nullptr || params == nullptr || ambi_params == nullptr || params->positions == nullptr || + params->numSpeakers == 0 || params->numSpeakers > MaxSpeakers || params->memory == nullptr || + params->memorySize < sizeof(SpeakerArray) || reinterpret_cast(params->memory) % alignof(SpeakerArray) != 0) { + APS5_INVALID_ARG_EX; + } + auto* array = new (params->memory) SpeakerArray{SpeakerArrayMagic, params->numSpeakers, {}}; + std::copy_n(params->positions, params->numSpeakers, array->directions); + *handle = array; return 0; } int APS5_VABI sceAudioOut2SpeakerArrayDestroy(AudioOut2SpeakerArrayHandle handle) { - (void)handle; - NotImplemented_nid_no_patch(__func__); + Array(handle).magic = 0; return 0; } +// Basic sampling decoder: speaker gain = sum over channels of Y(speaker) / numSpeakers, so a stereo pair decodes +// first order as two cardioids. Orders the layout cannot resolve (2D rule: 2N+1 speakers) get zero. int APS5_VABI sceAudioOut2GetSpeakerArrayAmbisonicsCoefficients(AudioOut2SpeakerArrayHandle handle, uint32_t ambisonics_channel, float* coefficients, uint32_t num_coefficients) { - (void)handle; - (void)ambisonics_channel; - (void)coefficients; - (void)num_coefficients; - NotImplemented_nid_no_patch(__func__); + const auto& array = Array(handle); + if (coefficients == nullptr || num_coefficients != array.numSpeakers) APS5_INVALID_ARG_EX; + // ponytail: only the 0x40-flagged encoding seen so far (assumed ACN/SN3D); other encodings throw until seen. + const std::uint32_t acn = ambisonics_channel & ~AmbisonicsChannelFlag; + if ((ambisonics_channel & AmbisonicsChannelFlag) == 0 || acn >= (MaxAmbisonicsOrder + 1) * (MaxAmbisonicsOrder + 1)) { + throw std::invalid_argument("unsupported ambisonics channel " + std::to_string(ambisonics_channel)); + } + const auto order = static_cast(std::sqrt(static_cast(acn))); + const std::uint32_t decodable = std::max(1, (array.numSpeakers - 1) / 2); + for (std::uint32_t speaker = 0; speaker < array.numSpeakers; ++speaker) { + coefficients[speaker] = order > decodable ? 0.0f : Sn3d(acn, array.directions[speaker]) / static_cast(array.numSpeakers); + } return 0; } @@ -41,11 +118,10 @@ int APS5_VABI sceAudioOut2GetSpeakerArrayCoefficients(AudioOut2SpeakerArrayHandl } size_t APS5_VABI sceAudioOut2GetSpeakerArrayMemorySize(uint32_t num_speakers, uint8_t is_3d, uint8_t is_ambisonics) { - (void)num_speakers; (void)is_3d; (void)is_ambisonics; - NotImplemented_nid_no_patch(__func__); - return 0; + if (num_speakers == 0 || num_speakers > MaxSpeakers) APS5_INVALID_ARG_EX; + return sizeof(SpeakerArray); } int APS5_VABI sceAudioOut2GetSpeakerInfo(AudioOut2SpeakerInfo* info, uint32_t flags) { diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index 91ae9bd80..44e6e9967 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -1,118 +1,213 @@ #include #include +#include +#include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +// A console with no headset and no voice peers: the library runs its lifecycle and keeps ports and +// their connections, but no voice ever arrives, so output ports stay empty. +namespace { + +constexpr std::int32_t PortInDevice = 0; +constexpr std::int32_t PortInPcm = 1; +constexpr std::int32_t PortInVoice = 2; +constexpr std::int32_t PortOutPcm = 3; +constexpr std::int32_t PortOutVoice = 4; +constexpr std::int32_t PortOutDevice = 5; +constexpr std::int32_t PortStateReady = 1; +constexpr std::uint32_t VoiceFrameMs = 20; + +struct Port { + std::int32_t type; + float volume; + std::uint32_t bitrate; +}; + +struct Voice { + std::mutex mutex; + bool initialized = false; + bool started = false; + std::uint32_t nextId = 0; + std::map ports; + std::set> connections; +}; + +Voice& State() { + static Voice voice; + return voice; +} + +[[noreturn]] void Fail(const char* function, const std::string& why) { + throw std::logic_error(std::string(function) + ": " + why); +} + +Port& RequirePort(Voice& voice, std::uint32_t id, const char* function) { + if (!voice.initialized) Fail(function, "library not initialized"); + const auto found = voice.ports.find(id); + if (found == voice.ports.end()) Fail(function, "unknown port " + std::to_string(id)); + return found->second; +} + +bool IsInput(std::int32_t type) { return type == PortInDevice || type == PortInPcm || type == PortInVoice; } + +} + extern "C" { -int APS5_VABI sceVoiceConnectIPortToOPort(uint32_t input_port_id, uint32_t output_port_id) { - (void)input_port_id; - (void)output_port_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceCreatePort(uint32_t* port_id, const VoicePortParam* param) { - (void)port_id; - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceDeletePort(uint32_t port_id) { - (void)port_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceDisconnectIPortFromOPort(uint32_t input_port_id, uint32_t output_port_id) { - (void)input_port_id; - (void)output_port_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceEnd_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceGetBitRate(uint32_t port_id, uint32_t* bitrate) { - (void)port_id; - (void)bitrate; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceGetPortAttr(uint32_t port_id, int32_t attr, void* value, int32_t size) { - (void)port_id; - (void)attr; - (void)value; - (void)size; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceGetPortInfo(uint32_t port_id, VoicePortInfo* info) { - (void)port_id; - (void)info; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceGetVolume(uint32_t port_id, float* volume) { - (void)port_id; - (void)volume; - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceVoiceInit(VoiceInitParam* param, int32_t version) { - (void)param; - (void)version; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceReadFromOPort(uint32_t output_port_id, void* data, uint32_t* size) { - (void)output_port_id; - (void)data; - (void)size; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)version; + if (param == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (voice.initialized) Fail(__func__, "already initialized"); + voice.initialized = true; + return 0; } int APS5_VABI sceVoiceSetThreadsParams(void* params) { - (void)params; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceSetVolume(uint32_t port_id, float volume) { - (void)port_id; - (void)volume; - NotImplemented_nid_no_patch(__func__); - return 0; + // Thread priorities and affinities for the voice threads; this library runs none. + if (params == nullptr) APS5_INVALID_ARG_EX; + return 0; } int APS5_VABI sceVoiceStart(const VoiceStartParam* param) { - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; + if (param == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) Fail(__func__, "library not initialized"); + voice.started = true; + return 0; } int APS5_VABI sceVoiceStop(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) Fail(__func__, "library not initialized"); + voice.started = false; + return 0; +} + +int APS5_VABI sceVoiceEnd_nid_postfix(void) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) Fail(__func__, "library not initialized"); + voice.ports.clear(); + voice.connections.clear(); + voice.started = false; + voice.initialized = false; + return 0; +} + +int APS5_VABI sceVoiceCreatePort(uint32_t* port_id, const VoicePortParam* param) { + if (port_id == nullptr || param == nullptr) APS5_INVALID_ARG_EX; + if (param->port_type < PortInDevice || param->port_type > PortOutDevice) Fail(__func__, "unknown port type " + std::to_string(param->port_type)); + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) Fail(__func__, "library not initialized"); + const bool voicePort = param->port_type == PortInVoice || param->port_type == PortOutVoice; + const auto id = voice.nextId++; + voice.ports.emplace(id, Port{param->port_type, param->volume, voicePort ? static_cast(param->voice.bitrate) : 0u}); + *port_id = id; + return 0; +} + +int APS5_VABI sceVoiceDeletePort(uint32_t port_id) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + RequirePort(voice, port_id, __func__); + std::erase_if(voice.connections, [port_id](const auto& link) { return link.first == port_id || link.second == port_id; }); + voice.ports.erase(port_id); + return 0; +} + +int APS5_VABI sceVoiceConnectIPortToOPort(uint32_t input_port_id, uint32_t output_port_id) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!IsInput(RequirePort(voice, input_port_id, __func__).type)) Fail(__func__, "first port is not an input port"); + if (IsInput(RequirePort(voice, output_port_id, __func__).type)) Fail(__func__, "second port is not an output port"); + voice.connections.emplace(input_port_id, output_port_id); + return 0; +} + +int APS5_VABI sceVoiceDisconnectIPortFromOPort(uint32_t input_port_id, uint32_t output_port_id) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + RequirePort(voice, input_port_id, __func__); + RequirePort(voice, output_port_id, __func__); + if (voice.connections.erase({input_port_id, output_port_id}) == 0) Fail(__func__, "ports are not connected"); + return 0; +} + +int APS5_VABI sceVoiceGetBitRate(uint32_t port_id, uint32_t* bitrate) { + if (bitrate == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + const auto& port = RequirePort(voice, port_id, __func__); + if (port.type != PortInVoice && port.type != PortOutVoice) Fail(__func__, "port does not carry encoded voice"); + *bitrate = port.bitrate; + return 0; +} + +int APS5_VABI sceVoiceSetVolume(uint32_t port_id, float volume) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + RequirePort(voice, port_id, __func__).volume = volume; + return 0; +} + +int APS5_VABI sceVoiceGetVolume(uint32_t port_id, float* volume) { + if (volume == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + *volume = RequirePort(voice, port_id, __func__).volume; + return 0; +} + +int APS5_VABI sceVoiceGetPortInfo(uint32_t port_id, VoicePortInfo* info) { + if (info == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + const auto& port = RequirePort(voice, port_id, __func__); + *info = VoicePortInfo{}; + info->port_type = port.type; + info->state = PortStateReady; + // Titles pace voice by frame (a title divides by the frame length it derives from this). + // ponytail: the codec's real frame layout is unknown; 20 ms frames at the port's bitrate. + if (port.bitrate != 0) info->frame_size = (port.bitrate * VoiceFrameMs + 7999u) / 8000u; + return 0; +} + +int APS5_VABI sceVoiceGetPortAttr(uint32_t port_id, int32_t attr, void* value, int32_t size) { + (void)value; + (void)size; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + RequirePort(voice, port_id, __func__); + Fail(__func__, "attribute " + std::to_string(attr) + " not implemented"); +} + +int APS5_VABI sceVoiceReadFromOPort(uint32_t output_port_id, void* data, uint32_t* size) { + if (data == nullptr || size == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (IsInput(RequirePort(voice, output_port_id, __func__).type)) Fail(__func__, "port is not an output port"); + // Inputs are the absent microphone or WriteToIPort, which is not implemented: nothing to read. + *size = 0; + return 0; } int APS5_VABI sceVoiceWriteToIPort(uint32_t input_port_id, const void* data, uint32_t* size, int16_t frame_gaps) { - (void)input_port_id; - (void)data; - (void)size; - (void)frame_gaps; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)input_port_id; + (void)data; + (void)size; + (void)frame_gaps; + NotImplemented_nid_no_patch(__func__); + return 0; } } From 502a79381a72cf68e15b43449876ad8eff5822c9 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 07:54:05 +0300 Subject: [PATCH 07/40] feat(libs): system services for a console without PSN, dialogs or streaming - NP: signed-out callbacks and reachability; owned add-on entitlements from anyps5-entitlements.ini next to the executable (or the file ANYPS5_ENTITLEMENTS names) - web browser, message, player invitation and player selection dialogs finish without a UI; Remote Play, IME keyboard, SharePlay, live streaming and video recording lifecycles for a console without them - app content temporary data mount, format and free space (download data quota from param.json); content export, search and delete lifecycles - pad output-only calls, system service notice-screen flags, RazorCpu markers, ulobjmgr - throwing stubs for the remaining imports, so missing behavior fails loudly Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAppContent/Export.cpp | 61 ++++++++--- core/libs/prx/libSceContentDelete/Export.cpp | 22 +++- core/libs/prx/libSceContentExport/Export.cpp | 37 ++++++- core/libs/prx/libSceContentSearch/Export.cpp | 42 +++++++- .../prx/libSceGameLiveStreaming/Export.cpp | 9 +- core/libs/prx/libSceIme/Export.cpp | 27 +++-- .../prx/libSceMsgDialog.native/Export.cpp | 35 ++++++ core/libs/prx/libSceNpCommerce/Export.cpp | 10 ++ .../prx/libSceNpEntitlementAccess/Export.cpp | 102 ++++++++++++++++-- core/libs/prx/libSceNpManager/Export.cpp | 56 +++++----- core/libs/prx/libSceNpUtility/Export.cpp | 25 +++++ .../libScePlayerInvitationDialog/Export.cpp | 18 ++++ .../libScePlayerSelectionDialog/Export.cpp | 13 +++ core/libs/prx/libSceRazorCpu/Export.cpp | 18 ++++ core/libs/prx/libSceRemoteplay/Export.cpp | 14 +-- core/libs/prx/libSceSharePlay/Export.cpp | 10 +- core/libs/prx/libSceSystemService/Export.cpp | 12 ++- .../libs/prx/libSceVideoRecordingP/Export.cpp | 80 ++++++++++++++ .../prx/libSceWebBrowserDialog/Export.cpp | 32 +++++- core/libs/prx/ulobjmgr/Export.cpp | 20 ++++ 20 files changed, 564 insertions(+), 79 deletions(-) create mode 100644 core/libs/prx/libScePlayerInvitationDialog/Export.cpp create mode 100644 core/libs/prx/libScePlayerSelectionDialog/Export.cpp create mode 100644 core/libs/prx/libSceVideoRecordingP/Export.cpp create mode 100644 core/libs/prx/ulobjmgr/Export.cpp diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index 9469c86d6..1740dea13 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -2,12 +2,31 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" +#include #include +#include +#include +#include static constexpr int SCE_APP_CONTENT_ERROR_PARAMETER = static_cast(0x80D90002); static constexpr uint32_t APPPARAM_ID_SKU_FLAG = 1; static constexpr int32_t SKU_FLAG_FULL = 3; +static constexpr char TEMPORARY_MOUNT_POINT[] = "/temp0"; +static constexpr char DOWNLOAD_MOUNT_POINT[] = "/download0"; +static constexpr uint32_t TEMPORARY_DATA_OPTION_FORMAT = 1; + +// Temporary data lives in /temp0 on the host and survives until the title formats it. +static std::filesystem::path TemporaryDirectory(const AppContentMountPoint* mount_point) { + if (!mount_point || std::strncmp(mount_point->data, TEMPORARY_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX; + return ResolvePath_nid_no_patch(TEMPORARY_MOUNT_POINT); +} + +static void ClearDirectory(const std::filesystem::path& directory) { + for (const auto& entry : std::filesystem::directory_iterator(directory)) std::filesystem::remove_all(entry.path()); +} + extern "C" { int APS5_VABI sceAppContentAddcontMount(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label, AppContentMountPoint* mount_point) { @@ -39,10 +58,20 @@ int APS5_VABI sceAppContentAppParamGetInt(uint32_t param_id, int32_t* value) { } int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMountPoint* mount_point, size_t* available_space_kb) { - (void)mount_point; - (void)available_space_kb; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!available_space_kb) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!mount_point || std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX; + const std::uint64_t quotaKb = GetAppDownloadDataSizeMiB_nid_postfix() * 1024u; + if (quotaKb == 0) throw std::logic_error(std::string(__func__) + ": the title declares no download data"); + // The system mounts the title's download data before boot; on the host it is /download0. + const auto directory = ResolvePath_nid_no_patch(DOWNLOAD_MOUNT_POINT); + std::filesystem::create_directories(directory); + std::uint64_t usedKb = 0; + for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) { + if (entry.is_regular_file()) usedKb += (entry.file_size() + 1023u) / 1024u; + } + const std::uint64_t hostKb = std::filesystem::space(directory).available / 1024u; + *available_space_kb = static_cast(std::min(quotaKb - std::min(quotaKb, usedKb), hostKb)); + return 0; } int APS5_VABI sceAppContentInitialize(const AppContentInitParam* init_param, AppContentBootParam* boot_param) { @@ -53,23 +82,25 @@ int APS5_VABI sceAppContentInitialize(const AppContentInitParam* init_param, App } int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint* mount_point) { - (void)mount_point; - NotImplemented_nid_no_patch(__func__); - return 0; + ClearDirectory(TemporaryDirectory(mount_point)); + return 0; } int APS5_VABI sceAppContentTemporaryDataGetAvailableSpaceKb(const AppContentMountPoint* mount_point, size_t* available_space_kb) { - (void)mount_point; - (void)available_space_kb; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!available_space_kb) return SCE_APP_CONTENT_ERROR_PARAMETER; + *available_space_kb = static_cast(std::filesystem::space(TemporaryDirectory(mount_point)).available / 1024); + return 0; } int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t option, AppContentMountPoint* mount_point) { - (void)option; - (void)mount_point; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (option > TEMPORARY_DATA_OPTION_FORMAT) throw std::invalid_argument(std::string(__func__) + ": unknown option " + std::to_string(option)); + std::memset(mount_point->data, 0, sizeof(mount_point->data)); + std::memcpy(mount_point->data, TEMPORARY_MOUNT_POINT, sizeof(TEMPORARY_MOUNT_POINT)); + const auto directory = TemporaryDirectory(mount_point); + std::filesystem::create_directories(directory); + if (option == TEMPORARY_DATA_OPTION_FORMAT) ClearDirectory(directory); + return 0; } } diff --git a/core/libs/prx/libSceContentDelete/Export.cpp b/core/libs/prx/libSceContentDelete/Export.cpp index 74793082b..7d77a1ffb 100644 --- a/core/libs/prx/libSceContentDelete/Export.cpp +++ b/core/libs/prx/libSceContentDelete/Export.cpp @@ -2,13 +2,33 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include +#include +#include + +namespace { +// Deletes exported content; the lifecycle is honest, deletion is not implemented. +std::atomic g_initialized{false}; +} extern "C" { int APS5_VABI sceContentDeleteInitialize(const ContentDeleteInitParam* init_param) { - (void)init_param; + if (init_param == nullptr) APS5_INVALID_ARG_EX; + bool expected = false; + if (!g_initialized.compare_exchange_strong(expected, true)) throw std::logic_error(std::string(__func__) + ": already initialized"); + return 0; +} + +int APS5_VABI sceContentDeleteByPath(void) { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceContentDeleteTerminate(void) { + bool expected = true; + if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized"); + return 0; +} + } diff --git a/core/libs/prx/libSceContentExport/Export.cpp b/core/libs/prx/libSceContentExport/Export.cpp index 124b82263..a7a90886b 100644 --- a/core/libs/prx/libSceContentExport/Export.cpp +++ b/core/libs/prx/libSceContentExport/Export.cpp @@ -2,13 +2,48 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include +#include +#include + +namespace { +// Exports to the system gallery; the lifecycle is honest, exports themselves are not implemented. +std::atomic g_initialized{false}; +} extern "C" { int APS5_VABI sceContentExportInit2(const ContentExportInitParam2* init_param) { - (void)init_param; + if (init_param == nullptr) APS5_INVALID_ARG_EX; + bool expected = false; + if (!g_initialized.compare_exchange_strong(expected, true)) throw std::logic_error(std::string(__func__) + ": already initialized"); + return 0; +} + +int APS5_VABI sceContentExportFinish(void) { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceContentExportFromFile(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFromFileWithThumbnail(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportTerm(void) { + bool expected = true; + if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized"); + return 0; +} + } diff --git a/core/libs/prx/libSceContentSearch/Export.cpp b/core/libs/prx/libSceContentSearch/Export.cpp index bb6042a3c..0bd423b39 100644 --- a/core/libs/prx/libSceContentSearch/Export.cpp +++ b/core/libs/prx/libSceContentSearch/Export.cpp @@ -2,13 +2,53 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include +#include +#include + +namespace { +// Searches the system content library; the lifecycle is honest, searches are not implemented. +std::atomic g_initialized{false}; +} extern "C" { int APS5_VABI sceContentSearchInit(const ContentSearchInitParam* init_param) { - (void)init_param; + if (init_param == nullptr) APS5_INVALID_ARG_EX; + bool expected = false; + if (!g_initialized.compare_exchange_strong(expected, true)) throw std::logic_error(std::string(__func__) + ": already initialized"); + return 0; +} + +int APS5_VABI sceContentSearchCloseMetadata(void) { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceContentSearchGetMetadataFieldInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentSearchGetMetadataValue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentSearchOpenMetadata(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentSearchSearchContent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentSearchTerm(void) { + bool expected = true; + if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized"); + return 0; +} + } diff --git a/core/libs/prx/libSceGameLiveStreaming/Export.cpp b/core/libs/prx/libSceGameLiveStreaming/Export.cpp index 8322715b2..a0401f1bb 100644 --- a/core/libs/prx/libSceGameLiveStreaming/Export.cpp +++ b/core/libs/prx/libSceGameLiveStreaming/Export.cpp @@ -6,14 +6,13 @@ extern "C" { int APS5_VABI sceGameLiveStreamingInitialize(size_t heap_size) { - (void)heap_size; - NotImplemented_nid_no_patch(__func__); - return 0; + // Nothing is ever broadcast; the library only has to accept its lifecycle. + if (heap_size == 0) APS5_INVALID_ARG_EX; + return 0; } int APS5_VABI sceGameLiveStreamingTerminate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 0; } } diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index a9bdabf45..c4303becc 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -1,8 +1,21 @@ #include #include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +// The host keyboard drives the virtual pad (docs/INPUT_MAPPING.md), so no USB keyboard is attached: +// opening the keyboard succeeds, as on a console with none plugged in, and it never reports events. +std::mutex g_keyboardMutex; +std::set g_openKeyboards; + +} + extern "C" { int APS5_VABI sceImeClose_nid_postfix(void) { @@ -19,8 +32,8 @@ int APS5_VABI sceImeGetPanelSize(const Param* param, uint32_t* width, uint32_t* } int APS5_VABI sceImeKeyboardClose(int32_t user_id) { - (void)user_id; - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_keyboardMutex); + if (g_openKeyboards.erase(user_id) == 0) throw std::logic_error("sceImeKeyboardClose: keyboard not open for user " + std::to_string(user_id)); return 0; } @@ -39,9 +52,9 @@ int APS5_VABI sceImeKeyboardGetResourceId(int32_t user_id, KeyboardResourceIdArr } int APS5_VABI sceImeKeyboardOpen(int32_t user_id, const KeyboardParam* param) { - (void)user_id; - (void)param; - NotImplemented_nid_no_patch(__func__); + if (!param) APS5_INVALID_ARG_EX; + std::lock_guard lock(g_keyboardMutex); + if (!g_openKeyboards.insert(user_id).second) throw std::logic_error("sceImeKeyboardOpen: keyboard already open for user " + std::to_string(user_id)); return 0; } @@ -85,8 +98,8 @@ int APS5_VABI sceImeSetTextGeometry(TextAreaMode mode, const TextGeometry* geome } int APS5_VABI sceImeUpdate(EventHandler handler) { - (void)handler; - NotImplemented_nid_no_patch(__func__); + if (!handler) APS5_INVALID_ARG_EX; + // Only keyboard events could be pending (sceImeOpen is not implemented), and none are generated. return 0; } diff --git a/core/libs/prx/libSceMsgDialog.native/Export.cpp b/core/libs/prx/libSceMsgDialog.native/Export.cpp index dbe06e1d1..208106db8 100644 --- a/core/libs/prx/libSceMsgDialog.native/Export.cpp +++ b/core/libs/prx/libSceMsgDialog.native/Export.cpp @@ -1,10 +1,45 @@ +#include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { +// Common dialog status: 0 none, 1 initialized, 2 running, 3 finished. Nothing is ever opened here, so it never runs. +std::atomic g_status{0}; +} + extern "C" { int SceMsgDialogNativeModuleLoaded_nid_no_patch = 1; +int APS5_VABI sceMsgDialogGetResult(void* result) { + (void)result; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMsgDialogInitialize(void) { + int expected = 0; + if (!g_status.compare_exchange_strong(expected, 1)) throw std::logic_error("sceMsgDialogInitialize: already initialized"); + return 0; +} + +int APS5_VABI sceMsgDialogOpen(const void* param) { + (void)param; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMsgDialogTerminate(void) { + int expected = 1; + if (!g_status.compare_exchange_strong(expected, 0)) throw std::logic_error("sceMsgDialogTerminate: not initialized or still running"); + return 0; +} + +int APS5_VABI sceMsgDialogUpdateStatus(void) { + return g_status.load(); +} + } diff --git a/core/libs/prx/libSceNpCommerce/Export.cpp b/core/libs/prx/libSceNpCommerce/Export.cpp index e56b3e859..e9d65b9be 100644 --- a/core/libs/prx/libSceNpCommerce/Export.cpp +++ b/core/libs/prx/libSceNpCommerce/Export.cpp @@ -65,4 +65,14 @@ int APS5_VABI sceNpCommerceDialogTerminate() { return 0; } +int APS5_VABI sceNpCommerceHidePsStoreIcon(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCommerceShowPsStoreIcon(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index 1e31e7b5a..8de1356bd 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -2,27 +2,90 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#endif -// No add-on content is installed; the base game is the full SKU. +// The base game is the full SKU; the add-ons the account owns come from anyps5-entitlements.ini (below). static constexpr int SCE_NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER = static_cast(0x80558003); static constexpr int SCE_NP_ENTITLEMENT_ACCESS_ERROR_NOT_FOUND = static_cast(0x80558007); static constexpr uint32_t SKU_FLAG_FULL = 3; +namespace { + +std::filesystem::path EntitlementsPath() { + if (const char* configured = std::getenv("ANYPS5_ENTITLEMENTS"); configured != nullptr && configured[0] != '\0') return configured; +#ifdef _WIN32 + wchar_t module[MAX_PATH]; + const auto length = GetModuleFileNameW(nullptr, module, MAX_PATH); + if (length == 0 || length == MAX_PATH) throw std::runtime_error("NpEntitlementAccess: cannot locate the executable"); + return std::filesystem::path(module).parent_path() / "anyps5-entitlements.ini"; +#else + return std::filesystem::read_symlink("/proc/self/exe").parent_path() / "anyps5-entitlements.ini"; +#endif +} + +// The add-on entitlements the account owns: one unified entitlement label per line of +// anyps5-entitlements.ini next to the executable (ANYPS5_ENTITLEMENTS names another file), '#' and ';' +// starting comments. Without the file the account owns no add-on. They are reported as installed +// license-only add-ons (package type 3, download status 4), as Kyty reports them. +const std::vector& OwnedAddons() { + static const auto owned = [] { + std::vector addons; + const auto path = EntitlementsPath(); + std::ifstream file(path); + if (!file) { + if (std::getenv("ANYPS5_ENTITLEMENTS") != nullptr || std::filesystem::exists(path)) throw std::runtime_error("NpEntitlementAccess: cannot read " + path.string()); + return addons; + } + std::string line; + while (std::getline(file, line)) { + line.erase(std::min(line.find_first_of("#;"), line.size())); + const auto first = line.find_first_not_of(" \t\r"); + if (first == std::string::npos) continue; + const auto label = line.substr(first, line.find_last_not_of(" \t\r") + 1 - first); + NpEntitlementAccessAddcontEntitlementInfo info{}; + if (label.size() >= sizeof(info.entitlement_label.data)) throw std::runtime_error("NpEntitlementAccess: entitlement label '" + label + "' in " + path.string() + " is longer than 16 characters"); + std::memcpy(info.entitlement_label.data, label.data(), label.size()); + info.package_type = 3; + info.download_status = 4; + addons.push_back(info); + } + return addons; + }(); + return owned; +} + +} + extern "C" { int APS5_VABI sceNpEntitlementAccessGetAddcontEntitlementInfo(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label, NpEntitlementAccessAddcontEntitlementInfo* info) { (void)service_label; - (void)entitlement_label; - (void)info; + if (!entitlement_label || !info) return SCE_NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER; + for (const auto& addon : OwnedAddons()) { + if (std::strncmp(addon.entitlement_label.data, entitlement_label->data, sizeof(entitlement_label->data)) == 0) { + *info = addon; + return 0; + } + } return SCE_NP_ENTITLEMENT_ACCESS_ERROR_NOT_FOUND; } int APS5_VABI sceNpEntitlementAccessGetAddcontEntitlementInfoList(uint32_t service_label, NpEntitlementAccessAddcontEntitlementInfo* list, uint32_t list_num, uint32_t* hit_num) { (void)service_label; - (void)list; - (void)list_num; - if (!hit_num) return SCE_NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER; - *hit_num = 0; + if (!hit_num || (!list && list_num != 0)) return SCE_NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER; + const auto& owned = OwnedAddons(); + *hit_num = static_cast(owned.size()); + std::copy_n(owned.begin(), std::min(list_num, owned.size()), list); return 0; } @@ -38,4 +101,29 @@ int APS5_VABI sceNpEntitlementAccessInitialize(const NpEntitlementAccessInitPara return 0; } +int APS5_VABI sceNpEntitlementAccessAbortRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessDeleteRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessGenerateTransactionId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessPollConsumeEntitlement(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessRequestConsumeUnifiedEntitlement(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index dac4c2cfa..87d0fd9f8 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -9,6 +9,7 @@ static constexpr int SCE_NP_ERROR_INVALID_ARGUMENT = static_cast(0x80550003 static constexpr int SCE_NP_ERROR_SIGNED_OUT = static_cast(0x80550006); static constexpr uint32_t NP_STATE_SIGNED_OUT = 1; static constexpr int NP_POLL_ASYNC_FINISHED = 0; +static constexpr uint32_t NP_REACHABILITY_STATE_UNAVAILABLE = 0; static std::atomic g_nextRequest{1}; @@ -87,10 +88,10 @@ int APS5_VABI sceNpGetNpId(int user_id, NpId* np_id) { } int APS5_VABI sceNpGetNpReachabilityState(int user_id, uint32_t* state) { - (void)user_id; - (void)state; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)user_id; + if (!state) return SCE_NP_ERROR_INVALID_ARGUMENT; + *state = NP_REACHABILITY_STATE_UNAVAILABLE; + return 0; } int APS5_VABI sceNpGetOnlineId(int user_id, NpOnlineId* online_id) { @@ -107,10 +108,10 @@ int APS5_VABI sceNpGetState(int user_id, uint32_t* state) { } int APS5_VABI sceNpHasSignedUp(int user_id, bool* has_signed_up) { - (void)user_id; - (void)has_signed_up; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)user_id; + if (!has_signed_up) return SCE_NP_ERROR_INVALID_ARGUMENT; + *has_signed_up = false; + return 0; } int APS5_VABI sceNpPollAsync(int req_id, int* result) { @@ -126,31 +127,31 @@ void APS5_VABI sceNpRegisterGamePresenceCallback(void* callback, void* userdata) } int APS5_VABI sceNpRegisterNpReachabilityStateCallback(void* callback, void* userdata) { - (void)callback; - (void)userdata; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)userdata; + // The signed-out state never changes, so registered callbacks never fire. + if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; + return 0; } int APS5_VABI sceNpRegisterPlusEventCallback(void* callback, void* userdata) { - (void)callback; - (void)userdata; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)userdata; + // The signed-out state never changes, so registered callbacks never fire. + if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; + return 0; } int APS5_VABI sceNpRegisterPremiumEventCallback(void* callback, void* userdata) { - (void)callback; - (void)userdata; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)userdata; + // The signed-out state never changes, so registered callbacks never fire. + if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; + return 0; } int APS5_VABI sceNpRegisterStateCallback(void* callback, void* userdata) { - (void)callback; - (void)userdata; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)userdata; + // The signed-out state never changes, so registered callbacks never fire. + if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; + return 0; } int APS5_VABI sceNpSetContentRestriction(const NpContentRestriction* restriction) { @@ -166,8 +167,7 @@ int APS5_VABI sceNpSetNpTitleId(const NpTitleId* title_id, const NpTitleSecret* } int APS5_VABI sceNpUnregisterStateCallback(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 0; } int APS5_VABI sceNpGetAccountLanguage2(int req_id, int user_id, void* language) { @@ -193,4 +193,8 @@ int APS5_VABI sceNpUnregisterStateCallbackA(int callback_id) { return 0; } +int APS5_VABI sceNpUnregisterPremiumEventCallback(void) { + return 0; +} + } diff --git a/core/libs/prx/libSceNpUtility/Export.cpp b/core/libs/prx/libSceNpUtility/Export.cpp index ef15719ce..a6150edc9 100644 --- a/core/libs/prx/libSceNpUtility/Export.cpp +++ b/core/libs/prx/libSceNpUtility/Export.cpp @@ -7,4 +7,29 @@ extern "C" { int SceNpUtilityModuleLoaded_nid_no_patch = 1; +int APS5_VABI sceNpBandwidthTestAbort(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBandwidthTestGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBandwidthTestInitStartDownload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBandwidthTestInitStartUpload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBandwidthTestShutdown(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libScePlayerInvitationDialog/Export.cpp b/core/libs/prx/libScePlayerInvitationDialog/Export.cpp new file mode 100644 index 000000000..50ecd53dc --- /dev/null +++ b/core/libs/prx/libScePlayerInvitationDialog/Export.cpp @@ -0,0 +1,18 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI scePlayerInvitationDialogTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayerInvitationDialogUpdateStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp new file mode 100644 index 000000000..ef0798b7b --- /dev/null +++ b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp @@ -0,0 +1,13 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI scePlayerSelectionDialogTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceRazorCpu/Export.cpp b/core/libs/prx/libSceRazorCpu/Export.cpp index 7c7110e30..27bfe2560 100644 --- a/core/libs/prx/libSceRazorCpu/Export.cpp +++ b/core/libs/prx/libSceRazorCpu/Export.cpp @@ -10,4 +10,22 @@ uint32_t APS5_VABI sceRazorCpuIsCapturing(void) { return 0; } +int APS5_VABI sceRazorCpuJobManagerDispatch(const void* args) { + (void)args; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRazorCpuJobManagerJob(const void* args) { + (void)args; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRazorCpuJobManagerSequence(const void* args) { + (void)args; + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceRemoteplay/Export.cpp b/core/libs/prx/libSceRemoteplay/Export.cpp index cf953dfcc..ca410a355 100644 --- a/core/libs/prx/libSceRemoteplay/Export.cpp +++ b/core/libs/prx/libSceRemoteplay/Export.cpp @@ -1,26 +1,28 @@ #include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +// A PC has no Remote Play: the library initializes and every user stays disconnected. +constexpr int REMOTEPLAY_CONNECTION_STATUS_DISCONNECT = 0; + extern "C" { int APS5_VABI sceRemoteplayGetConnectionStatus(int user_id, int* status) { (void)user_id; - (void)status; - NotImplemented_nid_no_patch(__func__); + if (!status) APS5_INVALID_ARG_EX; + *status = REMOTEPLAY_CONNECTION_STATUS_DISCONNECT; return 0; } int APS5_VABI sceRemoteplayInitialize(void* heap, size_t heap_size) { - (void)heap; - (void)heap_size; - NotImplemented_nid_no_patch(__func__); + if (!heap || heap_size == 0) APS5_INVALID_ARG_EX; return 0; } int APS5_VABI sceRemoteplayTerminate(void) { - NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceSharePlay/Export.cpp b/core/libs/prx/libSceSharePlay/Export.cpp index 03970342a..4b4f66b2b 100644 --- a/core/libs/prx/libSceSharePlay/Export.cpp +++ b/core/libs/prx/libSceSharePlay/Export.cpp @@ -6,15 +6,13 @@ extern "C" { int APS5_VABI sceSharePlayInitialize(void* heap, size_t heap_size) { - (void)heap; - (void)heap_size; - NotImplemented_nid_no_patch(__func__); - return 0; + // No Share Play session can ever start; the library only has to accept its lifecycle. + if (heap == nullptr || heap_size == 0) APS5_INVALID_ARG_EX; + return 0; } int APS5_VABI sceSharePlayTerminate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 0; } } diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 9ff7183b3..3778c5f9d 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -20,8 +20,7 @@ int APS5_VABI sceSystemServiceLoadExec(const char* path, const char* const* argu } int APS5_VABI sceSystemServiceDisableNoticeScreenSkipFlagAutoSet(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SYSTEM_SERVICE_OK; } int APS5_VABI sceSystemServiceGetDisplaySafeAreaInfo(SystemServiceDisplaySafeAreaInfo* info) { @@ -103,6 +102,15 @@ int APS5_VABI sceSystemServiceReportAbnormalTermination(const void* info) { } int APS5_VABI sceSystemServiceSetNoticeScreenSkipFlag(void) { + return SYSTEM_SERVICE_OK; +} + +int APS5_VABI sceSystemServiceInitializePlayerDialogParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchPlayerDialog(void) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceVideoRecordingP/Export.cpp b/core/libs/prx/libSceVideoRecordingP/Export.cpp new file mode 100644 index 000000000..599945ffb --- /dev/null +++ b/core/libs/prx/libSceVideoRecordingP/Export.cpp @@ -0,0 +1,80 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include + +namespace { +// Recording metadata the title sets for the system recorder (titles, permission levels, guard +// areas); kept as given. Nothing records, so recording itself is not implemented. +std::mutex g_infoMutex; +std::map> g_info; +} + +extern "C" { + +int APS5_VABI sceVideoRecordingClose(int discard) { + (void)discard; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingGetInfo(int32_t info, void* data, size_t size) { + if (data == nullptr) APS5_INVALID_ARG_EX; + std::lock_guard lock(g_infoMutex); + const auto found = g_info.find(info); + if (found == g_info.end()) throw std::logic_error(std::string(__func__) + ": info " + std::to_string(info) + " was never set"); + if (size < found->second.size()) APS5_INVALID_ARG_EX; + std::memcpy(data, found->second.data(), found->second.size()); + return 0; +} + +int APS5_VABI sceVideoRecordingGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingOpen(void* buffer, size_t size, const void* param) { + (void)buffer; + (void)size; + (void)param; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingQueryMemSize(const void* param) { + (void)param; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingSetInfo(int32_t info, const void* data, size_t size) { + if (data == nullptr && size != 0) APS5_INVALID_ARG_EX; + const auto* bytes = static_cast(data); + std::lock_guard lock(g_infoMutex); + g_info[info].assign(bytes, bytes + size); + return 0; +} + +int APS5_VABI sceVideoRecordingStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +APS5_EXPORT("iQS6DUtLybE", videoRecordingUnknown_iQS6DUtLybE); +int APS5_VABI videoRecordingUnknown_iQS6DUtLybE(void) { + NotImplemented_nid_no_patch("iQS6DUtLybE"); + return 0; +} + +} diff --git a/core/libs/prx/libSceWebBrowserDialog/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index 4ef69b42b..b26299820 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -1,18 +1,46 @@ +#include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { +// Common dialog status: 0 none, 1 initialized, 2 running, 3 finished. Nothing is ever opened here, so it never runs. +std::atomic g_status{0}; +} + extern "C" { int APS5_VABI sceWebBrowserDialogInitialize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + int expected = 0; + if (!g_status.compare_exchange_strong(expected, 1)) throw std::logic_error("sceWebBrowserDialogInitialize: already initialized"); + return 0; } int APS5_VABI sceWebBrowserDialogTerminate(void) { + int expected = 1; + if (!g_status.compare_exchange_strong(expected, 0)) throw std::logic_error("sceWebBrowserDialogTerminate: not initialized or still running"); + return 0; +} + +int APS5_VABI sceWebBrowserDialogClose(void) { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceWebBrowserDialogGetResult(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWebBrowserDialogOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWebBrowserDialogUpdateStatus(void) { + return g_status.load(); +} + } diff --git a/core/libs/prx/ulobjmgr/Export.cpp b/core/libs/prx/ulobjmgr/Export.cpp new file mode 100644 index 000000000..528a0dfc2 --- /dev/null +++ b/core/libs/prx/ulobjmgr/Export.cpp @@ -0,0 +1,20 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI _sceUlobjmgrRegisterObject(void* object) { + (void)object; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceUlobjmgrUnregisterObject(void* object) { + (void)object; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} From 59a6998f1a89bcf234c913844ca3ad049b67ef40 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 09:24:11 +0300 Subject: [PATCH 08/40] fix(agc): EOP PatchData takes (cmd, data); -KRzWekV120 sets the index size sceAgcQueueEndOfPipeActionPatchData is called with the packet and the new 64-bit label value only; reading a context id and a data selector from the unused argument registers wrote garbage into the RELEASE_MEM, so fences signalled with wrong values. -KRzWekV120 is sceAgcDcbSetIndexSize with the per-instance object id flag (VGT_INDEX_TYPE bit 14), as Kyty and SharpEmu build it. The no-op left the previous index type in place, so 32-bit index buffers were read as 16-bit. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp | 10 ++++++++++ .../libSceAgc/Patch/src/QueueEndOfPipeAction.cpp | 13 +++---------- core/libs/prx/libSceAgc/Unimplemented.cpp | 13 ------------- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp b/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp index 09e477f28..f96b5badd 100644 --- a/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp +++ b/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp @@ -35,6 +35,16 @@ std::uint32_t APS5_VABI sceAgcDcbSetIndexSizeGetSize() { return 12; // SET_UCONFIG_REG_INDEX: header + 2 dwords } +// Unnamed: sceAgcDcbSetIndexSize with the per-instance object id flag (VGT_INDEX_TYPE bit 14), as +// Kyty and SharpEmu build it. +APS5_EXPORT("-KRzWekV120", sceAgcUnknown__MKRzWekV120); +std::uint32_t* APS5_VABI sceAgcUnknown__MKRzWekV120(CommandBuffer* buf, std::uint8_t indexSize, std::uint8_t cachePolicy, std::uint8_t perInstanceObjectId) { + Agc::Command::CheckBits(perInstanceObjectId, 1, __func__); + auto* packet = sceAgcDcbSetIndexSize(buf, indexSize, cachePolicy); + packet[2] |= static_cast(perInstanceObjectId) << 14u; + return packet; +} + std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint32_t dataOffsetInBytes) { (void)buf; (void)dataOffsetInBytes; diff --git a/core/libs/prx/libSceAgc/Patch/src/QueueEndOfPipeAction.cpp b/core/libs/prx/libSceAgc/Patch/src/QueueEndOfPipeAction.cpp index c7be7ba5f..d0fc39566 100644 --- a/core/libs/prx/libSceAgc/Patch/src/QueueEndOfPipeAction.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/QueueEndOfPipeAction.cpp @@ -30,19 +30,12 @@ int APS5_VABI sceAgcQueueEndOfPipeActionPatchAddress(std::uint32_t* cmd, const v return 0; } -int APS5_VABI sceAgcQueueEndOfPipeActionPatchData(uint32_t* cmd, uint32_t context_id, uint32_t data_sel, uint64_t data) { - // RELEASE_MEM as built by sceAgcCbReleaseMem: [2] dst | interrupt << 24 | data select << 29, [5..6] data, [7] interrupt context. +int APS5_VABI sceAgcQueueEndOfPipeActionPatchData(uint32_t* cmd, uint64_t data) { + // RELEASE_MEM as built by sceAgcCbReleaseMem: [2] data select << 29, [5..6] data. Agc::Command::ValidatePacket(cmd, 0x49u, 8, __func__); - Agc::Command::Require(data_sel <= 3, __func__, "invalid release data selector"); - Agc::Command::CheckBits(context_id, 0x7ffffffu, __func__); - if (data_sel == 1) { - Agc::Command::CheckBits(data, 0xffffffffu, __func__); - } - Agc::Command::Require(((cmd[2] >> 24u) & 7u) != 4u || data_sel == 0, __func__, "interrupt-only release cannot write data"); - cmd[2] = (cmd[2] & 0x1fffffffu) | (data_sel << 29u); + if ((cmd[2] >> 29u) == 1u) Agc::Command::CheckBits(data, 0xffffffffu, __func__); cmd[5] = static_cast(data); cmd[6] = static_cast(data >> 32u); - cmd[7] = context_id; return 0; } diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index 3cb827709..e53a08cb3 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -84,19 +84,6 @@ int APS5_VABI sceAgcSetAmmSemaphoreMemory() { return 0; } -APS5_EXPORT("-KRzWekV120", sceAgcUnknown__MKRzWekV120); -// Unnamed DCB state toggle: games call it as (cb, enable, 0, 0) and ignore the result. It sits -// between SetBaseIndirectArgs and SetIndexBuffer in the import table; treated as a no-op. -std::uint32_t* APS5_VABI sceAgcUnknown__MKRzWekV120(void* buf, std::uint32_t enable, std::uint64_t a2, std::uint64_t a3) { - static bool logged = false; - if (!logged) { - logged = true; - std::fprintf(stderr, "[agc] -KRzWekV120 treated as no-op (enable=%u args=0x%llx,0x%llx)" "\n", enable, static_cast(a2), static_cast(a3)); - } - (void)buf; - return nullptr; -} - APS5_EXPORT("6nths4DHNrs", sceAgcUnknown_6nths4DHNrs); int APS5_VABI sceAgcUnknown_6nths4DHNrs() { NotImplemented_nid_no_patch(__func__); From b7eaabbc1e410a50e796e605f139ad44895fd65a Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 09:24:12 +0300 Subject: [PATCH 09/40] feat(kernel): share direct memory mapped at two addresses read-only A title maps physical pages at a new address while the GPU still reads them through the old one. Every view of such a page gets the same contents and loses write access while the page is shared; the last view left gets its protection back. A CPU write to a shared page faults, and views that differ when one is unmapped (a GPU write through an import) throw, instead of the views silently diverging. Co-Authored-By: Claude Opus 5.5 --- .../libkernel/DirectMemory/DirectMemory.cpp | 92 +++++++++++++++---- 1 file changed, 76 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 812c8b3de..27546e6c4 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -220,8 +221,8 @@ void Trace(const char* format, ...) { // Direct memory keeps its contents across mappings: titles unmap physical pages and map them again at // another address (titles move their pools that way) and read back what they wrote through the old one. -// ponytail: the contents move by copying at unmap and map, so a physical page mapped at two live -// addresses throws; a shared section with views (as in Kyty) lifts that but loses write watching. +// The contents move by copying at unmap and map; a shared section with views (as in Kyty) would alias +// them for real but loses the write watching of the guest arena. struct DirectMapping { std::uintptr_t end; std::uint64_t phys; @@ -247,6 +248,51 @@ void SaveContents(std::uintptr_t address, std::size_t len, std::uint64_t phys) { } } +// A physical page mapped at more than one address has the same contents in every view, and every +// view lacks write access while it is shared (g_shared). +// ponytail: a write to a shared page faults; handing the page to the writing view on the fault, with +// the GPU driver's imports of the other views, is the upgrade if a title writes through an alias. +std::set g_shared; + +// The views of physPage in g_directMappings. +std::vector Views(std::uint64_t physPage) { + std::vector views; + for (const auto& [base, mapping] : g_directMappings) { + if (physPage >= mapping.phys && physPage < mapping.phys + (mapping.end - base)) views.push_back(base + (physPage - mapping.phys)); + } + return views; +} + +int NativeProtection(std::uintptr_t view) { + int prot = 0; + if (!GuestProtection(view, &prot)) throw std::runtime_error("direct memory view has no recorded protection"); + return LinuxProtFromSce(prot); +} + +int SharedProtection(int nativeProt) { + return nativeProt & ~PROT_WRITE; +} + +// Takes the shared page out of its sharing; the views left keep sharing it, or the last one gets its +// own protection back. The views must still hold the same bytes: a write that reached one of them +// (the GPU writes through its imports of guest memory) would have been lost to the others. +void Unshare(std::uintptr_t page, std::uint64_t physPage) { + const auto others = Views(physPage); + if (others.empty()) throw std::runtime_error("shared direct memory page has no other view"); + ProtectOrThrow(page, PS5_PAGE_SIZE, PROT_READ); + ProtectOrThrow(others.front(), PS5_PAGE_SIZE, PROT_READ); + if (std::memcmp(reinterpret_cast(page), reinterpret_cast(others.front()), PS5_PAGE_SIZE) != 0) { + char message[160]; + std::snprintf(message, sizeof(message), "direct memory 0x%llx mapped at 0x%llx and 0x%llx was written through one view; aliased writes are not implemented", static_cast(physPage), + static_cast(page), static_cast(others.front())); + throw std::runtime_error(message); + } + const bool last = others.size() == 1; + if (last) g_shared.erase(others.front()); + const auto prot = NativeProtection(others.front()); + ProtectOrThrow(others.front(), PS5_PAGE_SIZE, last ? prot : SharedProtection(prot)); +} + // Takes [start, end) out of the direct mappings; with save its bytes stay as the contents of its pages. void EraseMappings(std::uintptr_t start, std::uintptr_t end, bool save) { auto it = g_directMappings.lower_bound(start); @@ -256,33 +302,46 @@ void EraseMappings(std::uintptr_t start, std::uintptr_t end, bool save) { const auto mapping = it->second; it = g_directMappings.erase(it); const auto cutStart = std::max(base, start); - if (save) SaveContents(cutStart, std::min(mapping.end, end) - cutStart, mapping.phys + (cutStart - base)); + const auto cutEnd = std::min(mapping.end, end); if (base < start) g_directMappings.emplace(base, DirectMapping{start, mapping.phys}); if (mapping.end > end) it = g_directMappings.emplace(end, DirectMapping{mapping.end, mapping.phys + (end - base)}).first; - } -} - -void RequireUnmapped(std::uint64_t phys, std::size_t len, std::uintptr_t address) { - for (const auto& [base, mapping] : g_directMappings) { - if (mapping.phys < phys + len && phys < mapping.phys + (mapping.end - base)) { - char message[192]; - std::snprintf(message, sizeof(message), "direct memory 0x%llx+0x%zx is already mapped at 0x%llx; mapping it at 0x%llx too is not implemented", static_cast(phys), len, - static_cast(base), static_cast(address)); - throw std::runtime_error(message); + if (g_shared.empty()) { + if (save) SaveContents(cutStart, cutEnd - cutStart, mapping.phys + (cutStart - base)); + continue; + } + for (auto page = cutStart; page < cutEnd; page += PS5_PAGE_SIZE) { + const auto physPage = mapping.phys + (page - base); + if (g_shared.erase(page) != 0) Unshare(page, physPage); + else if (save) SaveContents(page, PS5_PAGE_SIZE, physPage); } } } // Records a fresh mapping of phys at address (zero-filled, already protected nativeProt) and brings its contents back. void AddMapping(std::uintptr_t address, std::size_t len, std::uint64_t phys, int nativeProt) { - RequireUnmapped(phys, len, address); + std::vector> shared; + for (const auto& [base, mapping] : g_directMappings) { + const auto physEnd = std::min(phys + len, mapping.phys + (mapping.end - base)); + for (auto page = std::max(phys, mapping.phys); page < physEnd; page += PS5_PAGE_SIZE) shared.emplace_back(page - phys, base + (page - mapping.phys)); + } g_directMappings.emplace(address, DirectMapping{address + len, phys}); auto it = g_physPages.lower_bound(phys); const auto last = g_physPages.lower_bound(phys + len); - if (it == last) return; + if (it == last && shared.empty()) return; ProtectOrThrow(address, len, PROT_READ | PROT_WRITE); for (; it != last; it = g_physPages.erase(it)) std::memcpy(reinterpret_cast(address + (it->first - phys)), it->second.get(), PS5_PAGE_SIZE); + for (const auto& [offset, view] : shared) { + ProtectOrThrow(view, PS5_PAGE_SIZE, PROT_READ); + std::memcpy(reinterpret_cast(address + offset), reinterpret_cast(view), PS5_PAGE_SIZE); + ProtectOrThrow(view, PS5_PAGE_SIZE, SharedProtection(NativeProtection(view))); + g_shared.insert(view); + } ProtectOrThrow(address, len, nativeProt); + for (const auto& [offset, view] : shared) { + ProtectOrThrow(address + offset, PS5_PAGE_SIZE, SharedProtection(nativeProt)); + g_shared.insert(address + offset); + } + if (!shared.empty()) Trace("alias %p+0x%zx shares %zu physical pages with older views", reinterpret_cast(address), len, shared.size()); } bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr, size_t len, int prot, int flags, int64_t physStart = -1) { @@ -434,7 +493,6 @@ int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, } if (*addr != nullptr && (flags & GuestMapFixedFlag) != 0) mutation.RequireAvailable(*addr, len); std::lock_guard lock(g_directLock); - RequireUnmapped(static_cast(physStart), len, reinterpret_cast(*addr)); void* mapped = MapAligned(*addr, len, LinuxProtFromSce(prot), flags, alignment); try { mutation.Add(mapped, len, (prot & 3) != 0, (prot & 2) != 0); @@ -495,6 +553,8 @@ int DoMprotect(const void* addr, size_t len, int prot) { #endif mutation.Protect(pointer, bytes, (prot & 3) != 0, (prot & 2) != 0, [&] { if (mprotect(const_cast(pointer), bytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); + std::lock_guard lock(g_directLock); + for (auto it = g_shared.lower_bound(first); it != g_shared.end() && *it < end; ++it) ProtectOrThrow(*it, PS5_PAGE_SIZE, SharedProtection(nativeProtection)); }); RecordProtection(pointer, bytes, prot); return 0; From 30c0cf1dfbb54fe291fedda55f65954b4ac8afff Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 11:19:03 +0300 Subject: [PATCH 10/40] feat(agc-driver): sample depth and stencil surfaces from their GPU images A depth target lives only in a GPU image (its guest surface is never written), so a texture over its depth or stencil plane now samples that image through a view of the plane's aspect: R32_SFLOAT for D32, R16_UNORM for D16, R8_UINT for stencil, one level and slice at the surface's extent. Other shapes, and storage access to a depth surface, throw. Depth images get the sampled usage, and the barriers after a render pass and before a dispatch include depth attachment writes. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/VulkanDevice.cpp | 2 +- .../Graphics/include/DepthSurface.hpp | 11 ++++- .../Graphics/include/Texture.hpp | 3 ++ .../Graphics/src/DepthSurface.cpp | 47 ++++++++++++++++++- .../Graphics/src/GuestTextureResource.cpp | 7 --- .../libSceAgcDriver/Graphics/src/Recorder.cpp | 2 +- .../Graphics/src/ShaderResources.cpp | 8 ++++ .../libSceAgcDriver/Graphics/src/Texture.cpp | 11 +++++ 8 files changed, 80 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 9aa13dfb5..2ead2f5ea 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2871,7 +2871,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { Graphics::Recorder::CountMerged(CommandClass::DispatchLeading); } else { const auto timing = recorder.BeginGpuTiming(CommandClass::DispatchLeading); - Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); Graphics::Recorder::CountBarriers(CommandClass::DispatchLeading); recorder.EndGpuTiming(timing); ++d.preBarriersRecorded; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 424a80d85..551ceb320 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -1,15 +1,20 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DEPTHSURFACE_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DEPTHSURFACE_HPP +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include +#include +#include namespace AgcDriver::Graphics { +class Texture; + // The attachment view of the device's image for a depth/stencil target, made on first use. The // image lives on the GPU only, always in the general layout: its guest surface (tiled, HTILE // compressed) is never read or written, so a new image starts at the target's clear values, and -// texture reads of the surface are refused (DepthSurfaceAt). Images stay until the device goes +// shaders sample the image itself (DepthSurfaceTexture). Images stay until the device goes // (ClearDepthSurfaces), so their views are stable for cached framebuffers. // ponytail: never evicted, one image per distinct surface; evict (and keep recorded ones alive) if // titles churn through many. @@ -17,6 +22,10 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); void ClearDepthSurfaces(VkDevice device); // Whether the depth or stencil plane of a surface drawn with starts at `address`. bool DepthSurfaceAt(std::uint64_t address); +// The texture sampling the depth or stencil plane that starts at the descriptor's base address, +// or null when no surface drawn with on this device has a plane there. The plane is read in place, +// so the descriptor must describe it (its format, extent, one level and slice); anything else throws. +std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index ff073f026..640f3e9ab 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -57,6 +57,9 @@ public: // compute pass writing it and the next pass sampling it share one image and copy nothing. // CanCopyFrom says whether the two descriptors address the same surface compatibly. Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components); + // A view of one plane (aspect) of a depth surface's image, which stays in the general layout; + // the surface (DepthSurface.hpp) owns the image and its views. + Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components); static bool CanCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor); ~Texture(); Texture(const Texture&) = delete; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index ddb073359..cf507d855 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -1,9 +1,15 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include +#include +#include +#include #include #include +#include #include namespace AgcDriver::Graphics { @@ -16,6 +22,7 @@ public: VkFormatProperties properties{}; context.formatProperties(context.physical, target.format, &properties); Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0, "depth/stencil format " + std::to_string(target.format) + " cannot be an attachment on this device"); + Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0, "depth/stencil format " + std::to_string(target.format) + " cannot be sampled on this device"); Require(target.extent.width <= context.limits.maxFramebufferWidth && target.extent.height <= context.limits.maxFramebufferHeight, "depth target exceeds framebuffer limits"); const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (target.stencilAddress != 0 ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); try { @@ -27,7 +34,7 @@ public: info.arrayLayers = 1; info.samples = VK_SAMPLE_COUNT_1_BIT; info.tiling = VK_IMAGE_TILING_OPTIMAL; - info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage depth"); @@ -74,6 +81,31 @@ public: DepthSurface(const DepthSurface&) = delete; DepthSurface& operator=(const DepthSurface&) = delete; + // The texture sampling the plane at the descriptor's base address, one per descriptor. + std::shared_ptr Sampled(std::span words, const GuestTextureResource& resource, VkComponentMapping components) { + std::array key{}; + std::copy_n(words.begin(), std::min(words.size(), 8), key.begin()); + key[8] = components.r; + key[9] = components.g; + key[10] = components.b; + key[11] = components.a; + if (const auto found = textures.find(key); found != textures.end()) return found->second; + const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; + const auto format = ResolveTextureFormat(resource.format); + if (format != expected || resource.dimension != TextureDimension::k2D || resource.width != target.extent.width || resource.height != target.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || resource.baseArray != 0) { + char text[448]; + std::snprintf(text, sizeof(text), "AGC graphics: sampling the %s plane of depth surface 0x%llx (%ux%u, vk format %d) as a %ux%u texture of guest format %u (vk %d), tile mode %u, dimension %d, levels %u-%u, slice %u is not implemented (T# %08x %08x %08x %08x %08x %08x %08x %08x)", + stencil ? "stencil" : "depth", static_cast(target.address), target.extent.width, target.extent.height, static_cast(target.format), resource.width, resource.height, resource.format, static_cast(format), + static_cast(resource.tileMode), static_cast(resource.dimension), resource.baseLevel, resource.lastLevel, resource.baseArray, key[0], key[1], key[2], key[3], key[4], key[5], key[6], key[7]); + throw std::runtime_error(text); + } + auto texture = std::make_shared(context, image, target.format, stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, components); + textures.emplace(key, texture); + return texture; + } + const Context context; const DepthTarget target; VkImage image = VK_NULL_HANDLE; @@ -81,7 +113,10 @@ public: VkDeviceMemory memory = VK_NULL_HANDLE; private: + std::map, std::shared_ptr> textures; + void release() noexcept { + textures.clear(); if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); @@ -122,6 +157,16 @@ void ClearDepthSurfaces(VkDevice device) { std::erase_if(surfaces(), [&](const auto& surface) { return surface->context.device == device; }); } +std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components) { + std::lock_guard lock(surfacesMutex()); + const auto& list = surfaces(); + // The newest surface made at the address. + const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { + return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); + }); + return found == list.rend() ? nullptr : (*found)->Sampled(words, resource, components); +} + bool DepthSurfaceAt(std::uint64_t address) { std::lock_guard lock(surfacesMutex()); return std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& surface) { return surface->target.address == address || surface->target.stencilAddress == address; }); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index cf4db9175..3cd364eae 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -1,7 +1,6 @@ #include #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" -#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include #include #include @@ -47,12 +46,6 @@ GuestTextureResource DecodeTextureResource(std::span words) const auto base40 = (static_cast(words[0]) | (static_cast(words[1]) << 32u)) & 0xffffffffffull; const auto baseAddress = base40 << 8u; Require(baseAddress != 0, "guest texture descriptor has a null base address"); - // A depth surface's contents live on the GPU only; its guest memory is stale. - if (DepthSurfaceAt(baseAddress)) { - char text[96]; - std::snprintf(text, sizeof(text), "AGC graphics: sampling depth/stencil surface 0x%llx is not implemented", static_cast(baseAddress)); - throw std::runtime_error(text); - } const auto minLod = (words[1] >> 8u) & 0xfffu; const auto format = (words[1] >> 20u) & 0x1ffu; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index f796220fd..f682ab5a8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1304,7 +1304,7 @@ void Recorder::endOpenRenderPass() { context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(open->commands); // The pass's attachment and shader writes are visible to everything recorded after it (the // host sees them at the batch's fence). - recordBarrier(open->commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + recordBarrier(open->commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); CountBarriers(CommandClass::Draw); EndGpuTiming(pass.timing); open->coveredAccess = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index d84bf94de..ec4d398ea 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include @@ -262,6 +263,8 @@ std::shared_ptr sampledStorageSource(const Context& context, con // `guestBytes` is the surface size when the caller described the surface already (0: described here). std::shared_ptr cachedTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components, std::uint64_t guestBytes = 0) { CaptureTrace::Log("sampled-lookup address=%llx width=%u height=%u dcc=%llx", static_cast(resource.baseAddress), resource.width, resource.height, static_cast(resource.dccAddress)); + // A depth surface's contents live in its GPU image; its guest memory is stale. + if (auto depth = DepthSurfaceTexture(context, words, resource, components)) return depth; static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; const bool profile = LookupOutcomes::Profiled(); const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; @@ -474,6 +477,11 @@ std::array SurfaceKey(const Context& context, const GuestTextu // `guestBytes` is the surface size when the caller described the surface already (0: described here). std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes) { + if (DepthSurfaceAt(resource.baseAddress)) { + char text[112]; + std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented", static_cast(resource.baseAddress)); + throw std::runtime_error(text); + } static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; if (disabled) return std::make_shared(context, *context.detiler, resource, mip); static_cast(words); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 7f5008dd6..f5cd4eccb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -457,6 +457,17 @@ Texture::Texture(const Context& context, const std::shared_ptr& } } +Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components) : context(context) { + layout = VK_IMAGE_LAYOUT_GENERAL; + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = depthImage; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = depthFormat; + viewInfo.components = components; + viewInfo.subresourceRange = {aspect, 0, 1, 0, 1}; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth plane"); +} + Texture::~Texture() { release(); } From 19521c2ecde5f45c3b1f53e01a4a6b0aa37fb487 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 11:19:14 +0300 Subject: [PATCH 11/40] feat(agc-driver): occlusion counters from Vulkan occlusion queries EVENT_WRITE PIXEL_PIPE_STAT_DUMP (event 0x39) dumps the DB sample counters. The recorder, once a title asks for them, keeps an occlusion query open over each batch (precise when the device supports it) and adds the result to a running count when the batch is reaped. A dump drains recorded work like a label and writes the count in the PS5 layout: 16 DBs 16 bytes apart, begin and end counters interleaved, the count in the first and bit 63 marking each ready. Kyty and SharpEmu write a fixed "visible" result instead. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Execution/src/Driver.cpp | 29 +++++++++++++++- .../prx/libSceAgcDriver/Execution/src/Pm4.cpp | 7 ++++ .../Execution/src/VulkanDevice.cpp | 5 +++ .../Graphics/include/Context.hpp | 2 ++ .../Graphics/include/Recorder.hpp | 10 ++++++ .../libSceAgcDriver/Graphics/src/Recorder.cpp | 34 +++++++++++++++++++ core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 11 ++++-- 7 files changed, 95 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp index 55bd2b36e..8281143af 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp @@ -5146,6 +5146,30 @@ private: return true; } + // PIXEL_PIPE_STAT_DUMP, drained like a label: every draw before it completed, so the recorder's + // sample count is the DB counter at this point. The PS5 layout interleaves a begin and an end + // counter per DB (16 of them, 16 bytes apart; a query's begin dump writes the first of each + // pair, its end dump the second): the count goes to the first DB and the others stay at zero, + // all with bit 63 marking the result ready. + // ponytail: a GPU drain per dump; accumulate on the GPU behind the batch if titles dump many + // per frame. + void dumpSampleCounters(std::uint64_t address) { + std::uint64_t samples = 0; + { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + auto* recorder = Graphics::Recorder::Active(); + require(recorder != nullptr, "occlusion counters without the command recorder are not implemented"); + recorder->CountSamples(); + samples = Graphics::Recorder::SamplesPassed(); + } + constexpr std::uint64_t ready = 1ull << 63u; + for (std::uint64_t db = 0; db < 16; ++db) { + const std::uint64_t value = ready | (db == 0 ? samples : 0u); + GuestMemory::Write(address + db * 16u, std::as_bytes(std::span(&value, 1)), 8); + } + } + // Before a packet's CPU read of guest memory outside any lock (the group counts of a // DISPATCH_INDIRECT resolved on the CPU): a label this queue still has queued may write those // bytes and the flush hook only knows recorded stores, so the queued labels are recorded first. @@ -5990,11 +6014,12 @@ private: // draw's preparation reads goes through the flush hook. Debug aid: APS5_DRAW_DRAIN=1 restores. static const bool drawDrain = std::getenv("APS5_DRAW_DRAIN") != nullptr; const bool drawPacket = Pm4::DrawOpcode(opcode); + const bool sampleDump = opcode == 0x46 && (packet[1] & 0x3fu) == 0x39u; // Flips no longer drain either: the frame's batches are submitted and the presenter's blit // follows them on the same queue (see Driver::Present). Debug aid: APS5_SYNC_FLIP=1 restores. static const bool syncFlip = std::getenv("APS5_SYNC_FLIP") != nullptr; const bool drains = drainAll ? ((Pm4::AccessesMemory(header) && opcode != 0x16) || opcode == 0x42 || opcode == 0x46 || opcode == 0x58 || header == FlipPacketHeader) - : (!wroteOnGpu && !orderedAlready && (opcode == 0x49 || opcode == 0x37 || opcode == 0x40 || opcode == 0x50 || opcode == 0x83 || (drawPacket && drawDrain) || (header == FlipPacketHeader && syncFlip))); + : (!wroteOnGpu && !orderedAlready && (opcode == 0x49 || opcode == 0x37 || opcode == 0x40 || opcode == 0x50 || opcode == 0x83 || sampleDump || (drawPacket && drawDrain) || (header == FlipPacketHeader && syncFlip))); if (drains) { // The GPU wait happens without the mutex: the batches are submitted under it, the // timeline value is waited for outside (the other workers keep recording), then the @@ -6175,6 +6200,8 @@ private: } }); }); finishDrawPacket(drawn); + } else if (sampleDump) { + dumpSampleCounters(packet[2] | (static_cast(packet[3]) << 32u)); } else if (opcode != 0x42 && opcode != 0x46 && opcode != 0x58) { if (!wroteOnGpu) Pm4::Execute(packet, queue); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index ea62413b1..403435280 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -312,6 +312,13 @@ void Validate(std::span packet, std::uint32_t queue) { size(2); require(eventIndex == 0 || eventIndex == 7, "invalid cache-flush event index"); break; + case 0x39: + // PIXEL_PIPE_STAT_DUMP: the occlusion counters (see Driver::dumpSampleCounters). + graphics(); + size(4); + require(eventIndex == 1, "invalid occlusion counter dump event index"); + require(address(packet[2], packet[3]) != 0 && (packet[2] & 7u) == 0, "null or misaligned occlusion counter dump address"); + break; default: throw std::runtime_error("EVENT_WRITE event type " + std::to_string(eventType) + " is not implemented"); } break; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 2ead2f5ea..60ef67cdf 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -188,6 +188,7 @@ struct VulkanDevice::State { bool descriptorIndexing = false; bool depthClipControl = false; bool depthClamp = false; + bool occlusionQueryPrecise = false; VkDeviceSize hostImportAlignment = 0; bool depthRangeUnrestricted = false; bool samplerAnisotropy = false; @@ -776,6 +777,9 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u // PA_CL_CLIP_CNTL near/far clip disable maps to depth clamping. enabled.depthClamp = available.depthClamp; state->depthClamp = enabled.depthClamp == VK_TRUE; + // PIXEL_PIPE_STAT_DUMP reports sample counts (see Recorder::CountSamples). + enabled.occlusionQueryPrecise = available.occlusionQueryPrecise; + state->occlusionQueryPrecise = enabled.occlusionQueryPrecise == VK_TRUE; // Recompiled storage-image access declares no format (the guest descriptor decides it). enabled.shaderStorageImageWriteWithoutFormat = available.shaderStorageImageWriteWithoutFormat; enabled.shaderStorageImageReadWithoutFormat = available.shaderStorageImageReadWithoutFormat; @@ -2162,6 +2166,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.drawIndirectFirstInstance = state->drawIndirectFirstInstance; context.multiDrawIndirect = state->multiDrawIndirect; context.drawIndirectCount = state->drawIndirectCount; + context.occlusionQueryPrecise = state->occlusionQueryPrecise; context.copiedWriters = state->copiedWriters.get(); context.functions = state->functionsReady ? &state->deviceFunctions : nullptr; context.descriptorIndexing = state->descriptorIndexing; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index db6ae2af5..e10a8c504 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -116,6 +116,8 @@ struct Context { bool drawIndirectFirstInstance = false; bool multiDrawIndirect = false; bool drawIndirectCount = false; + // Occlusion queries count exact samples (occlusionQueryPrecise), not just any-passed. + bool occlusionQueryPrecise = false; // The device's list of recorded dispatches whose copied written buffers await a CPU write-back // (VulkanDevice's State::copiedWriters; the draw counterpart is DrawCopiedWriters): an indirect // draw whose records one of them produces reads them on the CPU. Null in tests. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 80ea18a88..176e0432c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -165,6 +165,11 @@ public: void FinishUpTo(std::uint64_t serial); // Submits and waits for every batch, running completions in order. void Sync(); + // Sample counting for PIXEL_PIPE_STAT_DUMP: once on (the first dump), every batch counts the + // samples its draws pass in an occlusion query begun outside any render pass, and adds them to + // SamplesPassed when it completes. Under the mutex; an open batch starts counting at once. + void CountSamples(); + static std::uint64_t SamplesPassed(); // Waits only for the batches up to the newest one that writes the range (submitting the open // batch when it is that one); later batches stay in flight. Fences of one queue signal in // submission order, so completions still run in order. Debug aid: APS5_NO_SYNC_THROUGH=1 syncs all. @@ -486,6 +491,8 @@ private: std::uint32_t queue = 0xffffffffu; std::chrono::steady_clock::time_point submittedAt{}; VkQueryPool queries = VK_NULL_HANDLE; + // The occlusion query counting this batch's samples (see CountSamples). + VkQueryPool samples = VK_NULL_HANDLE; std::vector timedKeys; std::vector timedBytes; // The whole-batch timed range (BatchTimingKey) and its stamps once read (see Completed). @@ -582,6 +589,9 @@ private: bool behindCompletion = false; }; void readGpuTiming(Batch& batch); + void beginSamples(Batch& batch); + void readSamples(Batch& batch); + bool countingSamples = false; // BeginGpuTiming on the open batch without Commands() (RecordStore times its own run, which // Commands() would close). std::uint32_t beginTiming(std::uint64_t key); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index f682ab5a8..df4ac9cee 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -166,6 +166,7 @@ std::atomic& WaitersOf(std::uint64_t id) { return unlockedWaiters[id % Wait std::mutex liveRecordersMutex; std::vector liveRecorders; std::atomic nextRecorderId{1}; +std::atomic samplesPassed{0}; bool RecorderAlive(std::uint64_t id) { std::lock_guard lock(liveRecordersMutex); @@ -1405,6 +1406,7 @@ void Recorder::ensureOpen() { throw; } batch->queue = GuestMemory::GpuLockThreadTag(); + if (countingSamples) beginSamples(*batch); open = std::move(batch); // The whole-batch range: its start stamp waits for the previous batches like any // bottom-of-pipe stamp, so it marks when this batch's execution began. @@ -1788,6 +1790,34 @@ void Recorder::EndGpuTiming(std::uint32_t index, std::uint64_t bytes) { context.Function("vkCmdWriteTimestamp")(open->commands, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, open->queries, index * 2 + 1); } +void Recorder::CountSamples() { + GuestMemory::AssertGpuLockHeld("Recorder::CountSamples"); + countingSamples = true; + if (open == nullptr || open->samples != VK_NULL_HANDLE) return; + if (open->renderPass.open) endOpenRenderPass(); + beginSamples(*open); +} + +std::uint64_t Recorder::SamplesPassed() { + return samplesPassed.load(std::memory_order_acquire); +} + +void Recorder::beginSamples(Batch& batch) { + VkQueryPoolCreateInfo info{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO}; + info.queryType = VK_QUERY_TYPE_OCCLUSION; + info.queryCount = 1; + Check(context.Function("vkCreateQueryPool")(context.device, &info, nullptr, &batch.samples), "vkCreateQueryPool occlusion"); + context.Function("vkCmdResetQueryPool")(batch.commands, batch.samples, 0, 1); + context.Function("vkCmdBeginQuery")(batch.commands, batch.samples, 0, context.occlusionQueryPrecise ? VK_QUERY_CONTROL_PRECISE_BIT : 0u); +} + +void Recorder::readSamples(Batch& batch) { + if (batch.samples == VK_NULL_HANDLE || !batch.submitted) return; + std::uint64_t samples = 0; + Check(context.Function("vkGetQueryPoolResults")(context.device, batch.samples, 0, 1, sizeof(samples), &samples, sizeof(samples), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT), "vkGetQueryPoolResults occlusion"); + samplesPassed.fetch_add(samples, std::memory_order_acq_rel); +} + void Recorder::readGpuTiming(Batch& batch) { if (batch.queries == VK_NULL_HANDLE || batch.timedKeys.empty()) return; std::vector stamps(batch.timedKeys.size() * 2); @@ -2327,6 +2357,7 @@ void Recorder::Submit() { GuestMemory::AssertGpuLockHeld("Recorder::Submit"); if (!open->keyStores.empty()) recordKeyStores(false); if (open->renderPass.open) endOpenRenderPass(); + if (open->samples != VK_NULL_HANDLE) context.Function("vkCmdEndQuery")(open->commands, open->samples, 0); const bool hostReadCovered = (open->run.open || !open->run.queued.empty()) && closeStoreRun(true); if (BarrierValidate()) validateBatchEnds.fetch_add(1, std::memory_order_relaxed); if (open->hostReadOwed && !hostReadCovered) { @@ -2819,6 +2850,7 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source) { } } readGpuTiming(*batch); + readSamples(*batch); if (batch->serial != 0) { std::lock_guard ringLock(completedMutex); auto& entry = completed[batch->serial % completed.size()]; @@ -2893,6 +2925,8 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source) { void Recorder::release(Batch& batch) noexcept { if (batch.queries != VK_NULL_HANDLE) context.Function("vkDestroyQueryPool")(context.device, batch.queries, nullptr); batch.queries = VK_NULL_HANDLE; + if (batch.samples != VK_NULL_HANDLE) context.Function("vkDestroyQueryPool")(context.device, batch.samples, nullptr); + batch.samples = VK_NULL_HANDLE; // A completed (or never submitted) batch's objects are kept for reuse: the fence is signaled or // untouched, so resetting it cannot block, and the command buffer is no longer pending. if (batch.commands != VK_NULL_HANDLE && batch.fence != VK_NULL_HANDLE && spare.size() < 64 && function(resetFences, "vkResetFences")(context.device, 1, &batch.fence) == VK_SUCCESS) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 5723d6ec8..20c919a16 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -376,12 +376,19 @@ void testEventWrite() { } expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {eventType}), 0x20); }, "compute queue"); } + AgcDriver::Pm4::Validate(makePacket(0x46, {0x139, 0x1000, 0x2}), 0); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x139, 0x1000, 0x2}), 0x20); }, "compute queue"); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x039, 0x1000, 0x2}), 0); }, "counter dump event index"); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x139, 0x1004, 0x2}), 0); }, "misaligned occlusion counter"); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x139, 0, 0}), 0); }, "null or misaligned occlusion counter"); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x139, 0x1000}), 0); }, "packet size"); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x138, 0x1000, 0x2}), 0); }, "event type 56"); for (const auto bit : {0x40u, 0x80u, 0x800u, 0x80000000u}) { expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x410u | bit}), 0); }, "reserved bits"); } expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x410}, 1), 0); }, "header flags"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x410, 0, 0}), 0); }, "packet size"); - expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x139, 0, 0}), 0); }, "event type 57"); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x13a, 0, 0}), 0); }, "event type 58"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x0d}), 0); }, "event type 13"); } @@ -450,7 +457,7 @@ void testDriverSubmission() { AgcDriverWaitIdle_nid_postfix(); check(destination[0] == 83, "worker did not execute PM4 memory operations"); auto rejectedCommands = commands; - const auto unsupportedEvent = makePacket(0x46, {0x139, 0, 0}); + const auto unsupportedEvent = makePacket(0x46, {0x0d}); rejectedCommands.insert(rejectedCommands.end(), unsupportedEvent.begin(), unsupportedEvent.end()); Packet rejectedPacket{rejectedCommands.data(), static_cast(rejectedCommands.size()), 0, {}}; destination[0] = 0; From c1692bf9398b056c43b916af69a0487c2148b85b Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 11:19:25 +0300 Subject: [PATCH 12/40] feat(shader): read the bits of a D32 depth plane as a 32-bit integer texture A sampled T# of a 32-bit integer format over a depth layout (a Z swizzle mode) reads the raw bits of a depth plane, and a host cannot view a depth image with an integer format. Such an image is specialized as float and bound as the depth surface's float view: a fetch returns the same bits, which the shader keeps as they are, and swizzle constants read as the integers 0 and 1. Filtering is forced to point, as for integer formats. The same T# over guest memory rather than a depth surface throws. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Graphics/src/DepthSurface.cpp | 6 +++++- .../libSceAgcDriver/Graphics/src/ShaderResources.cpp | 8 ++++++++ .../IrMetadata/Resources.hpp | 3 +++ .../include/Optimization/ResourceMaterializer.hpp | 1 + .../Optimization/src/ResourceMaterializer.cpp | 12 ++++++++++-- .../include/RdnaDecoder/RdnaDescriptorFormat.hpp | 4 ++++ .../RdnaDecoder/src/RdnaDescriptorFormat.cpp | 8 ++++++++ .../SpirvBackend/src/SpirvImageEmitter.cpp | 10 ++++++++++ 8 files changed, 49 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index cf507d855..888197768 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" #include #include #include @@ -94,7 +95,10 @@ public: const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; const auto format = ResolveTextureFormat(resource.format); - if (format != expected || resource.dimension != TextureDimension::k2D || resource.width != target.extent.width || resource.height != target.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || resource.baseArray != 0) { + // A 32-bit integer read of a D32 plane takes the float view: the shader keeps the bits + // (ShaderRecompiler::IsDepthBitsTexture). + const bool depthBits = !stencil && !d16 && words.size() >= 4 && ShaderRecompiler::IsDepthBitsTexture(words[1], words[3]); + if ((format != expected && !depthBits) || resource.dimension != TextureDimension::k2D || resource.width != target.extent.width || resource.height != target.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || resource.baseArray != 0) { char text[448]; std::snprintf(text, sizeof(text), "AGC graphics: sampling the %s plane of depth surface 0x%llx (%ux%u, vk format %d) as a %ux%u texture of guest format %u (vk %d), tile mode %u, dimension %d, levels %u-%u, slice %u is not implemented (T# %08x %08x %08x %08x %08x %08x %08x %08x)", stencil ? "stencil" : "depth", static_cast(target.address), target.extent.width, target.extent.height, static_cast(target.format), resource.width, resource.height, resource.format, static_cast(format), diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index ec4d398ea..40682d222 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -16,6 +16,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libc/include/General.hpp" #include "Optimization/include/Optimization/ShaderStageInputInfo.hpp" +#include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" #include #include #include @@ -265,6 +266,13 @@ std::shared_ptr cachedTexture(const Context& context, std::span(resource.baseAddress), resource.width, resource.height, static_cast(resource.dccAddress)); // A depth surface's contents live in its GPU image; its guest memory is stale. if (auto depth = DepthSurfaceTexture(context, words, resource, components)) return depth; + // The recompiler reads such a T# through a float view (IsDepthBitsTexture), which only a depth + // surface provides. + if (words.size() >= 4 && ShaderRecompiler::IsDepthBitsTexture(words[1], words[3])) { + char text[160]; + std::snprintf(text, sizeof(text), "AGC graphics: 32-bit integer read of the depth-layout texture 0x%llx, which is no depth surface drawn with, is not implemented", static_cast(resource.baseAddress)); + throw std::runtime_error(text); + } static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; const bool profile = LookupOutcomes::Profiled(); const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 26e6f78c1..a99cedc31 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -49,6 +49,9 @@ struct ImageResource { bool depthCompare = false; bool cube = false; bool r128 = false; + // Specialized: a 32-bit integer read of a depth plane (IsDepthBitsTexture), bound as the float + // depth view; the texel bits are the result, the swizzle's constants integers. + bool depthBits = false; std::uint32_t indirectRoot = NoIndirectImage; std::uint32_t indirectMappingOffset = 0; std::uint32_t indirectSearchIterations = 0; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp index 762ac4cf5..b81416778 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp @@ -28,6 +28,7 @@ struct ResourceSpecialization { std::uint32_t indirectSearchIterations = 0; bool cube = false; bool fmask = false; + bool depthBits = false; bool operator==(const Image& other) const; }; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 37ec5bea2..0bc94901a 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -42,6 +42,7 @@ struct DecodedImage { std::uint32_t shaderSwizzle = ShaderImageIdentitySwizzle; bool cube = false; bool fmask = false; + bool depthBits = false; }; ShaderBufferResource decodeBufferDescriptor(const DescriptorValue& value) { @@ -161,6 +162,11 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag } const bool rawSintStorage = storage && format == IrBufferFormat::Format32SInt && base.written && !base.read && !base.atomic; decoded.numericClass = SampledTextureNumericClass(format); + if (!storage && !base.depthCompare && IsDepthBitsTexture(descriptor.dwords[1], descriptor.dwords[3])) { + decoded.depthBits = true; + decoded.numericClass = IrTextureNumericClass::Float; + decoded.shaderSwizzle = descriptorImageSwizzle(descriptor); + } if (storage) { if ((!rawSintStorage && decoded.numericClass == IrTextureNumericClass::Sint) || decoded.numericClass == IrTextureNumericClass::Unsupported) { throw std::runtime_error("storage image descriptor uses an unsupported format"); @@ -175,7 +181,7 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag } bool requiresPointSampler(const ResourceSpecialization::Image& image) { - return image.numericClass == IrTextureNumericClass::Sint || image.conversionFormat != IrBufferFormat::Invalid; + return image.numericClass == IrTextureNumericClass::Sint || image.conversionFormat != IrBufferFormat::Invalid || image.depthBits; } constexpr std::uint32_t TableEntryBytes = 32; @@ -522,6 +528,7 @@ void buildResourceSpecialization(const IrResourcePlan& plan, ResourceSnapshot& s entry.indirectSearchIterations = 0u; entry.cube = decoded.cube; entry.fmask = decoded.fmask; + entry.depthBits = decoded.depthBits; result.images.push_back(entry); } @@ -605,6 +612,7 @@ void ResourceMaterializer::Apply(IrProgram& program, const ResourceSpecializatio image.indirectMappingOffset = source.indirectMappingOffset; image.indirectSearchIterations = source.indirectSearchIterations; image.cube = source.cube; + image.depthBits = source.depthBits; image.indirectResources.clear(); } for (std::uint32_t index = 0; index < images.size(); index++) { @@ -870,7 +878,7 @@ bool ResourceSpecialization::Buffer::operator==(const Buffer& other) const { } bool ResourceSpecialization::Image::operator==(const Image& other) const { - return numericClass == other.numericClass && dimension == other.dimension && mipCount == other.mipCount && conversionFormat == other.conversionFormat && shaderSwizzle == other.shaderSwizzle && indirectRoot == other.indirectRoot && indirectMappingOffset == other.indirectMappingOffset && indirectSearchIterations == other.indirectSearchIterations && cube == other.cube && fmask == other.fmask; + return numericClass == other.numericClass && dimension == other.dimension && mipCount == other.mipCount && conversionFormat == other.conversionFormat && shaderSwizzle == other.shaderSwizzle && indirectRoot == other.indirectRoot && indirectMappingOffset == other.indirectMappingOffset && indirectSearchIterations == other.indirectSearchIterations && cube == other.cube && fmask == other.fmask && depthBits == other.depthBits; } bool ResourceSpecialization::operator==(const ResourceSpecialization& other) const { diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp index b6927d343..12e355631 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp @@ -20,6 +20,10 @@ enum class ImageType : std::uint32_t { [[nodiscard]] bool IsFmaskTextureFormat(IrBufferFormat format); [[nodiscard]] IrTextureNumericClass SampledTextureNumericClass(IrBufferFormat format); [[nodiscard]] IrBufferFormat RemapTextureFormat(IrBufferFormat format); +// A sampled T# (dwords 1 and 3) of a 32-bit integer format over a depth layout (a Z swizzle mode): +// the raw bits of a D32 depth plane. A host cannot view a depth image with an integer format, so +// the image binds its float depth view and the shader takes the texel bits (ImageResource::depthBits). +[[nodiscard]] bool IsDepthBitsTexture(std::uint32_t word1, std::uint32_t word3); } diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp index 33cb9cf71..eecd30628 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp @@ -121,4 +121,12 @@ IrBufferFormat RemapTextureFormat(IrBufferFormat format) { return format == IrBufferFormat::Format11_11_10UInt ? IrBufferFormat::Format32UInt : format; } +bool IsDepthBitsTexture(std::uint32_t word1, std::uint32_t word3) { + const auto format = static_cast((word1 >> 20u) & 0x1ffu); + // SW_4KB_Z, SW_64KB_Z, SW_64KB_Z_T, SW_4KB_Z_X, SW_64KB_Z_X. + const auto swizzle = (word3 >> 20u) & 0x1fu; + const bool depthLayout = swizzle == 4u || swizzle == 8u || swizzle == 16u || swizzle == 20u || swizzle == 24u; + return depthLayout && (format == IrBufferFormat::Format32UInt || format == IrBufferFormat::Format32SInt); +} + } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 1748e2f67..c83d6c0fb 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -254,6 +254,9 @@ std::uint32_t ResultVector(SpirvValueEmitContext& ctx, const ImageEmitAccess& ac valueClass = IrTextureNumericClass::Float; } const bool integer = valueClass == IrTextureNumericClass::Uint || valueClass == IrTextureNumericClass::Sint; + if (mem.dataBits == 16u && access.image.depthBits) { + ctx.Fail(access.inst, "reads the bits of a depth plane as 16-bit results"); + } if (mem.dataBits == 16u) { if (mem.dataDwords > 4u) { ctx.Fail(access.inst, "has more than four packed image result dwords"); @@ -296,6 +299,13 @@ std::uint32_t ResultVector(SpirvValueEmitContext& ctx, const ImageEmitAccess& ac component[index] = index == 0u ? F32BitsToU32(ctx, value) : ConstantU32(state, 0); continue; } + // Through a depth view read for its bits, only the depth channel (X) is a texel: the view + // gives 1.0 where the integer view gives 1 (ONE, or W of the single channel). + const auto selector = (access.image.shaderSwizzle >> (index * 3u)) & 7u; + if (access.image.depthBits && !gather && selector != 4u) { + component[index] = ConstantU32(state, selector == 1u || selector == 7u ? 1u : 0u); + continue; + } const auto scalar = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, ImageScalarType(state, valueClass), scalar, value, index); component[index] = SampledComponentBits(ctx, scalar, valueClass); From 21cb5676c7218a5aa7193a233c3c382fdaf65814 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 11:19:40 +0300 Subject: [PATCH 13/40] feat(shader): buffer access through V#s computed on the GPU A V# the SRT walker cannot compute (loaded from a descriptor heap at a GPU offset, or built by the program) has no binding to hold it. Raw dword loads and stores of 1-4 dwords through one now decode the V# in the shader, apply the RDNA2 bounds rules (OOB_SELECT, stride, swizzle, ADD_TID, format 0) and go through the BDA page table, as Kyty does. Stores need Write permission, which only ranges that are a writable allocation's host import get, so a store into a mirror or a copy faults rather than being lost. The shader notes each written page in an open-addressed set behind the fault record; once the work completed the host marks those pages GPU-written, and an overflowing set throws. Formatted, typed, sub-dword and atomic access through such a V#, and their use outside compute shaders, still fail. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/BdaResources.hpp | 3 + .../Graphics/src/BdaResources.cpp | 32 ++++- .../Graphics/src/GuestBufferMemory.cpp | 6 +- core/shader/recompiler/BdaAbi.hpp | 14 ++- .../IrMetadata/ResourceInfo.hpp | 3 + .../IrMetadata/ShaderInfo.hpp | 3 + .../Optimization/src/ResourceTracker.cpp | 32 +++++ .../include/SpirvBackend/SpirvBda.hpp | 3 + .../SpirvBackend/SpirvEmitterState.hpp | 4 + .../SpirvBackend/src/SpirvAnalysis.cpp | 8 +- .../SpirvBackend/src/SpirvBdaLookup.cpp | 56 ++++++++- .../SpirvBackend/src/SpirvBdaRead.cpp | 19 +++ .../SpirvMemory/SpirvMemoryInstructions.cpp | 112 ++++++++++++++++++ 13 files changed, 284 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp index 32028ea79..f3b4ec590 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp @@ -38,6 +38,9 @@ private: // The page table is read-only to the shader, so consecutive builds mapping the same ranges to the // same device addresses share one buffer while one of them is alive (see the table cache in // BdaResources.cpp, which refers to it weakly). + // The pages stores through the table noted in the fault buffer (BdaAbi::WrittenPageSlots) are + // marked GPU-written, and the set emptied; CheckFault does it first. + void markWrittenPages() const; std::shared_ptr table; std::unique_ptr fault; std::size_t tableBytes = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 1b8b03525..d89a8833b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include #include #include @@ -120,8 +121,8 @@ std::uint64_t hashRanges(const std::vector& ran BdaResources::BdaResources(const Context& context) { static_assert(std::endian::native == std::endian::little); - Require(sizeof(ShaderRecompiler::BdaAbi::Fault) <= context.limits.maxStorageBufferRange, "BDA fault buffer exceeds storage buffer range limit"); - fault = std::make_unique(context, sizeof(ShaderRecompiler::BdaAbi::Fault), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Require(ShaderRecompiler::BdaAbi::FaultBufferBytes <= context.limits.maxStorageBufferRange, "BDA fault buffer exceeds storage buffer range limit"); + fault = std::make_unique(context, ShaderRecompiler::BdaAbi::FaultBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); std::memset(fault->Bytes().data(), 0, fault->Bytes().size()); } @@ -134,7 +135,7 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo Require(ranges.size() <= std::numeric_limits::max(), "BDA table range count overflow"); Require(ranges.size() <= (std::numeric_limits::max() - sizeof(ShaderRecompiler::BdaAbi::Header)) / sizeof(ShaderRecompiler::BdaAbi::Range), "BDA table size overflow"); tableBytes = sizeof(ShaderRecompiler::BdaAbi::Header) + ranges.size() * sizeof(ShaderRecompiler::BdaAbi::Range); - Require(tableBytes <= context.limits.maxStorageBufferRange && sizeof(ShaderRecompiler::BdaAbi::Fault) <= context.limits.maxStorageBufferRange, "BDA descriptors exceed storage buffer range limit"); + Require(tableBytes <= context.limits.maxStorageBufferRange && ShaderRecompiler::BdaAbi::FaultBufferBytes <= context.limits.maxStorageBufferRange, "BDA descriptors exceed storage buffer range limit"); auto& cache = Tables(); std::uint64_t hash = 0; if (tableCacheEnabled()) { @@ -222,10 +223,11 @@ VkDescriptorBufferInfo BdaResources::Table() const { } VkDescriptorBufferInfo BdaResources::Fault() const { - return {fault->Handle(), 0, sizeof(ShaderRecompiler::BdaAbi::Fault)}; + return {fault->Handle(), 0, ShaderRecompiler::BdaAbi::FaultBufferBytes}; } void BdaResources::CheckFault() const { + markWrittenPages(); ShaderRecompiler::BdaAbi::Fault report{}; std::memcpy(&report, fault->Bytes().data(), sizeof(report)); if (report.state == ShaderRecompiler::BdaAbi::FaultState::Empty) { @@ -246,3 +248,25 @@ void BdaResources::CheckFault() const { } } + +namespace AgcDriver::Graphics { + +// ponytail: the pages are marked once the work completed, so a CPU read of them before that (a +// capture or texture upload of later work in the same batch) is not made to wait for the stores, +// as descriptor-bound writes are (Recorder::NotePendingWrites); note the V#s' ranges at record +// time if that shows up. +void BdaResources::markWrittenPages() const { + namespace Abi = ShaderRecompiler::BdaAbi; + auto* words = reinterpret_cast(fault->Bytes().data()); + Require(words[Abi::WrittenOverflowWord] == 0, "more than " + std::to_string(Abi::WrittenPageSlots) + " pages stored to through GPU-selected buffer descriptors in one use are not implemented"); + bool any = false; + for (std::uint32_t slot = 0; slot < Abi::WrittenPageSlots; ++slot) { + const auto page = words[Abi::WrittenSlotsWord + slot]; + if (page == 0) continue; + GuestMemory::MarkWritten(static_cast(page - 1u) << Abi::WrittenPageShift, std::size_t{1} << Abi::WrittenPageShift); + any = true; + } + if (any) std::memset(words + Abi::WrittenSlotsWord, 0, Abi::WrittenPageSlots * sizeof(std::uint32_t)); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 7213c9085..60ddf5a79 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1946,7 +1946,11 @@ ShaderRecompiler::BdaAbi::Range GuestBufferMemory::addressRange(const Region& re Require(region.buffer != nullptr || region.direct != nullptr || region.mirror != nullptr, "incomplete guest GPU upload"); const auto address = region.direct != nullptr ? region.direct->address + (region.begin - region.direct->base) : region.mirror != nullptr ? region.mirror->buffer->DeviceAddress() + (region.begin - region.mirror->base) : region.buffer->DeviceAddress(); Require(region.end - region.begin <= std::numeric_limits::max() - address, "GPU address range overflow"); - return {region.begin, region.end, address, ShaderRecompiler::BdaAbi::Read, 0}; + // Stores through the table (GPU-selected V#s) land in guest memory only where the range is the + // allocation itself: a writable one served in place by its import. Mirrors and copies would + // drop them, so a store there faults. + const auto permissions = ShaderRecompiler::BdaAbi::Read | (region.direct != nullptr && region.writable ? ShaderRecompiler::BdaAbi::Write : 0u); + return {region.begin, region.end, address, permissions, 0}; } std::vector GuestBufferMemory::AddressRanges() const { diff --git a/core/shader/recompiler/BdaAbi.hpp b/core/shader/recompiler/BdaAbi.hpp index 148746e9d..753a178b9 100644 --- a/core/shader/recompiler/BdaAbi.hpp +++ b/core/shader/recompiler/BdaAbi.hpp @@ -27,7 +27,7 @@ struct Range { }; enum class FaultState : std::uint32_t { Empty, Writing, Ready }; -enum class FaultReason : std::uint32_t { Unmapped = 1, Permission, Overflow, InvalidTable, InvalidRectangle, LoopLimit }; +enum class FaultReason : std::uint32_t { Unmapped = 1, Permission, Overflow, InvalidTable, InvalidRectangle, LoopLimit, Unaligned }; struct Fault { FaultState state; @@ -45,6 +45,18 @@ static_assert(offsetof(Range, deviceAddress) == 16 && offsetof(Range, permission static_assert(std::is_standard_layout_v && std::is_trivially_copyable_v && sizeof(Fault) == 32); static_assert(offsetof(Fault, address) == 8 && offsetof(Fault, instruction) == 24); +// After the Fault record, the fault buffer holds the pages the shader stored to through the table +// (Write permission), for the host to mark GPU-written once the work completed: an overflow word, +// then an open-addressed set of page number + 1 (0 is free), probed WrittenPageProbes slots from a +// multiplicative hash. A store that finds no slot sets the overflow word. +inline constexpr std::uint32_t WrittenPageShift = 12; +inline constexpr std::uint32_t WrittenPageSlots = 4096; +inline constexpr std::uint32_t WrittenPageProbes = 8; +inline constexpr std::uint32_t WrittenOverflowWord = sizeof(Fault) / sizeof(std::uint32_t); +inline constexpr std::uint32_t WrittenSlotsWord = WrittenOverflowWord + 1; +inline constexpr std::size_t FaultBufferBytes = (WrittenSlotsWord + WrittenPageSlots) * sizeof(std::uint32_t); +static_assert((WrittenPageSlots & (WrittenPageSlots - 1)) == 0); + } #endif diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp index 7abb73613..2a10445db 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp @@ -45,6 +45,9 @@ struct MemoryInfo { bool idxen = false; bool offen = false; bool planningOnly = false; + // A buffer access whose V# the program computes on the GPU (loaded at a runtime offset, say), + // so no binding can hold it: the shader decodes the V# and goes through the BDA page table. + bool gpuDescriptor = false; bool operator==(const MemoryInfo& other) const = default; }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp index 39228dc3b..e4f9a5863 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp @@ -32,6 +32,9 @@ struct ShaderInfo { bool instanceOffsetConflict = false; bool hasBitwiseXor = false; bool usesDma = false; + // Some access stores through the BDA page table (MemoryInfo::gpuDescriptor): the pages it + // writes are noted in the fault buffer (BdaAbi::WrittenPageSlots). + bool bdaWrites = false; bool operator==(const ShaderInfo& other) const = default; }; diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 933ad2454..a2d03dbce 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -96,6 +96,7 @@ public: m_info.samplers.clear(); m_info.sampledPairs.clear(); m_info.usesDma = false; + m_info.bdaWrites = false; } void Run() { @@ -523,6 +524,34 @@ private: source = InternSource(descriptor); } + // A raw dword load or store (1-4 dwords) through a V# the SRT walker cannot compute: the program + // loads it at a GPU-computed offset (a descriptor heap) or builds it. No binding holds such a + // V#, so the shader decodes it and accesses memory through the BDA page table + // (MemoryInfo::gpuDescriptor). Other accesses keep failing in GetHandle. + bool TakeGpuDescriptor(IrValue& inst, std::uint32_t memoryIndex) { + const IrValue* handle = inst.Argument(0)->Resolve(); + if (handle->Opcode() != IrOpcode::GetBufferResource || handle->ArgumentCount() != 4u) { + return false; + } + DescriptorSource descriptor; + MakeSource(*handle, 4u, false, false, descriptor); + std::uint32_t badDword = 0; + if (ValidateSource(descriptor, badDword)) { + return false; + } + const auto op = inst.Opcode(); + const bool load = op == IrOpcode::LoadBufferU32 || op == IrOpcode::LoadBufferU32x2 || op == IrOpcode::LoadBufferU32x3 || op == IrOpcode::LoadBufferU32x4; + const bool store = op == IrOpcode::StoreBufferU32 || op == IrOpcode::StoreBufferU32x2 || op == IrOpcode::StoreBufferU32x3 || op == IrOpcode::StoreBufferU32x4; + auto& memory = m_program.Resources().memoryInfo[memoryIndex]; + if ((!load && !store) || memory.formatted || memory.typed || memory.dataBits != 32u) { + return false; + } + memory.gpuDescriptor = true; + m_info.usesDma = true; + m_info.bdaWrites = m_info.bdaWrites || store; + return true; + } + void ValidateAddressHandle(IrValue* value) const { const IrValue* handle = value->Resolve(); if (handle->Opcode() != IrOpcode::GetAddressResource) { @@ -681,6 +710,9 @@ private: std::uint32_t resource = 0; if (buffer != BufferAccess::None) { + if (TakeGpuDescriptor(inst, flags.index)) { + return; + } GetHandle(inst.Argument(0), IrOpcode::GetBufferResource, 4, handle, source); resource = AddBuffer(source, memory, op, flags.pc); if (resource == std::numeric_limits::max()) { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp index be56f6c72..fd1430ce1 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp @@ -20,6 +20,9 @@ void ValidateBdaTarget(const IrProgram& program, const SpirvTargetOptions& targe bool BdaInvocationsMayStop(const IrProgram& program); void StopBdaInvocationIf(SpirvEmitterState& state, std::uint32_t condition); std::uint32_t EmitBdaRead(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t bits); +// Stores a dword at a 4-aligned guest address through a range with Write permission and notes its +// page in the fault buffer's written set; a failure records its fault and stores nothing. +void EmitBdaWrite(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t value); // Reads the dwords of a 1-4 dword load at address + offset that the program extracts, with one // table lookup for the whole span; the per-byte lookups of EmitBdaRead remain the fallback (and // the only path under APS5_BDA_BYTE_READS=1). Dwords the program never extracts are neither read diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 9d7681752..cb4e5c2eb 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -99,6 +99,10 @@ struct SpirvEmitterState { // The lookup without fault recording that wide reads try first (see EmitBdaDwordReads); 0 when // every read takes the byte path. std::uint32_t bdaProbeFunction = 0; + // The lookup requiring Write and the written-page note of stores through the table (0 unless + // ShaderInfo::bdaWrites). + std::uint32_t bdaWritePointerFunction = 0; + std::uint32_t bdaNoteWriteFunction = 0; // False for programs with workgroup barriers: faulting BDA accesses then continue (see BdaInvocationsMayStop). bool bdaStopsInvocations = true; // Execution scope of the barriers that keep one guest wave's LDS accesses in program order across diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 4647104f4..a34fb380b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -31,7 +31,13 @@ SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program) { throw std::runtime_error("buffer operation has invalid memory metadata"); } const auto& memory = program.Resources().memoryInfo.at(memoryIndex); - if (memory.kind == ResourceKind::Buffer) { + if (memory.gpuDescriptor) { + // The V#'s ADD_TID is known at run time only. + if (program.Resources().stage != IrShaderStage::Compute) { + throw std::runtime_error("GPU-selected buffer descriptors outside compute shaders are not implemented"); + } + requirements.subgroupLocalInvocationId = true; + } else if (memory.kind == ResourceKind::Buffer) { if (memory.resource >= program.Info().buffers.size()) { throw std::runtime_error("buffer operation has invalid resource metadata"); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp index b67516b27..2ff55d064 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp @@ -1,6 +1,7 @@ #include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" +#include namespace ShaderRecompiler { @@ -17,9 +18,10 @@ void ReturnBdaZeroIf(SpirvEmitterState& state, std::uint32_t condition) { } // (u64 address, u32 bytes, u32 instruction) -> u64 device address of the range holding all the bytes, -// or 0. With recordFaults the failure is published first; without, the caller retries byte-wise, and -// the fault is recorded there if the bytes are unmapped for real. -std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool recordFaults) { +// or 0; the range must grant `permission`. With recordFaults the failure is published first; +// without, the caller retries byte-wise, and the fault is recorded there if the bytes are unmapped +// for real. +std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool recordFaults, std::uint32_t permission = BdaAbi::Read) { const auto u32 = TypeU32(state); const auto u64 = TypeScalarU64(state); const auto boolean = TypeBool(state); @@ -103,7 +105,7 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r fail(binary(spv::OpINotEqual, boolean, BdaLoadWord(state, at(7)), constant(0)), BdaAbi::FaultReason::InvalidTable); fail(binary(spv::OpUGreaterThanEqual, boolean, begin, finish), BdaAbi::FaultReason::InvalidTable); fail(binary(spv::OpUGreaterThan, boolean, end, finish), BdaAbi::FaultReason::Unmapped); - fail(binary(spv::OpIEqual, boolean, binary(spv::OpBitwiseAnd, u32, permissions, constant(BdaAbi::Read)), constant(0)), BdaAbi::FaultReason::Permission); + fail(binary(spv::OpIEqual, boolean, binary(spv::OpBitwiseAnd, u32, permissions, constant(permission)), constant(0)), BdaAbi::FaultReason::Permission); fail(binary(spv::OpIEqual, boolean, base, BdaConstant(state, 0)), BdaAbi::FaultReason::InvalidTable); const auto offset = binary(spv::OpISub, u64, address, begin); const auto result = binary(spv::OpIAdd, u64, base, offset); @@ -117,10 +119,56 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r } +// (u64 address) -> void: notes the page of a store through the table in the fault buffer's written +// set (see BdaAbi::WrittenPageSlots). +static std::uint32_t DefineBdaNoteWrite(SpirvEmitterState& state) { + const auto u32 = TypeU32(state); + const auto boolean = TypeBool(state); + const auto constant = [&](std::uint32_t value) { return ConstantU32(state, value); }; + const auto binary = [&](std::uint32_t op, std::uint32_t type, std::uint32_t left, std::uint32_t right) { return Binary(state, op, type, left, right); }; + const auto voidType = state.module.Type(spv::OpTypeVoid); + const auto function = state.module.AllocateId(); + state.module.AddName(function, "note_bda_write"); + state.module.AddFunction(spv::OpFunction, voidType, function, spv::FunctionControlMaskNone, state.module.Type(spv::OpTypeFunction, voidType, TypeScalarU64(state))); + const auto address = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionParameter, TypeScalarU64(state), address); + EmitLabel(state, state.module.AllocateId()); + const auto done = state.module.AllocateId(); + state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, boolean), done, spv::StorageClassFunction); + state.module.AddFunction(spv::OpStore, done, ConstantBool(state, false)); + const auto page = binary(spv::OpIAdd, u32, Unary(state, spv::OpUConvert, u32, binary(spv::OpShiftRightLogical, TypeScalarU64(state), address, BdaConstant(state, BdaAbi::WrittenPageShift))), constant(1)); + const auto hash = binary(spv::OpShiftRightLogical, u32, binary(spv::OpIMul, u32, page, constant(0x9e3779b1u)), constant(32u - static_cast(std::countr_zero(BdaAbi::WrittenPageSlots)))); + const auto scope = constant(spv::ScopeDevice); + const auto relaxed = constant(spv::MemorySemanticsMaskNone); + for (std::uint32_t probe = 0; probe < BdaAbi::WrittenPageProbes; probe++) { + const auto pending = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, boolean, pending, done); + EmitIfCondition(state, Unary(state, spv::OpLogicalNot, boolean, pending), [&] { + const auto slot = binary(spv::OpIAdd, u32, constant(BdaAbi::WrittenSlotsWord), binary(spv::OpBitwiseAnd, u32, binary(spv::OpIAdd, u32, hash, constant(probe)), constant(BdaAbi::WrittenPageSlots - 1u))); + const auto previous = state.module.AllocateId(); + state.module.AddFunction(spv::OpAtomicCompareExchange, u32, previous, BdaWord(state, state.faultBufferVariable, slot), scope, relaxed, relaxed, page, constant(0u)); + const auto taken = binary(spv::OpLogicalOr, boolean, binary(spv::OpIEqual, boolean, previous, constant(0u)), binary(spv::OpIEqual, boolean, previous, page)); + state.module.AddFunction(spv::OpStore, done, taken); + }); + } + const auto noted = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, boolean, noted, done); + EmitIfCondition(state, Unary(state, spv::OpLogicalNot, boolean, noted), [&] { + state.module.AddFunction(spv::OpAtomicStore, BdaWord(state, state.faultBufferVariable, constant(BdaAbi::WrittenOverflowWord)), scope, relaxed, constant(1u)); + }); + state.module.AddFunction(spv::OpReturn); + state.module.AddFunction(spv::OpFunctionEnd); + return function; +} + void DefineGetBdaPointer(SpirvEmitterState& state) { if (!state.program.Info().usesDma) return; state.bdaPointerFunction = DefineBdaLookup(state, "get_bda_pointer", true); if (!BdaByteReadsForced()) state.bdaProbeFunction = DefineBdaLookup(state, "probe_bda_pointer", false); + if (state.program.Info().bdaWrites) { + state.bdaWritePointerFunction = DefineBdaLookup(state, "get_bda_write_pointer", true, BdaAbi::Write); + state.bdaNoteWriteFunction = DefineBdaNoteWrite(state); + } } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index 5f5c947ee..9c0333992 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -133,6 +133,25 @@ std::uint32_t EmitBdaRead(SpirvValueEmitContext& ctx, const IrValue& inst, std:: return EmitBdaBytes(state, address, bits / 8u, instruction); } +void EmitBdaWrite(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t value) { + auto& state = ctx.state; + if (state.bdaWritePointerFunction == 0 || state.bdaNoteWriteFunction == 0) ctx.Fail(inst, "BDA write functions are missing"); + const auto instruction = ConstantU32(state, inst.Flags().pc); + const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), address, BdaConstant(state, 3u)), BdaConstant(state, 0u)); + EmitIfCondition(state, unaligned, [&] { RecordBdaFault(state, address, ConstantU32(state, 4u), instruction, BdaAbi::FaultReason::Unaligned); }); + EmitIfCondition(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), [&] { + // A failed lookup (unmapped, or a range without Write) has recorded its fault. + const auto physical = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), physical, state.bdaWritePointerFunction, address, ConstantU32(state, 4u), instruction); + EmitIfCondition(state, Binary(state, spv::OpINotEqual, TypeBool(state), physical, BdaConstant(state, 0u)), [&] { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer, physical); + state.module.AddFunction(spv::OpStore, pointer, value, spv::MemoryAccessAlignedMask, 4u); + state.module.AddFunction(spv::OpFunctionCall, state.module.Type(spv::OpTypeVoid), state.module.AllocateId(), state.bdaNoteWriteFunction, address); + }); + }); +} + // One probe of the span from the first to the last extracted dword replaces 4 lookups per dword (a // Bink DC pass spent ~450 dependent table levels per iteration on them). The span is loaded // dword-wise from the returned device address when it lies in one range and that address is diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index 59ac11712..827ec4512 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -172,6 +172,91 @@ std::uint32_t DeviceAddressFromWords(SpirvEmitterState& state, std::uint32_t low return Binary(state, spv::OpBitwiseOr, TypeScalarU64(state), low64, high64); } +// The byte offset of a buffer access from a V# known only at run time (stride and swizzle are ids), +// as BufferByteAddress computes it from a specialized one: {offset + immediate, full byte offset}. +std::pair RuntimeBufferByteAddress(SpirvEmitterState& state, std::uint32_t index, std::uint32_t offset, std::uint32_t soffset, std::uint32_t immediate, std::uint32_t stride, std::uint32_t swizzle, std::uint32_t indexStride) { + const auto u32 = TypeU32(state); + const auto add = [&](std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, spv::OpIAdd, u32, lhs, rhs); }; + const auto mul = [&](std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, spv::OpIMul, u32, lhs, rhs); }; + if (immediate != 0u) offset = add(offset, ConstantU32(state, immediate)); + const auto linear = add(mul(index, stride), offset); + const auto indexShift = add(indexStride, ConstantU32(state, 3u)); + const auto indices = Binary(state, spv::OpShiftLeftLogical, u32, ConstantU32(state, 1u), indexShift); + const auto indexMsb = Binary(state, spv::OpShiftRightLogical, u32, index, indexShift); + const auto indexLsb = Binary(state, spv::OpBitwiseAnd, u32, index, Binary(state, spv::OpISub, u32, indices, ConstantU32(state, 1u))); + const auto offsetMsb = Binary(state, spv::OpBitwiseAnd, u32, offset, ConstantU32(state, ~3u)); + const auto offsetLsb = Binary(state, spv::OpBitwiseAnd, u32, offset, ConstantU32(state, 3u)); + const auto swizzled = add(mul(add(mul(indexMsb, stride), offsetMsb), indices), add(Binary(state, spv::OpShiftLeftLogical, u32, indexLsb, ConstantU32(state, 2u)), offsetLsb)); + return {offset, add(Select(state, u32, swizzle, swizzled, linear), soffset)}; +} + +// Visits each dword of a raw access through a GPU-selected V# (MemoryInfo::gpuDescriptor, the four +// dwords are the handle's arguments) with its guest address and whether RDNA2 bounds checking +// (OOB_SELECT, per dword for raw vectors) lets it through; a V# of format 0 lets nothing through. +template +void ForEachGpuDescriptorDword(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t components, TFunction&& function) { + auto& state = ctx.state; + const IrValue* handle = inst.Argument(0)->Resolve(); + if (handle->Opcode() != IrOpcode::GetBufferResource || handle->ArgumentCount() != 4u) { + ctx.Fail(inst, "has no GPU-selected V#"); + } + const auto u32 = TypeU32(state); + const auto boolean = TypeBool(state); + const auto word1 = ctx.Arg(*handle, 1); + const auto records = ctx.Arg(*handle, 2); + const auto word3 = ctx.Arg(*handle, 3); + const auto field = [&](std::uint32_t word, std::uint32_t first, std::uint32_t count) { return EmitBitFieldUExtract(state, word, ConstantU32(state, first), ConstantU32(state, count)); }; + const auto nonzero = [&](std::uint32_t value) { return Binary(state, spv::OpINotEqual, boolean, value, ConstantU32(state, 0u)); }; + const auto hasDword = [&](std::uint32_t offset, std::uint32_t size) { + return AndCondition(state, Binary(state, spv::OpUGreaterThanEqual, boolean, size, ConstantU32(state, 4u)), Binary(state, spv::OpULessThanEqual, boolean, offset, Binary(state, spv::OpISub, u32, size, ConstantU32(state, 4u)))); + }; + const auto stride = field(word1, 16u, 14u); + const auto swizzle = AndCondition(state, nonzero(stride), nonzero(field(word1, 31u, 1u))); + const auto lane = Binary(state, spv::OpBitwiseAnd, u32, EmitSubgroupLocalInvocationId(state), ConstantU32(state, 63u)); + const auto index = Binary(state, spv::OpIAdd, u32, ctx.Arg(inst, 1), Select(state, u32, nonzero(field(word3, 23u, 1u)), lane, ConstantU32(state, 0u))); + const auto soffset = ctx.Arg(inst, 3); + const auto base = DeviceAddressFromWords(state, ctx.Arg(*handle, 0), field(word1, 0u, 16u)); + const auto mode = field(word3, 28u, 2u); + const auto indexInBounds = Binary(state, spv::OpULessThan, boolean, index, records); + const auto scalarInBounds = Binary(state, spv::OpULessThanEqual, boolean, soffset, records); + const auto rawRecords = Binary(state, spv::OpISub, u32, records, soffset); + const auto rawIndexInBounds = Binary(state, spv::OpULessThan, boolean, index, rawRecords); + for (std::uint32_t component = 0; component < components; component++) { + const auto [offset, byte] = RuntimeBufferByteAddress(state, index, ctx.Arg(inst, 2), soffset, mem.offset + component * 4u, stride, swizzle, field(word3, 21u, 2u)); + // OOB_SELECT 0: index and offset against the stride; 1: index only; 2: NUM_RECORDS nonzero; + // 3: raw, NUM_RECORDS reduced by SOFFSET first. + const auto structured = AndCondition(state, indexInBounds, Binary(state, spv::OpULessThan, boolean, offset, stride)); + const auto raw = AndCondition(state, scalarInBounds, Select(state, boolean, swizzle, AndCondition(state, rawIndexInBounds, hasDword(offset, stride)), hasDword(offset, rawRecords))); + auto inBounds = Select(state, boolean, Binary(state, spv::OpIEqual, boolean, mode, ConstantU32(state, 0u)), structured, indexInBounds); + inBounds = Select(state, boolean, Binary(state, spv::OpULessThan, boolean, mode, ConstantU32(state, 2u)), inBounds, Select(state, boolean, Binary(state, spv::OpIEqual, boolean, mode, ConstantU32(state, 2u)), nonzero(records), raw)); + const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), base, Unary(state, spv::OpUConvert, TypeScalarU64(state), byte)); + function(component, guest, AndCondition(state, nonzero(field(word3, 12u, 7u)), inBounds)); + } +} + +std::uint32_t ConstructU32Composite(SpirvEmitterState& state, std::uint32_t components, const std::array& values); + +std::uint32_t LoadGpuDescriptor(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t components) { + std::array values{}; + ForEachGpuDescriptorDword(ctx, inst, ctx.Memory(inst), components, [&](std::uint32_t component, std::uint32_t guest, std::uint32_t inBounds) { + values[component] = EmitValueOrZeroIfCondition(ctx.state, inBounds, [&] { return EmitBdaRead(ctx, inst, guest, 32u); }); + }); + return components == 1u ? values[0] : ConstructU32Composite(ctx.state, components, values); +} + +void StoreGpuDescriptor(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t components) { + auto& state = ctx.state; + const auto data = ctx.Arg(inst, inst.ArgumentCount() - 2u); + ForEachGpuDescriptorDword(ctx, inst, ctx.Memory(inst), components, [&](std::uint32_t component, std::uint32_t guest, std::uint32_t inBounds) { + auto value = data; + if (components != 1u) { + value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, data, component); + } + EmitIfCondition(state, inBounds, [&] { EmitBdaWrite(ctx, inst, guest, value); }); + }); +} + std::uint32_t GuestAddressBase(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem) { auto& state = ctx.state; auto low = ctx.Arg(inst, 1); @@ -563,9 +648,28 @@ const MemoryInfo& BufferMemory(SpirvValueEmitContext& ctx, const IrValue& inst) if (mem.kind != ResourceKind::Buffer) { ctx.Fail(inst, "must access a buffer resource"); } + if (mem.gpuDescriptor) { + ctx.Fail(inst, "accesses a GPU-selected V# in a way only raw dword loads and stores support"); + } return mem; } +// A raw dword load or store (1-4 dwords) through a GPU-selected V#; false for a bound buffer. +bool EmitGpuDescriptorAccess(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t components, bool store) { + if (!ctx.Memory(inst).gpuDescriptor) { + return false; + } + auto& state = ctx.state; + if (store) { + EmitIfCondition(state, ActiveArgument(ctx, inst), [&] { StoreGpuDescriptor(ctx, inst, components); }); + return true; + } + const auto type = components == 1u ? TypeU32(state) : TypeU32Composite(state, components); + const auto zero = components == 1u ? ConstantU32(state, 0u) : ConstantU32CompositeZero(state, components); + ctx.Define(inst, EmitValueOrDefaultIfCondition(state, ActiveArgument(ctx, inst), type, zero, [&] { return LoadGpuDescriptor(ctx, inst, components); })); + return true; +} + const MemoryInfo& SharedMemory(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto& mem = ctx.Memory(inst); if (mem.kind != ResourceKind::Lds && mem.kind != ResourceKind::Gds) { @@ -1000,19 +1104,23 @@ void EmitLoadBufferU16(SpirvValueEmitContext& ctx, const IrValue& inst) { } void EmitLoadBufferU32(SpirvValueEmitContext& ctx, const IrValue& inst) { + if (EmitGpuDescriptorAccess(ctx, inst, 1u, false)) return; const auto& mem = BufferMemory(ctx, inst); ctx.Define(inst, mem.formatted ? FormattedLoad(ctx, inst, mem) : LoadWord(ctx, inst, mem)); } void EmitLoadBufferU32x2(SpirvValueEmitContext& ctx, const IrValue& inst) { + if (EmitGpuDescriptorAccess(ctx, inst, 2u, false)) return; ctx.Define(inst, LoadWideBuffer(ctx, inst, BufferMemory(ctx, inst), 2u)); } void EmitLoadBufferU32x3(SpirvValueEmitContext& ctx, const IrValue& inst) { + if (EmitGpuDescriptorAccess(ctx, inst, 3u, false)) return; ctx.Define(inst, LoadWideBuffer(ctx, inst, BufferMemory(ctx, inst), 3u)); } void EmitLoadBufferU32x4(SpirvValueEmitContext& ctx, const IrValue& inst) { + if (EmitGpuDescriptorAccess(ctx, inst, 4u, false)) return; ctx.Define(inst, LoadWideBuffer(ctx, inst, BufferMemory(ctx, inst), 4u)); } @@ -1025,6 +1133,7 @@ void EmitStoreBufferU16(SpirvValueEmitContext& ctx, const IrValue& inst) { } void EmitStoreBufferU32(SpirvValueEmitContext& ctx, const IrValue& inst) { + if (EmitGpuDescriptorAccess(ctx, inst, 1u, true)) return; const auto& mem = BufferMemory(ctx, inst); if (mem.formatted) { FormattedStore(ctx, inst, mem); @@ -1034,14 +1143,17 @@ void EmitStoreBufferU32(SpirvValueEmitContext& ctx, const IrValue& inst) { } void EmitStoreBufferU32x2(SpirvValueEmitContext& ctx, const IrValue& inst) { + if (EmitGpuDescriptorAccess(ctx, inst, 2u, true)) return; StoreWideBuffer(ctx, inst, BufferMemory(ctx, inst), 2u); } void EmitStoreBufferU32x3(SpirvValueEmitContext& ctx, const IrValue& inst) { + if (EmitGpuDescriptorAccess(ctx, inst, 3u, true)) return; StoreWideBuffer(ctx, inst, BufferMemory(ctx, inst), 3u); } void EmitStoreBufferU32x4(SpirvValueEmitContext& ctx, const IrValue& inst) { + if (EmitGpuDescriptorAccess(ctx, inst, 4u, true)) return; StoreWideBuffer(ctx, inst, BufferMemory(ctx, inst), 4u); } From 029dcdacac47e19040cedfcd8d0aa4a002da6212 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 11:19:48 +0300 Subject: [PATCH 14/40] feat(agc-driver): null V#s access nothing, as on hardware A V# without records (all zero, say, for a slot the taken path never uses) makes every access out of bounds on hardware: loads read zero or the format's default, stores and atomics are dropped. The recompiler now specializes such a buffer as empty, so every access through it is out of bounds, and the driver binds a small per-device placeholder buffer to the element instead of rejecting the V#. A V# with records at address zero still throws. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 5 +++++ core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp | 6 ++++++ .../libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp | 4 ++++ .../prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp | 6 +++++- .../IntermediateRepresentation/IrMetadata/Resources.hpp | 3 +++ .../include/Optimization/ResourceMaterializer.hpp | 1 + .../recompiler/Optimization/src/ResourceMaterializer.cpp | 4 +++- .../SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp | 4 ++++ 8 files changed, 31 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 60ef67cdf..b0d983b5e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -218,6 +218,8 @@ struct VulkanDevice::State { std::map> computePipelines; std::unique_ptr colorTransfer; std::shared_ptr bufferPool; + // Bound for null V#s (Context::emptyBuffer). + std::unique_ptr emptyBuffer; std::unique_ptr textureCache; std::unique_ptr pipelineCache; std::unique_ptr descriptorCache; @@ -502,6 +504,7 @@ struct VulkanDevice::State { resourceCache.Clear(); Graphics::ClearCachedTextures(device); descriptorCache.reset(); + emptyBuffer.reset(); samplerCache.reset(); textureCache.reset(); detiler.reset(); @@ -869,6 +872,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u poolInfo.queueFamilyIndex = family; check(state->DeviceFunction("vkCreateCommandPool")(state->device, &poolInfo, nullptr, &state->pool), "vkCreateCommandPool"); state->bufferPool = std::make_shared(graphicsContext()); + state->emptyBuffer = std::make_unique(graphicsContext(), Graphics::EmptyBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); state->pipelineCache = std::make_unique(graphicsContext()); state->detiler = std::make_unique(graphicsContext()); state->textureCache = std::make_unique(graphicsContext()); @@ -2167,6 +2171,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.multiDrawIndirect = state->multiDrawIndirect; context.drawIndirectCount = state->drawIndirectCount; context.occlusionQueryPrecise = state->occlusionQueryPrecise; + context.emptyBuffer = state->emptyBuffer ? state->emptyBuffer->Handle() : VK_NULL_HANDLE; context.copiedWriters = state->copiedWriters.get(); context.functions = state->functionsReady ? &state->deviceFunctions : nullptr; context.descriptorIndexing = state->descriptorIndexing; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index e10a8c504..69dc27774 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -74,6 +74,9 @@ inline std::uint64_t& DeviceProcLookups() { return count; } +// The size of Context::emptyBuffer. +inline constexpr std::size_t EmptyBufferBytes = 16; + struct Context { VkDevice device; VkPhysicalDevice physical; @@ -118,6 +121,9 @@ struct Context { bool drawIndirectCount = false; // Occlusion queries count exact samples (occlusionQueryPrecise), not just any-passed. bool occlusionQueryPrecise = false; + // A small storage buffer bound for a null V# (no records): the recompiler specializes such a + // buffer as empty (BufferResource::empty), so shaders never access it. + VkBuffer emptyBuffer = VK_NULL_HANDLE; // The device's list of recorded dispatches whose copied written buffers await a CPU write-back // (VulkanDevice's State::copiedWriters; the draw counterpart is DrawCopiedWriters): an indirect // draw whose records one of them produces reads them on the CPU. Null in tests. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp index 8077e8882..630eba344 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp @@ -34,6 +34,10 @@ void ShaderResources::prepareAddressBindings(std::span sha VkDescriptorBufferInfo ShaderResources::descriptor(const Allocation& allocation) const { if (allocation.guest) return guestMemory.Descriptor(allocation.address, allocation.size); + if (allocation.role == ShaderRecompiler::DescriptorRole::GuestBuffers) { + Require(context.emptyBuffer != VK_NULL_HANDLE, "no placeholder buffer for a null V#"); + return {context.emptyBuffer, 0, allocation.size}; + } if (allocation.role == ShaderRecompiler::DescriptorRole::BdaPagetable || allocation.role == ShaderRecompiler::DescriptorRole::FaultBuffer) { Require(bda != nullptr, "BDA descriptors have no memory owner"); return allocation.role == ShaderRecompiler::DescriptorRole::BdaPagetable ? bda->Table() : bda->Fault(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 40682d222..ef3cfe76d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2015,8 +2015,12 @@ std::size_t ShaderResources::addGuestBuffer(std::span words Require(descriptor.Type() == 0u, "buffer descriptor uses an unsupported type"); const auto address = descriptor.Base48(); const auto byteSize = descriptor.GetSize(); + if (byteSize == 0) { + // A null V#: every access is out of bounds in the shader (BufferResource::empty). + allocations.push_back({0, EmptyBufferBytes, false, nullptr, ShaderRecompiler::DescriptorRole::GuestBuffers, false}); + return allocations.size() - 1; + } Require(address != 0, "null shader buffer descriptor address"); - Require(byteSize != 0, "empty shader buffer descriptor"); Require(byteSize <= context.limits.maxStorageBufferRange, "shader buffer exceeds descriptor range limit"); Require(byteSize <= std::numeric_limits::max(), "shader buffer size exceeds host address space"); const auto size = static_cast(byteSize); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index a99cedc31..1701a2927 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -25,6 +25,9 @@ struct BufferResource { bool atomic = false; bool formatted = false; bool scalar = false; + // Specialized: the V# has no records (a null V#), so every access is out of bounds: loads read + // zero (or the format's default), stores and atomics are dropped, as on hardware. + bool empty = false; bool operator==(const BufferResource& other) const = default; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp index b81416778..8ef7c5cdb 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp @@ -13,6 +13,7 @@ struct ResourceSpecialization { std::uint32_t packedStride = 0; IrBufferFormat descriptorFormat = IrBufferFormat::Invalid; std::uint32_t descriptorSwizzle = ShaderImageIdentitySwizzle; + bool empty = false; bool operator==(const Buffer& other) const; }; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 0bc94901a..199bc18a2 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -507,6 +507,7 @@ void buildResourceSpecialization(const IrResourcePlan& plan, ResourceSnapshot& s entry.packedStride = packedStride; entry.descriptorFormat = buffer.formatted ? decoded.Format() : IrBufferFormat::Invalid; entry.descriptorSwizzle = buffer.formatted ? decoded.DstSelXYZW() : DstSel(4, 5, 6, 7); + entry.empty = decoded.GetSize() == 0u; result.buffers.push_back(entry); } @@ -590,6 +591,7 @@ void ResourceMaterializer::Apply(IrProgram& program, const ResourceSpecializatio buffers[i].packedStride = specialization.buffers[i].packedStride; buffers[i].descriptorFormat = specialization.buffers[i].descriptorFormat; buffers[i].descriptorSwizzle = specialization.buffers[i].descriptorSwizzle; + buffers[i].empty = specialization.buffers[i].empty; } auto images = resources.info.images; @@ -874,7 +876,7 @@ void ResourceMaterializer::CountBindlessRejection(BindlessRejection reason) { } bool ResourceSpecialization::Buffer::operator==(const Buffer& other) const { - return packedStride == other.packedStride && descriptorFormat == other.descriptorFormat && descriptorSwizzle == other.descriptorSwizzle; + return packedStride == other.packedStride && descriptorFormat == other.descriptorFormat && descriptorSwizzle == other.descriptorSwizzle && empty == other.empty; } bool ResourceSpecialization::Image::operator==(const Image& other) const { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index 4954e162c..d97843e58 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -111,6 +111,10 @@ MemoryResourceAccess PrepareStorageBufferResourceAccess(SpirvEmitterState& state access.byteOffset = state.memoryByteOffsets.at(arrayIndex); access.length = state.module.AllocateId(); state.module.AddFunction(spv::OpArrayLength, TypeU32(state), access.length, access.objectPointer, 0u); + // A null V# binds a placeholder the shader must not touch: nothing is in bounds. + if (mem.resource < state.program.Info().buffers.size() && state.program.Info().buffers[mem.resource].empty) { + access.length = ConstantU32(state, 0u); + } return access; } From 000d0bf4f054f6bbdf375b61986dea6c08519f7a Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 13:58:35 +0300 Subject: [PATCH 15/40] feat(shader): retire threads past a partial thread-dimension dispatch A dispatch sized in threads (USE_THREAD_DIMENSIONS) launches a partial last workgroup on RDNA2, but the host rounds it up to whole groups, so the extra threads ran: past the end of a node list they read past it and wrote over live records. Such dispatches now compile a separate variant (ShaderComputeStageInfo:: partialThreads, keyed only on whether it is set) whose entry EXEC excludes threads at or past the requested size. The size reaches the shader as three shader data dwords after the memory offsets (StageInputKind:: DispatchThreadLimit), is mixed into the dispatch cache key and the result memo hash, and serialized in recompile requests (version 3). Whole-group dispatches keep their variant. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Execution/src/Driver.cpp | 12 +++++++++++- .../prx/libSceAgcDriver/tools/ShaderReplay.cpp | 1 + core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 17 ++++++++++++++--- .../IrMetadata/DescriptorBinding.hpp | 8 +++++++- .../IrMetadata/ShaderInfo.hpp | 2 ++ .../IrMetadata/StageIO.hpp | 3 +++ .../Optimization/DescriptorBindingBuilder.hpp | 5 +++-- .../Optimization/ShaderStageInputInfo.hpp | 4 ++-- .../Optimization/src/BindingAllocator.cpp | 1 + .../src/DescriptorBindingBuilder.cpp | 17 ++++++++++++----- .../Optimization/src/ShaderInfoCollector.cpp | 7 ++++--- core/shader/recompiler/Recompiler.cpp | 9 +++++++-- core/shader/recompiler/Recompiler.hpp | 8 ++++++++ .../SpirvBackend/src/SpirvModuleEmitter.cpp | 3 +++ .../Translation/src/InstructionTranslator.cpp | 7 +++++++ .../Translation/src/ShaderInputInfoBuilder.cpp | 1 + 17 files changed, 87 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp index 8281143af..3630973de 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp @@ -3217,7 +3217,7 @@ private: for (std::uint32_t i = 0; i < userCount; ++i) { userData.push_back(readRegister(queue.shader, 0x240 + i)); } - const auto compute = Graphics::DecodeComputeStageInfo(queue.shader); + auto compute = Graphics::DecodeComputeStageInfo(queue.shader); const std::array memory{{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}, {snapshot.headerAddress, snapshot.header}}}; // The GpuMutex is only taken to create the device: taking it just to copy the pointer made // every dispatch wait behind another queue's whole device phase before its lock-free prologue. @@ -3250,6 +3250,14 @@ private: pendingDispatchPhases().outcome = DispatchOutcome::CopyHle; return; } + // USE_THREAD_DIMENSIONS that is no whole number of groups: the host launches whole groups, + // the partial-group variant retires the threads past the size. + if (indirectArguments == 0 && (packet[4] & 0x20u) != 0) { + const std::array threads{packet[1], packet[2], packet[3]}; + for (std::uint32_t axis = 0; axis < 3; ++axis) { + if (threads[axis] % compute.numThreads[axis] != 0) compute.partialThreads = threads; + } + } ShaderRecompiler::RecompileRequest request{ {ShaderRecompiler::ShaderStage::Compute, address, std::span(snapshot.code).subspan(codeOffset), snapshot.headerAddress, snapshot.header}, {(packet[4] & 0x8000u) != 0 ? 32u : 64u, 0, userData, compute, std::nullopt, std::nullopt, memory}, @@ -3314,6 +3322,8 @@ private: }; mix(address); mix(packet[4] & 0x8000u); + // The partial-group size is shader data of the compiled result. + for (const auto threads : compute.partialThreads) mix(threads); for (const auto word : userData) mix(word); // Only the shader registers the request reads (thread counts and RSRC1/2; the program // address is `address`): on queue 0 the bank also holds the graphics stages' registers, diff --git a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp index 4536f474b..2a7ce5eb8 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp @@ -45,6 +45,7 @@ bool Replay(const char* path) { if (request.request.context.compute.has_value()) { const auto& compute = *request.request.context.compute; std::printf(" compute: threads %ux%ux%u, lds %u dwords, group ids %d%d%d, tg size %d, thread id components %u\n", compute.numThreads[0], compute.numThreads[1], compute.numThreads[2], compute.ldsSizeDwords, compute.groupIdEnable[0], compute.groupIdEnable[1], compute.groupIdEnable[2], compute.tgSizeEnable, compute.threadIdComponentCount); + if (compute.PartialGroups()) std::printf(" partial groups: dispatch of %ux%ux%u threads\n", compute.partialThreads[0], compute.partialThreads[1], compute.partialThreads[2]); } if (g_memory) { // --mem: the captured inputs. User data words are printed; each memory region is written to diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 24e4b66b4..bf86cb817 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -103,6 +103,7 @@ private: append(key, value.groupIdEnable); append(key, value.tgSizeEnable); append(key, value.threadIdComponentCount); + append(key, value.PartialGroups()); } static void append(std::vector& key, const ShaderPixelStageInfo& value) { diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index be8e4e2f7..ee3c35b6e 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -660,7 +660,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(2u); + writer.WriteU32(3u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -670,6 +670,12 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writeGraphicsCompileContext(writer, *request.graphics); } writer.WriteBool(request.useCache); + // Version 3: a compute request's partial-group size. + if (request.context.compute.has_value()) { + for (const std::uint32_t value : request.context.compute->partialThreads) { + writer.WriteU32(value); + } + } return base64Encode(buffer); } @@ -678,7 +684,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version != 1u && version != 2u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 3u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result); @@ -688,7 +694,12 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const result.graphicsStorage = std::make_unique(); result.request.graphics = readGraphicsCompileContext(reader, *result.graphicsStorage); } - if (version == 2u) result.request.useCache = reader.ReadBool(); + if (version >= 2u) result.request.useCache = reader.ReadBool(); + if (version >= 3u && result.request.context.compute.has_value()) { + for (std::uint32_t& value : result.request.context.compute->partialThreads) { + value = reader.ReadU32(); + } + } return result; } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp index 9ebdb4267..227915e95 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp @@ -49,12 +49,18 @@ struct IrBindingLayout { std::uint32_t pushDataStartDword = PushData::NoStart; std::uint32_t memoryOffsetDword = 0; std::uint32_t memoryOffsetCount = 0; + // Three dwords after the memory offsets hold the partial-group dispatch size (ShaderInfo:: + // dispatchThreadLimit). + bool dispatchThreadLimit = false; std::vector userDataRegisters; std::vector descriptors; - [[nodiscard]] std::uint32_t ShaderDataDwords() const { + [[nodiscard]] std::uint32_t DispatchThreadLimitDword() const { return memoryOffsetDword + (memoryOffsetCount + 3u) / 4u; } + [[nodiscard]] std::uint32_t ShaderDataDwords() const { + return DispatchThreadLimitDword() + (dispatchThreadLimit ? 3u : 0u); + } [[nodiscard]] bool UsesPushData() const { return pushDataStartDword != PushData::NoStart; } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp index e4f9a5863..6a53521ce 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp @@ -35,6 +35,8 @@ struct ShaderInfo { // Some access stores through the BDA page table (MemoryInfo::gpuDescriptor): the pages it // writes are noted in the fault buffer (BdaAbi::WrittenPageSlots). bool bdaWrites = false; + // The entry reads the partial-group dispatch size (StageInputKind::DispatchThreadLimit). + bool dispatchThreadLimit = false; bool operator==(const ShaderInfo& other) const = default; }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp index af874c1f8..bb5972d64 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp @@ -33,6 +33,9 @@ enum class StageInputKind { LocalInvocationId, LocalInvocationIndex, GlobalInvocationId, + // Not an input: a partial-group dispatch's size in threads, from shader data + // (IrBindingLayout::DispatchThreadLimitDword). + DispatchThreadLimit, Parameter, }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index f417cbbdf..bb5d6f791 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp @@ -9,8 +9,9 @@ namespace ShaderRecompiler { class DescriptorBindingBuilder { public: - void Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot) const; - void Populate(BindingAllocationResult& allocation, const ShaderInfo& info, IrShaderStage stage, std::uint32_t userDataBase, const ResourceSnapshot& snapshot) const; + // `partialThreads`: ShaderComputeStageInfo::partialThreads (zero outside compute). + void Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array& partialThreads) const; + void Populate(BindingAllocationResult& allocation, const ShaderInfo& info, IrShaderStage stage, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const; }; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index e1a9a0452..97afa4e9a 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -147,9 +147,9 @@ struct ShaderVertexInputInfo { }; struct ShaderComputeInputInfo: ShaderWorkgroupInputInfo { - std::uint32_t dispatchThreadsNum[3] = {0, 0, 0}; bool groupId[3] = {false, false, false}; - bool dispatchThreadDimensions = false; + // ShaderComputeStageInfo::PartialGroups: threads past the dispatch size start inactive. + bool partialGroups = false; int threadIdsNum = 0; int workgroupRegister = 0; bool tgSizeEn = false; diff --git a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp index 362fe9cc4..ed0b6987d 100644 --- a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp +++ b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp @@ -95,6 +95,7 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin next.userDataRegisters = collectUserData(program); next.memoryOffsetDword = static_cast(next.userDataRegisters.size()); next.memoryOffsetCount = static_cast(info.buffers.size()); + next.dispatchThreadLimit = info.dispatchThreadLimit; const std::uint32_t pushDataStartDword = layout.pushConstantOffsetBytes / 4u; const std::uint32_t pushConstantSizeDwords = layout.pushConstantSizeBytes / 4u; const bool usesPushData = next.ShaderDataDwords() != 0u && next.ShaderDataDwords() <= pushConstantSizeDwords; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index eac82b77c..cc715ce6a 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -1,6 +1,7 @@ #include "Optimization/DescriptorBindingBuilder.hpp" #include "SpirvBackend/SpirvEmitterHelpers.hpp" #include +#include #include #include #include @@ -178,7 +179,7 @@ std::vector GuestSamplersDescriptor(const std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, std::uint32_t userDataBase, const ResourceSnapshot& snapshot) { +std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) { std::vector result(layout.ShaderDataDwords(), 0u); for (std::size_t i = 0; i < layout.userDataRegisters.size(); i++) { const std::uint32_t reg = layout.userDataRegisters[i]; @@ -187,18 +188,24 @@ std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, st } result[i] = snapshot.userData[reg - userDataBase]; } + if (layout.dispatchThreadLimit) { + if (partialThreads == std::array{}) { + fail("DescriptorBindingBuilder::Populate partial-group shader has no dispatch size"); + } + std::copy(partialThreads.begin(), partialThreads.end(), result.begin() + layout.DispatchThreadLimitDword()); + } return result; } } -void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot) const { - Populate(allocation, program.Info(), program.Resources().stage, program.Resources().userDataBase, snapshot); +void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array& partialThreads) const { + Populate(allocation, program.Info(), program.Resources().stage, program.Resources().userDataBase, snapshot, partialThreads); } -void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const ShaderInfo& info, IrShaderStage stage, std::uint32_t userDataBase, const ResourceSnapshot& snapshot) const { +void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const ShaderInfo& info, IrShaderStage stage, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const { const IrBindingLayout& layout = allocation.layout; - const std::vector shaderData = ShaderDataDwordsFor(layout, userDataBase, snapshot); + const std::vector shaderData = ShaderDataDwordsFor(layout, userDataBase, snapshot, partialThreads); std::vector bindings; bindings.reserve(layout.descriptors.size()); diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index ac8d97456..84916b283 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -148,6 +148,7 @@ void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inpu case StageInputKind::WorkgroupId: case StageInputKind::LocalInvocationId: case StageInputKind::GlobalInvocationId: + case StageInputKind::DispatchThreadLimit: if (component >= 3u) { return Fail("typed invocation builtin component is out of range"); } @@ -249,9 +250,6 @@ void CollectComputeInputs(const ShaderComputeInputInfo* compute, ShaderInfo& inf if (compute->threadIdsNum > 0 || compute->tgSizeEn) { AddInput(info, StageInputKind::LocalInvocationIndex, 0, 1, "gl_LocalInvocationIndex"); } - if (compute->dispatchThreadDimensions) { - AddInput(info, StageInputKind::GlobalInvocationId, 0, 3, "gl_GlobalInvocationID"); - } } void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { @@ -310,6 +308,9 @@ void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { case StageInputKind::GlobalInvocationId: AddInput(info, kind, 0, 3, "gl_GlobalInvocationID"); break; + case StageInputKind::DispatchThreadLimit: + info.dispatchThreadLimit = true; + break; case StageInputKind::PackedAncillary: case StageInputKind::Parameter: break; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 0e0552bf4..e237d71c2 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -273,6 +273,10 @@ std::shared_ptr getSource(const RecompileRequest& request) { return source; } +std::array partialThreads(const RecompileRequest& request) { + return request.context.compute ? request.context.compute->partialThreads : std::array{}; +} + CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, const ResourceSnapshot& resourceSnapshot, const ResourceSpecialization& resourceSpecialization) { const auto inputInfo = BuildShaderStageInputInfo(toShaderStageKind(request.shader.stage), request.context, HostSubgroupSize(request)); constexpr DeadCodeEliminator deadCodeEliminator; @@ -289,7 +293,7 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra auto bindings = bindingAllocator.Allocate(program, request.layout); constexpr DescriptorBindingBuilder descriptorBindingBuilder; - descriptorBindingBuilder.Populate(bindings, program, resourceSnapshot); + descriptorBindingBuilder.Populate(bindings, program, resourceSnapshot, partialThreads(request)); SpirvTargetOptions targetOptions {}; targetOptions.vulkanVersion = request.target.vulkanVersion; @@ -343,7 +347,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil bindings.layout = variant.bindings.layout; bindings.pushConstantOffsetBytes = variant.bindings.pushConstantOffsetBytes; bindings.pushConstantSizeBytes = variant.bindings.pushConstantSizeBytes; - DescriptorBindingBuilder{}.Populate(bindings, variant.info.info, variant.info.stage, variant.info.userDataBase, snapshot); + DescriptorBindingBuilder{}.Populate(bindings, variant.info.info, variant.info.stage, variant.info.userDataBase, snapshot, partialThreads(request)); result.bindings = std::move(bindings.bindings); result.pushConstants = std::move(bindings.pushConstants); for (auto& attribute : result.vertexAttributes) { @@ -466,6 +470,7 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh for (const auto stride : snapshot.uniformFill.groupStride) mix(stride); mix(snapshot.uniformFill.words); mix(snapshot.uniformFill.value); + for (const auto threads : partialThreads(request)) mix(threads); if (request.context.vertex) { const auto& vertex = *request.context.vertex; const auto count = std::min(vertex.resourcesNum, ShaderVertexStageInfo::MaxResources); diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 2cf0221f7..68faec109 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -43,6 +43,14 @@ struct ShaderComputeStageInfo { std::array groupIdEnable; bool tgSizeEnable; std::uint32_t threadIdComponentCount; + // A dispatch sized in threads (USE_THREAD_DIMENSIONS) that is no whole number of workgroups: + // its size per axis, the last workgroup partial; zero otherwise. Variants key on whether it + // is set only; the size reaches the shader as shader data. + std::array partialThreads; + + [[nodiscard]] bool PartialGroups() const { + return partialThreads != std::array{}; + } }; struct ShaderPixelStageInfo { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 1f6127396..f82fedb04 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -39,6 +39,9 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: if (kind == StageInputKind::LocalInvocationIndex) { return EmitLocalInvocationIndex(state); } + if (kind == StageInputKind::DispatchThreadLimit) { + return EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.DispatchThreadLimitDword() + component); + } if (state.laneCount == 2 && (kind == StageInputKind::LocalInvocationId || kind == StageInputKind::GlobalInvocationId)) { const auto* workgroup = ShaderWorkgroupInputFor(state); if (workgroup == nullptr) { diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index b000be782..4cfc32819 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -200,6 +200,13 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO initialExec = &entryIr.ULessThan(builtin(StageInputKind::LocalInvocationIndex), entryIr.Constant(totalThreads)); } } + // A dispatch sized in threads launches a partial last workgroup; the host rounds it up to + // whole groups, so threads past the dispatch size start inactive. + if (options.stage == ShaderStageKind::Compute && options.inputInfo.compute->partialGroups) { + for (std::uint32_t axis = 0; axis < 3u; axis++) { + initialExec = &entryIr.LogicalAnd(*initialExec, entryIr.ULessThan(builtin(StageInputKind::GlobalInvocationId, axis), builtin(StageInputKind::DispatchThreadLimit, axis))); + } + } entryIr.SetExec(*initialExec); IrValue& initialMask = entryIr.Emit(IrOpcode::Ballot, IrOpcodeType(IrOpcode::Ballot), {initialExec}); entryIr.SetExecLo(entryIr.CompositeExtract(initialMask, 0u)); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index ecd870cdc..d29849d3a 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -84,6 +84,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues computeStorage.groupId[2] = compute.groupIdEnable[2]; computeStorage.tgSizeEn = compute.tgSizeEnable; computeStorage.threadIdsNum = static_cast(compute.threadIdComponentCount); + computeStorage.partialGroups = compute.PartialGroups(); // Workgroup ids (and the thread-group size word) follow the user SGPRs. computeStorage.workgroupRegister = static_cast(context.userDataBaseRegister + context.userData.size()); ShaderStageInputInfo result; From 3dec6ceda7c47b3ac6fe4c036154ba829ccc23a4 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 13:58:52 +0300 Subject: [PATCH 16/40] feat(agc-driver): bind guest buffers below their view at the storage offset alignment A view whose offset in its GPU owner is not a multiple of minStorageBufferOffsetAlignment could not be bound, and an address-based build (its registered heaps imported whole) put descriptor-bound buffers at arbitrary offsets inside them, so such dispatches failed. GuestBufferMemory::Descriptor now binds from the aligned offset below the view and reports the difference; ShaderResources stores it, per guest buffer element, into the push constant block at the shader's memory byte offsets (RecompileResult::memoryOffsetDword), which the shader already adds to every access. Every push constant path (dispatches, recorded and synchronous draws, draw recipes) takes the patches; the assembling overload of Pipeline::PushConstants is gone. A shader whose shader data is a buffer fails when it would need one. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/VulkanDevice.cpp | 3 ++- .../Graphics/include/GuestBufferMemory.hpp | 5 ++++- .../Graphics/include/Pipeline.hpp | 3 +-- .../Graphics/include/ShaderResources.hpp | 14 +++++++++++++- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 15 ++++++++++++--- .../Graphics/src/GuestBufferMemory.cpp | 11 +++++++---- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 7 ------- .../Graphics/src/ShaderAddressResources.cpp | 4 ++-- .../Graphics/src/ShaderResources.cpp | 15 ++++++++++++++- .../libSceAgcDriver/tests/BdaResources.cpp | 19 ++++++++++++------- core/shader/recompiler/Recompiler.cpp | 1 + core/shader/recompiler/Recompiler.hpp | 4 ++++ 12 files changed, 72 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index b0d983b5e..a8b6d56ca 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2937,7 +2937,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec if (pushStages != 0 && state->properties.limits.maxPushConstantsSize < Graphics::PipelinePushConstantBytes) { throw std::runtime_error("Vulkan dispatch: compute push constant range exceeds device limit"); } - const auto pushBytes = Graphics::AssemblePushConstants(shaders); + auto pushBytes = Graphics::AssemblePushConstants(shaders); const auto context = graphicsContext(); const auto* limit = state->properties.limits.maxComputeWorkGroupCount; if (arguments == 0 && (x > limit[0] || y > limit[1] || z > limit[2])) { @@ -3067,6 +3067,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec if (resources->Reusable()) insert(); } } + resources->PatchPushConstants(pushBytes); if (profile) timer.add(PhaseResourcesHookWaits, Graphics::Recorder::ThreadWaitedMs() - waitedBefore); // The lookup outcomes of this call: rows with counts on the [indirect] line, and a summary for // the longest call. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index aca08e13e..5677b2a1b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -163,7 +163,10 @@ public: void Upload(bool addressable); void UploadPrepare(bool addressable); void UploadFinish(bool addressable); - VkDescriptorBufferInfo Descriptor(std::uint64_t address, std::size_t bytes) const; + // A view off minStorageBufferOffsetAlignment in its GPU owner (a view inside a merged or + // imported range) is bound from the aligned offset below it; `adjustment` is the difference, + // which the shader adds to every access (RecompileResult::memoryOffsetDword). + VkDescriptorBufferInfo Descriptor(std::uint64_t address, std::size_t bytes, std::uint32_t& adjustment) const; std::vector AddressRanges() const; // The BDA table of the cached address space when it serves this upload alone (an address-based // build with no region outside it): its ranges, immutable while the space lives, and the diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index ef98ccc0f..4e74c0cac 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -51,8 +51,7 @@ public: // The same inside a render pass another pipeline of the same attachments began (compatible by // construction: the attachment formats alone decide). void Continue(VkCommandBuffer commands, const VkViewport& viewport, const VkRect2D& scissor) const; - void PushConstants(VkCommandBuffer commands, std::span shaders) const; - // The same from an assembled block (a draw recipe's, made by AssemblePushConstants once). + // An assembled block (AssemblePushConstants, then ShaderResources::PatchPushConstants). void PushConstants(VkCommandBuffer commands, VkShaderStageFlags stages, std::span bytes) const; // Forgets the Vulkan objects without destroying them: for entries of a device that is already gone. void Abandon() noexcept; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 2092a2b68..136607e0a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -185,6 +185,11 @@ public: // compiled shader's descriptors. A recipe hit refreshes the template iff the two differ. std::uint64_t DataWordsHash() const { return dataWordsHash; } static std::uint64_t DataWordsHash(const CompiledShader& shader); + // Stores the byte offsets of the guest buffers bound below their views (GuestBufferMemory:: + // Descriptor) into a push constant block assembled from the shaders this object was built for. + void PatchPushConstants(std::span bytes) const { + for (const auto& [position, adjustment] : pushPatches) bytes[position] = static_cast(adjustment); + } // Whether RefreshData would record anything for `shader` (the per-word compare; verification). bool DataWordsDiffer(const CompiledShader& shader) const; // Why the fast proof of a Revalidate left the object to the full walk (the [rescache] @@ -237,6 +242,11 @@ private: bool written = true; // Data buffers: the words the buffer holds once the recorded work ran (see RefreshData). std::vector dataWords; + // Guest buffers: how far below the view the descriptor starts (GuestBufferMemory:: + // Descriptor), and where in the push constant block the shader reads that (-1: the + // shader data is a buffer). + std::uint32_t adjustment = 0; + std::int32_t pushByte = -1; }; struct Binding { @@ -308,7 +318,7 @@ private: void forgetDeferredInputs(); void release() noexcept; void prepareAddressBindings(std::span shaders, std::span snapshots); - VkDescriptorBufferInfo descriptor(const Allocation& allocation) const; + VkDescriptorBufferInfo descriptor(Allocation& allocation); void noteReusable(); void reportDescriptorCaches() const; // What a sampled texture was proved current against when the build (or the last full Revalidate) @@ -400,6 +410,8 @@ private: // FNV-1a offset basis: the hash of no data buffers (DataWordsHash). std::uint64_t dataWordsHash = 14695981039346656037ull; void rehashDataWords(); + // The nonzero guest buffer adjustments by push constant byte (PatchPushConstants). + std::vector> pushPatches; BuildTiming timing; // Build state carried from stage A to stage B: the bindings in plan order, the image bindings // still to look up (index into `bindings`; the DescriptorBinding lives in the compiled shader), diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index e6b9ae5c5..4cd4dbac5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1159,7 +1159,11 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam } APS5_LOG_CHARS_OUT_DEBUG("Resources bound"); if (record.pushBytes != nullptr) record.pipeline->PushConstants(commands, record.pushStages, *record.pushBytes); - else record.pipeline->PushConstants(commands, shaders); + else { + auto bytes = AssemblePushConstants(shaders); + resources.PatchPushConstants(bytes); + record.pipeline->PushConstants(commands, PushConstantStages(shaders), bytes); + } APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset); if (args != nullptr) CountIndirectDraw(record.indirect->path, record.indirect->readMs, rewritten); @@ -1488,7 +1492,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& recipe->passKey = passKey; recipe->vertexInput = inputs.vertexInput; recipe->pushStages = PushConstantStages(shaders); - if (recipe->pushStages != 0) recipe->pushBytes = AssemblePushConstants(shaders); + if (recipe->pushStages != 0) { + recipe->pushBytes = AssemblePushConstants(shaders); + resources->PatchPushConstants(recipe->pushBytes); + } recipe->masked = masked; recipe->fragmentOutputs = inputs.fragmentOutputs; recipe->shaderStages = inputs.shaderStages; @@ -1577,7 +1584,9 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& APS5_LOG_CHARS_OUT_DEBUG("Pipeline Begin OK"); resources->Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline->Layout()); APS5_LOG_CHARS_OUT_DEBUG("Resources bound"); - pipeline->PushConstants(commands, shaders); + auto pushBytes = AssemblePushConstants(shaders); + resources->PatchPushConstants(pushBytes); + pipeline->PushConstants(commands, PushConstantStages(shaders), pushBytes); APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset); if (args != nullptr) CountIndirectDraw(indirect.path, indirect.readMs, rewritten); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 60ddf5a79..ca290d06f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1927,7 +1927,7 @@ void GuestBufferMemory::RecordCopyBacks(Recorder& recorder) { recorder.EndGpuTiming(timing, copiedBytes); } -VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std::size_t bytes) const { +VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std::size_t bytes, std::uint32_t& adjustment) const { Require(uploaded && !committed, "guest GPU memory is not available"); Require(bytes != 0 && bytes <= std::numeric_limits::max() - address, "invalid guest buffer view"); const auto* found = owner(address); @@ -1936,10 +1936,13 @@ VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std: Require(address >= region.begin && address + bytes <= region.end && (region.buffer != nullptr || region.direct != nullptr || region.mirror != nullptr), "guest buffer view exceeds its GPU owner"); const auto base = region.direct != nullptr ? region.direct->base : region.mirror != nullptr ? region.mirror->base : region.begin; const auto offset = address - base; - Require(context.limits.minStorageBufferOffsetAlignment != 0 && offset % context.limits.minStorageBufferOffsetAlignment == 0, "guest buffer view violates storage buffer offset alignment"); - Require(bytes <= context.limits.maxStorageBufferRange, "guest buffer view exceeds descriptor range limit"); + Require(context.limits.minStorageBufferOffsetAlignment != 0, "no storage buffer offset alignment"); + // At most 255 (the alignment is at most 256); the shader adds it in DWORDs. + adjustment = static_cast(offset % context.limits.minStorageBufferOffsetAlignment); + Require(adjustment % 4 == 0, "guest buffer view off the storage buffer offset alignment is not DWORD aligned"); + Require(bytes + adjustment <= context.limits.maxStorageBufferRange, "guest buffer view exceeds descriptor range limit"); const auto handle = region.direct != nullptr ? region.direct->buffer : region.mirror != nullptr ? region.mirror->buffer->Handle() : region.buffer->Handle(); - return {handle, offset, bytes}; + return {handle, offset - adjustment, bytes + adjustment}; } ShaderRecompiler::BdaAbi::Range GuestBufferMemory::addressRange(const Region& region) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index c06f4471b..558cefb4f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -277,13 +277,6 @@ void Pipeline::Continue(VkCommandBuffer commands, const VkViewport& viewport, co context.Resolved(&DeviceFunctions::cmdSetScissor, "vkCmdSetScissor")(commands, 0, 1, &scissor); } -void Pipeline::PushConstants(VkCommandBuffer commands, std::span shaders) const { - const auto stages = PushConstantStages(shaders); - if (stages == 0) return; - const auto bytes = AssemblePushConstants(shaders); - context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, layout, stages, 0, PipelinePushConstantBytes, bytes.data()); -} - void Pipeline::PushConstants(VkCommandBuffer commands, VkShaderStageFlags stages, std::span bytes) const { if (stages == 0) return; context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, layout, stages, 0, PipelinePushConstantBytes, bytes.data()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp index 630eba344..895fa359d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp @@ -32,8 +32,8 @@ void ShaderResources::prepareAddressBindings(std::span sha } } -VkDescriptorBufferInfo ShaderResources::descriptor(const Allocation& allocation) const { - if (allocation.guest) return guestMemory.Descriptor(allocation.address, allocation.size); +VkDescriptorBufferInfo ShaderResources::descriptor(Allocation& allocation) { + if (allocation.guest) return guestMemory.Descriptor(allocation.address, allocation.size, allocation.adjustment); if (allocation.role == ShaderRecompiler::DescriptorRole::GuestBuffers) { Require(context.emptyBuffer != VK_NULL_HANDLE, "no placeholder buffer for a null V#"); return {context.emptyBuffer, 0, allocation.size}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index ef3cfe76d..097b53ade 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -828,7 +828,15 @@ void ShaderResources::buildPrepare(std::span shaders, cons // vector) counts as written, like imageWritten below. const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; const bool atomic = element < binding.bufferAtomic.size() && binding.bufferAtomic[element]; - item.allocations.push_back(addGuestBuffer(std::span(binding.guestDescriptor).subspan(static_cast(element) * 4, 4), target, indexAddress, indexBytes, written, atomic)); + const auto index = addGuestBuffer(std::span(binding.guestDescriptor).subspan(static_cast(element) * 4, 4), target, indexAddress, indexBytes, written, atomic); + // The element's byte offset in the shader data (RecompileResult::memoryOffsetDword). + const auto& push = shader.program->pushConstants; + if (!push.empty()) { + const auto position = shader.program->memoryOffsetDword * 4u + element; + Require(position < push.size(), "guest buffer offset lies outside the shader's push constants"); + allocations[index].pushByte = static_cast(shader.pushConstantOffset + position); + } + item.allocations.push_back(index); } } else if (addressRole) { item.allocations.push_back(allocations.size()); @@ -966,6 +974,11 @@ void ShaderResources::buildComplete() { } context.Resolved(&DeviceFunctions::updateDescriptorSets, "vkUpdateDescriptorSets")(context.device, static_cast(writes.size()), writes.data(), 0, nullptr); } + for (const auto& allocation : allocations) { + if (allocation.adjustment == 0) continue; + Require(allocation.pushByte >= 0, "a guest buffer off the storage buffer offset alignment in a shader whose shader data is a buffer is not implemented"); + pushPatches.emplace_back(static_cast(allocation.pushByte), allocation.adjustment); + } timing.descriptorsMs += phase(BuildPhase::Descriptors); noteReusable(); completed = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index d5c87f22e..c1b432a7f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -36,9 +36,11 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { memory.AddWritable(address, sizeof(guest)); memory.AddWritable(address + 16, 16); memory.Upload(true); - const auto first = memory.Descriptor(address, sizeof(guest)); - const auto alias = memory.Descriptor(address + 16, 16); - Require(first.buffer == alias.buffer && alias.offset == 16, "aliased guest buffers have different owners"); + std::uint32_t adjustment = 0; + const auto first = memory.Descriptor(address, sizeof(guest), adjustment); + Require(adjustment == 0, "a view at its owner's start is bound off it"); + const auto alias = memory.Descriptor(address + 16, 16, adjustment); + Require(first.buffer == alias.buffer && alias.offset + adjustment == 16 && alias.range == 16 + adjustment, "aliased guest buffers have different owners"); const auto ranges = memory.AddressRanges(); Require(ranges.size() == 1 && ranges[0].begin == address && ranges[0].end == address + sizeof(guest), "incorrect BDA range bounds"); Require(ranges[0].deviceAddress != 0 && ranges[0].permissions == ShaderRecompiler::BdaAbi::Read, "incorrect BDA address or permissions"); @@ -118,8 +120,10 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { GuestBufferMemory unaligned(aligned); unaligned.AddWritable(address, sizeof(guest)); unaligned.Upload(true); - reject([&] { unaligned.Descriptor(address + 4, 4); }, "offset alignment"); - reject([&] { unaligned.Descriptor(address + sizeof(guest), 4); }, "exceeds its GPU owner"); + std::uint32_t adjustment = 0; + const auto view = unaligned.Descriptor(address + 4, 4, adjustment); + Require(adjustment == 4 && view.offset == 0 && view.range == 8, "a view off the offset alignment binds from below it"); + reject([&] { unaligned.Descriptor(address + sizeof(guest), 4, adjustment); }, "exceeds its GPU owner"); } { // The cached address space: a second build in an unchanged registry takes the first one's @@ -139,8 +143,9 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { leased.AcquireRegistered(); Require(leased.HoldsLease(), "address-based build holds no lease"); leased.Upload(true); - const auto view = leased.Descriptor(blockAddress, 64); - Require(view.range == 64, "leased block has no descriptor"); + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(blockAddress, 64, adjustment); + Require(view.range == 64 + adjustment, "leased block has no descriptor"); const auto ranges = leased.AddressRanges(); Require(std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin == blockAddress && range.end == blockAddress + 64; }), "leased block is missing from the BDA table"); leased.WriteBack(); diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index e237d71c2..584303d94 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -314,6 +314,7 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra #endif result.bdaAbiVersion = program.Info().usesDma ? request.target.bdaAbiVersion : 0u; + result.memoryOffsetDword = bindings.layout.memoryOffsetDword; result.vertexOffsetSgpr = program.Info().vertexOffsetSgpr; result.instanceOffsetSgpr = program.Info().instanceOffsetSgpr; result.vertexOffsetShared = program.Info().vertexOffsetShared; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 68faec109..4d13c5c86 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -327,6 +327,10 @@ struct RecompileResult { SharedSpirv spirv; std::vector bindings; std::vector pushConstants; + // The shader data dword of the guest buffers' byte offsets, one byte per GuestBuffers element + // (four per dword), zero as populated: a driver binding an element below its view (the storage + // buffer offset alignment) stores the difference there, the shader adds it to every access. + std::uint32_t memoryOffsetDword = 0; std::uint32_t bdaAbiVersion = 0; std::vector vertexAttributes; std::int32_t vertexOffsetSgpr = -1; From 6a73ec426f2ab4b0de8818ee65eddeb5ee2de155 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 13:59:02 +0300 Subject: [PATCH 17/40] fix(shader): a selection whose arm continues merges where the other arm goes findSelectionMerge took the nearest common post-dominator of a selection's targets, computed across the loop's back edge. When one arm only continues (reaching that block in a later iteration) and the other target is shared with an enclosing selection, the shared merge was split off the header, the post-dominator stayed where it was, and splitting repeated until the budget threw. Inside a loop, an arm that reaches the post-dominator only past the continue or merge block now leaves the selection by continuing, and the merge is the other arm's target; after one split that is the header's own synthetic merge. agc_structurizer_tests builds the smallest such graph; it hit the budget before. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 7 ++- .../libSceAgcDriver/tests/Structurizer.cpp | 53 +++++++++++++++++++ .../ControlFlow/src/Structurizer.cpp | 30 +++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index fb36e364d..e8a37c6c6 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -158,6 +158,11 @@ target_link_libraries(agc_shader_memory_tests PRIVATE shader_recompiler_agc_driv set_target_properties(agc_shader_memory_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shader_memory_tests) +add_executable(agc_structurizer_tests EXCLUDE_FROM_ALL tests/Structurizer.cpp) +target_link_libraries(agc_structurizer_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_structurizer_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_structurizer_tests) + add_executable(agc_driver_flip_tests EXCLUDE_FROM_ALL tests/Flip.cpp ) @@ -287,7 +292,7 @@ set_target_properties(agc_driver_bda_device_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_bda_device_tests) if(BUILD_TESTING) - foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_driver_flip_tests agc_driver_pm4_tests) + foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_driver_flip_tests agc_driver_pm4_tests) string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}") set_target_properties(${agcTest} PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME ${agcTestName} COMMAND ${agcTest}) diff --git a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp new file mode 100644 index 000000000..ff4ec555f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp @@ -0,0 +1,53 @@ +// A selection in a loop with an arm that continues and another target it shares with an enclosing +// selection: its merge is where the other arm goes, so splitting the shared merge terminates. +#include "ControlFlow/Structurizer.hpp" +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +int main() { + // 0 -> 1 (loop header) -> 2; 2: 5 | 3; 3: 5 | 4; 4 -> 7 (continue); 5: 6 (exit) | 7; 6 returns; + // 7 -> 1. Around the back edge every path from 4 passes 5, so 5 post-dominates both selections. + const std::vector> successors{{1}, {2}, {5, 3}, {5, 4}, {7}, {6, 7}, {}, {1}}; + ControlFlowGraph graph; + graph.entryBlock = 0; + for (std::uint32_t id = 0; id < successors.size(); ++id) { + BasicBlock block; + block.id = id; + block.startProgramCounter = id * 8; + block.endProgramCounter = id * 8 + 8; + block.instructionBegin = id * 2; + block.instructionEnd = id * 2 + 2; + block.successors = successors[id]; + auto& terminator = block.terminator; + if (successors[id].empty()) { + terminator.kind = TerminatorKind::Return; + } else if (successors[id].size() == 1) { + terminator.kind = TerminatorKind::Branch; + terminator.trueBlock = successors[id][0]; + } else { + terminator.kind = TerminatorKind::ConditionalBranch; + terminator.condition = BranchCondition::SccNonZero; + terminator.trueBlock = successors[id][0]; + terminator.falseBlock = successors[id][1]; + } + graph.blocks.push_back(std::move(block)); + } + for (const auto& block : graph.blocks) { + for (const auto successor : block.successors) graph.blocks[successor].predecessors.push_back(block.id); + } + try { + Structurizer{}.Structurize(graph); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } + if (graph.FindBlock(2).terminator.mergeBlock == graph.FindBlock(3).terminator.mergeBlock) { + std::fprintf(stderr, "the nested selections share merge block %u\n", graph.FindBlock(2).terminator.mergeBlock); + return 1; + } + return 0; +} diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index 591c76217..8b7f2259f 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -288,6 +288,26 @@ bool canReachBefore(const ControlFlowGraph& graph, std::uint32_t start, std::uin return false; } +// Whether `start` reaches `target` without passing the loop's continue or merge block. +bool reachesWithinIteration(const ControlFlowGraph& graph, const NaturalLoop& loop, std::uint32_t start, std::uint32_t target) { + std::vector pending = {start}; + std::vector visited(graph.blocks.size(), false); + while (!pending.empty()) { + const auto blockId = pending.back(); + pending.pop_back(); + if (blockId == target) { + return true; + } + if (blockId == loop.continueBlock || blockId == loop.mergeBlock || visited[blockId]) { + continue; + } + visited[blockId] = true; + const auto& block = graph.FindBlock(blockId); + pending.insert(pending.end(), block.successors.begin(), block.successors.end()); + } + return false; +} + std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock& block) { const auto globalMerge = graph.FindNearestCommonPostDominator(block.terminator.trueBlock, block.terminator.falseBlock); const auto* loop = findInnermostContainingLoop(graph, block.id); @@ -325,6 +345,16 @@ std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock if (isLoopControlGateway(graph, *loop, falseTarget) && graph.Dominates(block.id, falseTarget) && isInsideLoopConstruct(graph, *loop, trueTarget)) { return falseTarget; } + // An arm that reaches the post-dominator only in a later iteration (around the back edge) + // leaves the selection by continuing; the merge is where the other arm goes. Splitting the + // post-dominator off would leave it where it is, and the split repeats without end. + if (globalMerge != InvalidControlFlowId) { + const bool trueJoins = reachesWithinIteration(graph, *loop, trueTarget, globalMerge); + const bool falseJoins = reachesWithinIteration(graph, *loop, falseTarget, globalMerge); + if (trueJoins != falseJoins) { + return trueJoins ? trueTarget : falseTarget; + } + } return globalMerge; } From 3d3f71224e8227012bf9bd8801a689876921f0e9 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 13:59:11 +0300 Subject: [PATCH 18/40] feat(shader): V#s at address 0 access nothing A V# with records but a base address of 0 threw at bind time ("null shader buffer descriptor address"), although a shader that never reaches that slot works on hardware; an access through it would fault there, so a working title does not make one. Such a V# is specialized empty like one without records (every access out of bounds) and binds the placeholder buffer; the draw cache's alias check skips it as the build does. The tests expecting the old rejections are dropped. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 2 ++ .../prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp | 3 +-- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 2 -- .../IntermediateRepresentation/IrMetadata/Resources.hpp | 6 ++++-- .../recompiler/Optimization/src/ResourceMaterializer.cpp | 3 ++- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 4cd4dbac5..98df34f00 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -503,6 +503,8 @@ void CheckBufferAliases(std::span shaders, const ColorTarg const ShaderRecompiler::ShaderBufferResource descriptor{{words[offset], words[offset + 1], words[offset + 2], words[offset + 3]}}; const auto address = descriptor.Base48(); const auto size = descriptor.GetSize(); + // A null V# binds nothing (ShaderResources::addGuestBuffer). + if (size == 0 || address == 0) continue; Require(!overlap(address, size, target.address, target.bytes), "shader buffer aliases the render target"); Require(!overlap(address, size, indexAddress, indexBytes), "writable shader buffer aliases the index buffer"); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 097b53ade..19355f2d2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2028,12 +2028,11 @@ std::size_t ShaderResources::addGuestBuffer(std::span words Require(descriptor.Type() == 0u, "buffer descriptor uses an unsupported type"); const auto address = descriptor.Base48(); const auto byteSize = descriptor.GetSize(); - if (byteSize == 0) { + if (byteSize == 0 || address == 0) { // A null V#: every access is out of bounds in the shader (BufferResource::empty). allocations.push_back({0, EmptyBufferBytes, false, nullptr, ShaderRecompiler::DescriptorRole::GuestBuffers, false}); return allocations.size() - 1; } - Require(address != 0, "null shader buffer descriptor address"); Require(byteSize <= context.limits.maxStorageBufferRange, "shader buffer exceeds descriptor range limit"); Require(byteSize <= std::numeric_limits::max(), "shader buffer size exceeds host address space"); const auto size = static_cast(byteSize); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index d636c3d97..979da2522 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -807,8 +807,6 @@ void resourceTests() { expectSingleFailure(changed([](auto& binding) { binding.role = Role::ShaderData; binding.count = 2; binding.guestDescriptor = {1, 2}; }), "must not be arrays"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::ShaderData; binding.guestDescriptor.clear(); }), "empty shader data descriptor"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::FlattenedSrt; binding.guestDescriptor.clear(); }), "empty shader data descriptor"); - expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[0] = 0; binding.guestDescriptor[1] = 0; }), "null shader buffer descriptor address"); - expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[2] = 0; }), "empty shader buffer descriptor"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x40000000u; }), "reserved bits"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[3] |= 0x40000000u; }), "unsupported type"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x3fffu << 16u; binding.guestDescriptor[2] = 0xffffffffu; }), "descriptor range limit"); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 1701a2927..98d92896f 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -25,8 +25,10 @@ struct BufferResource { bool atomic = false; bool formatted = false; bool scalar = false; - // Specialized: the V# has no records (a null V#), so every access is out of bounds: loads read - // zero (or the format's default), stores and atomics are dropped, as on hardware. + // Specialized: the V# has no records or is at address 0 (a null V#), so every access is out of + // bounds: loads read zero (or the format's default), stores and atomics are dropped. For no + // records that is the hardware's behavior; at address 0 an access would fault on hardware, so a + // working title never reaches such a slot. bool empty = false; bool operator==(const BufferResource& other) const = default; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 199bc18a2..a431d1f9a 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -507,7 +507,8 @@ void buildResourceSpecialization(const IrResourcePlan& plan, ResourceSnapshot& s entry.packedStride = packedStride; entry.descriptorFormat = buffer.formatted ? decoded.Format() : IrBufferFormat::Invalid; entry.descriptorSwizzle = buffer.formatted ? decoded.DstSelXYZW() : DstSel(4, 5, 6, 7); - entry.empty = decoded.GetSize() == 0u; + // A V# at address 0 is a slot the shader does not reach (an access faults on hardware). + entry.empty = decoded.GetSize() == 0u || decoded.Base48() == 0u; result.buffers.push_back(entry); } From 28b09d4da2f29dd7d91a9865703a3bdab1d964dc Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 13:59:21 +0300 Subject: [PATCH 19/40] feat(shader): GLC/DLC buffer loads and stores are coherent A buffer load with GLC/DLC bypasses the caches another workgroup's stores may be stale in; a radix sort polling a partition's published prefix with one read stale flags on the host, handed overlapping ranges to two partitions, and lost half its keys. A tree built from the duplicates held cycles, and the shaders walking it looped until the loop guard stopped them. As in the Kyty recompiler (GLC/DLC buffer cache policy support): the decoder reads DLC on MUBUF/MTBUF, a buffer load or store with GLC or DLC (atomics use GLC only to return data) is MemoryInfo::coherent, the guest storage buffers of a program with one are decorated Coherent, and its accesses are Volatile. Through the BDA table (GPU-selected V#s) the loads and stores are Volatile. Co-Authored-By: Claude Opus 5.5 --- .../IrMetadata/ResourceInfo.hpp | 3 +++ .../include/RdnaDecoder/RdnaInstruction.hpp | 1 + .../RdnaDecoder/src/RdnaMemoryOpDecoder.cpp | 2 ++ .../include/SpirvBackend/SpirvAnalysis.hpp | 2 ++ .../SpirvBackend/SpirvEmitterState.hpp | 2 ++ .../SpirvBackend/src/SpirvAnalysis.cpp | 2 ++ .../SpirvBackend/src/SpirvBdaRead.cpp | 26 ++++++++++++------- .../src/SpirvMemory/SpirvBufferAccess.cpp | 2 ++ .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 6 +++++ .../SpirvMemory/SpirvMemoryInstructions.cpp | 15 ++++++++--- .../MemoryInstructions/BufferInstructions.cpp | 6 +++-- 11 files changed, 52 insertions(+), 15 deletions(-) diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp index 2a10445db..5b4bed6d5 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp @@ -45,6 +45,9 @@ struct MemoryInfo { bool idxen = false; bool offen = false; bool planningOnly = false; + // A buffer load or store with GLC or DLC: it bypasses the caches another workgroup's stores + // may be stale in (a look-back polling a flag another workgroup publishes). + bool coherent = false; // A buffer access whose V# the program computes on the GPU (loaded at a runtime offset, say), // so no binding can hold it: the shader decodes the V# and goes through the BDA page table. bool gpuDescriptor = false; diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstruction.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstruction.hpp index bd8bfbb80..e27d5fb18 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstruction.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstruction.hpp @@ -148,6 +148,7 @@ struct RdnaInstruction { bool formatted = false; bool gds = false; bool glc = false; + bool dlc = false; bool slc = false; bool idxen = false; bool offen = false; diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp index 04d2f8ece..b643c7fbb 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp @@ -423,6 +423,7 @@ RdnaInstruction DecodeRdnaMubuf(std::uint32_t programCounter, std::span> 12u) & 1u) != 0u; instruction.idxen = ((word0 >> 13u) & 1u) != 0u; instruction.glc = ((word0 >> 14u) & 1u) != 0u; + instruction.dlc = ((word0 >> 15u) & 1u) != 0u; instruction.slc = ((word1 >> 22u) & 1u) != 0u; applyMemoryInfo(instruction, info); setRawWords(instruction, code, wordIndex, 2u); @@ -462,6 +463,7 @@ RdnaInstruction DecodeRdnaMtbuf(std::uint32_t programCounter, std::span> 12u) & 1u) != 0u; instruction.idxen = ((word0 >> 13u) & 1u) != 0u; instruction.glc = ((word0 >> 14u) & 1u) != 0u; + instruction.dlc = ((word0 >> 15u) & 1u) != 0u; instruction.slc = ((word1 >> 22u) & 1u) != 0u; applyMemoryInfo(instruction, info); setRawWords(instruction, code, wordIndex, 2u); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp index a8f27bdf0..53eefef93 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp @@ -18,6 +18,8 @@ struct SpirvRequirements { bool functionScratch = false; bool pixelValidMask = false; bool bufferInt64Atomics = false; + // A buffer load or store is coherent (MemoryInfo::coherent): the guest buffers are Coherent. + bool coherentBuffers = false; std::vector capabilities; std::vector extensions; }; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index cb4e5c2eb..8e555b598 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -67,6 +67,8 @@ struct MemoryResourceAccess { std::uint32_t indexOffset = 0; std::uint32_t byteOffset = 0; bool addIndexOffset = false; + // Memory operands of the loads and stores: Volatile for a coherent access (MemoryInfo::coherent). + std::uint32_t memoryAccess = 0; }; struct SpirvEmitterState { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index a34fb380b..4fd16c7db 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -31,6 +31,8 @@ SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program) { throw std::runtime_error("buffer operation has invalid memory metadata"); } const auto& memory = program.Resources().memoryInfo.at(memoryIndex); + // Through the BDA table a coherent access is a Volatile load or store (BdaAccessMask). + requirements.coherentBuffers = requirements.coherentBuffers || (memory.coherent && !memory.gpuDescriptor); if (memory.gpuDescriptor) { // The V#'s ADD_TID is known at run time only. if (program.Resources().stage != IrShaderStage::Compute) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index 9c0333992..18761547e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -16,7 +16,15 @@ void EmitBdaOverflowCheck(SpirvEmitterState& state, std::uint32_t address, std:: StopBdaInvocationIf(state, overflow); } -std::uint32_t EmitBdaByte(SpirvEmitterState& state, std::uint32_t guest, std::uint32_t instruction) { +// The memory operands of a BDA load or store: Volatile for a coherent access (MemoryInfo::coherent), +// which must see what other workgroups store while the dispatch runs. +std::uint32_t BdaAccessMask(const SpirvEmitterState& state, const IrValue& inst) { + const auto index = inst.Flags().index; + const auto& memory = state.program.Resources().memoryInfo; + return spv::MemoryAccessAlignedMask | (index < memory.size() && memory[index].coherent ? spv::MemoryAccessVolatileMask : 0u); +} + +std::uint32_t EmitBdaByte(SpirvEmitterState& state, std::uint32_t guest, std::uint32_t instruction, std::uint32_t accessMask) { const auto byteType = state.module.Type(spv::OpTypeInt, 8u, 0u); const auto bytePointer = TypePointer(state, spv::StorageClassPhysicalStorageBuffer, byteType); const auto physical = state.module.AllocateId(); @@ -26,7 +34,7 @@ std::uint32_t EmitBdaByte(SpirvEmitterState& state, std::uint32_t guest, std::ui const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpConvertUToPtr, bytePointer, pointer, physical); const auto loaded = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, byteType, loaded, pointer, spv::MemoryAccessAlignedMask, 1u); + state.module.AddFunction(spv::OpLoad, byteType, loaded, pointer, accessMask, 1u); return Unary(state, spv::OpUConvert, TypeU32(state), loaded); } // Unmapped bytes read as zero; the fault is recorded and every invocation reaches the @@ -41,7 +49,7 @@ std::uint32_t EmitBdaByte(SpirvEmitterState& state, std::uint32_t guest, std::ui const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpConvertUToPtr, bytePointer, pointer, physical); const auto loaded = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, byteType, loaded, pointer, spv::MemoryAccessAlignedMask, 1u); + state.module.AddFunction(spv::OpLoad, byteType, loaded, pointer, accessMask, 1u); const auto widened = Unary(state, spv::OpUConvert, TypeU32(state), loaded); state.module.AddFunction(spv::OpBranch, merge); EmitLabel(state, merge); @@ -50,11 +58,11 @@ std::uint32_t EmitBdaByte(SpirvEmitterState& state, std::uint32_t guest, std::ui return value; } -std::uint32_t EmitBdaBytes(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction) { +std::uint32_t EmitBdaBytes(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, std::uint32_t accessMask) { auto result = ConstantU32(state, 0u); for (std::uint32_t byte = 0; byte < bytes; ++byte) { const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), address, BdaConstant(state, byte)); - const auto value = EmitBdaByte(state, guest, instruction); + const auto value = EmitBdaByte(state, guest, instruction, accessMask); result = Binary(state, spv::OpBitwiseOr, TypeU32(state), result, Binary(state, spv::OpShiftLeftLogical, TypeU32(state), value, ConstantU32(state, byte * 8u))); } return result; @@ -130,7 +138,7 @@ std::uint32_t EmitBdaRead(SpirvValueEmitContext& ctx, const IrValue& inst, std:: if (bits == 32u) return EmitBdaDwordReads(ctx, inst, address, 0u, 1u)[0]; const auto instruction = ConstantU32(state, inst.Flags().pc); EmitBdaOverflowCheck(state, address, bits / 8u, instruction); - return EmitBdaBytes(state, address, bits / 8u, instruction); + return EmitBdaBytes(state, address, bits / 8u, instruction, BdaAccessMask(state, inst)); } void EmitBdaWrite(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t value) { @@ -146,7 +154,7 @@ void EmitBdaWrite(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t EmitIfCondition(state, Binary(state, spv::OpINotEqual, TypeBool(state), physical, BdaConstant(state, 0u)), [&] { const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer, physical); - state.module.AddFunction(spv::OpStore, pointer, value, spv::MemoryAccessAlignedMask, 4u); + state.module.AddFunction(spv::OpStore, pointer, value, BdaAccessMask(state, inst), 4u); state.module.AddFunction(spv::OpFunctionCall, state.module.Type(spv::OpTypeVoid), state.module.AllocateId(), state.bdaNoteWriteFunction, address); }); }); @@ -186,7 +194,7 @@ std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const if (!isUsed(dword)) continue; if (dword != first) addresses[dword] = dwordAddress(dword); EmitBdaOverflowCheck(state, addresses[dword], 4u, instruction); - into[dword] = EmitBdaBytes(state, addresses[dword], 4u, instruction); + into[dword] = EmitBdaBytes(state, addresses[dword], 4u, instruction, BdaAccessMask(state, inst)); } }; addresses[first] = dwordAddress(first); @@ -213,7 +221,7 @@ std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer, element); wideValues[dword] = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, TypeU32(state), wideValues[dword], pointer, spv::MemoryAccessAlignedMask, 4u); + state.module.AddFunction(spv::OpLoad, TypeU32(state), wideValues[dword], pointer, BdaAccessMask(state, inst), 4u); } state.module.AddFunction(spv::OpBranch, merge); EmitLabel(state, byteLabel); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index d97843e58..068400061 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -109,6 +109,8 @@ MemoryResourceAccess PrepareStorageBufferResourceAccess(SpirvEmitterState& state access.objectPointer = state.module.AllocateId(); state.module.AddFunction(spv::OpAccessChain, pointerType, access.objectPointer, variable, ConstantU32(state, arrayIndex)); access.byteOffset = state.memoryByteOffsets.at(arrayIndex); + // A GLC/DLC access polls or publishes what other workgroups see: never cached or combined. + access.memoryAccess = mem.coherent ? spv::MemoryAccessVolatileMask : 0u; access.length = state.module.AllocateId(); state.module.AddFunction(spv::OpArrayLength, TypeU32(state), access.length, access.objectPointer, 0u); // A null V# binds a placeholder the shader must not touch: nothing is in bounds. diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 926b7ec1a..39c27768a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -356,6 +356,12 @@ void DefineDescriptors(SpirvEmitterState& state) { state.module.AddAnnotation(spv::OpDecorate, state.storageBufferVariable, spv::DecorationAliased); state.module.AddAnnotation(spv::OpDecorate, state.storageBufferU64Variable, spv::DecorationAliased); } + if (state.requirements.coherentBuffers) { + // A coherent load polls what other workgroups store (RDNA stores reach L2 without + // GLC too): every alias of the buffers takes part in device-scope visibility. + state.module.AddAnnotation(spv::OpDecorate, state.storageBufferVariable, spv::DecorationCoherent); + if (state.storageBufferU64Variable != 0u) state.module.AddAnnotation(spv::OpDecorate, state.storageBufferU64Variable, spv::DecorationCoherent); + } break; case DescriptorBindingKind::BdaPagetable: state.bdaPagetableVariable = Define(StorageBufferBlockType(state), "bda_pagetable"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index 827ec4512..f539dcaf5 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -110,7 +110,8 @@ std::uint32_t LoadWordInBounds(SpirvValueEmitContext& ctx, const MemoryResourceA auto& state = ctx.state; const auto pointer = EmitMemoryElementPointer(state, resource, index); const auto value = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); + if (resource.memoryAccess != 0u) state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer, resource.memoryAccess); + else state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); return value; } @@ -374,7 +375,8 @@ std::uint32_t FormattedLoad(SpirvValueEmitContext& ctx, const IrValue& inst, con void StoreWordInBounds(SpirvValueEmitContext& ctx, const MemoryResourceAccess& resource, std::uint32_t index, std::uint32_t data) { auto& state = ctx.state; - state.module.AddFunction(spv::OpStore, EmitMemoryElementPointer(state, resource, index), data); + if (resource.memoryAccess != 0u) state.module.AddFunction(spv::OpStore, EmitMemoryElementPointer(state, resource, index), data, resource.memoryAccess); + else state.module.AddFunction(spv::OpStore, EmitMemoryElementPointer(state, resource, index), data); } void StoreSubwordInBounds(SpirvValueEmitContext& ctx, const MemoryInfo& mem, const MemoryResourceAccess& resource, std::uint32_t address, std::uint32_t index, std::uint32_t bits, std::uint32_t data) { @@ -392,8 +394,13 @@ void StoreSubwordInBounds(SpirvValueEmitContext& ctx, const MemoryInfo& mem, con }; if (mem.kind == ResourceKind::Scratch) { const auto old = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, TypeU32(state), old, pointer); - state.module.AddFunction(spv::OpStore, pointer, merge(old)); + if (resource.memoryAccess != 0u) { + state.module.AddFunction(spv::OpLoad, TypeU32(state), old, pointer, resource.memoryAccess); + state.module.AddFunction(spv::OpStore, pointer, merge(old), resource.memoryAccess); + } else { + state.module.AddFunction(spv::OpLoad, TypeU32(state), old, pointer); + state.module.AddFunction(spv::OpStore, pointer, merge(old)); + } } else { AtomicUpdate(state, pointer, mem.kind, merge); } diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/BufferInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/BufferInstructions.cpp index 92b69df70..fa9006ed7 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/BufferInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/BufferInstructions.cpp @@ -33,7 +33,8 @@ MemoryInfo bufferMemoryInfoFromInstruction(const RdnaInstruction& inst) { } bool TranslationContext::bufferLoad(const RdnaInstruction& inst) { - const MemoryInfo memory = bufferMemoryInfoFromInstruction(inst); + MemoryInfo memory = bufferMemoryInfoFromInstruction(inst); + memory.coherent = inst.glc || inst.dlc; IrOpcode opcode; switch (memory.dataBits) { case 8u: @@ -80,7 +81,8 @@ bool TranslationContext::bufferLoad(const RdnaInstruction& inst) { } bool TranslationContext::bufferStore(const RdnaInstruction& inst) { - const MemoryInfo memory = bufferMemoryInfoFromInstruction(inst); + MemoryInfo memory = bufferMemoryInfoFromInstruction(inst); + memory.coherent = inst.glc || inst.dlc; IrValue* resource = getBufferResource(memory); const BufferAddress address = readBufferAddress(inst); const IrU32 data = readU32(inst.destination); From 28bfe7747ad3535560a3fe7b37dacfd06f3b904d Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 13:59:30 +0300 Subject: [PATCH 20/40] fix(shader): accept SDWA source selectors on V_BFREV_B32 and V_FFBH_U32 Operand reads already extract SDWA source fields for every instruction, so bit reversal and find-first-bit-high only need the decoder rule, as for V_FFBL_B32. The rejection names the selector, the opcode and the pc. Co-Authored-By: Claude Opus 5.5 --- .../recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index cb3fb15f1..63161841b 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -1,4 +1,5 @@ #include "RdnaDecoder/RdnaVectorOpDecoder.hpp" +#include #include #include #include @@ -729,6 +730,8 @@ constexpr Vop1SdwaRule vop1SdwaRules[] = { {RdnaOpcode::VCvtF32Ubyte0, sdwaSelAll(), 0, 0, false}, {RdnaOpcode::VNotB32, sdwaSelAll(), 0, 0, false}, {RdnaOpcode::VFfblB32, sdwaSelAll(), 0, 0, false}, + {RdnaOpcode::VFfbhU32, sdwaSelAll(), 0, 0, false}, + {RdnaOpcode::VBfrevB32, sdwaSelAll(), 0, 0, false}, {RdnaOpcode::VCvtF32F16, sdwaSelWords() | sdwaSelFull(), 0, 0, true}, {RdnaOpcode::VCvtF16F32, sdwaSelAll(), sdwaSelWords(), sdwaSelAll(), true}, {RdnaOpcode::VCvtF16U16, sdwaSelWords() | sdwaSelFull(), sdwaSelWords(), sdwaSelWords() | sdwaSelFull(), false}, @@ -796,7 +799,9 @@ void validateVop1Sdwa(const RdnaInstruction& instruction, std::uint32_t destinat throw std::invalid_argument("VOP1 SDWA destination selector is not supported"); } if (!isVop1SdwaSourceSupported(instruction.op, sourceSelector, sourceNegate, sourceAbsolute)) { - throw std::invalid_argument("VOP1 SDWA source selector is not supported"); + char text[96]; + std::snprintf(text, sizeof(text), "VOP1 SDWA source selector %u of opcode 0x%x at pc 0x%x is not supported", sourceSelector, instruction.opcodeId, instruction.programCounter); + throw std::invalid_argument(text); } } From f98b28d0d764baa7d403227c785ac064c61d02e4 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 13:59:40 +0300 Subject: [PATCH 21/40] chore(agc-driver): debug aids for looping shaders - The loop guard's report names the shader (its code address when its plan was built, now the translator's shader hash), and LoopGuardTripped() tells whether any dispatch reported one. - APS5_WATCH_MEMORY=: (with APS5_SYNC_DISPATCH=1) keeps the last 64 contents of a range with the dispatch that left each, and writes them to watch__.bin when the loop guard first trips; the address matches any heap base above bit 36. - agc_shader_replay --mem prints the guest buffer V#s and which the shader writes. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/VulkanDevice.cpp | 50 +++++++++++++++++++ .../Graphics/include/BdaResources.hpp | 3 ++ .../Graphics/src/BdaResources.cpp | 12 ++++- .../libSceAgcDriver/tools/ShaderReplay.cpp | 10 ++++ core/shader/recompiler/Recompiler.cpp | 2 + .../SpirvBackend/src/SpirvFlowEmitter.cpp | 2 +- 6 files changed, 77 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index a8b6d56ca..8d61abf45 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2309,6 +2309,54 @@ bool TraceDispatchIo() { return traceIo; } +// Debug aid: APS5_WATCH_MEMORY=: (with APS5_SYNC_DISPATCH=1) keeps the last +// 64 contents of the range, each with the dispatch that left it, and writes them to +// watch__.bin when the loop guard first trips: the dispatch that corrupted a structure a +// looping shader walks is the one whose snapshot first shows the damage. +void WatchMemory(std::uint64_t programAddress) { + static const std::pair range = [] { + const char* text = std::getenv("APS5_WATCH_MEMORY"); + if (text == nullptr) return std::pair{0, 0}; + char* end = nullptr; + const auto address = std::strtoull(text, &end, 16); + const auto bytes = end != nullptr && *end == ':' ? std::strtoull(end + 1, nullptr, 16) : 0ull; + if (address == 0 || bytes == 0) throw std::runtime_error("APS5_WATCH_MEMORY takes :"); + if (!SyncEachDispatch()) throw std::runtime_error("APS5_WATCH_MEMORY needs APS5_SYNC_DISPATCH=1"); + return std::pair{address, bytes}; + }(); + if (range.second == 0) return; + static std::mutex mutex; + static std::deque>> history; + static bool written = false; + std::lock_guard lock(mutex); + if (written) return; + // The heap base differs between runs above bit 36: the first of those bases the range is + // mapped at, once it is. + static std::uint64_t address = 0; + if (address == 0) { + for (std::uint64_t high = 0; high < 16 && address == 0; ++high) { + const auto candidate = (range.first & 0xfffffffffull) | (high << 36u); + if (GuestMemory::Accessible(reinterpret_cast(candidate), static_cast(range.second))) address = candidate; + } + if (address == 0) return; + std::fprintf(stderr, "[watch] watching 0x%llx+0x%llx\n", static_cast(address), static_cast(range.second)); + } + const auto* bytes = reinterpret_cast(address); + if (!GuestMemory::Accessible(bytes, static_cast(range.second))) return; + if (history.empty() || std::memcmp(history.back().second.data(), bytes, static_cast(range.second)) != 0) { + history.emplace_back(programAddress, std::vector(bytes, bytes + range.second)); + if (history.size() > 64) history.pop_front(); + } + if (!Graphics::LoopGuardTripped()) return; + written = true; + for (std::size_t i = 0; i < history.size(); ++i) { + char name[64]; + std::snprintf(name, sizeof(name), "watch_%02zu_%llx.bin", i, static_cast(history[i].first)); + std::ofstream(name, std::ios::binary).write(reinterpret_cast(history[i].second.data()), static_cast(history[i].second.size())); + } + std::fprintf(stderr, "[watch] loop guard tripped: %zu contents of 0x%llx+0x%llx written (watch_*.bin, oldest first)\n", history.size(), static_cast(address), static_cast(range.second)); +} + // The [dispatch] phases for indirect dispatches alone, every 10 s on an [indirect] line: what the // 'indirect' GpuMutex hold ([lock] line) spends its time on inside the device call (the label // record before it is timed in the driver, [labels] line), with the longest hold seen and the @@ -3178,6 +3226,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec Graphics::CountLeaseOutcome(false, recorder.Submissions() + 1); } if (TraceDispatchIo()) std::fprintf(stderr, "[dispatch-io] %s:%s\n", groupsText, resources->Describe().c_str()); + WatchMemory(programAddress); // The recipe for the caller's dispatch-cache variant (design_cpu_final M4, rule R3): only an // object the resource cache serves under this content key (reusable: no lease, no copied // writes, every direct region import- or mirror-served), so a hit's proof is the template's @@ -3314,6 +3363,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu timer.phase(PhaseSync); } if (TraceDispatchIo()) std::fprintf(stderr, "[dispatch-io] %s:%s\n", groupsText, hit->resources->Describe().c_str()); + WatchMemory(programAddress); timer.finish(lookupsBefore, groupsText, indirect); if (profile) reportRecipes(); return RecipeOutcome::Recorded; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp index f3b4ec590..922dbd2d8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp @@ -5,6 +5,9 @@ namespace AgcDriver::Graphics { +// APS5_LOOP_GUARD: whether a dispatch has reported a loop that ran past the guard. +bool LoopGuardTripped(); + class BdaResources { public: explicit BdaResources(const Context& context); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index d89a8833b..954db6398 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include +#include #include #include #include @@ -226,6 +227,14 @@ VkDescriptorBufferInfo BdaResources::Fault() const { return {fault->Handle(), 0, ShaderRecompiler::BdaAbi::FaultBufferBytes}; } +namespace { +std::atomic loopGuardTripped{false}; +} + +bool LoopGuardTripped() { + return loopGuardTripped.load(std::memory_order_relaxed); +} + void BdaResources::CheckFault() const { markWrittenPages(); ShaderRecompiler::BdaAbi::Fault report{}; @@ -238,7 +247,8 @@ void BdaResources::CheckFault() const { Require(report.reason != ShaderRecompiler::BdaAbi::FaultReason::InvalidRectangle, "rect-list requires finite nondegenerate axis-aligned positions with equal positive W"); if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::LoopLimit) { // APS5_LOOP_GUARD: the shader left a loop that ran past the guard; the dispatch result is kept. - std::fprintf(stderr, "[gpu] loop guard: the loop exit at pc 0x%x ran past %u evaluations\n", report.instruction, report.bytes); + loopGuardTripped.store(true, std::memory_order_relaxed); + std::fprintf(stderr, "[gpu] loop guard: the loop exit at pc 0x%x of shader 0x%llx ran past %u evaluations\n", report.instruction, static_cast(report.address), report.bytes); std::memset(fault->Bytes().data(), 0, fault->Bytes().size()); return; } diff --git a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp index 2a7ce5eb8..80bc24880 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp @@ -78,6 +78,16 @@ bool Replay(const char* path) { try { const auto result = ShaderRecompiler::Recompile(request.request); std::printf(" recompiled: %zu SPIR-V words\n", result.spirv.size()); + if (g_memory) { + for (const auto& binding : result.bindings) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; + for (std::size_t i = 0; i + 4 <= binding.guestDescriptor.size(); i += 4) { + const auto* v = binding.guestDescriptor.data() + i; + const bool written = i / 4 >= binding.bufferWritten.size() || binding.bufferWritten[i / 4]; + std::printf(" buffer %zu: V# %08x %08x %08x %08x%s\n", i / 4, v[0], v[1], v[2], v[3], written ? " written" : ""); + } + } + } #if ANYPS5_ENABLE_SPIRV_TOOLS if (g_disassemble) { spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2); diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 584303d94..34fa4db37 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -97,6 +97,8 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request) { TranslateOptions translateOptions {}; translateOptions.stage = stageKind; + // For diagnostics: where the code ran when its plan was built (programs share plans by code). + translateOptions.shaderHash = request.shader.codeAddress; translateOptions.waveSize = request.context.waveSize; translateOptions.userDataBaseRegister = request.context.userDataBaseRegister; translateOptions.userDataCount = static_cast(request.context.userData.size()); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 215f616e5..418b359c2 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -78,7 +78,7 @@ void EmitReturnTerminator(SpirvValueEmitContext& ctx) { const auto pc = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeU32(state), pc, state.loopGuardPc); EmitIfCondition(state, Binary(state, spv::OpINotEqual, TypeBool(state), pc, ConstantU32(state, 0u)), [&] { - RecordBdaFault(state, BdaConstant(state, 0u), ConstantU32(state, state.loopGuardLimit), EmitBinaryU32(state, spv::OpISub, pc, ConstantU32(state, 1u)), BdaAbi::FaultReason::LoopLimit); + RecordBdaFault(state, BdaConstant(state, state.program.Resources().shaderHash), ConstantU32(state, state.loopGuardLimit), EmitBinaryU32(state, spv::OpISub, pc, ConstantU32(state, 1u)), BdaAbi::FaultReason::LoopLimit); }); } EmitKillIfPixelValidMaskInactive(state); From d76ecfac06a832fcad61a1df9ccc7fb21b129769 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 14:57:38 +0300 Subject: [PATCH 22/40] fix(shader): DPP row and bank masks gate the instruction's VGPR write The decoder puts an instruction's DPP fields on its source, and only a destination carrying them has writeRawU32 keep the lanes the masks leave out (and, without bound_ctrl, the lanes with no source lane). destinationOperand copied them over, but no write used it, so every row was written: a wave64 scan adding the other row's total into the odd rows only (row_mask:0xa) added it into every row, the prefix sums a key pass indexes primitives by came out wrong, and the tree built from the keys held cycles its walkers looped on. As in the Kyty recompiler (writes through DestinationOperand): TranslateInstruction gives the destination its source's DPP fields once, and a mac accumulator reads the destination without them (accumulatorOperand). agc_dpp_write_tests checks a masked DPP add writes through DppUpdateU32. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 7 +++- .../prx/libSceAgcDriver/tests/DppWrite.cpp | 40 +++++++++++++++++++ .../Translation/TranslationContext.hpp | 1 + .../Translation/src/DispatchInstructions.cpp | 7 +++- .../Translation/src/FloatInstructions.cpp | 4 +- .../src/TranslationContext/OperandAccess.cpp | 7 ++++ 6 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/DppWrite.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e8a37c6c6..9c712a10c 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -163,6 +163,11 @@ target_link_libraries(agc_structurizer_tests PRIVATE shader_recompiler_agc_drive set_target_properties(agc_structurizer_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_structurizer_tests) +add_executable(agc_dpp_write_tests EXCLUDE_FROM_ALL tests/DppWrite.cpp) +target_link_libraries(agc_dpp_write_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_dpp_write_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_dpp_write_tests) + add_executable(agc_driver_flip_tests EXCLUDE_FROM_ALL tests/Flip.cpp ) @@ -292,7 +297,7 @@ set_target_properties(agc_driver_bda_device_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_bda_device_tests) if(BUILD_TESTING) - foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_driver_flip_tests agc_driver_pm4_tests) + foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_dpp_write_tests agc_driver_flip_tests agc_driver_pm4_tests) string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}") set_target_properties(${agcTest} PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME ${agcTestName} COMMAND ${agcTest}) diff --git a/core/libs/prx/libSceAgcDriver/tests/DppWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/DppWrite.cpp new file mode 100644 index 000000000..7955b83f1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/DppWrite.cpp @@ -0,0 +1,40 @@ +// A DPP instruction's row and bank masks gate its VGPR write, not only its source read: the rows a +// mask leaves out keep their value (a scan adding the other row's total into the odd rows only). +#include "ControlFlow/GraphBuilder.hpp" +#include "ControlFlow/Structurizer.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include "Translation/InstructionTranslator.hpp" +#include "Translation/ShaderInputInfoBuilder.hpp" +#include +#include +#include + +using namespace ShaderRecompiler; + +int main() { + // v_add_nc_u32_dpp v1, v0, v1 quad_perm:[0,1,2,3] row_mask:0xa bank_mask:0xf; s_endpgm + const std::vector code{0x4a0202fau, 0xaf00e400u, 0xbf810000u}; + try { + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + auto cfg = GraphBuilder{}.Build(decoded); + Structurizer{}.Structurize(cfg); + GuestContext context{}; + context.waveSize = 32; + context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + TranslateOptions options{}; + options.stage = ShaderStageKind::Compute; + options.waveSize = 32; + options.userDataCount = 0; + options.inputInfo = BuildShaderStageInputInfo(ShaderStageKind::Compute, context, 32); + const auto program = InstructionTranslator{}.Translate(decoded, cfg, options); + for (const auto& block : program.Blocks()) { + for (const auto* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::DppUpdateU32 && inst->Flags().rowMask == 0xa) return 0; + } + } + std::fprintf(stderr, "the DPP add writes v1 without its row mask\n"); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + } + return 1; +} diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index eb9c775ac..b69cded08 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -30,6 +30,7 @@ private: const RdnaOperand& sourceAt(const RdnaInstruction& inst, std::uint32_t index); RdnaOperand destinationOperand(const RdnaInstruction& inst); + RdnaOperand accumulatorOperand(const RdnaInstruction& inst); RdnaOperand offsetOperand(const RdnaOperand& operand, std::uint32_t offset); RdnaOperand scalarDestinationOperand(const RdnaOperand& operand, std::uint32_t offset); RdnaOperand plainOperand(const RdnaOperand& operand); diff --git a/core/shader/recompiler/Translation/src/DispatchInstructions.cpp b/core/shader/recompiler/Translation/src/DispatchInstructions.cpp index 66a4ecbb0..f43a2bb10 100644 --- a/core/shader/recompiler/Translation/src/DispatchInstructions.cpp +++ b/core/shader/recompiler/Translation/src/DispatchInstructions.cpp @@ -12,7 +12,12 @@ void DispatchInstruction(IrBuilder& builder, const RdnaInstruction& instruction, throw std::runtime_error("DispatchInstruction not implemented"); } -void TranslationContext::TranslateInstruction(const RdnaInstruction& instruction) { +void TranslationContext::TranslateInstruction(const RdnaInstruction& decoded) { + // DPP masks the instruction's VGPR write as well as its source read (row and bank masks, lanes + // with no source lane without bound_ctrl keep their value): the destination takes the source's + // DPP fields for writeRawU32. + RdnaInstruction instruction = decoded; + instruction.destination = destinationOperand(decoded); currentOpcode = instruction.op; currentProgramCounter = instruction.programCounter; if (instruction.op == RdnaOpcode::Unknown || instruction.op == RdnaOpcode::Count) { diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index b3a9b4677..64ca4ff4e 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -100,7 +100,7 @@ bool TranslationContext::float16Binary(const RdnaInstruction& inst, IrOpcode opc bool TranslationContext::float16Ternary(const RdnaInstruction& inst, IrOpcode opcode, bool accumulator, bool mix) { std::array args{}; for (std::uint32_t index = 0u; index < args.size(); ++index) { - const RdnaOperand& operand = accumulator && index == 2u ? inst.destination : sourceAt(inst, index); + const RdnaOperand& operand = accumulator && index == 2u ? accumulatorOperand(inst) : sourceAt(inst, index); args[index] = mix ? &readMixF32(operand).Value() : &readF16AsF32(operand).Value(); } writeF16(inst.destination, IrF32(ir.Emit(opcode, IrType::F32, {args[0], args[1], args[2]}))); @@ -128,7 +128,7 @@ bool TranslationContext::floatBinary(const RdnaInstruction& inst, IrOpcode opcod bool TranslationContext::floatTernary(const RdnaInstruction& inst, IrOpcode opcode, bool accumulator, bool mix) { std::array args{}; for (std::uint32_t index = 0u; index < args.size(); ++index) { - const RdnaOperand& operand = accumulator && index == 2u ? inst.destination : sourceAt(inst, index); + const RdnaOperand& operand = accumulator && index == 2u ? accumulatorOperand(inst) : sourceAt(inst, index); const IrType type = IrOpcodeArgumentType(opcode, index); args[index] = type == IrType::F32 && mix ? &readMixF32(operand).Value() : readOperand(operand, type); } diff --git a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp index bd1d89b8c..1bda28419 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp @@ -36,6 +36,13 @@ RdnaOperand TranslationContext::destinationOperand(const RdnaInstruction& inst) return destination; } +// The destination read as a mac accumulator: its own value, which the instruction's DPP does not move. +RdnaOperand TranslationContext::accumulatorOperand(const RdnaInstruction& inst) { + RdnaOperand accumulator = inst.destination; + accumulator.dpp = false; + return accumulator; +} + RdnaOperand TranslationContext::offsetOperand(const RdnaOperand& operand, std::uint32_t offset) { if (offset == 0u) { return operand; From 4622efb4bb2ac5d46c739c3c1881e6b99c1ae7da Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 14:57:51 +0300 Subject: [PATCH 23/40] chore(agc-driver): APS5_WATCH_MEMORY watches several ranges APS5_WATCH_MEMORY takes : ranges separated by commas; a snapshot holds them back to back, in order, and is kept when any changed. Each range matches its own heap base above bit 36, and watching starts once all are mapped. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/VulkanDevice.cpp | 73 ++++++++++++------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 8d61abf45..efeb1845f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2309,42 +2309,63 @@ bool TraceDispatchIo() { return traceIo; } -// Debug aid: APS5_WATCH_MEMORY=: (with APS5_SYNC_DISPATCH=1) keeps the last -// 64 contents of the range, each with the dispatch that left it, and writes them to -// watch__.bin when the loop guard first trips: the dispatch that corrupted a structure a -// looping shader walks is the one whose snapshot first shows the damage. +// Debug aid: APS5_WATCH_MEMORY=:[,:...] (with +// APS5_SYNC_DISPATCH=1) keeps the last 64 contents of the ranges, each with the dispatch that left it, +// and writes them to watch__.bin (the ranges back to back, in order) when the loop guard +// first trips: the dispatch that corrupted a structure a looping shader walks is the one whose +// snapshot first shows the damage. void WatchMemory(std::uint64_t programAddress) { - static const std::pair range = [] { + struct Range { + std::uint64_t address = 0; + std::uint64_t bytes = 0; + }; + static const std::vector ranges = [] { + std::vector parsed; const char* text = std::getenv("APS5_WATCH_MEMORY"); - if (text == nullptr) return std::pair{0, 0}; - char* end = nullptr; - const auto address = std::strtoull(text, &end, 16); - const auto bytes = end != nullptr && *end == ':' ? std::strtoull(end + 1, nullptr, 16) : 0ull; - if (address == 0 || bytes == 0) throw std::runtime_error("APS5_WATCH_MEMORY takes :"); + if (text == nullptr) return parsed; + while (true) { + char* end = nullptr; + const auto address = std::strtoull(text, &end, 16); + const auto bytes = *end == ':' ? std::strtoull(end + 1, &end, 16) : 0ull; + if (address == 0 || bytes == 0 || (*end != ',' && *end != '\0')) throw std::runtime_error("APS5_WATCH_MEMORY takes :[,...]"); + parsed.push_back({address, bytes}); + if (*end == '\0') break; + text = end + 1; + } if (!SyncEachDispatch()) throw std::runtime_error("APS5_WATCH_MEMORY needs APS5_SYNC_DISPATCH=1"); - return std::pair{address, bytes}; + return parsed; }(); - if (range.second == 0) return; + if (ranges.empty()) return; static std::mutex mutex; static std::deque>> history; static bool written = false; std::lock_guard lock(mutex); if (written) return; - // The heap base differs between runs above bit 36: the first of those bases the range is - // mapped at, once it is. - static std::uint64_t address = 0; - if (address == 0) { - for (std::uint64_t high = 0; high < 16 && address == 0; ++high) { - const auto candidate = (range.first & 0xfffffffffull) | (high << 36u); - if (GuestMemory::Accessible(reinterpret_cast(candidate), static_cast(range.second))) address = candidate; + // The heap base differs between runs above bit 36: the first of those bases each range is + // mapped at, once all are. + static std::vector addresses; + if (addresses.empty()) { + std::vector found; + for (const auto& range : ranges) { + std::uint64_t address = 0; + for (std::uint64_t high = 0; high < 16 && address == 0; ++high) { + const auto candidate = (range.address & 0xfffffffffull) | (high << 36u); + if (GuestMemory::Accessible(reinterpret_cast(candidate), static_cast(range.bytes))) address = candidate; + } + if (address == 0) return; + found.push_back(address); } - if (address == 0) return; - std::fprintf(stderr, "[watch] watching 0x%llx+0x%llx\n", static_cast(address), static_cast(range.second)); + addresses = std::move(found); + for (std::size_t i = 0; i < ranges.size(); ++i) std::fprintf(stderr, "[watch] watching 0x%llx+0x%llx\n", static_cast(addresses[i]), static_cast(ranges[i].bytes)); } - const auto* bytes = reinterpret_cast(address); - if (!GuestMemory::Accessible(bytes, static_cast(range.second))) return; - if (history.empty() || std::memcmp(history.back().second.data(), bytes, static_cast(range.second)) != 0) { - history.emplace_back(programAddress, std::vector(bytes, bytes + range.second)); + std::vector contents; + for (std::size_t i = 0; i < ranges.size(); ++i) { + const auto* bytes = reinterpret_cast(addresses[i]); + if (!GuestMemory::Accessible(bytes, static_cast(ranges[i].bytes))) return; + contents.insert(contents.end(), bytes, bytes + ranges[i].bytes); + } + if (history.empty() || history.back().second != contents) { + history.emplace_back(programAddress, std::move(contents)); if (history.size() > 64) history.pop_front(); } if (!Graphics::LoopGuardTripped()) return; @@ -2354,7 +2375,7 @@ void WatchMemory(std::uint64_t programAddress) { std::snprintf(name, sizeof(name), "watch_%02zu_%llx.bin", i, static_cast(history[i].first)); std::ofstream(name, std::ios::binary).write(reinterpret_cast(history[i].second.data()), static_cast(history[i].second.size())); } - std::fprintf(stderr, "[watch] loop guard tripped: %zu contents of 0x%llx+0x%llx written (watch_*.bin, oldest first)\n", history.size(), static_cast(address), static_cast(range.second)); + std::fprintf(stderr, "[watch] loop guard tripped: %zu contents written (watch_*.bin, oldest first)\n", history.size()); } // The [dispatch] phases for indirect dispatches alone, every 10 s on an [indirect] line: what the From 74454ee9ca1c17d265884eb2ff162e74c921bb24 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 16:18:16 +0300 Subject: [PATCH 24/40] feat(shader): scalar buffer loads through a GPU-selected V# A traversal builds the V# of a bottom-level structure from a pointer it loads from an instance record, and reads it with s_buffer_load: no binding holds such a V#, and TakeGpuDescriptor took vector raw dword loads and stores only, so the resource analysis failed on the SGPR chain. A ReadConstBuffer through a V# the SRT walker cannot compute is a MemoryInfo::gpuDescriptor access too: the dword at the V#'s base plus the dword-aligned offset, read through the BDA page table, zero past STRIDE * NUM_RECORDS bytes (NUM_RECORDS for stride 0). Co-Authored-By: Claude Opus 5.5 --- .../Optimization/src/ResourceTracker.cpp | 11 +++++----- .../SpirvMemory/SpirvMemoryInstructions.cpp | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index a2d03dbce..88d9a6678 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -524,10 +524,10 @@ private: source = InternSource(descriptor); } - // A raw dword load or store (1-4 dwords) through a V# the SRT walker cannot compute: the program - // loads it at a GPU-computed offset (a descriptor heap) or builds it. No binding holds such a - // V#, so the shader decodes it and accesses memory through the BDA page table - // (MemoryInfo::gpuDescriptor). Other accesses keep failing in GetHandle. + // A raw dword load or store (1-4 dwords, or a scalar dword load) through a V# the SRT walker + // cannot compute: the program loads it at a GPU-computed offset (a descriptor heap) or builds + // it. No binding holds such a V#, so the shader decodes it and accesses memory through the BDA + // page table (MemoryInfo::gpuDescriptor). Other accesses keep failing in GetHandle. bool TakeGpuDescriptor(IrValue& inst, std::uint32_t memoryIndex) { const IrValue* handle = inst.Argument(0)->Resolve(); if (handle->Opcode() != IrOpcode::GetBufferResource || handle->ArgumentCount() != 4u) { @@ -540,7 +540,8 @@ private: return false; } const auto op = inst.Opcode(); - const bool load = op == IrOpcode::LoadBufferU32 || op == IrOpcode::LoadBufferU32x2 || op == IrOpcode::LoadBufferU32x3 || op == IrOpcode::LoadBufferU32x4; + // A scalar buffer load (s_buffer_load) reads one dword per ReadConstBuffer. + const bool load = op == IrOpcode::LoadBufferU32 || op == IrOpcode::LoadBufferU32x2 || op == IrOpcode::LoadBufferU32x3 || op == IrOpcode::LoadBufferU32x4 || op == IrOpcode::ReadConstBuffer; const bool store = op == IrOpcode::StoreBufferU32 || op == IrOpcode::StoreBufferU32x2 || op == IrOpcode::StoreBufferU32x3 || op == IrOpcode::StoreBufferU32x4; auto& memory = m_program.Resources().memoryInfo[memoryIndex]; if ((!load && !store) || memory.formatted || memory.typed || memory.dataBits != 32u) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index f539dcaf5..6383809f0 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -1039,6 +1039,26 @@ void EmitReadConstBuffer(SpirvValueEmitContext& ctx, const IrValue& inst) { } auto& state = ctx.state; const auto address = Binary(state, spv::OpIAdd, TypeU32(state), ctx.Arg(inst, 1), ConstantU32(state, mem.offset)); + if (mem.gpuDescriptor) { + // Through a GPU-selected V# (MemoryInfo::gpuDescriptor): the dword at the base plus the + // dword-aligned offset, zero past the buffer's STRIDE * NUM_RECORDS bytes (NUM_RECORDS for + // stride 0). + const IrValue* handle = inst.Argument(0)->Resolve(); + if (handle->Opcode() != IrOpcode::GetBufferResource || handle->ArgumentCount() != 4u) { + ctx.Fail(inst, "has no GPU-selected V#"); + } + const auto u32 = TypeU32(state); + const auto word1 = ctx.Arg(*handle, 1); + const auto records = ctx.Arg(*handle, 2); + const auto stride = EmitBitFieldUExtract(state, word1, ConstantU32(state, 16u), ConstantU32(state, 14u)); + const auto size = Select(state, u32, Binary(state, spv::OpIEqual, TypeBool(state), stride, ConstantU32(state, 0u)), records, Binary(state, spv::OpIMul, u32, stride, records)); + const auto byte = Binary(state, spv::OpBitwiseAnd, u32, address, ConstantU32(state, ~3u)); + const auto inBounds = Binary(state, spv::OpULessThan, TypeBool(state), byte, size); + const auto base = DeviceAddressFromWords(state, ctx.Arg(*handle, 0), EmitBitFieldUExtract(state, word1, ConstantU32(state, 0u), ConstantU32(state, 16u))); + const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), base, Unary(state, spv::OpUConvert, TypeScalarU64(state), byte)); + ctx.Define(inst, EmitValueOrZeroIfCondition(state, inBounds, [&] { return EmitBdaRead(ctx, inst, guest, 32u); })); + return; + } const auto rawIndex = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)); const auto access = PrepareMemoryResourceAccess(state, mem); const auto element = EmitMemoryElementIndex(state, access, rawIndex); From 9bb94163ea9e4d08f08bcce9c467801a1c8948a5 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 16:18:34 +0300 Subject: [PATCH 25/40] feat(shader): image_bvh_intersect_ray tests the ray against the BVH node Every ray query returned a miss (a stub). The node test is now the GFX10 (RT IP 1.1) one AMD's GPURT software fallback defines (IntersectCommon.hlsl) with the TriangleNode1_0 and BoxNode1_0 layouts, as SharpEmu ports it: the T# (base, size, box grow and sort, triangle return mode) and the node pointer are run-time values, triangle nodes return t over its denominator with the barycentric numerators or the triangle id, float16/float32 box nodes the hit children (clipped slabs, NaN misses, optionally sorted by entry time); an inactive lane, a null BVH, an out-of-range node or another type returns four invalid dwords. The decoder addresses 11 dwords (8 with A16, the directions as halves), the translation emits ImageBvhIntersectRay with the T#, MakeImageAddress and exec, and the program takes the BDA path: one probe covers the node and direct loads read it (a node the table does not map as one range records an Unmapped fault); EmitBdaDwordReads can read every dword for the per-byte fallback. Co-Authored-By: Claude Opus 5.5 --- .../IntermediateRepresentation/IrOpcode.hpp | 1 + .../src/IrOpcode.cpp | 1 + .../Optimization/src/ResourceTracker.cpp | 5 + .../RdnaDecoder/src/RdnaImageOpDecoder.cpp | 10 +- .../include/SpirvBackend/SpirvBda.hpp | 5 +- .../SpirvBackend/SpirvEmitterInstructions.hpp | 1 + .../SpirvBackend/src/SpirvBdaRead.cpp | 4 +- .../SpirvBackend/src/SpirvFlowEmitter.cpp | 1 + .../SpirvBackend/src/SpirvImageEmitter.cpp | 246 ++++++++++++++++++ .../MemoryInstructions/ImageInstructions.cpp | 18 +- 10 files changed, 282 insertions(+), 10 deletions(-) diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp index ce0db25cb..9aa0fa561 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp @@ -341,6 +341,7 @@ enum class IrOpcode : std::uint16_t { TtraceData, InstPrefetch, SelectU32, + ImageBvhIntersectRay, Count }; diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp index e7aa60504..2c44ed9cb 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp @@ -382,6 +382,7 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("TtraceData", Void), makeMeta("InstPrefetch", Void), makeMeta("SelectU32", U32, U1, U32, U32), + makeMeta("ImageBvhIntersectRay", U32x4, U32x4, ImageAddress, U1), }}; const OpcodeMeta& metaOf(IrOpcode opcode) { diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 88d9a6678..23a5a76f0 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -689,6 +689,11 @@ private: void Collect(IrValue& inst) { const auto op = inst.Opcode(); + if (op == IrOpcode::ImageBvhIntersectRay) { + // A ray query reads its node through the BDA page table (its T# is a run-time value). + m_info.usesDma = true; + return; + } const auto buffer = BufferAccessOf(op); const auto addressInfo = AddressOpcodeInfoOf(op); const auto imageInfo = ImageOpcodeInfoOf(op); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp index efa90d7b2..c9499b82e 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp @@ -262,7 +262,10 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span> 8u) & 255u; if (nsa != 0u && addressDwords > 1u + nsa * 4u) { @@ -311,7 +315,7 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords); +// nor fault-checked, as when every dword was its own instruction; `everyDword` reads them all (the +// dwords are not the instruction's result). +std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword = false); bool BdaByteReadsForced(); std::uint32_t AddBdaAddress(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, bool subtract); std::uint32_t AddBdaImmediate(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::int32_t immediate); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp index 0d73dbf0c..c7fd6db42 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp @@ -269,6 +269,7 @@ std::uint32_t EmitDataConsume(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageQueryDimensions(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageQueryLod(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageRead(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageBvhIntersectRay(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageWrite(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageSampleRaw(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageGatherRaw(SpirvValueEmitContext& ctx, const IrValue& inst); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index 18761547e..93957c152 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -167,13 +167,13 @@ void EmitBdaWrite(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t // unaligned mirror or a wrapping span falls back to the byte path. That path repeats, per extracted // dword in order, the address, overflow check and byte lookups of a separate dword load, so every // fault is recorded exactly as one; the probe records none, and a span it accepts cannot fault. -std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords) { +std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword) { auto& state = ctx.state; if (dwords == 0u || dwords > 4u) ctx.Fail(inst, "unsupported BDA read width"); if (state.bdaPointerFunction == 0) ctx.Fail(inst, "BDA lookup function is missing"); const auto u64 = TypeScalarU64(state); const auto instruction = ConstantU32(state, inst.Flags().pc); - const auto used = UsedBdaDwords(inst, dwords); + const auto used = everyDword ? (1u << dwords) - 1u : UsedBdaDwords(inst, dwords); const auto isUsed = [&](std::uint32_t dword) { return (used & (1u << dword)) != 0u; }; std::array values{}; std::uint32_t first = dwords; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 418b359c2..0be156253 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -489,6 +489,7 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::ImageQueryDimensions: return Invoke(EmitImageQueryDimensions, ctx, inst); case IrOpcode::ImageQueryLod: return Invoke(EmitImageQueryLod, ctx, inst); case IrOpcode::ImageRead: return Invoke(EmitImageRead, ctx, inst); + case IrOpcode::ImageBvhIntersectRay: return Invoke(EmitImageBvhIntersectRay, ctx, inst); case IrOpcode::ImageWrite: return Invoke(EmitImageWrite, ctx, inst); case IrOpcode::ImageSampleRaw: return Invoke(EmitImageSampleRaw, ctx, inst); case IrOpcode::ImageGatherRaw: return Invoke(EmitImageGatherRaw, ctx, inst); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index c83d6c0fb..3792dd278 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1,4 +1,5 @@ #include "SpirvBackend/SpirvImageEmitter.hpp" +#include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvBufferFormat.hpp" #include "SpirvBackend/SpirvEmitterInstructions.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" @@ -11,6 +12,7 @@ #include #include #include +#include #include namespace ShaderRecompiler { @@ -955,4 +957,248 @@ void EmitImageAtomicXor32(SpirvValueEmitContext& ctx, const IrValue& inst) { EmitImage(ctx, inst); } +// image_bvh_intersect_ray tests one ray against one BVH node; the guest shader owns the traversal +// and its stack. This is the GFX10 (RT IP 1.1) node test as AMD's GPURT software fallback defines it +// (IntersectCommon.hlsl), with the node layouts of GPURT's gfx10 TriangleNode1_0 and BoxNode1_0, as +// SharpEmu ports it. The node is read through the BDA page table; an inactive lane, a null BVH, an +// out-of-range node or another node type returns four invalid dwords. +void EmitImageBvhIntersectRay(SpirvValueEmitContext& ctx, const IrValue& inst) { + auto& state = ctx.state; + const auto u32 = TypeU32(state); + const auto u64 = TypeScalarU64(state); + const auto f32 = TypeF32(state); + const auto boolean = TypeBool(state); + const auto result4 = TypeU32Composite(state, 4u); + const auto invalid = ConstantU32(state, 0xffffffffu); + const auto uint = [&](std::uint32_t value) { return ConstantU32(state, value); }; + const auto real = [&](std::uint32_t bits) { return ConstantF32(state, bits); }; + const auto op = [&](spv::Op opcode, std::uint32_t type, std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, opcode, type, lhs, rhs); }; + const auto compose = [&](const std::array& values) { + const auto id = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, result4, id, values[0], values[1], values[2], values[3]); + return id; + }; + const auto asFloat = [&](std::uint32_t value) { return Unary(state, spv::OpBitcast, f32, value); }; + const auto asUint = [&](std::uint32_t value) { return Unary(state, spv::OpBitcast, u32, value); }; + const auto selectF = [&](std::uint32_t condition, std::uint32_t whenTrue, std::uint32_t whenFalse) { return Select(state, f32, condition, whenTrue, whenFalse); }; + const auto selectU = [&](std::uint32_t condition, std::uint32_t whenTrue, std::uint32_t whenFalse) { return Select(state, u32, condition, whenTrue, whenFalse); }; + const auto both = [&](std::uint32_t lhs, std::uint32_t rhs) { return op(spv::OpLogicalAnd, boolean, lhs, rhs); }; + const auto either = [&](std::uint32_t lhs, std::uint32_t rhs) { return op(spv::OpLogicalOr, boolean, lhs, rhs); }; + const auto isNan = [&](std::uint32_t value) { return Unary(state, spv::OpIsNan, boolean, value); }; + const auto widen = [&](std::uint32_t value) { return Unary(state, spv::OpUConvert, u64, value); }; + const auto pair = [&](std::uint32_t low, std::uint32_t high) { return op(spv::OpBitwiseOr, u64, widen(low), op(spv::OpShiftLeftLogical, u64, widen(high), BdaConstant(state, 32u))); }; + + const auto query = [&] { + const auto descriptor = ctx.Arg(inst, 0); + std::array word{}; + for (std::uint32_t i = 0; i < 4u; ++i) { + word[i] = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, word[i], descriptor, i); + } + const IrValue* address = ctx.ImageAddress(inst.Argument(1)); + const auto component = [&](std::uint32_t index) { return ctx.Arg(*address, index); }; + const bool a16 = (ctx.Memory(inst).imageSampleFlags & RdnaImageSampleFlagA16) != 0u; + + // The node pointer: its byte offset divided by eight above bits 2:0, which hold the type + // (0-1 triangle, 4 float16 box, 5 float32 box). The T#: base_address[39:0] in 256-byte + // units, box_grow_value[62:55], box_sort_en[63], size[105:64] (the last valid 64-byte + // node), triangle_return_mode[120]. + const auto node = component(0); + const auto type = op(spv::OpBitwiseAnd, u32, node, uint(7u)); + const auto nodeIndex = op(spv::OpShiftRightLogical, u64, widen(node), BdaConstant(state, 3u)); + const auto baseUnits = pair(word[0], op(spv::OpBitwiseAnd, u32, word[1], uint(0xffu))); + const auto lastNode = pair(word[2], op(spv::OpBitwiseAnd, u32, word[3], uint(0x3ffu))); + const auto boxGrow = op(spv::OpBitwiseAnd, u32, op(spv::OpShiftRightLogical, u32, word[1], uint(23u)), uint(0xffu)); + const auto boxSort = op(spv::OpINotEqual, boolean, op(spv::OpBitwiseAnd, u32, word[1], uint(0x80000000u)), uint(0u)); + const auto barycentrics = op(spv::OpINotEqual, boolean, op(spv::OpBitwiseAnd, u32, word[3], uint(1u << 24u)), uint(0u)); + const auto nodeAddress = op(spv::OpIAdd, u64, op(spv::OpShiftLeftLogical, u64, baseUnits, BdaConstant(state, 8u)), op(spv::OpShiftLeftLogical, u64, widen(op(spv::OpBitwiseAnd, u32, node, uint(~7u))), BdaConstant(state, 3u))); + const auto present = op(spv::OpINotEqual, boolean, baseUnits, BdaConstant(state, 0u)); + const auto valid = both(present, op(spv::OpULessThanEqual, boolean, nodeIndex, lastNode)); + const auto isTriangle = both(valid, op(spv::OpULessThanEqual, boolean, type, uint(1u))); + const auto isBox16 = both(valid, op(spv::OpIEqual, boolean, type, uint(4u))); + // A float32 box node spans two 64-byte units. + const auto isBox32 = both(both(present, op(spv::OpULessThan, boolean, nodeIndex, lastNode)), op(spv::OpIEqual, boolean, type, uint(5u))); + + const auto extent = asFloat(component(1)); + std::array origin{}; + std::array direction{}; + std::array inverse{}; + for (std::uint32_t axis = 0; axis < 3u; ++axis) { + origin[axis] = asFloat(component(2u + axis)); + if (!a16) { + direction[axis] = asFloat(component(5u + axis)); + inverse[axis] = asFloat(component(8u + axis)); + continue; + } + // Six halves in three dwords: the direction, then the inverse direction. + const auto half = [&](std::uint32_t index) { + const auto packed = component(5u + index / 2u); + return EmitF16BitsToF32(state, index % 2u == 0u ? op(spv::OpBitwiseAnd, u32, packed, uint(0xffffu)) : op(spv::OpShiftRightLogical, u32, packed, uint(16u))); + }; + direction[axis] = half(axis); + inverse[axis] = half(3u + axis); + } + + // The node's first `count` dwords (a multiple of four). A node lies in one range, so one + // probe of its span and direct loads read it; a node the table does not map as one aligned + // range records an Unmapped fault and reads as zeros. APS5_BDA_BYTE_READS=1 (no probe) + // reads it with the per-byte lookups. + const auto nodeDwords = [&](std::uint32_t count) { + std::vector dwords; + if (state.bdaProbeFunction == 0) { + for (std::uint32_t first = 0; first < count; first += 4u) { + const auto read = EmitBdaDwordReads(ctx, inst, nodeAddress, first * 4u, 4u, true); + dwords.insert(dwords.end(), read.begin(), read.end()); + } + return dwords; + } + const auto pc = uint(inst.Flags().pc); + const auto bytes = uint(count * 4u); + const auto physical = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, u64, physical, state.bdaProbeFunction, nodeAddress, bytes, pc); + const auto mapped = both(op(spv::OpINotEqual, boolean, physical, BdaConstant(state, 0u)), op(spv::OpIEqual, boolean, op(spv::OpBitwiseAnd, u64, physical, BdaConstant(state, 3u)), BdaConstant(state, 0u))); + EmitIfCondition(state, Unary(state, spv::OpLogicalNot, boolean, mapped), [&] { RecordBdaFault(state, nodeAddress, bytes, pc, BdaAbi::FaultReason::Unmapped); }); + for (std::uint32_t first = 0; first < count; first += 4u) { + const auto quad = EmitValueOrDefaultIfCondition(state, mapped, result4, ConstantU32CompositeZero(state, 4u), [&] { + std::array values{}; + for (std::uint32_t i = 0; i < 4u; ++i) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer, op(spv::OpIAdd, u64, physical, BdaConstant(state, (first + i) * 4u))); + values[i] = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, u32, values[i], pointer, spv::MemoryAccessAlignedMask, 4u); + } + return compose(values); + }); + for (std::uint32_t i = 0; i < 4u; ++i) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, value, quad, i); + dwords.push_back(value); + } + } + return dwords; + }; + const auto sub = [&](const std::array& a, const std::array& b) { + return std::array{op(spv::OpFSub, f32, a[0], b[0]), op(spv::OpFSub, f32, a[1], b[1]), op(spv::OpFSub, f32, a[2], b[2])}; + }; + const auto cross = [&](const std::array& a, const std::array& b) { + const auto term = [&](std::uint32_t i, std::uint32_t j) { return op(spv::OpFSub, f32, op(spv::OpFMul, f32, a[i], b[j]), op(spv::OpFMul, f32, a[j], b[i])); }; + return std::array{term(1, 2), term(2, 0), term(0, 1)}; + }; + const auto dot = [&](const std::array& a, const std::array& b) { + return op(spv::OpFAdd, f32, op(spv::OpFAdd, f32, op(spv::OpFMul, f32, a[0], b[0]), op(spv::OpFMul, f32, a[1], b[1])), op(spv::OpFMul, f32, a[2], b[2])); + }; + + // fast_intersect_triangle and SwizzleBarycentrics: type 0 tests (v0, v1, v2), type 1 tests + // (v1, v3, v2). A miss returns t_num = +inf over t_denom = 1. The barycentric return mode + // yields the i/j numerators the builder's rotation maps back; the other mode yields the + // triangle id and a hit flag. + const auto triangle = [&] { + const auto d = nodeDwords(16u); + const auto second = op(spv::OpIEqual, boolean, type, uint(1u)); + std::array v1{}; + std::array v2{}; + std::array v3{}; + for (std::uint32_t axis = 0; axis < 3u; ++axis) { + v1[axis] = selectF(second, asFloat(d[3u + axis]), asFloat(d[axis])); + v2[axis] = selectF(second, asFloat(d[9u + axis]), asFloat(d[3u + axis])); + v3[axis] = asFloat(d[6u + axis]); + } + const auto e1 = sub(v2, v1); + const auto e2 = sub(v3, v1); + const auto e3 = sub(origin, v1); + const auto s1 = cross(direction, e2); + const auto s2 = cross(e3, e1); + auto tNum = dot(e2, s2); + auto tDenom = dot(s1, e1); + const auto iNum = dot(e3, s1); + const auto jNum = dot(direction, s2); + const auto t = op(spv::OpFDiv, f32, tNum, tDenom); + const auto u = op(spv::OpFDiv, f32, iNum, tDenom); + const auto v = op(spv::OpFDiv, f32, jNum, tDenom); + const auto zero = real(0u); + const auto one = real(0x3f800000u); + const auto missed = either(either(either(op(spv::OpFOrdLessThan, boolean, u, zero), op(spv::OpFOrdGreaterThan, boolean, u, one)), either(op(spv::OpFOrdLessThan, boolean, v, zero), op(spv::OpFOrdGreaterThan, boolean, op(spv::OpFAdd, f32, u, v), one))), op(spv::OpFOrdLessThan, boolean, t, zero)); + tNum = selectF(missed, real(0x7f800000u), tNum); + tDenom = selectF(missed, one, tDenom); + const auto triangleId = d[15]; + const auto shift = op(spv::OpShiftLeftLogical, u32, type, uint(3u)); + const auto barycentric = [&](std::uint32_t sourceShift) { + const auto source = op(spv::OpBitwiseAnd, u32, op(spv::OpShiftRightLogical, u32, triangleId, op(spv::OpIAdd, u32, shift, uint(sourceShift))), uint(3u)); + const auto rest = op(spv::OpFSub, f32, op(spv::OpFSub, f32, tDenom, iNum), jNum); + return selectF(op(spv::OpIEqual, boolean, source, uint(1u)), iNum, selectF(op(spv::OpIEqual, boolean, source, uint(2u)), jNum, rest)); + }; + const auto hit = Unary(state, spv::OpLogicalNot, boolean, missed); + return compose({asUint(tNum), asUint(tDenom), selectU(barycentrics, asUint(barycentric(0u)), triangleId), selectU(barycentrics, asUint(barycentric(2u)), selectU(hit, uint(1u), uint(0u)))}); + }; + + // IntersectNodeBvh4 with fast_intersect_bbox: a slab test clipped to [0, extent], a NaN + // interval misses, box_grow_value widens the exit time by that many 2^-24 steps, and the + // optional sort orders the hit children by entry time. + const auto boxes = [&](bool fp16) { + const auto d = nodeDwords(fp16 ? 16u : 28u); + // Bound 0-2 is the minimum, 3-5 the maximum of one child box, from dword 4. + const auto bound = [&](std::uint32_t child, std::uint32_t index) { + const auto position = child * 6u + index; + if (!fp16) { + return asFloat(d[4u + position]); + } + const auto packed = d[4u + position / 2u]; + return EmitF16BitsToF32(state, position % 2u == 0u ? op(spv::OpBitwiseAnd, u32, packed, uint(0xffffu)) : op(spv::OpShiftRightLogical, u32, packed, uint(16u))); + }; + // HLSL max3/min3: a NaN operand propagates to the NaN check. + const auto nanMax = [&](std::uint32_t a, std::uint32_t b) { return selectF(either(isNan(a), op(spv::OpFOrdGreaterThan, boolean, a, b)), a, b); }; + const auto nanMin = [&](std::uint32_t a, std::uint32_t b) { return selectF(either(isNan(a), op(spv::OpFOrdLessThan, boolean, a, b)), a, b); }; + const auto zero = real(0u); + const auto growFactor = op(spv::OpFAdd, f32, real(0x3f800000u), op(spv::OpFMul, f32, Unary(state, spv::OpConvertUToF, f32, boxGrow), real(0x33800000u))); + std::array children{}; + std::array keys{}; + for (std::uint32_t child = 0; child < 4u; ++child) { + std::uint32_t enter = 0; + std::uint32_t leave = 0; + for (std::uint32_t axis = 0; axis < 3u; ++axis) { + const auto planeMin = op(spv::OpFMul, f32, op(spv::OpFSub, f32, bound(child, axis), origin[axis]), inverse[axis]); + const auto planeMax = op(spv::OpFMul, f32, op(spv::OpFSub, f32, bound(child, axis + 3u), origin[axis]), inverse[axis]); + const auto positive = op(spv::OpFOrdGreaterThanEqual, boolean, inverse[axis], zero); + const auto near = selectF(positive, planeMin, planeMax); + const auto far = selectF(positive, planeMax, planeMin); + enter = axis == 0u ? near : nanMax(enter, near); + leave = axis == 0u ? far : nanMin(leave, far); + } + const auto nan = either(isNan(enter), isNan(leave)); + const auto minT = selectF(nan, real(0x7f800000u), selectF(op(spv::OpFOrdGreaterThan, boolean, enter, zero), enter, zero)); + const auto maxT = selectF(nan, real(0xff800000u), selectF(op(spv::OpFOrdLessThan, boolean, leave, extent), leave, extent)); + const auto hit = op(spv::OpFOrdLessThanEqual, boolean, minT, op(spv::OpFMul, f32, maxT, growFactor)); + children[child] = selectU(hit, d[child], invalid); + keys[child] = minT; + } + // The hardware sorting network; an invalid child always sinks. + auto sorted = children; + for (const auto [a, b] : std::array, 5>{{{0, 2}, {1, 3}, {0, 1}, {2, 3}, {1, 2}}}) { + const auto bValid = op(spv::OpINotEqual, boolean, sorted[b], invalid); + const auto aInvalid = op(spv::OpIEqual, boolean, sorted[a], invalid); + const auto swap = either(both(bValid, op(spv::OpFOrdLessThan, boolean, keys[b], keys[a])), aInvalid); + const auto sortedA = selectU(swap, sorted[b], sorted[a]); + const auto sortedB = selectU(swap, sorted[a], sorted[b]); + const auto keyA = selectF(swap, keys[b], keys[a]); + const auto keyB = selectF(swap, keys[a], keys[b]); + sorted[a] = sortedA; + sorted[b] = sortedB; + keys[a] = keyA; + keys[b] = keyB; + } + for (std::uint32_t child = 0; child < 4u; ++child) { + children[child] = selectU(boxSort, sorted[child], children[child]); + } + return compose(children); + }; + + const auto none = compose({invalid, invalid, invalid, invalid}); + const auto tested = EmitValueOrDefaultIfCondition(state, isTriangle, result4, none, triangle); + const auto box16 = EmitValueOrDefaultIfCondition(state, isBox16, result4, tested, [&] { return boxes(true); }); + return EmitValueOrDefaultIfCondition(state, isBox32, result4, box16, [&] { return boxes(false); }); + }; + const auto inactive = compose({invalid, invalid, invalid, invalid}); + ctx.Define(inst, EmitValueOrDefaultIfCondition(state, ctx.Arg(inst, 2), result4, inactive, query)); +} + } diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp index 916875142..c24dc1ef6 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp @@ -41,9 +41,21 @@ MemoryInfo imageMemoryInfoFromInstruction(const RdnaInstruction& inst) { } bool TranslationContext::imageBvhIntersectRay(const RdnaInstruction& inst) { - IrValue& miss = ir.Constant(0xffffffffu); - for (std::uint32_t i = 0u; i < inst.dataDwordCount; ++i) { - writeOperand(offsetOperand(inst.destination, i), &miss); + // The node test only: the guest shader owns the traversal and its stack. The T# is read at run + // time and the node through the BDA page table, so the access is address-based (kind Global); + // imageSampleFlags carries A16 (the direction and inverse direction as halves). + if (inst.dataDwordCount != 4u || inst.imageD16) { + throw std::runtime_error("image_bvh_intersect_ray returns four dwords"); + } + MemoryInfo memory; + memory.kind = ResourceKind::Global; + memory.dataDwords = 4u; + memory.imageSampleFlags = inst.imageA16 ? RdnaImageSampleFlagA16 : 0u; + IrValue* descriptor = constructU32x4(inst.source1, 4u); + IrValue* address = makeImageAddress(inst, inst.source0); + IrValue& result = ir.Emit(IrOpcode::ImageBvhIntersectRay, IrOpcodeType(IrOpcode::ImageBvhIntersectRay), {descriptor, address, &ir.GetExec()}, addMemoryInfo(memory, inst.programCounter)); + for (std::uint32_t i = 0u; i < 4u; ++i) { + writeOperand(offsetOperand(inst.destination, i), &ir.CompositeExtract(result, i)); } return true; } From cd158c6b023b55e5c14e44b5833f89ba70995ca8 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 16:18:44 +0300 Subject: [PATCH 26/40] fix(shader): a BDA fault in a continue target does not stop the invocation SPIR-V wants the back-edge block to structurally post-dominate the continue target, but the continue target is the guest latch, and a BDA access among its instructions returned from the invocation on a fault: spirv-val rejected such a loop ("The continue construct ... is not structurally post dominated by the back-edge block"), which only builds without SPIRV-Tools accepted. While the continue target is emitted, a BDA fault is recorded without stopping the invocation, as in a program with wave LDS barriers. Co-Authored-By: Claude Opus 5.5 --- .../SpirvBackend/src/SpirvFlowEmitter.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 0be156253..2158fc5c9 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -112,6 +112,13 @@ bool IsLoopMerge(const IrProgram& program, std::uint32_t block) { return false; } +bool IsContinueTarget(const IrProgram& program, std::uint32_t block) { + for (const auto& info : program.Metadata().blockInfo) { + if (info.terminator.loopHeader && info.terminator.continueBlock == block) return true; + } + return false; +} + std::uint32_t EmitBranchCondition(SpirvValueEmitContext& ctx, const BlockInfo& info) { if (ctx.otherHalf == nullptr || info.terminator.condition == BranchCondition::ScalarInstruction || info.terminator.condition == BranchCondition::GotoVariable) { return ctx.Def(info.condition); @@ -630,9 +637,15 @@ void EmitControlFlow(SpirvValueEmitContext& context, StructuredFunctionState& fu if (info == nullptr) { context.Fail("structured control flow block has no terminator metadata"); } + // A continue construct must reach its back edge (the back-edge block post-dominates the + // continue target), so a BDA fault in the continue target's instructions does not stop the + // invocation; the fault is recorded all the same. + const bool stops = state.bdaStopsInvocations; + state.bdaStopsInvocations = stops && !IsContinueTarget(program, info->id); EmitStructuredBlock(context, functionState, block); functionState.blockExitLabels.emplace(block, state.currentLabel); EmitStructuredTerminator(context, program, *info); + state.bdaStopsInvocations = stops; } PatchStructuredPhis(context, functionState); } From 8dbff97486ce2315333d117b3bd4e60ffba5499b Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 16:18:45 +0300 Subject: [PATCH 27/40] chore(agc-driver): agc_shader_replay --spv writes the recompiled SPIR-V --spv saves .spv in the working directory for spirv-val; the usage line lists --asm and --mem too. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp index 80bc24880..1cdb7a93e 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp @@ -38,6 +38,7 @@ std::string FirstLine(const std::string& text) { bool g_disassemble = false; bool g_assembly = false; bool g_memory = false; +bool g_spirv = false; bool Replay(const char* path) { const auto request = ShaderRecompiler::RequestSerializer{}.Deserialize(ReadText(path)); @@ -78,6 +79,12 @@ bool Replay(const char* path) { try { const auto result = ShaderRecompiler::Recompile(request.request); std::printf(" recompiled: %zu SPIR-V words\n", result.spirv.size()); + if (g_spirv) { + // For spirv-val: .spv in the working directory. + std::string name(path); + name = name.substr(name.find_last_of("/\\") + 1) + ".spv"; + std::ofstream(name, std::ios::binary).write(reinterpret_cast(result.spirv.data()), static_cast(result.spirv.size() * sizeof(result.spirv[0]))); + } if (g_memory) { for (const auto& binding : result.bindings) { if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; @@ -108,7 +115,7 @@ bool Replay(const char* path) { int main(int argc, char** argv) { if (argc < 2) { - std::fprintf(stderr, "usage: agc_shader_replay [--dis] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set\n"); + std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--mem] [--spv] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set\n"); return 2; } int failures = 0; @@ -125,6 +132,10 @@ int main(int argc, char** argv) { g_memory = true; continue; } + if (std::string(argv[i]) == "--spv") { + g_spirv = true; + continue; + } try { if (!Replay(argv[i])) ++failures; } catch (const std::exception& error) { From ee1d34230d4b5666fee684246d2d86bcaaf22d1b Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 17:23:11 +0300 Subject: [PATCH 28/40] feat(kernel): sceKernelDebugWriteCppExceptionInfo prints the uncaught C++ exception A title libc from sce_module calls it from __cxa_throw/__cxa_rethrow when no handler is found, right before std::terminate, with the thrown object, 0x400 (meaning unknown), the demangled type name and what() (or null). The console keeps it for the core dump; there are no guest core dumps here, so it is printed to stderr. It was the only import such a libc needed beyond the existing libs. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/System/src/Coredump.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/prx/libkernel/System/src/Coredump.cpp b/core/libs/prx/libkernel/System/src/Coredump.cpp index 9c6cc3986..1daea6429 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -23,4 +24,13 @@ int APS5_VABI sceCoredumpUnregisterCoredumpHandler(void) { return 0; } +// A libc that fails to throw (no handler: the exception is uncaught) reports it here right before +// std::terminate, after its own stderr line; the console keeps it for the core dump, we print it. +// The second argument is 0x400 from the libc that calls it; its meaning is not known. +int APS5_VABI sceKernelDebugWriteCppExceptionInfo(const void* exception, uint64_t unknown, const char* typeName, const char* what) { + (void)unknown; + std::fprintf(stderr, "[coredump] uncaught C++ exception %p of type %s%s%s\n", exception, typeName ? typeName : "(unknown)", what ? ", what(): " : "", what ? what : ""); + return 0; +} + } From c8913314bc1abdcf5a9a37e866891e3550f942b0 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 20:12:28 +0300 Subject: [PATCH 29/40] test(agc-driver): the graphics and BDA device tests link and run to the end Both suites stopped at their first stale expectation, so nothing after it ran; the BDA device tests did not link. - the BDA device tests define the one recorder entry Resources.cpp reaches (no recorder) and fail loudly on the others - Buffer::Invalidate refuses memory that is not host mapped again (lost in 8a69fef); the device tests expect it for a device-local buffer - APS5_PIN_WAIT_MS (60000 by default, unchanged) bounds the wait before a mutation of a leased range is refused, and the refusal names it; the graphics tests set 200 ms, as they expect about a dozen refusals - the expectations follow the behaviour the code chose since: a buffer's four-DWORD V# is refused as a T#, a sampler exceeds the mock's zero sampler slots, a V# at unmapped memory binds its committed pages only, a rect-list fragment input without a vertex export reads as zero, detiled rows span the padded block width, mip ranges are checked in address order, block-compressed and 128/129/132 formats use render target tiling, an XOR swizzle is refused only over a base that is not 64 KiB aligned, DCC and partial mip chain views decode, a cube reads as a 2D array, the sampler fixture's max LOD fits the 4.8 field, a BDA program with barriers is accepted, a tessellation control stage is refused, a corrupt request fails on its signature and a newer one on its version - the DCC comment in the texture decoder no longer claims metadata compression is refused Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/GuestTextureResource.cpp | 2 +- .../Graphics/src/Resources.cpp | 1 + .../libSceAgcDriver/tests/BdaContracts.cpp | 12 +++- .../prx/libSceAgcDriver/tests/BdaDevice.cpp | 8 +++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 34 +++++++++-- .../tests/GuestSamplerResource.cpp | 5 +- .../tests/GuestTextureResource.cpp | 59 ++++++++++--------- .../libSceAgcDriver/tests/TextureTiling.cpp | 25 ++++++-- core/libs/prx/libc/src/GuestAllocations.cpp | 16 ++++- 9 files changed, 114 insertions(+), 48 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 3cd364eae..6db1798dc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -103,7 +103,7 @@ GuestTextureResource DecodeTextureResource(std::span words) Require(!prtDefColor, "guest texture descriptor uses a partially resident default color which is not implemented"); Require(arrayPitch == 0, "guest texture descriptor uses a nonzero array pitch which is not implemented"); Require(!msaaDepth, "guest texture descriptor uses MSAA which is not implemented"); - // DCC block sizes only apply with metadata compression, which is rejected above. + // DCC block sizes only shape compressed blocks, and surfaces are read as stored (below). static_cast(maxUncompBlkSize); static_cast(maxCompBlkSize); // Surfaces are always written uncompressed here (render targets and storage images bypass DCC), so diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp index f39491ce0..cbbd53b0c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp @@ -81,6 +81,7 @@ std::span Buffer::Bytes() { } void Buffer::Invalidate() { + Require(mapping != nullptr, "cannot invalidate an unmapped GPU buffer"); VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE}; range.memory = memory; range.size = VK_WHOLE_SIZE; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp index 14c6348d8..ee0209973 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp @@ -38,7 +38,13 @@ void RunBdaContractTests() { auto& block = program.CreateBlock(); block.AppendInstruction(&program.CreateValue(IrOpcode::Barrier, IrType::Void)); program.BlockOrder().push_back(&block); - reject([&] { ValidateBdaTarget(program, target); }, "workgroup barrier"); + // A program with workgroup barriers is accepted: its faulting reads return zero instead of + // ending the invocation, so every invocation still reaches the barriers. + ValidateBdaTarget(program, target); + AgcDriver::Graphics::Require(!BdaInvocationsMayStop(program), "a BDA program with barriers must keep its invocations running"); + program.Resources().stage = IrShaderStage::TessellationControl; + reject([&] { ValidateBdaTarget(program, target); }, "barrier-safe"); + program.Resources().stage = IrShaderStage::Compute; RecompileRequest request{}; request.target.bdaAbiVersion = BdaAbi::Version; request.target.supportedCapabilities = capabilities; @@ -50,5 +56,7 @@ void RunBdaContractTests() { AgcDriver::Graphics::Require(decoded.request.target.bdaAbiVersion == BdaAbi::Version && decoded.request.target.supportedCapabilities.size() == capabilities.size() && decoded.request.target.supportedExtensions[1] == extensions[1], "BDA request serialization changed target contract"); auto invalid = encoded; invalid[0] = invalid[0] == 'A' ? 'B' : 'A'; - reject([&] { static_cast(serializer.Deserialize(invalid)); }, "serialization version"); + reject([&] { static_cast(serializer.Deserialize(invalid)); }, "request signature"); + // The signature followed by serialization version 99, in base64. + reject([&] { static_cast(serializer.Deserialize("NVNQQWMAAAA=")); }, "serialization version"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp index 1a578bb94..bd5f7a609 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp @@ -2,12 +2,20 @@ #include "ColorTransferTests.hpp" #include #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include #include #include #include +// Resources.cpp submits and reaps through the active recorder; these tests record nothing. +namespace AgcDriver::Graphics { +Recorder* Recorder::Active() { return nullptr; } +void Recorder::Submit() { Require(false, "the device tests have no recorder"); } +bool Recorder::Reap() { Require(false, "the device tests have no recorder"); return false; } +} + namespace { using namespace AgcDriver::Graphics; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 979da2522..d8c22f68f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -666,6 +666,18 @@ void expectSingleFailure(const ShaderRecompiler::DescriptorBinding& binding, std expectResourceFailure(vertex, fragment, reason); } +// A binding the resources accept (building them throws nothing), leaving no Vulkan object behind. +void expectSingleAccepted(const ShaderRecompiler::DescriptorBinding& binding, std::string_view what) { + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + vertex.bindings.push_back(binding); + mock = MockVulkan{}; + const auto context = mockContext(); + const auto color = AgcDriver::Graphics::DecodeState(makeState()).color; + { AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, color, 0, 0); } + Require(mock.live == 0, std::string(what) + " leaked Vulkan objects"); +} + void pushConstantTests() { ShaderRecompiler::RecompileResult vertex; ShaderRecompiler::RecompileResult fragment; @@ -790,9 +802,11 @@ void resourceTests() { mutate(binding); return binding; }; - expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestImages; binding.kind = Kind::SampledImage; }), "sampled and storage image resources are not implemented"); - expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestImages; binding.kind = Kind::StorageImage; }), "sampled and storage image resources are not implemented"); - expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestSamplers; binding.kind = Kind::Sampler; }), "sampled and storage image resources are not implemented"); + // Image and sampler bindings are implemented: a buffer's four-DWORD V# is no T#, and the mock + // context has no sampler slots, so a sampler binding exceeds its per-stage limit. + expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestImages; binding.kind = Kind::SampledImage; }), "guest texture descriptor must contain 8 dwords"); + expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestImages; binding.kind = Kind::StorageImage; }), "guest storage image descriptors must contain 8 dwords"); + expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestSamplers; binding.kind = Kind::Sampler; }), "shader sampler descriptors exceed per-stage limits"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::Gds; binding.guestDescriptor.clear(); }), "unsupported descriptor role Gds"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::BdaPagetable; binding.guestDescriptor.clear(); }), "BDA table and fault descriptors"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::FaultBuffer; binding.guestDescriptor.clear(); }), "BDA table and fault descriptors"); @@ -811,9 +825,11 @@ void resourceTests() { expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[3] |= 0x40000000u; }), "unsupported type"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x3fffu << 16u; binding.guestDescriptor[2] = 0xffffffffu; }), "descriptor range limit"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[2] = 8192; }), "descriptor range limit"); - expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(0x1000), 8); }), "not readable"); + // A V# left pointing at unmapped memory binds its committed pages only (none here); the rest reads + // as zeros (GuestBufferMemory::addDescriptorRegion), so it is not rejected. + expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(0x1000), 8); }), "an unmapped V#"); expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(state.color.address), 64); }), "aliases the render target"); - expectSingleFailure(changed([](auto& binding) { binding.count = 3; binding.guestDescriptor = join(join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)), vsharp(reinterpret_cast(0x1000), 8)); }), "not readable"); + expectSingleAccepted(changed([](auto& binding) { binding.count = 3; binding.guestDescriptor = join(join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)), vsharp(reinterpret_cast(0x1000), 8)); }), "an unmapped V# element"); expectSingleFailure(changed([&](auto& binding) { binding.count = 2; binding.guestDescriptor = join(vsharp(guestFirst.data(), 16), vsharp(reinterpret_cast(state.color.address), 64)); }), "aliases the render target"); expectSingleFailure(changed([](auto& binding) { binding.count = 17; binding.guestDescriptor.assign(68, 0); }), "per-stage limits"); { @@ -1035,7 +1051,10 @@ void rectListTests() { expectFailure([&] { static_cast(BuildRectListShaders(vertex, fragment, target)); }, "per-vertex interpolation"); fragment.fragmentParameters[0].perVertex = false; vertex.parameterExports.clear(); - expectFailure([&] { static_cast(BuildRectListShaders(vertex, fragment, target)); }, "no vertex export"); + // A fragment input the vertex shader does not export is built as a missing parameter (see + // RectListShaders.cpp), not rejected. + auto unexported = BuildRectListShaders(vertex, fragment, target); + Require(!unexported.control.spirv.empty() && !unexported.evaluation.spirv.empty(), "rect-list shaders with an unexported parameter are empty"); fragment.fragmentParameters.clear(); target.tessellation->maxPatchSize = 3; expectFailure([&] { static_cast(BuildRectListShaders(vertex, fragment, target)); }, "device limits"); @@ -1215,6 +1234,9 @@ void validationTests() { } int main() { + // The guest allocation tests expect mutations of leased ranges to be refused; they need not wait + // the default minute for each refusal. Set before the registry first reads it. + _putenv_s("APS5_PIN_WAIT_MS", "200"); try { { const AgcDriver::Graphics::Context context{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp index fe60064b0..0e706600b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp @@ -24,7 +24,8 @@ struct Fields { std::uint32_t filterMode = 0; bool disableDegamma = false; std::uint32_t minLodRaw = 0; - std::uint32_t maxLodRaw = 0x3c00; + // MAX_LOD is 12 bits of unsigned 4.8 fixed point: 0xc00 is LOD 12. + std::uint32_t maxLodRaw = 0xc00; std::uint32_t perfMip = 0; std::uint32_t perfZ = 0; std::uint32_t lodBiasRaw = 0; @@ -82,7 +83,7 @@ void RunGuestSamplerResourceTests() { Require(result.mipmapMode == VK_SAMPLER_MIPMAP_MODE_LINEAR, "linear mip filter must decode to linear mipmap mode"); Require(result.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, "clamp mode 2 must decode to clamp-to-edge"); Require(!result.anisotropyEnable && result.maxAnisotropy == 1.0f, "non-anisotropic filter must leave anisotropy disabled"); - Require(nearlyEqual(result.maxLod, static_cast(0x3c00) / 256.0f), "max LOD decoded incorrectly"); + Require(nearlyEqual(result.maxLod, 12.0f), "max LOD decoded incorrectly"); Require(result.borderColor == VK_BORDER_COLOR_INT_TRANSPARENT_BLACK, "border color type 0 must decode to transparent black"); Fields nearest = base; diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index eedc6e271..be1fe3d42 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -114,7 +114,12 @@ void RunGuestTextureResourceTests() { tileModes.tileModeRaw = 0x09; Require(DecodeTextureResource(pack(tileModes)).tileMode == TextureTileMode::kStandard64KB, "tile mode 0x09 must decode to standard 64KB"); tileModes.tileModeRaw = 0x1b; + // An XOR swizzle folds a pipe/bank XOR into the low base bits; only 64 KiB aligned bases are + // modeled, so the fixture's unaligned base is refused and an aligned one decodes. + rejectFields(tileModes, "pipe/bank XOR base"); + tileModes.base40 = 0x120000ull; Require(DecodeTextureResource(pack(tileModes)).tileMode == TextureTileMode::RenderTarget64KB, "tile mode 0x1b must decode to render target 64KB"); + tileModes.base40 = base.base40; tileModes.tileModeRaw = 0x02; rejectFields(tileModes, "unsupported tile mode"); @@ -215,31 +220,25 @@ void RunGuestTextureResourceTests() { badMsaa.msaaDepth = true; rejectFields(badMsaa, "MSAA"); - Fields badBlockSize = base; - badBlockSize.maxUncompBlkSize = 1; - rejectFields(badBlockSize, "DCC block size overrides"); - badBlockSize = base; - badBlockSize.maxCompBlkSize = 1; - rejectFields(badBlockSize, "DCC block size overrides"); + // DCC block size overrides only matter to DCC metadata, which is not read; they decode. + Fields blockSize = base; + blockSize.maxUncompBlkSize = 1; + blockSize.maxCompBlkSize = 1; + Require(DecodeTextureResource(pack(blockSize)).baseAddress == DecodeTextureResource(pack(base)).baseAddress, "DCC block size overrides changed texture storage"); - Fields badMeta = base; - badMeta.metaPipeAligned = true; - rejectFields(badMeta, "metadata compression"); - badMeta = base; - badMeta.writeCompress = true; - rejectFields(badMeta, "metadata compression"); - badMeta = base; - badMeta.metaCompress = true; - rejectFields(badMeta, "metadata compression"); - badMeta = base; - badMeta.dccAlphaPos = true; - rejectFields(badMeta, "metadata compression"); - badMeta = base; - badMeta.dccColorTransf = true; - rejectFields(badMeta, "metadata compression"); - badMeta = base; - badMeta.metaAddr = 1; - rejectFields(badMeta, "metadata compression"); + // DCC metadata decodes: its address only with metadata compression on, and where alpha sits; the + // pipe alignment, write compression and color transform flags do not change what a read returns. + Fields meta = base; + meta.metaPipeAligned = true; + meta.writeCompress = true; + meta.dccColorTransf = true; + meta.metaAddr = 1; + const auto uncompressed = DecodeTextureResource(pack(meta)); + Require(uncompressed.dccAddress == 0 && !uncompressed.dccAlphaOnMsb, "DCC metadata without compression was decoded"); + meta.metaCompress = true; + meta.dccAlphaPos = true; + const auto compressed = DecodeTextureResource(pack(meta)); + Require(compressed.dccAddress == 0x100 && compressed.dccAlphaOnMsb, "DCC metadata was decoded wrongly"); Fields badSwizzle = base; badSwizzle.bcSwizzle = 1; @@ -251,10 +250,12 @@ void RunGuestTextureResourceTests() { badLevels.maxMip = 1; rejectFields(badLevels, "base mip level past its last mip level"); - Fields badMaxMip = base; - badMaxMip.lastLevel = 1; - badMaxMip.maxMip = 2; - rejectFields(badMaxMip, "must expose every mip level"); + // A view may stop before the surface's last level: the surface keeps its mip count. + Fields partialMips = base; + partialMips.lastLevel = 1; + partialMips.maxMip = 2; + const auto partial = DecodeTextureResource(pack(partialMips)); + Require(partial.lastLevel == 1 && partial.mipCount == 3, "a view over part of the mip chain decoded wrongly"); std::array shortWords{}; reject([&] { DecodeTextureResource(shortWords); }, "8 dwords"); @@ -264,7 +265,7 @@ void RunGuestTextureResourceTests() { Require(MatchesGuestDimension(Shape::Image2D, TextureDimension::k2D), "2D shape must match 2D dimension"); Require(!MatchesGuestDimension(Shape::Image2D, TextureDimension::k2DArray), "2D shape must not match 2D array dimension"); Require(MatchesGuestDimension(Shape::Image2DArray, TextureDimension::k2DArray), "2D array shape must match 2D array dimension"); - Require(!MatchesGuestDimension(Shape::Image2DArray, TextureDimension::kCube), "2D array shape must not match cube dimension"); + Require(MatchesGuestDimension(Shape::Image2DArray, TextureDimension::kCube), "a cube must be readable as a 2D array of its faces"); Require(MatchesGuestDimension(Shape::ImageCube, TextureDimension::kCube), "cube shape must match cube dimension"); Require(!MatchesGuestDimension(Shape::ImageCube, TextureDimension::k1D), "cube shape must not match 1D dimension"); Require(!MatchesGuestDimension(Shape::Image3D, TextureDimension::k1D), "3D shape must never match a guest dimension"); diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp index 43f4061e7..8afa74332 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp @@ -1,8 +1,11 @@ #include "GraphicsTests.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include #include #include #include +#include +#include namespace { @@ -85,27 +88,37 @@ void RunTextureTilingTests() { { const auto mips = ComputeMipLayout(TextureTileMode::RenderTarget64KB, 56, 257, 129, 1); Require(mips[0].blocksPerRow == 3 && mips[0].tiledSize == 393216, "render target surfaces must pad to complete 128 by 128 blocks for 32-bit pixels"); - Require(mips[0].pitchBytes == 1028 && mips[0].linearSize == 132612, "detiled render target rows must use the actual texture width"); + // The detiler writes linear rows at the padded pitch (three 128-texel blocks), so the linear + // region spans whole padded rows. + Require(mips[0].pitchBytes == 1536 && mips[0].linearSize == 1536u * 129u, "detiled render target rows must span the padded block width"); Require(ComputeSurfaceSize(mips, 6) == 2359296, "render target cube faces must retain the padded guest slice stride"); } for (const auto format : std::array{1, 7, 56, 71, 77}) { const auto mips = ComputeMipLayout(TextureTileMode::RenderTarget64KB, format, 1024, 513, 11); - std::uint64_t linearEnd = 0; + // Linear offsets follow the tiled ones (the smallest regular level first), with the tail levels + // after the chain, so the ranges are checked for overlap in address order, not level order. + std::vector> ranges; bool tailSeen = false; for (const auto& mip : mips) { - Require(mip.linearOffset >= linearEnd && mip.linearOffset % 4 == 0, "detiled mip levels must occupy separate word-aligned ranges"); + Require(mip.linearOffset % 4 == 0, "detiled mip levels must start word-aligned"); Require(mip.linearSize >= static_cast(mip.pitchBytes) * mip.height, "detiled mip allocation must contain every row"); Require(mip.linearSize % 4 == 0, "detiled mip sizes must preserve word alignment between array layers"); - linearEnd = mip.linearOffset + mip.linearSize; + ranges.emplace_back(mip.linearOffset, mip.linearOffset + mip.linearSize); if (mip.tail) { tailSeen = true; Require(mip.tiledOffset == 0 && mip.tiledSize == 65536, "render target mip tails must share one guest 64KB block"); } } + std::sort(ranges.begin(), ranges.end()); + for (std::size_t index = 1; index < ranges.size(); ++index) Require(ranges[index].first >= ranges[index - 1].second, "detiled mip levels must occupy separate ranges"); Require(tailSeen && !mips.front().tail, "render target mip chains must cover both regular blocks and mip tails"); } - reject([] { ComputeMipLayout(TextureTileMode::RenderTarget64KB, 169, 64, 64, 1); }, "block compressed formats"); - reject([] { ComputeMipLayout(TextureTileMode::RenderTarget64KB, 132, 64, 64, 1); }, "does not support render target tiling"); + // Block-compressed formats are laid out in render target tiling too: a 64 by 64 texel level is one + // guest 64KB block. + const auto compressed = ComputeMipLayout(TextureTileMode::RenderTarget64KB, 169, 64, 64, 1); + Require(compressed.size() == 1 && compressed[0].tiledSize == 65536 && compressed[0].linearSize != 0, "block compressed render target layout is wrong"); + // Formats 128, 129 and 132 are laid out in render target tiling as well. + Require(ComputeMipLayout(TextureTileMode::RenderTarget64KB, 132, 64, 64, 1).size() == 1, "format 132 render target layout is missing"); reject([] { ComputeMipLayout(TextureTileMode::RenderTarget64KB, 74, 64, 64, 1); }, "unsupported bytes per element"); reject([] { ComputeMipLayout(TextureTileMode::kLinear, 1, 0, 4, 1); }, "zero-sized texture"); diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index f3d87781e..30a3896c4 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include #include #include @@ -35,6 +37,16 @@ std::atomic generation{1}; std::atomic invalidator{nullptr}; std::atomic pinWaiter{nullptr}; +// How long a mutation of a leased range waits for the lease before it is refused: +// APS5_PIN_WAIT_MS, 60 s by default (tests that expect the refusal set it short). +std::chrono::milliseconds pinWait() { + static const std::chrono::milliseconds value{[] { + const char* text = std::getenv("APS5_PIN_WAIT_MS"); + return text != nullptr ? std::strtoull(text, nullptr, 10) : 60000ull; + }()}; + return value; +} + void require(bool condition, const char* reason) { if (!condition) throw std::runtime_error(reason); } @@ -202,7 +214,7 @@ void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* poi // driver thread inside a long Vulkan call (importing host memory takes a lease for its whole // length), so every wait is bounded by the deadline only. auto* state = static_cast(mutation); - const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60); + const auto deadline = std::chrono::steady_clock::now() + pinWait(); int syncedRounds = 0; for (;;) { bool pinned = false; @@ -217,7 +229,7 @@ void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* poi } } if (!pinned) return; - if (std::chrono::steady_clock::now() >= deadline) PinnedFailure(address, bytes, "waited 60 s for the lease"); + if (std::chrono::steady_clock::now() >= deadline) PinnedFailure(address, bytes, ("waited " + std::to_string(pinWait().count()) + " ms for the lease (APS5_PIN_WAIT_MS)").c_str()); const auto waiter = pinWaiter.load(std::memory_order_acquire); bool progressed = false; if (waiter != nullptr && state != nullptr && state->lock.owns_lock()) { From 119560206d6c874d65dfdfd0f46bcc537aca3ebb Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 20:12:28 +0300 Subject: [PATCH 30/40] feat(agc-driver): heap ranges the host imports did not take are mirrored An address-based build maps every readable registered range. A heap range past APS5_HOST_IMPORT_MIB (or refused by the driver) was copied by every such build: about 3200 ranges, 670 MiB and 500 ms per build on the story-mode load, which then stalled. Such ranges are now served by persistent host-cached mirrors, as the main image is, and the load reaches its next failure. - a heap mirror keeps no shadow; it keeps a write-tracking generation per 64 KiB block and a build copies only the blocks stamped since. The generation is stored by the copy, so a build that throws before it leaves no block marked current with stale bytes - the build gathers its heap mirrors, sorts them and collects each run of adjacent ranges once (a collect costs about 4 us whatever its size; ~3000 mirrors took 16 ms per build, now about 3 ms) - writable ranges are mirrored too: each build keeps the bytes its merged written ranges held at upload and write-back stores only what the GPU changed from them, so a CPU store made meanwhile stays; recorded work writing a range completes before a refresh reads it - the mirrors are limited by APS5_HEAP_MIRROR_MIB (24 GiB by default); a mirror past it, or one whose buffer the driver refuses, stops the process. Heap mirrors are not given to descriptor sub-ranges, image mirror serials or reusable direct regions - past APS5_ADDRESS_COPY_MAX_MIB (64 MiB by default) of committed bytes still copied, a build stops the process, naming the largest ranges and why each is copied (unreadable pages, an image mirror the driver refused, a range outside the write-watched arena, mirrors disabled) - a direct memory page mapped at several addresses is read-only in every view while its range stays writable: copies, descriptor regions and mirrors take every readable page, write-back stores the writable ones and fails on a GPU change inside a read-only page instead of dropping it - a store through the BDA table into a mirrored or copied range faults as before (only writable ranges imported in place are writable there); the fault now names the table range and that rule - the BDA resource tests run with a storage range limit that holds the fault buffer and cover a mirror's fill, reuse, per-block refill, an interrupted refresh, a descriptor store beside a concurrent CPU store, and the copy limit Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/GuestBufferMemory.hpp | 29 ++ .../Graphics/src/BdaResources.cpp | 15 + .../Graphics/src/GuestBufferMemory.cpp | 297 ++++++++++++++++-- .../libSceAgcDriver/tests/BdaResources.cpp | 218 +++++++++++++ .../prx/libSceAgcDriver/tests/BdaTests.hpp | 2 + .../prx/libSceAgcDriver/tests/Graphics.cpp | 8 + 6 files changed, 537 insertions(+), 32 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 5677b2a1b..00cbae6da 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -6,6 +6,7 @@ #include "prx/libc/include/GuestAllocations.hpp" #include #include +#include #include #include @@ -123,6 +124,29 @@ struct AddressSpaceStats { }; AddressSpaceStats AddressSpaceCounters(); +// The image and heap mirrors (see GuestBufferMemory.cpp): heap mirrors held and their bytes, then +// cumulative: mirrors made, 64 KiB blocks copied into them after their fill, heap mirrors read again. +struct MirrorStats { + std::uint64_t heapMirrors = 0; + std::uint64_t heapBytes = 0; + std::uint64_t rebuilds = 0; + std::uint64_t blocksCopied = 0; + std::uint64_t heapRefills = 0; +}; +MirrorStats MirrorCounters(); + +// A registered range an address-based build copies instead of serving in place: its committed bytes +// and why no import or mirror serves it. +struct AddressCopy { + std::uint64_t begin; + std::uint64_t end; + std::uint64_t committed; + const char* reason; +}; +// The FATAL message for copies whose committed bytes exceed `limit` (APS5_ADDRESS_COPY_MAX_MIB), +// naming the largest ones; empty within the limit. +std::string AddressCopyOverflow(std::vector copies, std::uint64_t limit); + class GuestBufferMemory { public: explicit GuestBufferMemory(const Context& context); @@ -301,6 +325,9 @@ private: // Records the import-to-buffer copies of the given gpuCopy regions into the open batch, with // the barriers that order them after earlier recorded writes and before the shaders reading them. void recordGpuCopies(std::span copies, bool addressable); + // Keeps the bytes of every merged written range a writable heap mirror serves, as uploaded: the + // mirror keeps no shadow, and write-back stores only what the GPU changed from them. + void takeHeapReferences(); Context context; bool stagingAllowed = false; GuestAllocations::Lease lease; @@ -315,6 +342,8 @@ private: // regions follow the registry's order), so AddSnapshot can search instead of scanning. bool regionsSorted = false; std::vector> writes; + // By the begin of the merged written range (see takeHeapReferences). + std::vector>> heapReferences; // UploadPrepare ran (regions are frozen); `uploaded` once UploadFinish ran. bool prepared = false; bool uploaded = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 954db6398..7268aea59 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -254,6 +254,21 @@ void BdaResources::CheckFault() const { } std::ostringstream message; message << "BDA access failed: address=0x" << std::hex << report.address << " instruction=0x" << report.instruction << std::dec << " bytes=" << report.bytes << " stage=" << report.stage << " reason=" << static_cast(report.reason); + if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::Permission && table != nullptr) { + // Only a writable range imported in place is writable through the table (see + // GuestBufferMemory::addressRange): a store into a range served by a mirror or a copy would + // not reach guest memory, so it faults. Name the range and how it is served. + const auto bytes = table->Bytes(); + ShaderRecompiler::BdaAbi::Header header{}; + std::memcpy(&header, bytes.data(), sizeof(header)); + for (std::uint32_t index = 0; index < header.count; ++index) { + ShaderRecompiler::BdaAbi::Range range{}; + std::memcpy(&range, bytes.data() + sizeof(header) + index * sizeof(range), sizeof(range)); + if (report.address < range.begin || report.address >= range.end) continue; + message << std::hex << "; the store hit 0x" << range.begin << "+0x" << range.end - range.begin << ", read-only in the BDA table: stores through GPU-selected descriptors reach only writable ranges imported in place, not ones served by a mirror or a copy (past APS5_HOST_IMPORT_MIB, or refused by the driver)"; + break; + } + } throw std::runtime_error(message.str()); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index ca290d06f..e7f6beb9a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -36,10 +36,20 @@ namespace AgcDriver::Graphics { // mprotect, which replaces the registered Range object, and a mirror whose Range is gone is rebuilt. // Writable ranges keep a shadow of the guest bytes and copy the 64 KiB blocks that differ on each use. // A mirror never holds the Range shared_ptr: a pinned range makes the guest's mprotect spin and fail. +// Heap mirrors serve the readable registered heap ranges the host imports did not take (past +// APS5_HOST_IMPORT_MIB, or refused by the driver), which each address-based build copied instead. +// They keep no shadow: the arena's write tracking says which 64 KiB blocks changed since the mirror +// took them, and those blocks are read again. A writable one is written back per build from the +// bytes its written ranges held at upload (takeHeapReferences); stores through the BDA table fault +// there, as in a copy. struct ImageMirror { std::uint64_t base = 0; std::uint64_t bytes = 0; bool writable = false; + bool heap = false; + // Heap mirrors only: per tracker block of the range (block 0 holds `base`), the generation + // collected before the block was last read into the buffer. + std::vector generations; std::weak_ptr range; std::shared_ptr buffer; // Writable ranges only: the guest bytes the mirror holds, compared on refresh and at write-back. @@ -306,8 +316,14 @@ struct ImageMirrors { VkDevice device = VK_NULL_HANDLE; std::map> entries; std::set failed; + // Bytes of the heap mirrors in `entries` (against APS5_HEAP_MIRROR_MIB). + std::uint64_t heapBytes = 0; // APS5_PROFILE_DRAW counters, reported every 10 s as [buffers] image mirrors. std::uint64_t builds = 0; + // Heap mirrors read again because their range changed, and heap ranges left to copies because + // the arena does not track their writes. + std::uint64_t heapRefills = 0; + std::uint64_t heapUnwatched = 0; std::uint64_t subranges = 0; std::uint64_t rebuilds = 0; std::uint64_t refreshes = 0; @@ -361,6 +377,21 @@ bool mirrorsEnabled() { return !disabled; } +// The heap mirrors' limit: past it the process stops rather than copying gigabytes per dispatch. +std::uint64_t heapMirrorBudget() { + static const std::uint64_t bytes = [] { + const char* value = std::getenv("APS5_HEAP_MIRROR_MIB"); + return (value ? std::strtoull(value, nullptr, 10) : 24576ull) << 20u; + }(); + return bytes; +} + +[[noreturn]] void heapMirrorFatal(const GuestAllocations::Range& range, std::uint64_t held, const char* reason) { + std::fprintf(stderr, "FATAL: heap mirror of 0x%llx+0x%llx: %s; heap mirrors hold %llu MiB of %llu MiB (APS5_HEAP_MIRROR_MIB)\n", static_cast(range.address), static_cast(range.bytes), reason, static_cast(held >> 20u), static_cast(heapMirrorBudget() >> 20u)); + std::fflush(stderr); + std::abort(); +} + bool sameRange(const std::weak_ptr& mirrored, const std::shared_ptr& range) { return !mirrored.owner_before(range) && !range.owner_before(mirrored); } @@ -370,13 +401,22 @@ struct RefreshBlock { ImageMirror* mirror; std::uint64_t address; std::size_t length; + // Heap mirrors: the generation the block's copy makes current, stored only once the copy ran, + // so a build that throws before its copies leaves the block stale in the tracker's eyes too. + std::uint64_t generation = 0; }; // Compares a block's guest bytes with the mirror's shadow and copies a changed block into the device -// buffer and the shadow. Blocks are distinct, so blocks of one refresh can be compared in parallel. +// buffer and the shadow (a heap mirror's block is known to have changed and is copied). Blocks are +// distinct, so blocks of one refresh can be compared in parallel. bool compareBlock(const RefreshBlock& block) { const auto offset = static_cast(block.address - block.mirror->base); const auto* guest = reinterpret_cast(block.address); + if (block.mirror->heap) { + std::memcpy(block.mirror->buffer->Bytes().data() + offset, guest, block.length); + block.mirror->generations[static_cast(block.address / 65536 - block.mirror->base / 65536)] = block.generation; + return true; + } if (std::memcmp(block.mirror->shadow.data() + offset, guest, block.length) == 0) return false; std::memcpy(block.mirror->buffer->Bytes().data() + offset, guest, block.length); std::memcpy(block.mirror->shadow.data() + offset, guest, block.length); @@ -480,9 +520,10 @@ private: // completes first, so its results reach the guest and the shadow before the compare and no batch // writes a block being replaced; GPU reads of the mirror by earlier recorded work (an address-based // dispatch whose lease is released at completion) see the newer bytes, as they would on hardware. -// Returns whether there is anything to compare. -bool prepareRefresh(ImageMirror& mirror, std::uint64_t address, std::uint64_t bytes) { - if (!mirror.writable || bytes == 0) return false; +// prepareRange does it for [address, address + bytes), prepareRefresh for a writable image mirror and +// returns whether there is anything to compare. Heap mirrors are prepared a run of adjacent ranges at +// a time, before their write tracking is asked what changed (refreshHeapMirrors). +void prepareRange(std::uint64_t address, std::uint64_t bytes) { // The compare reads the pages directly; a mirror is only reused while its Range exists, but a // lease of a build in flight can keep a replaced Range alive past a protection change. Require(GuestMemory::Accessible(reinterpret_cast(address), static_cast(bytes)), "image mirror range became inaccessible"); @@ -498,6 +539,11 @@ bool prepareRefresh(ImageMirror& mirror, std::uint64_t address, std::uint64_t by recorder->Sync(); } ++state.refreshes; +} + +bool prepareRefresh(ImageMirror& mirror, std::uint64_t address, std::uint64_t bytes) { + if ((!mirror.writable && !mirror.heap) || bytes == 0) return false; + prepareRange(address, bytes); return true; } @@ -528,11 +574,49 @@ void refreshMirror(ImageMirror& mirror, std::uint64_t address, std::uint64_t byt compareBlocks(blocks); } +// Appends to `blocks` the blocks of one build's heap mirrors that a CPU or driver store changed +// since the mirrors took them. The mirrors of adjacent ranges are prepared and collected as one run: a +// collect walks the run's page tables at a fixed cost of microseconds, which paid per mirror (thousands +// on a story-mode load) was most of an address-based build. The copies follow and store the run's +// collected generation (see RefreshBlock). +void refreshHeapMirrors(std::vector& mirrors, std::vector& blocks) { + constexpr std::uint64_t block = 65536; + std::sort(mirrors.begin(), mirrors.end(), [](const ImageMirror* left, const ImageMirror* right) { return left->base < right->base; }); + thread_local std::vector changed; + for (std::size_t first = 0; first < mirrors.size();) { + auto last = first + 1; + while (last < mirrors.size() && mirrors[last]->base == mirrors[last - 1]->base + mirrors[last - 1]->bytes) ++last; + const auto begin = mirrors[first]->base; + const auto bytes = mirrors[last - 1]->base + mirrors[last - 1]->bytes - begin; + prepareRange(begin, bytes); + const auto generation = GuestMemory::CollectWrites(begin, static_cast(bytes)); + Require(generation != 0, "a heap mirror's range is no longer write-watched"); + for (auto index = first; index < last; ++index) { + auto& mirror = *mirrors[index]; + changed.assign(mirror.generations.size(), 0); + GuestMemory::ChangedBlocks(mirror.base, static_cast(mirror.bytes), mirror.generations, changed); + const auto aligned = mirror.base / block * block; + const auto end = mirror.base + mirror.bytes; + bool refilled = false; + for (std::size_t at = 0; at < changed.size(); ++at) { + if (changed[at] == 0) continue; + const auto from = std::max(mirror.base, aligned + at * block); + blocks.push_back({&mirror, from, static_cast(std::min(end, aligned + (at + 1) * block) - from), generation}); + refilled = true; + } + if (refilled) ++Mirrors().heapRefills; + } + first = last; + } +} + // The mirror for a leased image range: the existing one, or a new one when none exists or the // registered Range object changed (a protection change). Null when the range cannot be mirrored. An // existing writable mirror's blocks are appended to `blocks` for the caller's one compare of every -// mirror of the build (prepareRefresh ran); a new mirror is filled here. -std::shared_ptr acquireMirror(const Context& context, const std::shared_ptr& range, std::vector& blocks) { +// mirror of the build (prepareRefresh ran); the caller refreshes an existing heap mirror with the +// build's others (refreshHeapMirrors); a new mirror is filled here. A heap mirror past APS5_HEAP_MIRROR_MIB, or whose buffer the driver +// refuses, stops the process. +std::shared_ptr acquireMirror(const Context& context, const std::shared_ptr& range, std::vector& blocks, bool heap) { auto& state = Mirrors(); std::shared_ptr mirror; { @@ -540,26 +624,47 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh if (state.device != context.device) { state.entries.clear(); state.failed.clear(); + state.heapBytes = 0; state.device = context.device; } if (state.failed.contains(range->address)) return nullptr; const auto found = state.entries.find(range->address); - if (found != state.entries.end() && sameRange(found->second->range, range) && found->second->bytes == range->bytes && found->second->writable == range->writable) mirror = found->second; + if (found != state.entries.end() && sameRange(found->second->range, range) && found->second->bytes == range->bytes && found->second->writable == range->writable && found->second->heap == heap) mirror = found->second; } if (mirror != nullptr) { - if (prepareRefresh(*mirror, range->address, range->bytes)) appendBlocks(blocks, *mirror, range->address, range->bytes); + if (!heap && prepareRefresh(*mirror, range->address, range->bytes)) appendBlocks(blocks, *mirror, range->address, range->bytes); return mirror; } - // The registered flags were taken from the pages; a range that disagrees is copied by this build. - if (!GuestMemory::Accessible(reinterpret_cast(range->address), range->bytes, range->writable)) return nullptr; + // The registered flags were taken from the pages; an image range that disagrees is copied by this + // build. A heap mirror needs readable pages only: a writable range may hold read-only pages (a + // direct memory page mapped at several addresses is read-only in every view), whose bytes the GPU + // reads like the others and whose write-back storeChanged refuses. + if (!GuestMemory::Accessible(reinterpret_cast(range->address), range->bytes, range->writable && !heap)) return nullptr; mirror = std::make_shared(); mirror->base = range->address; mirror->bytes = range->bytes; mirror->writable = range->writable; + mirror->heap = heap; mirror->range = range; + if (heap) { + // Pending GPU results reach the range before the collect, so the fill below is current. + prepareRefresh(*mirror, range->address, range->bytes); + const auto generation = GuestMemory::CollectWrites(range->address, range->bytes); + constexpr std::uint64_t block = 65536; + mirror->generations.assign(static_cast(((range->address + range->bytes + block - 1) / block) - range->address / block), generation); + std::lock_guard lock(state.mutex); + if (generation == 0) { + ++state.heapUnwatched; + return nullptr; + } + std::uint64_t held = state.heapBytes; + if (const auto found = state.entries.find(range->address); found != state.entries.end() && found->second->heap) held -= found->second->bytes; + if (held + range->bytes > heapMirrorBudget()) heapMirrorFatal(*range, held, "past the budget"); + } try { mirror->buffer = std::make_shared(context, range->bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); } catch (const std::runtime_error& error) { + if (heap) heapMirrorFatal(*range, state.heapBytes, error.what()); std::fprintf(stderr, "[gpu] image mirror of 0x%llx+0x%llx failed: %s; falling back to copies\n", static_cast(range->address), static_cast(range->bytes), error.what()); std::lock_guard lock(state.mutex); state.failed.insert(range->address); @@ -571,17 +676,20 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::MirrorRefresh); GuestMemory::Read(range->address, mirror->buffer->Bytes()); } - if (range->writable) mirror->shadow.assign(mirror->buffer->Bytes().begin(), mirror->buffer->Bytes().end()); + if (range->writable && !heap) mirror->shadow.assign(mirror->buffer->Bytes().begin(), mirror->buffer->Bytes().end()); std::lock_guard lock(state.mutex); ++state.rebuilds; mirror->serial = (1ull << 63u) | ++state.serials; // A replaced mirror lives on in the regions of recorded work that bound it. + if (const auto found = state.entries.find(range->address); found != state.entries.end() && found->second->heap) state.heapBytes -= found->second->bytes; + if (heap) state.heapBytes += mirror->bytes; state.entries[range->address] = mirror; return mirror; } -// The mirror containing [begin, end) whose registered range still exists, or null. A mirror whose -// Range was replaced is only rebuilt by the next address-based build; until then the range is copied. +// The image mirror containing [begin, end) whose registered range still exists, or null. A mirror +// whose Range was replaced is only rebuilt by the next address-based build; until then the range is +// copied. Heap mirrors are not given out: only an address-based build refreshes them. std::shared_ptr findMirror(const Context& context, std::uint64_t begin, std::uint64_t end) { auto& state = Mirrors(); std::lock_guard lock(state.mutex); @@ -590,7 +698,7 @@ std::shared_ptr findMirror(const Context& context, std::uint64_t be if (found == state.entries.begin()) return nullptr; --found; const auto& mirror = found->second; - if (begin < mirror->base || end > mirror->base + mirror->bytes || mirror->range.expired()) return nullptr; + if (mirror->heap || begin < mirror->base || end > mirror->base + mirror->bytes || mirror->range.expired()) return nullptr; return mirror; } @@ -598,7 +706,14 @@ std::shared_ptr findMirror(const Context& context, std::uint64_t be void sweepMirrors() { auto& state = Mirrors(); std::lock_guard lock(state.mutex); - for (auto it = state.entries.begin(); it != state.entries.end();) it = it->second->range.expired() ? state.entries.erase(it) : std::next(it); + for (auto it = state.entries.begin(); it != state.entries.end();) { + if (!it->second->range.expired()) { + ++it; + continue; + } + if (it->second->heap) state.heapBytes -= it->second->bytes; + it = state.entries.erase(it); + } } void reportMirrors() { @@ -610,16 +725,20 @@ void reportMirrors() { state.lastReport = now; std::size_t count = 0; std::size_t writable = 0; + std::size_t heaps = 0; std::uint64_t bytes = 0; + std::uint64_t heapBytes = 0; { std::lock_guard lock(state.mutex); for (const auto& [base, mirror] : state.entries) { ++count; bytes += mirror->bytes; if (mirror->writable) ++writable; + if (mirror->heap) ++heaps; } + heapBytes = state.heapBytes; } - std::fprintf(stderr, "[buffers] image mirrors: %zu ranges (%.1f MiB, %zu writable), %llu address-based builds served, %llu descriptor sub-ranges bound, %llu rebuilds, %llu refreshes: %llu blocks compared, %llu copied, %llu refresh syncs\n", count, bytes / 1048576.0, writable, static_cast(state.builds), static_cast(state.subranges), static_cast(state.rebuilds), static_cast(state.refreshes), static_cast(state.blocksCompared), static_cast(state.blocksCopied), static_cast(state.syncs)); + std::fprintf(stderr, "[buffers] image mirrors: %zu ranges (%.1f MiB, %zu writable, %zu heap %.1f MiB), %llu address-based builds served, %llu descriptor sub-ranges bound, %llu rebuilds, %llu refreshes: %llu blocks compared, %llu copied, %llu refresh syncs; heap refills %llu, unwatched heaps %llu\n", count, bytes / 1048576.0, writable, heaps, heapBytes / 1048576.0, static_cast(state.builds), static_cast(state.subranges), static_cast(state.rebuilds), static_cast(state.refreshes), static_cast(state.blocksCompared), static_cast(state.blocksCopied), static_cast(state.syncs), static_cast(state.heapRefills), static_cast(state.heapUnwatched)); } // Deferred lease release. The lease an address-based build takes (AcquireRegistered) is dropped by its @@ -778,6 +897,30 @@ AddressSpaceStats AddressSpaceCounters() { return {addressSpaceCacheEnabled(), load(cache.hits), load(cache.rebuiltFirst), load(cache.rebuiltGeneration), load(cache.rebuiltEpoch), load(cache.rebuiltDevice), load(cache.rebuiltWaiterDrop), load(cache.unpublished), load(cache.dissolvedOverlap), load(cache.dissolvedImports), load(cache.waiterDrops)}; } +std::string AddressCopyOverflow(std::vector copies, std::uint64_t limit) { + std::uint64_t total = 0; + for (const auto& copy : copies) total += copy.committed; + if (total <= limit) return {}; + std::sort(copies.begin(), copies.end(), [](const AddressCopy& left, const AddressCopy& right) { return left.committed > right.committed; }); + char text[256]; + std::snprintf(text, sizeof(text), "an address-based build copies %llu MiB of %zu registered ranges, past %llu MiB (APS5_ADDRESS_COPY_MAX_MIB); the largest:", static_cast(total >> 20u), copies.size(), static_cast(limit >> 20u)); + std::string message = text; + for (std::size_t index = 0; index < copies.size() && index < 8; ++index) { + const auto& copy = copies[index]; + std::snprintf(text, sizeof(text), " 0x%llx+0x%llx (%.1f MiB committed, %s)", static_cast(copy.begin), static_cast(copy.end - copy.begin), copy.committed / 1048576.0, copy.reason); + message += text; + } + return message; +} + +MirrorStats MirrorCounters() { + auto& state = Mirrors(); + std::lock_guard lock(state.mutex); + MirrorStats stats{0, state.heapBytes, state.rebuilds, state.blocksCopied, state.heapRefills}; + for (const auto& [base, mirror] : state.entries) stats.heapMirrors += mirror->heap ? 1 : 0; + return stats; +} + const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes) { if (context.hostImportAlignment == 0 || bytes == 0 || bytes > std::numeric_limits::max() - address) return nullptr; auto& state = Imports(); @@ -859,6 +1002,8 @@ void GuestBufferMemory::AcquireRegistered() { // APS5_NO_SORTED_SNAPSHOT_LOOKUP=1: AddSnapshot scans the regions as before instead of searching. static const bool sortedLookup = std::getenv("APS5_NO_SORTED_SNAPSHOT_LOOKUP") == nullptr; std::vector blocks; + // The build's heap mirrors, refreshed together before the blocks are copied. + std::vector heaps; bool mirrored = false; auto& spaces = Spaces(); // Read before the lease below is acquired: a mutation ending in between leaves the space @@ -890,7 +1035,8 @@ void GuestBufferMemory::AcquireRegistered() { for (const auto& region : space->base) { if (region.mirror == nullptr) continue; mirrored = true; - if (region.mirror->writable && prepareRefresh(*region.mirror, region.begin, region.end - region.begin)) appendBlocks(blocks, *region.mirror, region.begin, region.end - region.begin); + if (region.mirror->heap) heaps.push_back(region.mirror.get()); + else if (region.mirror->writable && prepareRefresh(*region.mirror, region.begin, region.end - region.begin)) appendBlocks(blocks, *region.mirror, region.begin, region.end - region.begin); } lap(timing.mirrorsUs, at); } @@ -914,6 +1060,8 @@ void GuestBufferMemory::AcquireRegistered() { // The import lock is taken per range: a mirror's preparation may wait for recorded work, which // must not happen under it. regions.reserve(lease.size()); + // The ranges left to copies, with why no import or mirror serves them (the copy limit below). + std::vector copies; for (const auto& range : lease) { if (!range->readable) continue; validate(range->address, range->bytes); @@ -932,20 +1080,41 @@ void GuestBufferMemory::AcquireRegistered() { regions.push_back(std::move(region)); continue; } - if (!range->releasable && mirrorsEnabled()) { - // Main image: served by a persistent mirror (see ImageMirror); the writable ones are - // compared with guest memory below, all at once. - region.mirror = acquireMirror(context, range, blocks); + if (mirrorsEnabled()) { + // Main image and heaps: served by a persistent mirror (see ImageMirror); the writable + // image ones are compared with guest memory below, all at once. + region.mirror = acquireMirror(context, range, blocks, range->releasable); lap(timing.mirrorsUs, at); if (region.mirror != nullptr) { mirrored = true; + if (region.mirror->heap) heaps.push_back(region.mirror.get()); regions.push_back(std::move(region)); continue; } } addCopiedRange({range->address, range->address + range->bytes, range->writable}); + const auto& copied = regions.back(); + std::uint64_t committed = copied.end - copied.begin; + if (copied.sparse) { + committed = 0; + for (const auto& [first, last] : copied.backed) committed += last - first; + } + // A heap range with readable pages is mirrored unless the arena does not track its writes. + const char* reason = !mirrorsEnabled() ? "mirrors disabled by APS5_NO_LEASE_MIRROR" : copied.sparse ? "unreadable pages" : !range->releasable ? "image mirror refused" : "outside the write-watched arena"; + copies.push_back({copied.begin, copied.end, committed, reason}); } regionsSorted = sortedLookup; + // The ranges left to copies are copied by every build of this space; past the limit the + // process stops rather than copying them per dispatch. + static const std::uint64_t copyLimit = [] { + const char* value = std::getenv("APS5_ADDRESS_COPY_MAX_MIB"); + return (value ? std::strtoull(value, nullptr, 10) : 64ull) << 20u; + }(); + if (const auto overflow = AddressCopyOverflow(std::move(copies), copyLimit); !overflow.empty()) { + std::fprintf(stderr, "FATAL: %s\n", overflow.c_str()); + std::fflush(stderr); + std::abort(); + } lap(timing.importsUs, at); if (addressSpaceCacheEnabled()) { // Published only while the imports the regions point at stand: the table entries below @@ -991,6 +1160,8 @@ void GuestBufferMemory::AcquireRegistered() { } } } + refreshHeapMirrors(heaps, blocks); + lap(timing.mirrorsUs, at); const auto copiedBefore = Mirrors().blocksCopied; compareBlocks(blocks); lap(timing.compareUs, at); @@ -1018,7 +1189,8 @@ void GuestBufferMemory::addCopiedRange(const CopiedRange& range) { // cannot write them, so that equals a snapshot taken now without the extra copy. Reserved heaps // are registered whole while the guest commits pages on demand: committed pages only. region.hostBacked = !range.writable; - auto committed = GuestMemory::DescribeCommitted(range.begin, range.end - range.begin, range.writable); + // Readable pages are copied in, a writable range's read-only ones included (see storeChanged). + auto committed = GuestMemory::DescribeCommitted(range.begin, range.end - range.begin); if (!committed.whole) { region.sparse = true; region.backed = std::move(committed.ranges); @@ -1094,11 +1266,11 @@ void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t b // the shader stores to them; what is written back is decided by Writes() alone. Region region{address, address + bytes, true, {}, nullptr}; region.atomic = atomic; - auto committed = GuestMemory::DescribeCommitted(address, bytes, true); + auto committed = GuestMemory::DescribeCommitted(address, bytes); if (!committed.whole) { // A GPU heap bound whole while the guest commits its pages on demand, or a descriptor left - // pointing at unmapped memory: only committed pages are copied in and stored back. Shaders - // must not touch the rest, which reads as zeros. + // pointing at unmapped memory: only readable pages are copied in and stored back (writable + // ones only, see storeChanged). Shaders must not touch the rest, which reads as zeros. region.sparse = true; region.backed = std::move(committed.ranges); } @@ -1670,6 +1842,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { if (profile) copyUs += microsecondsSince(copyStart); } if (!gpuCopies.empty()) recordGpuCopies(gpuCopies, addressable); + takeHeapReferences(); if (!profile) return; const auto loopUs = microsecondsSince(loopStart); readUs.fetch_add(refreshUs, std::memory_order_relaxed); @@ -2003,14 +2176,64 @@ void GuestBufferMemory::MarkDirectWrites() const { } } -void GuestBufferMemory::WriteBack() { - Require(uploaded && !committed, "guest memory cannot be committed twice or before upload"); +namespace { + +// Stores what the GPU changed in [address, address + current.size()) against `original`. Only the +// writable pages are stored: a read-only page of a writable range (a direct memory page mapped at +// several addresses is read-only in every view, aliased writes are not implemented) must come back +// unchanged, else the write-back fails rather than dropping the GPU's store. +void storeChanged(std::uint64_t address, std::span current, std::span original) { + const auto writable = GuestMemory::DescribeCommitted(address, current.size(), true); + if (writable.whole) { + GuestMemory::WriteChanged(address, current, original); + return; + } + const auto unchanged = [&](std::uint64_t from, std::uint64_t to) { + const auto at = static_cast(from - address); + if (std::memcmp(current.data() + at, original.data() + at, static_cast(to - from)) == 0) return; + char message[192]; + std::snprintf(message, sizeof(message), "the GPU changed guest memory 0x%llx+0x%llx that is not writable (a direct memory page shared by several views; aliased writes are not implemented)", static_cast(from), static_cast(to - from)); + throw std::runtime_error(message); + }; + auto cursor = address; + for (const auto& [first, last] : writable.ranges) { + if (cursor < first) unchanged(cursor, first); + const auto at = static_cast(first - address); + const auto length = static_cast(last - first); + GuestMemory::WriteChanged(first, current.subspan(at, length), original.subspan(at, length)); + cursor = last; + } + if (cursor < address + current.size()) unchanged(cursor, address + current.size()); +} + +// The written ranges sorted, overlapping ones merged. +std::vector> mergeWrites(std::vector> writes) { std::sort(writes.begin(), writes.end()); std::vector> merged; for (const auto& range : writes) { if (!merged.empty() && range.first < merged.back().second) merged.back().second = std::max(merged.back().second, range.second); else merged.push_back(range); } + return merged; +} + +} + +void GuestBufferMemory::takeHeapReferences() { + heapReferences.clear(); + for (const auto& [begin, end] : mergeWrites(writes)) { + const auto* found = owner(begin); + if (found == nullptr || found->mirror == nullptr || !found->mirror->heap || !found->mirror->writable) continue; + Require(end <= found->end, "written range exceeds its heap mirror"); + const auto& mirror = *found->mirror; + const auto bytes = mirror.buffer->Bytes().subspan(static_cast(begin - mirror.base), static_cast(end - begin)); + heapReferences.emplace_back(begin, std::vector(bytes.begin(), bytes.end())); + } +} + +void GuestBufferMemory::WriteBack() { + Require(uploaded && !committed, "guest memory cannot be committed twice or before upload"); + const auto merged = mergeWrites(writes); // The CPU keeps running while the GPU works, so storing whole ranges would roll back its writes to // bytes the shader never touched. Only runs that differ from the uploaded copy are stored. for (const auto& [begin, end] : merged) { @@ -2033,6 +2256,14 @@ void GuestBufferMemory::WriteBack() { const auto offset = static_cast(begin - mirror.base); const auto length = static_cast(end - begin); const auto current = mirror.buffer->Bytes().subspan(offset, length); + if (mirror.heap) { + // The build's reference instead of a shadow; the stores stamp the blocks, and the + // next refresh takes them back from guest memory. + const auto reference = std::find_if(heapReferences.begin(), heapReferences.end(), [&](const auto& entry) { return entry.first == begin; }); + Require(reference != heapReferences.end() && reference->second.size() == length, "heap mirror write-back has no reference"); + storeChanged(begin, current, reference->second); + continue; + } GuestMemory::WriteChanged(begin, current, std::span(mirror.shadow).subspan(offset, length)); std::memcpy(mirror.shadow.data() + offset, current.data(), length); continue; @@ -2065,7 +2296,7 @@ void GuestBufferMemory::WriteBack() { if (!region.sparse) { const auto first = static_cast(begin - region.begin); const auto length = static_cast(end - begin); - GuestMemory::WriteChanged(begin, region.buffer->Bytes().subspan(first, length), std::span(region.uploaded).subspan(first, length)); + storeChanged(begin, region.buffer->Bytes().subspan(first, length), std::span(region.uploaded).subspan(first, length)); continue; } std::size_t packed = 0; @@ -2074,7 +2305,7 @@ void GuestBufferMemory::WriteBack() { const auto to = std::min(last, end); if (from < to) { const auto length = static_cast(to - from); - GuestMemory::WriteChanged(from, region.buffer->Bytes().subspan(static_cast(from - region.begin), length), std::span(region.uploaded).subspan(packed + static_cast(from - first), length)); + storeChanged(from, region.buffer->Bytes().subspan(static_cast(from - region.begin), length), std::span(region.uploaded).subspan(packed + static_cast(from - first), length)); } packed += static_cast(last - first); } @@ -2082,6 +2313,7 @@ void GuestBufferMemory::WriteBack() { committed = true; lease.clear(); space.reset(); + heapReferences.clear(); } std::optional>> GuestBufferMemory::DirectRegions() const { @@ -2089,16 +2321,17 @@ std::optional>> GuestBufferM std::vector> result; if (space != nullptr) { for (const auto& region : space->base) { - if (region.direct == nullptr && (region.mirror == nullptr || region.mirror->writable)) return std::nullopt; + if (region.direct == nullptr && (region.mirror == nullptr || region.mirror->writable || region.mirror->heap)) return std::nullopt; result.emplace_back(region.begin, region.end); } } for (const auto& region : regions) { // A read-only mirror is as fixed as an import: its bytes and VkBuffer never change while its - // Range exists (see ImageMirrorSerial). A writable one is refreshed and written back per build. + // Range exists (see ImageMirrorSerial). A writable one is refreshed and written back per build, + // a heap one refreshed per build. // A staged region is keyed by its import like one bound in place: the import is the source // of its copy-in and the destination of its copy-back, both recorded per use. - const bool fixedMirror = region.mirror != nullptr && !region.mirror->writable; + const bool fixedMirror = region.mirror != nullptr && !region.mirror->writable && !region.mirror->heap; const bool staged = region.gpuCopy && region.deviceLocal; if (region.direct == nullptr && !fixedMirror && !staged) return std::nullopt; if (staged) { diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index c1b432a7f..5b4028eeb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -2,10 +2,19 @@ #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestHeap.hpp" +#include "prx/libc/include/GuestArena.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif #include #include #include +#include #include +#include namespace { @@ -26,6 +35,205 @@ ShaderRecompiler::DescriptorBinding binding(Role role, std::uint32_t slot) { return {ShaderRecompiler::DescriptorKind::StorageBuffer, role, 0, slot, 1, {}, false}; } +// Heap ranges that no import serves are mirrored. A read-only one: the build binds the mirror, the +// next build with the range unchanged copies nothing into it, and the blocks a CPU write changed are +// read again. A writable one: a descriptor's store reaches guest memory at write-back, and only the +// bytes the GPU changed (a CPU store made while the GPU works stays). +void heapMirrorTests(const Context& context, const BdaTestAccess& access) { + // Heap mirrors refresh from the arena's write tracking; without it they are never made. + Require(GuestArena::GuestArenaAvailable_nid_postfix() && GuestArena::GuestArenaWriteWatched_nid_postfix(), "the heap mirror tests need the write-watched guest arena"); + constexpr std::size_t bytes = 2 * 65536; + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536); +#ifdef _WIN32 + Require(VirtualAlloc(block, bytes, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot commit the heap mirror block"); +#endif + auto* guest = static_cast(block); + std::memset(block, 0x11, bytes); + const auto address = reinterpret_cast(block); + const auto registry = [&](bool add, bool writable) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, writable); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + // An address-based build that writes 32 bytes at `written` (nothing for 0), with `gpu` run + // between its upload and its write-back; returns the device address serving the range. + const auto build = [&](std::uint64_t written, const std::function& gpu) { + GuestBufferMemory leased(context); + leased.AcquireRegistered(); + if (written != 0) leased.AddWritable(written, 32); + leased.Upload(true); + const auto ranges = leased.AddressRanges(); + const auto found = std::find_if(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin == address && range.end == address + bytes; }); + Require(found != ranges.end() && found->permissions == ShaderRecompiler::BdaAbi::Read, "the heap range is missing from the BDA table"); + const auto device = found->deviceAddress; + if (gpu) gpu(leased); + leased.WriteBack(); + return device; + }; + // An address-based build sweeps the mirrors whose ranges are gone (an earlier test's too). + const auto sweep = [&] { + GuestBufferMemory leased(context); + leased.AcquireRegistered(); + leased.Upload(true); + leased.WriteBack(); + }; + sweep(); + registry(true, false); + const auto before = MirrorCounters(); + const auto first = build(0, {}); + const auto made = MirrorCounters(); + Require(made.heapMirrors == before.heapMirrors + 1 && made.heapBytes == before.heapBytes + bytes, "a read-only heap range was not mirrored"); + Require(access.addressBytes(first)[65536 + 3] == std::byte{0x11}, "the heap mirror was not filled"); + Require(build(0, {}) == first && MirrorCounters().blocksCopied == made.blocksCopied && MirrorCounters().heapRefills == made.heapRefills, "an unchanged heap range was read into its mirror again"); + guest[65536 + 3] = 0x22; + Require(build(0, {}) == first && MirrorCounters().heapRefills == made.heapRefills + 1 && MirrorCounters().blocksCopied == made.blocksCopied + 1, "a written heap block was not read again alone"); + Require(access.addressBytes(first)[65536 + 3] == std::byte{0x22}, "the heap mirror missed the CPU write"); + const auto one = MirrorCounters(); + guest[5] = 0x33; + guest[65536 + 7] = 0x44; + Require(build(0, {}) == first && MirrorCounters().blocksCopied == one.blocksCopied + 2, "two written heap blocks were not read again"); + Require(access.addressBytes(first)[5] == std::byte{0x33} && access.addressBytes(first)[65536 + 7] == std::byte{0x44}, "the heap mirror missed the CPU writes"); + registry(false, false); + sweep(); + const auto swept = MirrorCounters(); + Require(swept.heapMirrors == before.heapMirrors && swept.heapBytes == before.heapBytes, "the heap mirror outlived its range"); + + registry(true, true); + const auto device = build(address + 16, [&](GuestBufferMemory& leased) { + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(address + 16, 32, adjustment); + access.bytes(view.buffer)[view.offset + adjustment] = std::byte{0x77}; + guest[16 + 8] = 0x55; + }); + Require(guest[16] == 0x77 && guest[16 + 8] == 0x55, "a writable heap mirror's write-back lost the GPU's store or rolled back the CPU's"); + Require(build(0, {}) == device && access.addressBytes(device)[16] == std::byte{0x77} && access.addressBytes(device)[16 + 8] == std::byte{0x55}, "the writable heap mirror missed the stores"); + registry(false, true); + sweep(); + Require(MirrorCounters().heapMirrors == before.heapMirrors, "the writable heap mirror outlived its range"); + +#ifdef _WIN32 + // A build that throws after queuing a mirror's changed blocks (another mirror's range became + // inaccessible) must not mark those blocks current: the next build copies them. + { + constexpr std::size_t half = 2 * 65536; + void* pair = GuestArena::GuestArenaAllocate_nid_postfix(2 * half, 65536); + Require(VirtualAlloc(pair, 2 * half, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot commit the heap mirror pair"); + std::memset(pair, 0x11, 2 * half); + auto* first = static_cast(pair); + auto* second = first + half; + const auto registerPair = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + for (auto* range : {first, second}) { + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, half, true, false); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + } + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto firstDevice = [&] { + GuestBufferMemory leased(context); + leased.AcquireRegistered(); + leased.Upload(true); + const auto ranges = leased.AddressRanges(); + const auto found = std::find_if(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin == reinterpret_cast(first); }); + Require(found != ranges.end(), "the first heap range is missing from the BDA table"); + const auto device = found->deviceAddress; + leased.WriteBack(); + return device; + }; + registerPair(true); + const auto device = firstDevice(); + first[5] = 0x66; + DWORD previous = 0; + // A protection change outside the registry, as a heap decommit is: the page cache is told. + Require(VirtualProtect(second, 65536, PAGE_NOACCESS, &previous) != 0, "cannot protect the second heap range"); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(second), 65536); + bool threw = false; + try { + GuestBufferMemory leased(context); + leased.AcquireRegistered(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(VirtualProtect(second, 65536, PAGE_READWRITE, &previous) != 0, "cannot unprotect the second heap range"); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(second), 65536); + Require(threw, "a build over an inaccessible heap mirror range did not fail"); + Require(firstDevice() == device && access.addressBytes(device)[5] == std::byte{0x66}, "an interrupted build left a changed heap block marked current"); + registerPair(false); + sweep(); + VirtualFree(pair, 2 * half, MEM_DECOMMIT); + GuestArena::GuestArenaRelease_nid_postfix(pair, 2 * half); + } +#endif +#ifdef _WIN32 + // A writable range with a read-only page (a direct memory page mapped at several addresses is + // read-only in every view): the mirror holds the page's bytes, a store next to it is written + // back, and a GPU change inside it fails the write-back instead of being dropped. A descriptor's + // copy outside an address-based build holds the page's bytes too. + { + constexpr std::size_t size = 2 * 65536; + void* raw = GuestArena::GuestArenaAllocate_nid_postfix(size, 65536); + Require(VirtualAlloc(raw, size, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot commit the aliased range"); + auto* bytes8 = static_cast(raw); + std::memset(raw, 0x11, size); + const auto base = reinterpret_cast(raw); + const auto page = base + 65536; + DWORD previous = 0; + Require(VirtualProtect(reinterpret_cast(page), 4096, PAGE_READONLY, &previous) != 0, "cannot make the aliased page read-only"); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(page, 4096); + const auto registerRange = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, raw, size, true, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, raw); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + registerRange(true); + const auto storeAt = [&](std::uint64_t at) { + GuestBufferMemory leased(context); + leased.AcquireRegistered(); + leased.AddWritable(page - 16, 32); + leased.Upload(true); + const auto ranges = leased.AddressRanges(); + const auto found = std::find_if(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin == base; }); + Require(found != ranges.end() && access.addressBytes(found->deviceAddress)[65536 + 8] == std::byte{0x11}, "a writable range with a read-only page was not mirrored with its bytes"); + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(page - 16, 32, adjustment); + access.bytes(view.buffer)[view.offset + adjustment + static_cast(at - (page - 16))] = std::byte{0x77}; + leased.WriteBack(); + }; + const auto made = MirrorCounters().heapMirrors; + storeAt(page - 8); + Require(MirrorCounters().heapMirrors == made + 1 && bytes8[65536 - 8] == 0x77, "a store next to a read-only page was not written back"); + bool refused = false; + try { + storeAt(page + 4); + } catch (const std::runtime_error& error) { + refused = std::string(error.what()).find("aliased writes are not implemented") != std::string::npos; + } + Require(refused && bytes8[65536 + 4] == 0x11, "a GPU change of a read-only page was not refused"); + { + GuestBufferMemory plain(context); + plain.AddReadable(page, 16); + plain.Upload(false); + std::uint32_t adjustment = 0; + const auto view = plain.Descriptor(page, 16, adjustment); + Require(access.bytes(view.buffer)[view.offset + adjustment] == std::byte{0x11}, "a descriptor over a read-only page of a writable range read zeros"); + plain.WriteBack(); + } + registerRange(false); + sweep(); + Require(VirtualProtect(reinterpret_cast(page), 4096, PAGE_READWRITE, &previous) != 0, "cannot restore the aliased page"); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(page, 4096); + VirtualFree(raw, size, MEM_DECOMMIT); + GuestArena::GuestArenaRelease_nid_postfix(raw, size); + } +#endif +#ifdef _WIN32 + VirtualFree(block, bytes, MEM_DECOMMIT); +#endif + GuestArena::GuestArenaRelease_nid_postfix(block, bytes); +} + } void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { @@ -96,6 +304,11 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { Require(range.begin == snapshots[0].address && range.end == range.begin + source.size(), "64-bit guest address was truncated"); const auto fault = access.bytes(access.descriptor(5).buffer); for (const auto byte : fault) Require(byte == std::byte{}, "fault buffer was not initialized"); + // A store into a read-only table range names the range and why stores cannot reach it. + const ShaderRecompiler::BdaAbi::Fault denied{ShaderRecompiler::BdaAbi::FaultState::Ready, ShaderRecompiler::BdaAbi::FaultReason::Permission, snapshots[0].address + 4, 4, 0, 0x88, 0}; + std::memcpy(fault.data(), &denied, sizeof(denied)); + reject([&] { resources.WriteBack(); }, "read-only in the BDA table"); + std::memset(fault.data(), 0, fault.size()); resources.WriteBack(); } { @@ -158,4 +371,9 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { const auto dropped = AddressSpaceCounters(); if (dropped.enabled) Require(dropped.waiterDrops == after.waiterDrops + 1 && LeaseCounters().cacheDrops == dropped.waiterDrops, "the free did not drop the cached address space"); } + heapMirrorTests(context, access); + // The copies left to an address-based build: nothing within the limit, the largest first past it. + Require(AddressCopyOverflow({{0x1000, 0x3000, 0x2000, "uncommitted pages"}}, 0x2000).empty(), "copies within the limit were refused"); + const auto copies = AddressCopyOverflow({{0x1000, 0x2000, 0x1000, "not mirrored"}, {0x10000, 0x30000, 0x18000, "uncommitted pages"}}, 0x2000); + Require(!copies.empty() && copies.find("0x10000+0x20000 (0.1 MiB committed, uncommitted pages)") < copies.find("0x1000+0x1000"), "the copy limit does not name the largest copy first"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp index 0ebb477a9..157ec6e93 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp @@ -8,6 +8,8 @@ struct BdaTestAccess { std::function(VkBuffer)> bytes; std::function descriptor; + // The bytes of the buffer behind a device address, from that address on. + std::function(VkDeviceAddress)> addressBytes; }; void RunBdaResourceTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index d8c22f68f..752475279 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1273,6 +1273,8 @@ int main() { mock = MockVulkan{}; auto bdaContext = mockContext(); bdaContext.bufferDeviceAddress = true; + // The fault buffer (with its written-page slots) is larger than the mock's 4 KiB default. + bdaContext.limits.maxStorageBufferRange = 1u << 27; RunBdaResourceTests(bdaContext, { [](VkBuffer buffer) -> std::span { return mock.memories.at(mock.bufferMemory.at(buffer)); }, [](std::uint32_t binding) { @@ -1280,6 +1282,12 @@ int main() { if (it->binding == binding) return it->buffers.at(0); } throw std::runtime_error("missing BDA test descriptor"); + }, + [](VkDeviceAddress address) { + // The inverse of mockGetBufferDeviceAddress. + const auto offset = address - 0x100000000000ULL; + const auto buffer = reinterpret_cast(offset / 0x10000); + return std::span(mock.memories.at(mock.bufferMemory.at(buffer))).subspan(offset % 0x10000); } }); Require(mock.live == 0, "BDA resources leaked Vulkan objects"); From 6b36c050fed368eaa3628f16e4ae1f2667306649 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 21:25:29 +0300 Subject: [PATCH 31/40] style: drop the branch's code comments per CONVENTIONS.md Only the lines this branch added lose their comments; the commits before keep them, with the reasoning behind each change, for whoever digs into that code later. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 2 - core/libs/prx/libSceAgc/Cb/src/Registers.cpp | 4 +- core/libs/prx/libSceAgc/Cb/src/Sync.cpp | 2 +- .../prx/libSceAgc/Command/src/Control.cpp | 2 - .../prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp | 2 +- .../prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp | 6 +- core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp | 2 +- .../prx/libSceAgc/Misc/src/ShaderFusion.cpp | 2 - core/libs/prx/libSceAgc/Patch/src/Jump.cpp | 2 - .../Patch/src/QueueEndOfPipeAction.cpp | 1 - core/libs/prx/libSceAgc/Patch/src/Rewind.cpp | 2 - .../Execution/include/VideoOutput.hpp | 2 - .../libSceAgcDriver/Execution/src/Driver.cpp | 26 --------- .../Execution/src/GuestMemory.cpp | 2 - .../prx/libSceAgcDriver/Execution/src/Pm4.cpp | 2 - .../Execution/src/VulkanDevice.cpp | 13 ----- .../Graphics/include/BdaResources.hpp | 3 - .../Graphics/include/Context.hpp | 4 -- .../Graphics/include/DepthSurface.hpp | 11 ---- .../Graphics/include/GuestBufferMemory.hpp | 12 ---- .../Graphics/include/Pipeline.hpp | 6 -- .../Graphics/include/Recorder.hpp | 4 -- .../Graphics/include/ShaderResources.hpp | 6 -- .../Graphics/include/State.hpp | 15 ----- .../Graphics/include/Texture.hpp | 2 - .../Graphics/src/BdaResources.cpp | 7 --- .../Graphics/src/ColorTargetLayout.cpp | 1 - .../Graphics/src/DepthSurface.cpp | 7 --- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 6 -- .../Graphics/src/GuestBufferMemory.cpp | 58 ------------------- .../Graphics/src/GuestTextureResource.cpp | 1 - .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 1 - .../Graphics/src/ShaderResources.cpp | 5 -- .../libSceAgcDriver/Graphics/src/State.cpp | 29 ---------- .../Graphics/src/TextureTiling.cpp | 2 - .../State/src/Configuration.cpp | 2 - .../libSceAgcDriver/tests/BdaContracts.cpp | 3 - .../prx/libSceAgcDriver/tests/BdaDevice.cpp | 1 - .../libSceAgcDriver/tests/BdaResources.cpp | 17 ------ .../prx/libSceAgcDriver/tests/BdaTests.hpp | 1 - .../tests/ColorTargetLayout.cpp | 1 - .../prx/libSceAgcDriver/tests/DppWrite.cpp | 3 - .../prx/libSceAgcDriver/tests/Graphics.cpp | 23 -------- .../tests/GuestSamplerResource.cpp | 1 - .../tests/GuestTextureResource.cpp | 6 -- core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 2 - .../libSceAgcDriver/tests/Structurizer.cpp | 4 -- .../libSceAgcDriver/tests/TextureTiling.cpp | 7 --- .../libSceAgcDriver/tools/ShaderReplay.cpp | 1 - core/libs/prx/libSceAjm.native/src/Ajm.cpp | 13 ----- core/libs/prx/libSceAppContent/Export.cpp | 2 - core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 1 - .../libSceAudioOut/src/AudioOut2Internal.hpp | 4 -- .../prx/libSceAudioOut/src/AudioOut2Port.cpp | 3 - .../src/AudioOut2SpeakerArray.cpp | 6 -- core/libs/prx/libSceContentDelete/Export.cpp | 1 - core/libs/prx/libSceContentExport/Export.cpp | 1 - core/libs/prx/libSceContentSearch/Export.cpp | 1 - .../prx/libSceGameLiveStreaming/Export.cpp | 1 - core/libs/prx/libSceIme/Export.cpp | 3 - .../prx/libSceMsgDialog.native/Export.cpp | 1 - .../prx/libSceNpEntitlementAccess/Export.cpp | 5 -- core/libs/prx/libSceNpManager/Export.cpp | 4 -- core/libs/prx/libSceRemoteplay/Export.cpp | 1 - core/libs/prx/libSceSharePlay/Export.cpp | 1 - .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 3 - .../libs/prx/libSceVideoRecordingP/Export.cpp | 2 - core/libs/prx/libSceVoice/Export.cpp | 6 -- .../prx/libSceWebBrowserDialog/Export.cpp | 1 - core/libs/prx/libc/include/GuestArena.hpp | 1 - .../prx/libc/include/WindowsFormatting.hpp | 1 - core/libs/prx/libc/src/Formatting.cpp | 3 - core/libs/prx/libc/src/General.cpp | 1 - core/libs/prx/libc/src/GuestAllocations.cpp | 5 -- core/libs/prx/libc/src/GuestArena.cpp | 4 -- core/libs/prx/libc/src/Math.cpp | 2 - core/libs/prx/libc/src/RuntimeSupport.cpp | 1 - core/libs/prx/libc/src/StdioExtras.cpp | 1 - .../AppMetadata/include/AppMetadata.hpp | 1 - .../AppMetadata/include/ParamJsonParser.hpp | 1 - core/libs/prx/libkernel/Apr/src/Apr.cpp | 3 - .../libkernel/DirectMemory/DirectMemory.cpp | 18 ------ .../libkernel/DirectMemory/DirectMemory.hpp | 3 - .../prx/libkernel/DirectMemory/Export.cpp | 1 - .../prx/libkernel/DirectMemory/MemoryPool.hpp | 1 - .../File/include/DirectoryDescriptor.hpp | 4 -- core/libs/prx/libkernel/File/src/Stdio.cpp | 3 - .../prx/libkernel/Pthread/Posix/Xthreads.cpp | 6 -- core/libs/prx/libkernel/Pthread/src/Attr.cpp | 1 - core/libs/prx/libkernel/Socket/src/Udp.cpp | 2 +- .../prx/libkernel/System/src/Coredump.cpp | 3 - core/libs/tests/GuestMemory.cpp | 1 - .../codegen/src/InstructionScanner.cpp | 1 - core/shader/recompiler/BdaAbi.hpp | 4 -- .../ControlFlow/src/RequestSerializer.cpp | 1 - .../ControlFlow/src/Structurizer.cpp | 4 -- .../IrMetadata/DescriptorBinding.hpp | 2 - .../IrMetadata/ResourceInfo.hpp | 4 -- .../IrMetadata/Resources.hpp | 6 -- .../IrMetadata/ShaderInfo.hpp | 3 - .../IrMetadata/StageIO.hpp | 2 - .../Optimization/DescriptorBindingBuilder.hpp | 1 - .../Optimization/ShaderStageInputInfo.hpp | 1 - .../Optimization/src/ResourceMaterializer.cpp | 1 - .../Optimization/src/ResourceTracker.cpp | 6 -- .../RdnaDecoder/RdnaDescriptorFormat.hpp | 3 - .../RdnaDecoder/src/RdnaDescriptorFormat.cpp | 1 - .../RdnaDecoder/src/RdnaImageOpDecoder.cpp | 7 --- core/shader/recompiler/Recompiler.cpp | 1 - core/shader/recompiler/Recompiler.hpp | 6 -- .../include/SpirvBackend/SpirvAnalysis.hpp | 1 - .../include/SpirvBackend/SpirvBda.hpp | 4 -- .../SpirvBackend/SpirvEmitterState.hpp | 4 -- .../SpirvBackend/src/SpirvAnalysis.cpp | 2 - .../SpirvBackend/src/SpirvBdaLookup.cpp | 5 -- .../SpirvBackend/src/SpirvBdaRead.cpp | 3 - .../SpirvBackend/src/SpirvFlowEmitter.cpp | 3 - .../SpirvBackend/src/SpirvImageEmitter.cpp | 28 --------- .../src/SpirvMemory/SpirvBufferAccess.cpp | 2 - .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 2 - .../SpirvMemory/SpirvMemoryInstructions.cpp | 11 ---- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 2 - .../src/ControlFlowInstructions.cpp | 1 - .../Translation/src/DispatchInstructions.cpp | 3 - .../Translation/src/InstructionTranslator.cpp | 2 - .../MemoryInstructions/ImageInstructions.cpp | 3 - .../src/TranslationContext/OperandAccess.cpp | 1 - 127 files changed, 8 insertions(+), 574 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9236f55fb..4f75e9677 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,10 +34,8 @@ set(ANYPS5_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") set(CMAKE_EXE_LINKER_FLAGS "") add_subdirectory(3rdparty/SDL2) set(CMAKE_EXE_LINKER_FLAGS "${ANYPS5_EXE_LINKER_FLAGS}") -# Prebuilt FFmpeg (AJM's MP3 decoder), downloaded for the submodule's commit as in Kyty. set(ARCHITECTURE x86_64) add_subdirectory(3rdparty/ffmpeg-core) -# The system libraries its pkg-config files name, which ffmpeg-core adds only for MSVC. target_link_libraries(ffmpeg INTERFACE bcrypt ole32 user32) set(FT_DISABLE_ZLIB ON CACHE BOOL "" FORCE) diff --git a/core/libs/prx/libSceAgc/Cb/src/Registers.cpp b/core/libs/prx/libSceAgc/Cb/src/Registers.cpp index f8ed2f09c..29b68fe01 100644 --- a/core/libs/prx/libSceAgc/Cb/src/Registers.cpp +++ b/core/libs/prx/libSceAgc/Cb/src/Registers.cpp @@ -17,11 +17,11 @@ std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer* buf, st } uint32_t APS5_VABI sceAgcCbSetShRegisterRangeDirectGetSize(uint32_t num_values) { - return (num_values + 2u) * 4u; // header + offset + values + return (num_values + 2u) * 4u; } std::uint32_t APS5_VABI sceAgcCbSetUcRegisterRangeDirectGetSize(std::uint32_t numValues) { - return (numValues + 2u) * 4u; // header + offset + values + return (numValues + 2u) * 4u; } std::uint32_t* APS5_VABI sceAgcCbSetShRegistersDirect(CommandBuffer* buf, const volatile ShaderRegister* regs, std::uint32_t numRegs) { diff --git a/core/libs/prx/libSceAgc/Cb/src/Sync.cpp b/core/libs/prx/libSceAgc/Cb/src/Sync.cpp index 05d5395f8..ad70ced12 100644 --- a/core/libs/prx/libSceAgc/Cb/src/Sync.cpp +++ b/core/libs/prx/libSceAgc/Cb/src/Sync.cpp @@ -15,7 +15,7 @@ std::uint32_t* APS5_VABI sceAgcCbNop_nid_postfix(CommandBuffer* buf, std::uint32 } uint32_t APS5_VABI sceAgcCbNopGetSize(uint32_t size_in_dwords) { - return size_in_dwords * 4u; // a NOP packet is exactly size_in_dwords dwords including its header + return size_in_dwords * 4u; } std::uint32_t APS5_VABI sceAgcCbQueueEndOfPipeActionGetSize() { diff --git a/core/libs/prx/libSceAgc/Command/src/Control.cpp b/core/libs/prx/libSceAgc/Command/src/Control.cpp index 9906cd432..e203a62d1 100644 --- a/core/libs/prx/libSceAgc/Command/src/Control.cpp +++ b/core/libs/prx/libSceAgc/Command/src/Control.cpp @@ -7,9 +7,7 @@ std::uint32_t* WriteJump(CommandBuffer* buffer, std::uint8_t chain, std::uint8_t CheckBits(chain, 1, function); CheckBits(cachePolicy, 3, function); CheckBits(sizeInDwords, 0xfffffu, function); - // Titles reserve a jump with a null target and write the address into the packet later. Require((guestAddress & 3u) == 0, function, "misaligned jump target"); - // INDIRECT_BUFFER: address, then size in bits 19:0, chain (no return) in bit 20, cache policy in 29:28. return Emit(buffer, 0x3fu, {static_cast(guestAddress) & ~3u, static_cast(guestAddress >> 32u), 0x0f200000u | (static_cast(cachePolicy) << 28u) | (static_cast(chain) << 20u) | sizeInDwords}, function); } diff --git a/core/libs/prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp b/core/libs/prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp index 6129e7174..88f863661 100644 --- a/core/libs/prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp +++ b/core/libs/prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp @@ -32,7 +32,7 @@ std::uint32_t* APS5_VABI sceAgcDcbDrawIndexOffset(CommandBuffer* buf, std::uint3 } uint32_t APS5_VABI sceAgcDcbDrawIndexOffsetGetSize(void) { - return 20; // DRAW_INDEX_OFFSET_2: header + 4 dwords + return 20; } uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirect(CommandBuffer* buf, uint32_t data_offset_in_bytes, uint64_t modifier) { diff --git a/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp b/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp index f96b5badd..e93ad3279 100644 --- a/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp +++ b/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp @@ -14,7 +14,7 @@ std::uint32_t* APS5_VABI sceAgcDcbSetIndexBuffer(CommandBuffer* buf, std::uint64 } std::uint32_t APS5_VABI sceAgcDcbSetIndexBufferGetSize() { - return 12; // INDEX_BASE: header + 2 dwords + return 12; } std::uint32_t* APS5_VABI sceAgcDcbSetIndexCount(CommandBuffer* buf, std::uint32_t indexCount) { @@ -32,11 +32,9 @@ std::uint32_t* APS5_VABI sceAgcDcbSetIndexSize(CommandBuffer* buf, std::uint8_t } std::uint32_t APS5_VABI sceAgcDcbSetIndexSizeGetSize() { - return 12; // SET_UCONFIG_REG_INDEX: header + 2 dwords + return 12; } -// Unnamed: sceAgcDcbSetIndexSize with the per-instance object id flag (VGT_INDEX_TYPE bit 14), as -// Kyty and SharpEmu build it. APS5_EXPORT("-KRzWekV120", sceAgcUnknown__MKRzWekV120); std::uint32_t* APS5_VABI sceAgcUnknown__MKRzWekV120(CommandBuffer* buf, std::uint8_t indexSize, std::uint8_t cachePolicy, std::uint8_t perInstanceObjectId) { Agc::Command::CheckBits(perInstanceObjectId, 1, __func__); diff --git a/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp b/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp index 2e196b43d..e7271811c 100644 --- a/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp +++ b/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp @@ -42,7 +42,7 @@ std::uint32_t* APS5_VABI sceAgcDcbEventWrite(CommandBuffer* buf, std::uint8_t ev } std::uint64_t APS5_VABI sceAgcDcbEventWriteGetSize(std::uint8_t eventType) { - return (eventType & 0xfeu) == 0x38u ? 16 : 8; // EVENT_WRITE, plus the address for the 0x38/0x39 events + return (eventType & 0xfeu) == 0x38u ? 16 : 8; } std::uint32_t* APS5_VABI sceAgcDcbStallCommandBufferParser(CommandBuffer* buf) { diff --git a/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp b/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp index 0f3cc6356..de4997d45 100644 --- a/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp @@ -77,7 +77,6 @@ int APS5_VABI sceAgcUnknownFuseShaderHalves(Shader* fused_result, const Shader* const auto& frontRsrc2 = FindRegister(front->sh_registers, frontCount, isGs ? SPI_SHADER_PGM_RSRC2_GS : SPI_SHADER_PGM_RSRC2_HS); auto& fusedRsrc1 = FindRegister(fused, fusedCount, isGs ? SPI_SHADER_PGM_RSRC1_GS : SPI_SHADER_PGM_RSRC1_HS); auto& fusedRsrc2 = FindRegister(fused, fusedCount, isGs ? SPI_SHADER_PGM_RSRC2_GS : SPI_SHADER_PGM_RSRC2_HS); - // Both halves run in one wave, so VGPRs are the larger of the two and shared VGPRs are reallocated (after Kyty). const std::uint32_t frontVgprs = ((frontRsrc1.value & 0x3fu) + 1u) * 4u; const std::uint32_t backVgprs = ((fusedRsrc1.value & 0x3fu) + 1u) * 4u; const std::uint32_t frontTotal = frontVgprs + (frontRsrc2.value >> 28u) * 8u; @@ -94,7 +93,6 @@ int APS5_VABI sceAgcUnknownFuseShaderHalves(Shader* fused_result, const Shader* MergeMax(fusedRsrc1, frontRsrc1, 28, 0x3u); } fusedRsrc2.value = (fusedRsrc2.value & 0xf7ffffc1u) | (frontRsrc2.value & 0x0800003eu); - // The fused program starts at the front half; its address goes in the ES/LS program registers. const std::uint32_t lo = isGs ? SPI_SHADER_PGM_LO_ES : SPI_SHADER_PGM_LO_LS; auto& loRegister = FindRegister(fused, fusedCount, lo); auto& hiRegister = FindRegister(fused, fusedCount, lo + 1u); diff --git a/core/libs/prx/libSceAgc/Patch/src/Jump.cpp b/core/libs/prx/libSceAgc/Patch/src/Jump.cpp index 435489880..e31894dd9 100644 --- a/core/libs/prx/libSceAgc/Patch/src/Jump.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/Jump.cpp @@ -9,7 +9,6 @@ namespace { -// INDIRECT_BUFFER as sceAgcDcbJump writes it: [1..2] target, [3] size in 19:0, chain in 20, cache policy in 29:28. void PatchJump(std::uint32_t* cmd, const volatile std::uint32_t* target, std::uint32_t sizeInDwords, const char* function) { Agc::Command::ValidatePacket(cmd, 0x3fu, 4, function); const auto address = reinterpret_cast(target); @@ -29,7 +28,6 @@ int APS5_VABI sceAgcJumpPatchSetTarget(uint32_t* cmd, const volatile uint32_t* t return 0; } -// Named after Kyty's reading: a jump patch that also sets the cache policy. APS5_EXPORT("Ikfdt-rIqCE", sceAgcUnknown_Ikfdt_MrIqCE); int APS5_VABI sceAgcUnknown_Ikfdt_MrIqCE(uint32_t* cmd, uint64_t cache_policy, const volatile uint32_t* target, uint32_t size_in_dwords) { Agc::Command::CheckBits(cache_policy, 3u, __func__); diff --git a/core/libs/prx/libSceAgc/Patch/src/QueueEndOfPipeAction.cpp b/core/libs/prx/libSceAgc/Patch/src/QueueEndOfPipeAction.cpp index d0fc39566..7e42fe012 100644 --- a/core/libs/prx/libSceAgc/Patch/src/QueueEndOfPipeAction.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/QueueEndOfPipeAction.cpp @@ -31,7 +31,6 @@ int APS5_VABI sceAgcQueueEndOfPipeActionPatchAddress(std::uint32_t* cmd, const v } int APS5_VABI sceAgcQueueEndOfPipeActionPatchData(uint32_t* cmd, uint64_t data) { - // RELEASE_MEM as built by sceAgcCbReleaseMem: [2] data select << 29, [5..6] data. Agc::Command::ValidatePacket(cmd, 0x49u, 8, __func__); if ((cmd[2] >> 29u) == 1u) Agc::Command::CheckBits(data, 0xffffffffu, __func__); cmd[5] = static_cast(data); diff --git a/core/libs/prx/libSceAgc/Patch/src/Rewind.cpp b/core/libs/prx/libSceAgc/Patch/src/Rewind.cpp index 26eabbc17..06dc2c9b9 100644 --- a/core/libs/prx/libSceAgc/Patch/src/Rewind.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/Rewind.cpp @@ -10,8 +10,6 @@ namespace { -// The valid bit (31) releases a queue stalled on the REWIND; the release store publishes the commands -// the title wrote behind it before setting the bit. int SetRewindState(std::uint32_t* cmd, std::uint8_t state, const char* function) { Agc::Command::ValidatePacket(cmd, 0x59u, 2, function); Agc::Command::CheckBits(state, 1, function); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp index 8669b4859..e26b9c657 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp @@ -40,8 +40,6 @@ class IVideoOutput { public: virtual ~IVideoOutput() = default; virtual std::shared_ptr Reserve(const FlipInfo& info) = 0; - // Blocks a submission carrying a flip until the output's flip queue has room, as the system - // holds back a submitter that runs ahead of the display. Called without the driver's lock. virtual void WaitForFlipRoom() {} virtual std::shared_ptr CaptureRenderingWait(std::uint32_t index) { throw std::runtime_error("VideoOut: rendering waits are unsupported by this output"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp index 3630973de..de0db5a3b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp @@ -203,8 +203,6 @@ struct Submission { // submission trusts only labels the recorder noted with a newer stamp (see Recorder::NoteLabel). std::uint64_t received = 0; std::chrono::steady_clock::time_point enqueuedAt{}; - // The guest commands behind a REWIND (commands stop at it): the title writes them after submitting - // and marks the REWIND valid, so they are read only then (Driver::executeRewindTail). const std::uint32_t* rewindTail = nullptr; std::size_t rewindWords = 0; }; @@ -528,17 +526,12 @@ private: public: - // Copies a submission's commands, stopping after the first REWIND: what follows it is read when the - // REWIND becomes valid. INDIRECT_BUFFER packets are followed as the CP follows them: a chained one - // (bit 20 of its size dword) replaces the rest of its buffer with the target, a call inserts the - // target and continues after the packet. static void copyCommands(Submission& submission, const std::uint32_t* guest, std::size_t words) { submission.commands.clear(); std::size_t budget = std::size_t{1} << 26u; copySegment(submission, guest, words, budget); } - // Appends one command segment; true when a REWIND ended the copy. static bool copySegment(Submission& submission, const std::uint32_t* guest, std::size_t words, std::size_t& budget) { require(words <= budget, "command buffer jumps exceed the copy limit (a jump loop?)"); budget -= words; @@ -547,7 +540,6 @@ public: if (Pm4::FillerPacket(header)) { submission.commands.push_back(header); ++cursor; continue; } const auto count = (header & 0xc0000000u) == 0xc0000000u ? Pm4::PacketWords(header) : words - cursor; if ((header & 0xc0000000u) != 0xc0000000u || count > words - cursor) { - // Malformed: copied as is for validate to report with its context. submission.commands.insert(submission.commands.end(), guest + cursor, guest + words); return false; } @@ -574,8 +566,6 @@ public: return false; } - // Holds the submitting thread while an output named by a flip in the submission has a full flip - // queue (mutex not held: the presenter needs the driver to finish flips). void waitForFlipRoom(const Submission& submission) { for (std::size_t cursor = 0; cursor < submission.commands.size(); cursor += Pm4::PacketWords(submission.commands[cursor])) { if (submission.commands[cursor] != FlipPacketHeader) continue; @@ -590,7 +580,6 @@ public: } } - // Takes the flip reservations and rendering waits the submission's packets name (mutex held). void reserveOutputs(Submission& submission) { for (std::size_t cursor = 0; cursor < submission.commands.size();) { const auto* words = submission.commands.data() + cursor; @@ -613,8 +602,6 @@ public: } } - // REWIND stalls the queue until the CPU marks it valid (sceAgcRewindPatchSetRewindState); the - // commands behind it are copied from guest memory only then and run as a continuation. void executeRewindTail(const Submission& stalled) { std::atomic_ref control(*const_cast(stalled.rewindTail - 1)); while ((control.load(std::memory_order_acquire) & 0x80000000u) == 0) { @@ -3250,8 +3237,6 @@ private: pendingDispatchPhases().outcome = DispatchOutcome::CopyHle; return; } - // USE_THREAD_DIMENSIONS that is no whole number of groups: the host launches whole groups, - // the partial-group variant retires the threads past the size. if (indirectArguments == 0 && (packet[4] & 0x20u) != 0) { const std::array threads{packet[1], packet[2], packet[3]}; for (std::uint32_t axis = 0; axis < 3; ++axis) { @@ -3322,7 +3307,6 @@ private: }; mix(address); mix(packet[4] & 0x8000u); - // The partial-group size is shader data of the compiled result. for (const auto threads : compute.partialThreads) mix(threads); for (const auto word : userData) mix(word); // Only the shader registers the request reads (thread counts and RSRC1/2; the program @@ -3997,9 +3981,6 @@ private: if (!drawParameters.indirect && !drawParameters.indexed && (drawParameters.indexCount == 0 || drawParameters.instanceCount == 0)) return DrawVerdict::Nothing; static const bool traceIndirect = std::getenv("APS5_TRACE_INDIRECT_DRAWS") != nullptr; if (traceIndirect && drawParameters.indirect) std::fprintf(stderr, "[draw] indirect packet %s args 0x%llx count %u reached\n", Pm4::Name(packet[0]).c_str(), static_cast(drawParameters.indirect->arguments), drawParameters.indirect->count); - // A draw without color writes or a pixel shader draws nothing, unless it tests depth or - // stencil against a bound surface: such depth-only draws need a pipeline without a - // fragment stage, which is not implemented. { const auto targetMask = queue.context.find(0x8e); const auto shaderMask = queue.context.find(0x8f); @@ -5156,13 +5137,6 @@ private: return true; } - // PIXEL_PIPE_STAT_DUMP, drained like a label: every draw before it completed, so the recorder's - // sample count is the DB counter at this point. The PS5 layout interleaves a begin and an end - // counter per DB (16 of them, 16 bytes apart; a query's begin dump writes the first of each - // pair, its end dump the second): the count goes to the first DB and the others stay at zero, - // all with bit 63 marking the result ready. - // ponytail: a GPU drain per dump; accumulate on the GPU behind the batch if titles dump many - // per frame. void dumpSampleCounters(std::uint64_t address) { std::uint64_t samples = 0; { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index c6f588ffe..c17e16f00 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1365,8 +1365,6 @@ void WriteChanged(std::uint64_t address, std::span current, std std::size_t lastChanged = 0; for (std::size_t at = 0; at < size; at += block) { if (!differs(at)) continue; - // Only the bytes the GPU changed: the rest of the block holds the snapshot, and storing it - // would roll back what the CPU wrote there since (a title lost an object's vtable pointer). const auto blockEnd = std::min(at + block, size); for (std::size_t run = at; run < blockEnd;) { if (current[run] == original[run]) { ++run; continue; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index 403435280..a914189c2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -313,7 +313,6 @@ void Validate(std::span packet, std::uint32_t queue) { require(eventIndex == 0 || eventIndex == 7, "invalid cache-flush event index"); break; case 0x39: - // PIXEL_PIPE_STAT_DUMP: the occlusion counters (see Driver::dumpSampleCounters). graphics(); size(4); require(eventIndex == 1, "invalid occlusion counter dump event index"); @@ -354,7 +353,6 @@ void Validate(std::span packet, std::uint32_t queue) { break; } case 0x59: - // REWIND: bit 31 is the valid flag the CPU sets; the driver waits for it (see Driver). size(2); require((packet[1] & 0x7fffffffu) == 0, "unsupported REWIND payload bits"); break; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index efeb1845f..b77359bef 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -218,7 +218,6 @@ struct VulkanDevice::State { std::map> computePipelines; std::unique_ptr colorTransfer; std::shared_ptr bufferPool; - // Bound for null V#s (Context::emptyBuffer). std::unique_ptr emptyBuffer; std::unique_ptr textureCache; std::unique_ptr pipelineCache; @@ -493,7 +492,6 @@ struct VulkanDevice::State { // Cached graphics pipelines (with their framebuffers, modules, render passes and layouts) // belong to this device and must be destroyed while it lives. Graphics::ClearCachedPipelines(device); - // Their framebuffers were the last users of the depth surfaces' views. Graphics::ClearDepthSurfaces(device); { std::lock_guard pipelines(computePipelinesMutex); @@ -780,7 +778,6 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u // PA_CL_CLIP_CNTL near/far clip disable maps to depth clamping. enabled.depthClamp = available.depthClamp; state->depthClamp = enabled.depthClamp == VK_TRUE; - // PIXEL_PIPE_STAT_DUMP reports sample counts (see Recorder::CountSamples). enabled.occlusionQueryPrecise = available.occlusionQueryPrecise; state->occlusionQueryPrecise = enabled.occlusionQueryPrecise == VK_TRUE; // Recompiled storage-image access declares no format (the guest descriptor decides it). @@ -789,7 +786,6 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u // Gathers with non-constant offsets (ImageGatherExtended). enabled.shaderImageGatherExtended = available.shaderImageGatherExtended; if (enabled.shaderImageGatherExtended) state->capabilities.push_back(spv::CapabilityImageGatherExtended); - // Sampling with an LOD clamp (image_sample_*_cl) as the MinLod image operand. enabled.shaderResourceMinLod = available.shaderResourceMinLod; if (enabled.shaderResourceMinLod) state->capabilities.push_back(spv::CapabilityMinLod); if (enabled.shaderStorageImageWriteWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageWriteWithoutFormat); @@ -1702,8 +1698,6 @@ bool NoResidentPresent() { // Debug aid: APS5_DUMP_FRAMES= saves the first n presented display buffers as frame_.bmp, // read back by the blit's own submission and written after its fence (RetireSlot), or with // APS5_NO_GPU_DUMP=1 decoded from the tiled guest buffer on the CPU, as before. -// APS5_DUMP_FRAMES_EVERY= dumps every k-th presented frame (up to APS5_DUMP_FRAMES of them) -// instead of the first ones, to reach screens far into a run. struct FrameDumps { int limit; bool cpu; @@ -2309,11 +2303,6 @@ bool TraceDispatchIo() { return traceIo; } -// Debug aid: APS5_WATCH_MEMORY=:[,:...] (with -// APS5_SYNC_DISPATCH=1) keeps the last 64 contents of the ranges, each with the dispatch that left it, -// and writes them to watch__.bin (the ranges back to back, in order) when the loop guard -// first trips: the dispatch that corrupted a structure a looping shader walks is the one whose -// snapshot first shows the damage. void WatchMemory(std::uint64_t programAddress) { struct Range { std::uint64_t address = 0; @@ -2341,8 +2330,6 @@ void WatchMemory(std::uint64_t programAddress) { static bool written = false; std::lock_guard lock(mutex); if (written) return; - // The heap base differs between runs above bit 36: the first of those bases each range is - // mapped at, once all are. static std::vector addresses; if (addresses.empty()) { std::vector found; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp index 922dbd2d8..301201afc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp @@ -5,7 +5,6 @@ namespace AgcDriver::Graphics { -// APS5_LOOP_GUARD: whether a dispatch has reported a loop that ran past the guard. bool LoopGuardTripped(); class BdaResources { @@ -41,8 +40,6 @@ private: // The page table is read-only to the shader, so consecutive builds mapping the same ranges to the // same device addresses share one buffer while one of them is alive (see the table cache in // BdaResources.cpp, which refers to it weakly). - // The pages stores through the table noted in the fault buffer (BdaAbi::WrittenPageSlots) are - // marked GPU-written, and the set emptied; CheckFault does it first. void markWrittenPages() const; std::shared_ptr table; std::unique_ptr fault; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 69dc27774..155caaeb4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -74,7 +74,6 @@ inline std::uint64_t& DeviceProcLookups() { return count; } -// The size of Context::emptyBuffer. inline constexpr std::size_t EmptyBufferBytes = 16; struct Context { @@ -119,10 +118,7 @@ struct Context { bool drawIndirectFirstInstance = false; bool multiDrawIndirect = false; bool drawIndirectCount = false; - // Occlusion queries count exact samples (occlusionQueryPrecise), not just any-passed. bool occlusionQueryPrecise = false; - // A small storage buffer bound for a null V# (no records): the recompiler specializes such a - // buffer as empty (BufferResource::empty), so shaders never access it. VkBuffer emptyBuffer = VK_NULL_HANDLE; // The device's list of recorded dispatches whose copied written buffers await a CPU write-back // (VulkanDevice's State::copiedWriters; the draw counterpart is DrawCopiedWriters): an indirect diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 551ceb320..7dbb70b89 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -11,20 +11,9 @@ namespace AgcDriver::Graphics { class Texture; -// The attachment view of the device's image for a depth/stencil target, made on first use. The -// image lives on the GPU only, always in the general layout: its guest surface (tiled, HTILE -// compressed) is never read or written, so a new image starts at the target's clear values, and -// shaders sample the image itself (DepthSurfaceTexture). Images stay until the device goes -// (ClearDepthSurfaces), so their views are stable for cached framebuffers. -// ponytail: never evicted, one image per distinct surface; evict (and keep recorded ones alive) if -// titles churn through many. VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); void ClearDepthSurfaces(VkDevice device); -// Whether the depth or stencil plane of a surface drawn with starts at `address`. bool DepthSurfaceAt(std::uint64_t address); -// The texture sampling the depth or stencil plane that starts at the descriptor's base address, -// or null when no surface drawn with on this device has a plane there. The plane is read in place, -// so the descriptor must describe it (its format, extent, one level and slice); anything else throws. std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 00cbae6da..1256823fe 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -124,8 +124,6 @@ struct AddressSpaceStats { }; AddressSpaceStats AddressSpaceCounters(); -// The image and heap mirrors (see GuestBufferMemory.cpp): heap mirrors held and their bytes, then -// cumulative: mirrors made, 64 KiB blocks copied into them after their fill, heap mirrors read again. struct MirrorStats { std::uint64_t heapMirrors = 0; std::uint64_t heapBytes = 0; @@ -135,16 +133,12 @@ struct MirrorStats { }; MirrorStats MirrorCounters(); -// A registered range an address-based build copies instead of serving in place: its committed bytes -// and why no import or mirror serves it. struct AddressCopy { std::uint64_t begin; std::uint64_t end; std::uint64_t committed; const char* reason; }; -// The FATAL message for copies whose committed bytes exceed `limit` (APS5_ADDRESS_COPY_MAX_MIB), -// naming the largest ones; empty within the limit. std::string AddressCopyOverflow(std::vector copies, std::uint64_t limit); class GuestBufferMemory { @@ -187,9 +181,6 @@ public: void Upload(bool addressable); void UploadPrepare(bool addressable); void UploadFinish(bool addressable); - // A view off minStorageBufferOffsetAlignment in its GPU owner (a view inside a merged or - // imported range) is bound from the aligned offset below it; `adjustment` is the difference, - // which the shader adds to every access (RecompileResult::memoryOffsetDword). VkDescriptorBufferInfo Descriptor(std::uint64_t address, std::size_t bytes, std::uint32_t& adjustment) const; std::vector AddressRanges() const; // The BDA table of the cached address space when it serves this upload alone (an address-based @@ -325,8 +316,6 @@ private: // Records the import-to-buffer copies of the given gpuCopy regions into the open batch, with // the barriers that order them after earlier recorded writes and before the shaders reading them. void recordGpuCopies(std::span copies, bool addressable); - // Keeps the bytes of every merged written range a writable heap mirror serves, as uploaded: the - // mirror keeps no shadow, and write-back stores only what the GPU changed from them. void takeHeapReferences(); Context context; bool stagingAllowed = false; @@ -342,7 +331,6 @@ private: // regions follow the registry's order), so AddSnapshot can search instead of scanning. bool regionsSorted = false; std::vector> writes; - // By the begin of the merged written range (see takeHeapReferences). std::vector>> heapReferences; // UploadPrepare ran (regions are frozen); `uploaded` once UploadFinish ran. bool prepared = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 4e74c0cac..aebce3e9b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -40,18 +40,12 @@ public: Pipeline(const Pipeline&) = delete; Pipeline& operator=(const Pipeline&) = delete; VkPipelineLayout Layout() const; - // The framebuffer of these attachment views: the color views, then the depth surface's when the - // pipeline has one. A resident target's view (`owners[i]` set) is stable while its - // StorageTexture lives, and a depth surface's while its device does, so framebuffers made of - // resident views are kept in the pipeline and reused when every owner is still the same - // object; any other set is built per call. std::shared_ptr AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent); // Begins the render pass on the framebuffer, binds the pipeline and sets viewport and scissor. void Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, VkExtent2D extent, const VkViewport& viewport, const VkRect2D& scissor) const; // The same inside a render pass another pipeline of the same attachments began (compatible by // construction: the attachment formats alone decide). void Continue(VkCommandBuffer commands, const VkViewport& viewport, const VkRect2D& scissor) const; - // An assembled block (AssemblePushConstants, then ShaderResources::PatchPushConstants). void PushConstants(VkCommandBuffer commands, VkShaderStageFlags stages, std::span bytes) const; // Forgets the Vulkan objects without destroying them: for entries of a device that is already gone. void Abandon() noexcept; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 176e0432c..02503c83c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -165,9 +165,6 @@ public: void FinishUpTo(std::uint64_t serial); // Submits and waits for every batch, running completions in order. void Sync(); - // Sample counting for PIXEL_PIPE_STAT_DUMP: once on (the first dump), every batch counts the - // samples its draws pass in an occlusion query begun outside any render pass, and adds them to - // SamplesPassed when it completes. Under the mutex; an open batch starts counting at once. void CountSamples(); static std::uint64_t SamplesPassed(); // Waits only for the batches up to the newest one that writes the range (submitting the open @@ -491,7 +488,6 @@ private: std::uint32_t queue = 0xffffffffu; std::chrono::steady_clock::time_point submittedAt{}; VkQueryPool queries = VK_NULL_HANDLE; - // The occlusion query counting this batch's samples (see CountSamples). VkQueryPool samples = VK_NULL_HANDLE; std::vector timedKeys; std::vector timedBytes; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 136607e0a..eaf43678a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -185,8 +185,6 @@ public: // compiled shader's descriptors. A recipe hit refreshes the template iff the two differ. std::uint64_t DataWordsHash() const { return dataWordsHash; } static std::uint64_t DataWordsHash(const CompiledShader& shader); - // Stores the byte offsets of the guest buffers bound below their views (GuestBufferMemory:: - // Descriptor) into a push constant block assembled from the shaders this object was built for. void PatchPushConstants(std::span bytes) const { for (const auto& [position, adjustment] : pushPatches) bytes[position] = static_cast(adjustment); } @@ -242,9 +240,6 @@ private: bool written = true; // Data buffers: the words the buffer holds once the recorded work ran (see RefreshData). std::vector dataWords; - // Guest buffers: how far below the view the descriptor starts (GuestBufferMemory:: - // Descriptor), and where in the push constant block the shader reads that (-1: the - // shader data is a buffer). std::uint32_t adjustment = 0; std::int32_t pushByte = -1; }; @@ -410,7 +405,6 @@ private: // FNV-1a offset basis: the hash of no data buffers (DataWordsHash). std::uint64_t dataWordsHash = 14695981039346656037ull; void rehashDataWords(); - // The nonzero guest buffer adjustments by push constant byte (PatchPushConstants). std::vector> pushPatches; BuildTiming timing; // Build state carried from stage A to stage B: the bindings in plan order, the image bindings diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index c4e32973a..94c91a071 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -40,10 +40,6 @@ struct ColorTarget { // DCC metadata of a compressed target (CB_COLOR_INFO DCC_ENABLE), or 0 (see DccMetadata.hpp). std::uint64_t dccAddress = 0; bool dccAlphaOnMsb = false; - // A view of one mip of a mipmapped surface (CB_COLOR_VIEW MIP_LEVEL, CB_COLOR_ATTRIB2 MAX_MIP): - // the surface's base, mip 0 extent and mip count, the rendered mip, and whether it lies in the - // packed mip tail (which shares one block with the smaller mips, so it is only rendered through - // the resident image of the whole chain). `address`, `extent` and `bytes` are the mip's. std::uint64_t surfaceAddress = 0; VkExtent2D surfaceExtent{}; std::uint32_t mipCount = 1; @@ -51,22 +47,17 @@ struct ColorTarget { bool mipTail = false; }; -// The depth/stencil surface a draw with depth or stencil tests renders with (DB_Z_INFO, -// DB_STENCIL_INFO and their bases). Its image lives on the GPU only (DepthSurface.hpp). struct DepthTarget { std::uint64_t address; - // The stencil plane, 0 without one. std::uint64_t stencilAddress; VkExtent2D extent; VkFormat format; - // DB_DEPTH_CLEAR / DB_STENCIL_CLEAR: what a new image starts as. float clearDepth; std::uint8_t clearStencil; }; struct State { ShaderStages stages; - // Set when the draw tests depth or stencil against a bound surface; the tests below are off otherwise. std::optional depth; bool depthTest = false; bool depthWrite = false; @@ -94,7 +85,6 @@ struct State { ShaderStages DecodeShaderStages(const QueueState& queue); State DecodeState(const QueueState& queue); -// The component mapping of each MRT slot's export (identity past the written slots). std::array ExportMappings(const State& state); // The message DecodeState (or the pixel stage decode after it) would throw for the register rules // this precheck covers, evaluated without exceptions before the draw is decoded; empty when they @@ -132,11 +122,6 @@ struct DrawKeyRange { std::uint32_t count; }; inline constexpr std::array DrawKeyRegisters{{ - // DB_RENDER_CONTROL, DB_DEPTH_VIEW, DB_DEPTH_SIZE_XY, DB_STENCIL_CLEAR/DB_DEPTH_CLEAR with - // PA_SC_SCREEN_SCISSOR, DB_Z_INFO .. DB_STENCIL_WRITE_BASE and their high words; the window - // offset/scissor and clip rect, the edge rule, the hardware screen offset, CB_TARGET_MASK/ - // CB_SHADER_MASK, the generic and viewport 0 scissors, the viewport 0 depth clamp, the blend - // constants, DB_STENCIL_CONTROL/DB_STENCILREFMASK(_BF), the viewport 0 transform. {RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 1}, {RegisterBank::Context, 0x007, 1}, {RegisterBank::Context, 0x00a, 4}, {RegisterBank::Context, 0x010, 6}, {RegisterBank::Context, 0x01a, 4}, {RegisterBank::Context, 0x080, 4}, {RegisterBank::Context, 0x08c, 4}, {RegisterBank::Context, 0x090, 2}, {RegisterBank::Context, 0x094, 2}, {RegisterBank::Context, 0x0b4, 2}, {RegisterBank::Context, 0x105, 4}, {RegisterBank::Context, 0x10b, 3}, {RegisterBank::Context, 0x10f, 6}, // SPI_PS_INPUT_CNTL_0..31, SPI_PS_INPUT_ENA/ADDR, SPI_PS_IN_CONTROL, SPI_SHADER_POS/Z/COL_FORMAT, diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 640f3e9ab..6fc5ea467 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -57,8 +57,6 @@ public: // compute pass writing it and the next pass sampling it share one image and copy nothing. // CanCopyFrom says whether the two descriptors address the same surface compatibly. Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components); - // A view of one plane (aspect) of a depth surface's image, which stays in the general layout; - // the surface (DepthSurface.hpp) owns the image and its views. Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components); static bool CanCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor); ~Texture(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 7268aea59..22bc67a98 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -255,9 +255,6 @@ void BdaResources::CheckFault() const { std::ostringstream message; message << "BDA access failed: address=0x" << std::hex << report.address << " instruction=0x" << report.instruction << std::dec << " bytes=" << report.bytes << " stage=" << report.stage << " reason=" << static_cast(report.reason); if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::Permission && table != nullptr) { - // Only a writable range imported in place is writable through the table (see - // GuestBufferMemory::addressRange): a store into a range served by a mirror or a copy would - // not reach guest memory, so it faults. Name the range and how it is served. const auto bytes = table->Bytes(); ShaderRecompiler::BdaAbi::Header header{}; std::memcpy(&header, bytes.data(), sizeof(header)); @@ -276,10 +273,6 @@ void BdaResources::CheckFault() const { namespace AgcDriver::Graphics { -// ponytail: the pages are marked once the work completed, so a CPU read of them before that (a -// capture or texture upload of later work in the same batch) is not made to wait for the stores, -// as descriptor-bound writes are (Recorder::NotePendingWrites); note the V#s' ranges at record -// time if that shows up. void BdaResources::markWrittenPages() const { namespace Abi = ShaderRecompiler::BdaAbi; auto* words = reinterpret_cast(fault->Bytes().data()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp index 1af888ca6..4c74d01bd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp @@ -33,7 +33,6 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, std::uint32_t paddedHeight = height; switch (mode) { case ColorTileMode::Linear: { - // Rows are padded to 256 bytes, addrlib's linear pitch alignment (linear textures use the same). const auto pitchAlignment = 256u / bytesPerElement; pitch = (width + pitchAlignment - 1u) / pitchAlignment * pitchAlignment; break; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 888197768..f8ae28b76 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -52,8 +52,6 @@ public: viewInfo.format = target.format; viewInfo.subresourceRange = {aspects, 0, 1, 0, 1}; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth"); - // Cleared in the general layout it keeps from then on, recorded ahead of the draw (a draw's - // pass begins behind a barrier from every earlier transfer write). auto* recorder = Recorder::Active(); std::unique_ptr batch; if (recorder == nullptr) batch = std::make_unique(context); @@ -82,7 +80,6 @@ public: DepthSurface(const DepthSurface&) = delete; DepthSurface& operator=(const DepthSurface&) = delete; - // The texture sampling the plane at the descriptor's base address, one per descriptor. std::shared_ptr Sampled(std::span words, const GuestTextureResource& resource, VkComponentMapping components) { std::array key{}; std::copy_n(words.begin(), std::min(words.size(), 8), key.begin()); @@ -95,8 +92,6 @@ public: const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; const auto format = ResolveTextureFormat(resource.format); - // A 32-bit integer read of a D32 plane takes the float view: the shader keeps the bits - // (ShaderRecompiler::IsDepthBitsTexture). const bool depthBits = !stencil && !d16 && words.size() >= 4 && ShaderRecompiler::IsDepthBitsTexture(words[1], words[3]); if ((format != expected && !depthBits) || resource.dimension != TextureDimension::k2D || resource.width != target.extent.width || resource.height != target.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || resource.baseArray != 0) { char text[448]; @@ -139,7 +134,6 @@ std::mutex& surfacesMutex() { return mutex; } -// Never destroyed with the statics: the images belong to a device that may be gone by then. std::vector>& surfaces() { static auto* list = new std::vector>(); return *list; @@ -164,7 +158,6 @@ void ClearDepthSurfaces(VkDevice device) { std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components) { std::lock_guard lock(surfacesMutex()); const auto& list = surfaces(); - // The newest surface made at the address. const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); }); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 98df34f00..8b46b0812 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -42,7 +42,6 @@ std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) { // The color buffer as a single-mip 2D surface descriptor (tile mode SW_64KB_R_X). GuestTextureResource SurfaceForTarget(const ColorTarget& color) { Require(color.tileMode == ColorTileMode::RenderTarget, "only 64 KiB tiled color targets are resident"); - // A mipmapped target is its whole chain; the attachment view picks the mip. const bool chain = color.mipCount > 1; GuestTextureResource surface{}; surface.baseAddress = chain ? color.surfaceAddress : color.address; @@ -503,7 +502,6 @@ void CheckBufferAliases(std::span shaders, const ColorTarg const ShaderRecompiler::ShaderBufferResource descriptor{{words[offset], words[offset + 1], words[offset + 2], words[offset + 3]}}; const auto address = descriptor.Base48(); const auto size = descriptor.GetSize(); - // A null V# binds nothing (ShaderResources::addGuestBuffer). if (size == 0 || address == 0) continue; Require(!overlap(address, size, target.address, target.bytes), "shader buffer aliases the render target"); Require(!overlap(address, size, indexAddress, indexBytes), "writable shader buffer aliases the index buffer"); @@ -1140,8 +1138,6 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam if (continued) { record.pipeline->Continue(commands, state.viewport, state.scissor); } else { - // Depth surfaces stay in the general layout too; an earlier pass's depth writes (the recorder's - // trailing barrier after a pass does not name them) are made visible here. const VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT}; context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, 0, 1, &before, 0, nullptr, 0, nullptr); countBarrier(1); @@ -1331,7 +1327,6 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& binding.target = std::make_unique(context, color, state.blends[index].blendEnable != 0); targetViews.push_back(binding.target->View()); } - // The depth surface is the last attachment (Pipeline::AcquireFramebuffer). if (state.depth) targetViews.push_back(DepthSurfaceView(context, *state.depth)); timer.phase(PhasePrepare); const bool recordDraws = RecordDraws(); @@ -1477,7 +1472,6 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // only a template the resource cache serves under this content key (reusable: no lease, // no copied writes, every direct region import- or mirror-served) of a direct draw, so a // hit's proof is the template's ProveCurrent and nothing needs completion work. - // ponytail: none for depth draws (the recipe replays color targets only); add the depth view when they need the hit path. if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !state.depth) { auto recipe = std::make_shared(); recipe->device = context.device; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index e7f6beb9a..8b8846c77 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -36,19 +36,11 @@ namespace AgcDriver::Graphics { // mprotect, which replaces the registered Range object, and a mirror whose Range is gone is rebuilt. // Writable ranges keep a shadow of the guest bytes and copy the 64 KiB blocks that differ on each use. // A mirror never holds the Range shared_ptr: a pinned range makes the guest's mprotect spin and fail. -// Heap mirrors serve the readable registered heap ranges the host imports did not take (past -// APS5_HOST_IMPORT_MIB, or refused by the driver), which each address-based build copied instead. -// They keep no shadow: the arena's write tracking says which 64 KiB blocks changed since the mirror -// took them, and those blocks are read again. A writable one is written back per build from the -// bytes its written ranges held at upload (takeHeapReferences); stores through the BDA table fault -// there, as in a copy. struct ImageMirror { std::uint64_t base = 0; std::uint64_t bytes = 0; bool writable = false; bool heap = false; - // Heap mirrors only: per tracker block of the range (block 0 holds `base`), the generation - // collected before the block was last read into the buffer. std::vector generations; std::weak_ptr range; std::shared_ptr buffer; @@ -316,12 +308,9 @@ struct ImageMirrors { VkDevice device = VK_NULL_HANDLE; std::map> entries; std::set failed; - // Bytes of the heap mirrors in `entries` (against APS5_HEAP_MIRROR_MIB). std::uint64_t heapBytes = 0; // APS5_PROFILE_DRAW counters, reported every 10 s as [buffers] image mirrors. std::uint64_t builds = 0; - // Heap mirrors read again because their range changed, and heap ranges left to copies because - // the arena does not track their writes. std::uint64_t heapRefills = 0; std::uint64_t heapUnwatched = 0; std::uint64_t subranges = 0; @@ -377,7 +366,6 @@ bool mirrorsEnabled() { return !disabled; } -// The heap mirrors' limit: past it the process stops rather than copying gigabytes per dispatch. std::uint64_t heapMirrorBudget() { static const std::uint64_t bytes = [] { const char* value = std::getenv("APS5_HEAP_MIRROR_MIB"); @@ -401,14 +389,10 @@ struct RefreshBlock { ImageMirror* mirror; std::uint64_t address; std::size_t length; - // Heap mirrors: the generation the block's copy makes current, stored only once the copy ran, - // so a build that throws before its copies leaves the block stale in the tracker's eyes too. std::uint64_t generation = 0; }; // Compares a block's guest bytes with the mirror's shadow and copies a changed block into the device -// buffer and the shadow (a heap mirror's block is known to have changed and is copied). Blocks are -// distinct, so blocks of one refresh can be compared in parallel. bool compareBlock(const RefreshBlock& block) { const auto offset = static_cast(block.address - block.mirror->base); const auto* guest = reinterpret_cast(block.address); @@ -520,9 +504,6 @@ private: // completes first, so its results reach the guest and the shadow before the compare and no batch // writes a block being replaced; GPU reads of the mirror by earlier recorded work (an address-based // dispatch whose lease is released at completion) see the newer bytes, as they would on hardware. -// prepareRange does it for [address, address + bytes), prepareRefresh for a writable image mirror and -// returns whether there is anything to compare. Heap mirrors are prepared a run of adjacent ranges at -// a time, before their write tracking is asked what changed (refreshHeapMirrors). void prepareRange(std::uint64_t address, std::uint64_t bytes) { // The compare reads the pages directly; a mirror is only reused while its Range exists, but a // lease of a build in flight can keep a replaced Range alive past a protection change. @@ -574,11 +555,6 @@ void refreshMirror(ImageMirror& mirror, std::uint64_t address, std::uint64_t byt compareBlocks(blocks); } -// Appends to `blocks` the blocks of one build's heap mirrors that a CPU or driver store changed -// since the mirrors took them. The mirrors of adjacent ranges are prepared and collected as one run: a -// collect walks the run's page tables at a fixed cost of microseconds, which paid per mirror (thousands -// on a story-mode load) was most of an address-based build. The copies follow and store the run's -// collected generation (see RefreshBlock). void refreshHeapMirrors(std::vector& mirrors, std::vector& blocks) { constexpr std::uint64_t block = 65536; std::sort(mirrors.begin(), mirrors.end(), [](const ImageMirror* left, const ImageMirror* right) { return left->base < right->base; }); @@ -613,9 +589,6 @@ void refreshHeapMirrors(std::vector& mirrors, std::vector acquireMirror(const Context& context, const std::shared_ptr& range, std::vector& blocks, bool heap) { auto& state = Mirrors(); std::shared_ptr mirror; @@ -635,10 +608,6 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh if (!heap && prepareRefresh(*mirror, range->address, range->bytes)) appendBlocks(blocks, *mirror, range->address, range->bytes); return mirror; } - // The registered flags were taken from the pages; an image range that disagrees is copied by this - // build. A heap mirror needs readable pages only: a writable range may hold read-only pages (a - // direct memory page mapped at several addresses is read-only in every view), whose bytes the GPU - // reads like the others and whose write-back storeChanged refuses. if (!GuestMemory::Accessible(reinterpret_cast(range->address), range->bytes, range->writable && !heap)) return nullptr; mirror = std::make_shared(); mirror->base = range->address; @@ -647,7 +616,6 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh mirror->heap = heap; mirror->range = range; if (heap) { - // Pending GPU results reach the range before the collect, so the fill below is current. prepareRefresh(*mirror, range->address, range->bytes); const auto generation = GuestMemory::CollectWrites(range->address, range->bytes); constexpr std::uint64_t block = 65536; @@ -687,9 +655,6 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh return mirror; } -// The image mirror containing [begin, end) whose registered range still exists, or null. A mirror -// whose Range was replaced is only rebuilt by the next address-based build; until then the range is -// copied. Heap mirrors are not given out: only an address-based build refreshes them. std::shared_ptr findMirror(const Context& context, std::uint64_t begin, std::uint64_t end) { auto& state = Mirrors(); std::lock_guard lock(state.mutex); @@ -1002,7 +967,6 @@ void GuestBufferMemory::AcquireRegistered() { // APS5_NO_SORTED_SNAPSHOT_LOOKUP=1: AddSnapshot scans the regions as before instead of searching. static const bool sortedLookup = std::getenv("APS5_NO_SORTED_SNAPSHOT_LOOKUP") == nullptr; std::vector blocks; - // The build's heap mirrors, refreshed together before the blocks are copied. std::vector heaps; bool mirrored = false; auto& spaces = Spaces(); @@ -1060,7 +1024,6 @@ void GuestBufferMemory::AcquireRegistered() { // The import lock is taken per range: a mirror's preparation may wait for recorded work, which // must not happen under it. regions.reserve(lease.size()); - // The ranges left to copies, with why no import or mirror serves them (the copy limit below). std::vector copies; for (const auto& range : lease) { if (!range->readable) continue; @@ -1081,8 +1044,6 @@ void GuestBufferMemory::AcquireRegistered() { continue; } if (mirrorsEnabled()) { - // Main image and heaps: served by a persistent mirror (see ImageMirror); the writable - // image ones are compared with guest memory below, all at once. region.mirror = acquireMirror(context, range, blocks, range->releasable); lap(timing.mirrorsUs, at); if (region.mirror != nullptr) { @@ -1099,13 +1060,10 @@ void GuestBufferMemory::AcquireRegistered() { committed = 0; for (const auto& [first, last] : copied.backed) committed += last - first; } - // A heap range with readable pages is mirrored unless the arena does not track its writes. const char* reason = !mirrorsEnabled() ? "mirrors disabled by APS5_NO_LEASE_MIRROR" : copied.sparse ? "unreadable pages" : !range->releasable ? "image mirror refused" : "outside the write-watched arena"; copies.push_back({copied.begin, copied.end, committed, reason}); } regionsSorted = sortedLookup; - // The ranges left to copies are copied by every build of this space; past the limit the - // process stops rather than copying them per dispatch. static const std::uint64_t copyLimit = [] { const char* value = std::getenv("APS5_ADDRESS_COPY_MAX_MIB"); return (value ? std::strtoull(value, nullptr, 10) : 64ull) << 20u; @@ -1189,7 +1147,6 @@ void GuestBufferMemory::addCopiedRange(const CopiedRange& range) { // cannot write them, so that equals a snapshot taken now without the extra copy. Reserved heaps // are registered whole while the guest commits pages on demand: committed pages only. region.hostBacked = !range.writable; - // Readable pages are copied in, a writable range's read-only ones included (see storeChanged). auto committed = GuestMemory::DescribeCommitted(range.begin, range.end - range.begin); if (!committed.whole) { region.sparse = true; @@ -1269,8 +1226,6 @@ void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t b auto committed = GuestMemory::DescribeCommitted(address, bytes); if (!committed.whole) { // A GPU heap bound whole while the guest commits its pages on demand, or a descriptor left - // pointing at unmapped memory: only readable pages are copied in and stored back (writable - // ones only, see storeChanged). Shaders must not touch the rest, which reads as zeros. region.sparse = true; region.backed = std::move(committed.ranges); } @@ -2110,7 +2065,6 @@ VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std: const auto base = region.direct != nullptr ? region.direct->base : region.mirror != nullptr ? region.mirror->base : region.begin; const auto offset = address - base; Require(context.limits.minStorageBufferOffsetAlignment != 0, "no storage buffer offset alignment"); - // At most 255 (the alignment is at most 256); the shader adds it in DWORDs. adjustment = static_cast(offset % context.limits.minStorageBufferOffsetAlignment); Require(adjustment % 4 == 0, "guest buffer view off the storage buffer offset alignment is not DWORD aligned"); Require(bytes + adjustment <= context.limits.maxStorageBufferRange, "guest buffer view exceeds descriptor range limit"); @@ -2122,9 +2076,6 @@ ShaderRecompiler::BdaAbi::Range GuestBufferMemory::addressRange(const Region& re Require(region.buffer != nullptr || region.direct != nullptr || region.mirror != nullptr, "incomplete guest GPU upload"); const auto address = region.direct != nullptr ? region.direct->address + (region.begin - region.direct->base) : region.mirror != nullptr ? region.mirror->buffer->DeviceAddress() + (region.begin - region.mirror->base) : region.buffer->DeviceAddress(); Require(region.end - region.begin <= std::numeric_limits::max() - address, "GPU address range overflow"); - // Stores through the table (GPU-selected V#s) land in guest memory only where the range is the - // allocation itself: a writable one served in place by its import. Mirrors and copies would - // drop them, so a store there faults. const auto permissions = ShaderRecompiler::BdaAbi::Read | (region.direct != nullptr && region.writable ? ShaderRecompiler::BdaAbi::Write : 0u); return {region.begin, region.end, address, permissions, 0}; } @@ -2178,10 +2129,6 @@ void GuestBufferMemory::MarkDirectWrites() const { namespace { -// Stores what the GPU changed in [address, address + current.size()) against `original`. Only the -// writable pages are stored: a read-only page of a writable range (a direct memory page mapped at -// several addresses is read-only in every view, aliased writes are not implemented) must come back -// unchanged, else the write-back fails rather than dropping the GPU's store. void storeChanged(std::uint64_t address, std::span current, std::span original) { const auto writable = GuestMemory::DescribeCommitted(address, current.size(), true); if (writable.whole) { @@ -2206,7 +2153,6 @@ void storeChanged(std::uint64_t address, std::span current, std if (cursor < address + current.size()) unchanged(cursor, address + current.size()); } -// The written ranges sorted, overlapping ones merged. std::vector> mergeWrites(std::vector> writes) { std::sort(writes.begin(), writes.end()); std::vector> merged; @@ -2257,8 +2203,6 @@ void GuestBufferMemory::WriteBack() { const auto length = static_cast(end - begin); const auto current = mirror.buffer->Bytes().subspan(offset, length); if (mirror.heap) { - // The build's reference instead of a shadow; the stores stamp the blocks, and the - // next refresh takes them back from guest memory. const auto reference = std::find_if(heapReferences.begin(), heapReferences.end(), [&](const auto& entry) { return entry.first == begin; }); Require(reference != heapReferences.end() && reference->second.size() == length, "heap mirror write-back has no reference"); storeChanged(begin, current, reference->second); @@ -2327,8 +2271,6 @@ std::optional>> GuestBufferM } for (const auto& region : regions) { // A read-only mirror is as fixed as an import: its bytes and VkBuffer never change while its - // Range exists (see ImageMirrorSerial). A writable one is refreshed and written back per build, - // a heap one refreshed per build. // A staged region is keyed by its import like one bound in place: the import is the source // of its copy-in and the destination of its copy-back, both recorded per use. const bool fixedMirror = region.mirror != nullptr && !region.mirror->writable && !region.mirror->heap; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 6db1798dc..a18b2f49a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -103,7 +103,6 @@ GuestTextureResource DecodeTextureResource(std::span words) Require(!prtDefColor, "guest texture descriptor uses a partially resident default color which is not implemented"); Require(arrayPitch == 0, "guest texture descriptor uses a nonzero array pitch which is not implemented"); Require(!msaaDepth, "guest texture descriptor uses MSAA which is not implemented"); - // DCC block sizes only shape compressed blocks, and surfaces are read as stored (below). static_cast(maxUncompBlkSize); static_cast(maxCompBlkSize); // Surfaces are always written uncompressed here (render targets and storage images bypass DCC), so diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 558cefb4f..efca469c4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -105,7 +105,6 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = static_cast(references.size()); subpass.pColorAttachments = references.empty() ? nullptr : references.data(); - // The depth surface stays in the general layout (DepthSurface.hpp). const VkAttachmentReference depthReference{static_cast(colors.size()), VK_IMAGE_LAYOUT_GENERAL}; if (state.depth) { VkAttachmentDescription depth{}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 19355f2d2..95e471b7a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -264,10 +264,7 @@ std::shared_ptr sampledStorageSource(const Context& context, con // `guestBytes` is the surface size when the caller described the surface already (0: described here). std::shared_ptr cachedTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components, std::uint64_t guestBytes = 0) { CaptureTrace::Log("sampled-lookup address=%llx width=%u height=%u dcc=%llx", static_cast(resource.baseAddress), resource.width, resource.height, static_cast(resource.dccAddress)); - // A depth surface's contents live in its GPU image; its guest memory is stale. if (auto depth = DepthSurfaceTexture(context, words, resource, components)) return depth; - // The recompiler reads such a T# through a float view (IsDepthBitsTexture), which only a depth - // surface provides. if (words.size() >= 4 && ShaderRecompiler::IsDepthBitsTexture(words[1], words[3])) { char text[160]; std::snprintf(text, sizeof(text), "AGC graphics: 32-bit integer read of the depth-layout texture 0x%llx, which is no depth surface drawn with, is not implemented", static_cast(resource.baseAddress)); @@ -829,7 +826,6 @@ void ShaderResources::buildPrepare(std::span shaders, cons const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; const bool atomic = element < binding.bufferAtomic.size() && binding.bufferAtomic[element]; const auto index = addGuestBuffer(std::span(binding.guestDescriptor).subspan(static_cast(element) * 4, 4), target, indexAddress, indexBytes, written, atomic); - // The element's byte offset in the shader data (RecompileResult::memoryOffsetDword). const auto& push = shader.program->pushConstants; if (!push.empty()) { const auto position = shader.program->memoryOffsetDword * 4u + element; @@ -2029,7 +2025,6 @@ std::size_t ShaderResources::addGuestBuffer(std::span words const auto address = descriptor.Base48(); const auto byteSize = descriptor.GetSize(); if (byteSize == 0 || address == 0) { - // A null V#: every access is out of bounds in the shader (BufferResource::empty). allocations.push_back({0, EmptyBufferBytes, false, nullptr, ShaderRecompiler::DescriptorRole::GuestBuffers, false}); return allocations.size() - 1; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 9c771a7ec..828f1e626 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -79,17 +79,12 @@ bool IgnoreDepthTest() { return ignore; } -// Whether DB_Z_INFO or DB_STENCIL_INFO names a plane (an absent register names none). bool depthSurfaceBound(const Registers& cx) { const auto z = find(cx, 0x010); const auto stencil = find(cx, 0x011); return (z != cx.end() && (z->second & 3u) != 0) || (stencil != cx.end() && (stencil->second & 1u) != 0); } -// One face of the stencil state (the compare function, DB_STENCIL_CONTROL's three operations and -// DB_STENCILREFMASK) as Vulkan's. AMD's compare functions are in Vulkan's order. Vulkan has one -// reference per face for the test and every replacement, so a replacement by another value -// (REPLACE_OP writes STENCILOPVAL, ONES 0xff) is taken where the bits it writes are not also compared. VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint32_t refMask, bool readOnly) { VkStencilOpState face{}; face.compareOp = static_cast(compare); @@ -98,8 +93,6 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint const auto test = refMask & 0xffu; const auto opValue = refMask >> 24u; auto reference = test; - // The reference bits already spoken for: the compared ones (none for NEVER and ALWAYS), then - // the replaced ones. auto fixed = compare == 0 || compare == 7 ? 0u : face.compareMask; const auto convert = [&](std::uint32_t op) { if (face.writeMask == 0) return VK_STENCIL_OP_KEEP; @@ -123,7 +116,6 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint case 6: case 8: case 9: - // ADD/SUB add STENCILOPVAL; Vulkan's increments add 1. if (opValue != 1) { std::snprintf(detail, sizeof(detail), "AGC graphics: stencil add/subtract of 0x%02x is unsupported", opValue); throw std::runtime_error(detail); @@ -142,15 +134,10 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint return face; } -// The depth/stencil surface and tests of a draw that tests depth or stencil with a plane bound. void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) { - // DEPTH/STENCIL_COMPRESS_DISABLE (bits 5, 6) and the ordering hints from bit 13 on do not change - // what is drawn. zero(cx, 0x000, 0x00001f9fu, "depth/stencil clear, copy, resummarize or decompress draws (DB_RENDER_CONTROL)"); const auto view = read(cx, 0x002); - // Z_READ_ONLY and STENCIL_READ_ONLY (bits 24, 25) turn the writes off. zero(cx, 0x002, ~0x03000000u, "depth array slices or mips (DB_DEPTH_VIEW)"); - // NUM_SAMPLES, PARTIALLY_RESIDENT, MAXMIP; the rest is tiling and compression the host image has none of. zero(cx, 0x010, 0x000f100cu, "multisampled, partially resident or mipmapped depth (DB_Z_INFO)"); zero(cx, 0x011, 0x00001000u, "partially resident stencil (DB_STENCIL_INFO)"); const auto zFormat = read(cx, 0x010) & 3u; @@ -171,7 +158,6 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) Require(!stencil || stencilReadOnly || base(0x015, 0x01d) == depth.stencilAddress, "stencil read and written at different addresses is unsupported"); const auto size = read(cx, 0x007); depth.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u}; - // A stencil-only surface gets an unused depth plane. depth.format = zFormat == 1 ? (stencil ? VK_FORMAT_D16_UNORM_S8_UINT : VK_FORMAT_D16_UNORM) : (stencil ? VK_FORMAT_D32_SFLOAT_S8_UINT : VK_FORMAT_D32_SFLOAT); depth.clearDepth = readFloat(cx, 0x00b); depth.clearStencil = static_cast(read(cx, 0x00a) & 0xffu); @@ -183,13 +169,10 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) if (result.stencilTest) { const auto ops = read(cx, 0x10b); result.stencilFront = stencilFace((depthControl >> 8u) & 7u, ops, read(cx, 0x10c), stencilReadOnly); - // BACKFACE_ENABLE gives back faces their own function, operations and masks. result.stencilBack = (depthControl & 0x80u) != 0 ? stencilFace((depthControl >> 20u) & 7u, ops >> 12u, read(cx, 0x10d), stencilReadOnly) : result.stencilFront; } } -// DB_DEPTH_CONTROL without a bound surface: depth and stencil tests whose function is ALWAYS and -// that write no depth cannot change color output, so they are accepted (stencil writes go nowhere). bool depthPassThrough(std::uint32_t depthControl) { const bool stencil = (depthControl & 1u) != 0; const bool depth = (depthControl & 2u) != 0; @@ -238,7 +221,6 @@ VkBlendOp blendOp(std::uint32_t value) { struct DecodedColorFormat { VkFormat format; std::uint32_t elementBytes; - // The exported component each attachment component stores, two bits each (0xe4 is identity). std::uint8_t componentMapping = 0xe4u; }; @@ -251,8 +233,6 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, throw std::runtime_error("AGC graphics: unsupported color format " + std::to_string(format) + " number type " + std::to_string(number) + " component swap " + std::to_string(swap)); }; const bool alternate = swap == 1; - // A one-component format stores the exported component COMP_SWAP names: R, G, B or A (an A8 - // target is COLOR_8 with SWAP_ALT_REV). const auto single = [&](VkFormat vkFormat, std::uint32_t bytes) { return DecodedColorFormat{vkFormat, bytes, static_cast((0xe4u & ~3u) | swap)}; }; if (swap > 1 && format != 1 && format != 2 && format != 4) return fail(); switch (format) { @@ -497,8 +477,6 @@ State DecodeState(const QueueState& queue) { for (std::uint32_t slot = 0; slot < slotCount; ++slot) { // Export formats only matter for the targets the draw writes. const auto slotExport = (exportFormat >> (4u * slot)) & 0xfu; - // FP16, UNORM16 and SNORM16 exports are unpacked to floats by the shader; 32_R, 32_GR, 32_AR and - // 32_ABGR are written as is, the components a format leaves out reading as 0 (alpha 1). if (slotExport == 0 || slotExport == 7 || slotExport == 8 || slotExport > 9) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " is unsupported"); const auto stride = slot * 0xfu; ColorTarget color{}; @@ -509,12 +487,8 @@ State DecodeState(const QueueState& queue) { const auto format = (info >> 2u) & 0x1fu; const auto decoded = DecodeColorFormat(format, number, swap); // ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written - // uncompressed and only its fast-clear keys matter (see DccMetadata.hpp). BLEND_BYPASS (bit 16) is - // checked against enabled blending below. if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")"); Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported"); - // CB_COLOR_VIEW (GFX10: SLICE_START 0-12, SLICE_MAX 13-25, MIP_LEVEL 26-29): MIP_LEVEL selects - // the rendered mip; array slices are not modeled. const auto view = read(cx, 0x31b + stride); Require((view & ~0x3c000000u) == 0, "color array views are unsupported"); const auto viewMip = (view >> 26u) & 0xfu; @@ -608,8 +582,6 @@ State DecodeState(const QueueState& queue) { APS5_LOG_OUT_DEBUG("Blend %u control=0x%x", slot, blend); Require((blend & 0x0000e000u) == 0, "reserved blend control bits"); VkPipelineColorBlendAttachmentState state{}; - // CB_TARGET_MASK names exported components; attachment component c stores exported component - // mapping[c], so its write bit and, when it stores alpha, its blend come from that component. const auto mapping = result.colors[slot].componentMapping; const auto exportedMask = (targetMask >> (4u * slot)) & 0xfu; for (std::uint32_t component = 0; component < 4; ++component) { @@ -665,7 +637,6 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (auto reason = indexed ? nonzero(queue.userConfig, 0x24b, ~0u, "primitive restart (GE_MULTI_PRIM_IB_RESET_EN)") : std::string(); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; if (value(cx, 0x200, word)) { - // Tests against a bound surface are decoded (and their limits checked) by DecodeState. const bool surface = (word & 3u) != 0 && depthSurfaceBound(cx); if (!surface && !depthPassThrough(word) && !IgnoreDepthTest() && (word & DepthControlMask) != 0) return zeroMessage(0x200, word, "depth, stencil or conditional color writes"); if (auto reason = require((word & 0xc0000008u) == 0, "depth bounds or depth-conditional color writes are unsupported"); !reason.empty()) return reason; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index 48506c345..656239486 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -291,8 +291,6 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) { SurfaceGeometry geometry; if (descriptor.dimension == TextureDimension::k3D && (descriptor.tileMode == TextureTileMode::kD64KBX || descriptor.tileMode == TextureTileMode::kR64KBX)) { - // addrlib keeps display and rotated swizzles thin for 3D (only Z and standard ones are thick): - // each depth slice is laid out as an array slice, its index XORed in through the equation. Require(descriptor.mipCount == 1, "mipmapped 3D textures are not implemented"); const auto depth = descriptor.depthOrLastArray + 1u; geometry.mips = ComputeMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, 1u); diff --git a/core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp b/core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp index 08c81ffb4..134e81a64 100644 --- a/core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp +++ b/core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp @@ -8,8 +8,6 @@ extern "C" { -// Tessellation runs as Vulkan tessellation stages, which keep hull-shader outputs and tessellation -// factors on the host GPU: the off-chip hull memory and the tessellation-factor ring are never read. int APS5_VABI sceAgcDriverSetHsOffchipParam(uint64_t value0, uint64_t value1, uint64_t value2) { (void)value0; (void)value1; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp index ee0209973..3af9d345d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp @@ -38,8 +38,6 @@ void RunBdaContractTests() { auto& block = program.CreateBlock(); block.AppendInstruction(&program.CreateValue(IrOpcode::Barrier, IrType::Void)); program.BlockOrder().push_back(&block); - // A program with workgroup barriers is accepted: its faulting reads return zero instead of - // ending the invocation, so every invocation still reaches the barriers. ValidateBdaTarget(program, target); AgcDriver::Graphics::Require(!BdaInvocationsMayStop(program), "a BDA program with barriers must keep its invocations running"); program.Resources().stage = IrShaderStage::TessellationControl; @@ -57,6 +55,5 @@ void RunBdaContractTests() { auto invalid = encoded; invalid[0] = invalid[0] == 'A' ? 'B' : 'A'; reject([&] { static_cast(serializer.Deserialize(invalid)); }, "request signature"); - // The signature followed by serialization version 99, in base64. reject([&] { static_cast(serializer.Deserialize("NVNQQWMAAAA=")); }, "serialization version"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp index bd5f7a609..484d60a7a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp @@ -9,7 +9,6 @@ #include #include -// Resources.cpp submits and reaps through the active recorder; these tests record nothing. namespace AgcDriver::Graphics { Recorder* Recorder::Active() { return nullptr; } void Recorder::Submit() { Require(false, "the device tests have no recorder"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 5b4028eeb..ea44c3f7a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -35,12 +35,7 @@ ShaderRecompiler::DescriptorBinding binding(Role role, std::uint32_t slot) { return {ShaderRecompiler::DescriptorKind::StorageBuffer, role, 0, slot, 1, {}, false}; } -// Heap ranges that no import serves are mirrored. A read-only one: the build binds the mirror, the -// next build with the range unchanged copies nothing into it, and the blocks a CPU write changed are -// read again. A writable one: a descriptor's store reaches guest memory at write-back, and only the -// bytes the GPU changed (a CPU store made while the GPU works stays). void heapMirrorTests(const Context& context, const BdaTestAccess& access) { - // Heap mirrors refresh from the arena's write tracking; without it they are never made. Require(GuestArena::GuestArenaAvailable_nid_postfix() && GuestArena::GuestArenaWriteWatched_nid_postfix(), "the heap mirror tests need the write-watched guest arena"); constexpr std::size_t bytes = 2 * 65536; void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536); @@ -56,8 +51,6 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) { else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); }; - // An address-based build that writes 32 bytes at `written` (nothing for 0), with `gpu` run - // between its upload and its write-back; returns the device address serving the range. const auto build = [&](std::uint64_t written, const std::function& gpu) { GuestBufferMemory leased(context); leased.AcquireRegistered(); @@ -71,7 +64,6 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) { leased.WriteBack(); return device; }; - // An address-based build sweeps the mirrors whose ranges are gone (an earlier test's too). const auto sweep = [&] { GuestBufferMemory leased(context); leased.AcquireRegistered(); @@ -113,8 +105,6 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) { Require(MirrorCounters().heapMirrors == before.heapMirrors, "the writable heap mirror outlived its range"); #ifdef _WIN32 - // A build that throws after queuing a mirror's changed blocks (another mirror's range became - // inaccessible) must not mark those blocks current: the next build copies them. { constexpr std::size_t half = 2 * 65536; void* pair = GuestArena::GuestArenaAllocate_nid_postfix(2 * half, 65536); @@ -145,7 +135,6 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) { const auto device = firstDevice(); first[5] = 0x66; DWORD previous = 0; - // A protection change outside the registry, as a heap decommit is: the page cache is told. Require(VirtualProtect(second, 65536, PAGE_NOACCESS, &previous) != 0, "cannot protect the second heap range"); GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(second), 65536); bool threw = false; @@ -166,10 +155,6 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) { } #endif #ifdef _WIN32 - // A writable range with a read-only page (a direct memory page mapped at several addresses is - // read-only in every view): the mirror holds the page's bytes, a store next to it is written - // back, and a GPU change inside it fails the write-back instead of being dropped. A descriptor's - // copy outside an address-based build holds the page's bytes too. { constexpr std::size_t size = 2 * 65536; void* raw = GuestArena::GuestArenaAllocate_nid_postfix(size, 65536); @@ -304,7 +289,6 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { Require(range.begin == snapshots[0].address && range.end == range.begin + source.size(), "64-bit guest address was truncated"); const auto fault = access.bytes(access.descriptor(5).buffer); for (const auto byte : fault) Require(byte == std::byte{}, "fault buffer was not initialized"); - // A store into a read-only table range names the range and why stores cannot reach it. const ShaderRecompiler::BdaAbi::Fault denied{ShaderRecompiler::BdaAbi::FaultState::Ready, ShaderRecompiler::BdaAbi::FaultReason::Permission, snapshots[0].address + 4, 4, 0, 0x88, 0}; std::memcpy(fault.data(), &denied, sizeof(denied)); reject([&] { resources.WriteBack(); }, "read-only in the BDA table"); @@ -372,7 +356,6 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { if (dropped.enabled) Require(dropped.waiterDrops == after.waiterDrops + 1 && LeaseCounters().cacheDrops == dropped.waiterDrops, "the free did not drop the cached address space"); } heapMirrorTests(context, access); - // The copies left to an address-based build: nothing within the limit, the largest first past it. Require(AddressCopyOverflow({{0x1000, 0x3000, 0x2000, "uncommitted pages"}}, 0x2000).empty(), "copies within the limit were refused"); const auto copies = AddressCopyOverflow({{0x1000, 0x2000, 0x1000, "not mirrored"}, {0x10000, 0x30000, 0x18000, "uncommitted pages"}}, 0x2000); Require(!copies.empty() && copies.find("0x10000+0x20000 (0.1 MiB committed, uncommitted pages)") < copies.find("0x1000+0x1000"), "the copy limit does not name the largest copy first"); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp index 157ec6e93..45d1d8f25 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp @@ -8,7 +8,6 @@ struct BdaTestAccess { std::function(VkBuffer)> bytes; std::function descriptor; - // The bytes of the buffer behind a device address, from that address on. std::function(VkDeviceAddress)> addressBytes; }; diff --git a/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp index 5b3c912d5..f95863fdc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp @@ -59,7 +59,6 @@ void RunColorTargetLayoutTests() { } reject([&] { layout.Detile(std::span(tiled).first(4), restored); }); reject([&] { layout.Tile(std::span(linear).first(4), tiled); }); - // Aligned by hand: a PE section cannot ask for 64 KiB alignment. static std::vector storage(2 * 65536); const std::span guest(reinterpret_cast((reinterpret_cast(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}), 65536); std::fill(guest.begin(), guest.end(), std::byte{0x6b}); diff --git a/core/libs/prx/libSceAgcDriver/tests/DppWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/DppWrite.cpp index 7955b83f1..0c5fccca7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DppWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DppWrite.cpp @@ -1,5 +1,3 @@ -// A DPP instruction's row and bank masks gate its VGPR write, not only its source read: the rows a -// mask leaves out keep their value (a scan adding the other row's total into the odd rows only). #include "ControlFlow/GraphBuilder.hpp" #include "ControlFlow/Structurizer.hpp" #include "RdnaDecoder/RdnaInstructionDecoder.hpp" @@ -12,7 +10,6 @@ using namespace ShaderRecompiler; int main() { - // v_add_nc_u32_dpp v1, v0, v1 quad_perm:[0,1,2,3] row_mask:0xa bank_mask:0xf; s_endpgm const std::vector code{0x4a0202fau, 0xaf00e400u, 0xbf810000u}; try { const auto decoded = RdnaInstructionDecoder{}.Decode(code); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 752475279..0b172c699 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -99,7 +99,6 @@ void stateTests() { queue.context[0x91] = 0x30020; state = AgcDriver::Graphics::DecodeState(queue); Require(state.scissor.offset.x == 3 && state.scissor.offset.y == 1 && state.scissor.extent.width == 29 && state.scissor.extent.height == 2, "scissor intersection changed"); - // DCC_ENABLE: the target is written uncompressed, its keys at CB_COLOR0_DCC_BASE. queue.context[0x31c] |= 0x10000000; queue.context[0x325] = 0x1234; Require(AgcDriver::Graphics::DecodeState(queue).color.dccAddress == 0x123400, "DCC key address decode changed"); @@ -110,11 +109,9 @@ void stateTests() { queue.context[0x3b8] |= 5u << 14u; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported color tile mode"); queue = makeState(); - // A linear width off the 256-byte pitch alignment is padded (rows of 64 texels here). queue.context[0x3b0] = (62u << 14u) | 3u; Require(AgcDriver::Graphics::DecodeState(queue).color.bytes == 64u * 4u * 4u, "padded linear pitch changed"); queue = makeState(); - // Slot 1 written by the shader needs an export format. queue.context[0x8e] = 0xff; queue.context[0x8f] = 0xff; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "color export format 0"); @@ -238,8 +235,6 @@ void DisabledColorTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); } -// Depth/stencil tests against a bound surface: Scaleform's mask pass (stencil ALWAYS, add 1) and -// content pass (stencil EQUAL), as a Scaleform menu draws them. void DepthStencilTests() { auto queue = makeState(); queue.context[0x000] = 0; @@ -270,35 +265,29 @@ void DepthStencilTests() { const auto& front = state.stencilFront; Require(front.compareOp == VK_COMPARE_OP_ALWAYS && front.passOp == VK_STENCIL_OP_INCREMENT_AND_CLAMP && front.failOp == VK_STENCIL_OP_KEEP && front.reference == 1 && front.writeMask == 0xff, "stencil mask pass changed"); Require(std::memcmp(&state.stencilBack, &front, sizeof(front)) == 0, "back faces without BACKFACE_ENABLE must use the front state"); - // The content pass: EQUAL to 2, nothing written. queue.context[0x10b] = 0; queue.context[0x10c] = 0x01ffff02; queue.context[0x200] = 0x00200211; state = AgcDriver::Graphics::DecodeState(queue); Require(state.stencilFront.compareOp == VK_COMPARE_OP_EQUAL && state.stencilFront.reference == 2 && state.stencilFront.passOp == VK_STENCIL_OP_KEEP, "stencil content pass changed"); - // Clearing to 0 with REPLACE_TEST under ALWAYS. queue.context[0x10b] = 0x00030030; queue.context[0x10c] = 0x01ffff00; queue.context[0x200] = 0x00700771; state = AgcDriver::Graphics::DecodeState(queue); Require(state.stencilFront.passOp == VK_STENCIL_OP_REPLACE && state.stencilFront.reference == 0 && std::memcmp(&state.stencilBack, &state.stencilFront, sizeof(state.stencilFront)) == 0, "stencil clear pass changed"); - // BACKFACE_ENABLE: back faces get STENCILFUNC_BF, the _BF operations and DB_STENCILREFMASK_BF. queue.context[0x200] = 0x007007f1; state = AgcDriver::Graphics::DecodeState(queue); Require(state.stencilBack.compareOp == VK_COMPARE_OP_ALWAYS && state.stencilBack.passOp == VK_STENCIL_OP_KEEP && state.stencilBack.writeMask == 0 && state.stencilBack.reference == 1, "back-face stencil state changed"); - // REPLACE_OP writing a value the test compares differently has no single Vulkan reference. queue.context[0x10b] = 0x40; queue.context[0x10c] = 0x05ffff02; queue.context[0x200] = 0x00200211; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement"); - // Adding anything but 1 has no Vulkan operation. queue.context[0x10b] = 0x50; queue.context[0x200] = 0x00700711; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil add/subtract"); queue.context[0x10b] = 0; queue.context[0x000] = 1; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DB_RENDER_CONTROL"); - // CB_COLOR_VIEW's mip level is bits 26-29. queue = makeState(); queue.context[0x31b] = 1u << 26u; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "mip exceeds"); @@ -334,7 +323,6 @@ void DepthClipTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "inverted viewport depth clamp"); queue.context[0xb4] = 0; for (std::uint32_t bit = 0; bit < 32; ++bit) { - // DX_CLIP_SPACE_DEF (19) and ZCLIP_NEAR/FAR_DISABLE (26, 27, depth clamping) are decoded. if (bit == 19 || bit == 26 || bit == 27) continue; queue.context[0x204] = 1u << bit; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "PA_CL_CLIP_CNTL"); @@ -666,7 +654,6 @@ void expectSingleFailure(const ShaderRecompiler::DescriptorBinding& binding, std expectResourceFailure(vertex, fragment, reason); } -// A binding the resources accept (building them throws nothing), leaving no Vulkan object behind. void expectSingleAccepted(const ShaderRecompiler::DescriptorBinding& binding, std::string_view what) { ShaderRecompiler::RecompileResult vertex; ShaderRecompiler::RecompileResult fragment; @@ -802,8 +789,6 @@ void resourceTests() { mutate(binding); return binding; }; - // Image and sampler bindings are implemented: a buffer's four-DWORD V# is no T#, and the mock - // context has no sampler slots, so a sampler binding exceeds its per-stage limit. expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestImages; binding.kind = Kind::SampledImage; }), "guest texture descriptor must contain 8 dwords"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestImages; binding.kind = Kind::StorageImage; }), "guest storage image descriptors must contain 8 dwords"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestSamplers; binding.kind = Kind::Sampler; }), "shader sampler descriptors exceed per-stage limits"); @@ -825,8 +810,6 @@ void resourceTests() { expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[3] |= 0x40000000u; }), "unsupported type"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x3fffu << 16u; binding.guestDescriptor[2] = 0xffffffffu; }), "descriptor range limit"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[2] = 8192; }), "descriptor range limit"); - // A V# left pointing at unmapped memory binds its committed pages only (none here); the rest reads - // as zeros (GuestBufferMemory::addDescriptorRegion), so it is not rejected. expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(0x1000), 8); }), "an unmapped V#"); expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(state.color.address), 64); }), "aliases the render target"); expectSingleAccepted(changed([](auto& binding) { binding.count = 3; binding.guestDescriptor = join(join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)), vsharp(reinterpret_cast(0x1000), 8)); }), "an unmapped V# element"); @@ -1051,8 +1034,6 @@ void rectListTests() { expectFailure([&] { static_cast(BuildRectListShaders(vertex, fragment, target)); }, "per-vertex interpolation"); fragment.fragmentParameters[0].perVertex = false; vertex.parameterExports.clear(); - // A fragment input the vertex shader does not export is built as a missing parameter (see - // RectListShaders.cpp), not rejected. auto unexported = BuildRectListShaders(vertex, fragment, target); Require(!unexported.control.spirv.empty() && !unexported.evaluation.spirv.empty(), "rect-list shaders with an unexported parameter are empty"); fragment.fragmentParameters.clear(); @@ -1234,8 +1215,6 @@ void validationTests() { } int main() { - // The guest allocation tests expect mutations of leased ranges to be refused; they need not wait - // the default minute for each refusal. Set before the registry first reads it. _putenv_s("APS5_PIN_WAIT_MS", "200"); try { { @@ -1273,7 +1252,6 @@ int main() { mock = MockVulkan{}; auto bdaContext = mockContext(); bdaContext.bufferDeviceAddress = true; - // The fault buffer (with its written-page slots) is larger than the mock's 4 KiB default. bdaContext.limits.maxStorageBufferRange = 1u << 27; RunBdaResourceTests(bdaContext, { [](VkBuffer buffer) -> std::span { return mock.memories.at(mock.bufferMemory.at(buffer)); }, @@ -1284,7 +1262,6 @@ int main() { throw std::runtime_error("missing BDA test descriptor"); }, [](VkDeviceAddress address) { - // The inverse of mockGetBufferDeviceAddress. const auto offset = address - 0x100000000000ULL; const auto buffer = reinterpret_cast(offset / 0x10000); return std::span(mock.memories.at(mock.bufferMemory.at(buffer))).subspan(offset % 0x10000); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp index 0e706600b..4af9a844c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp @@ -24,7 +24,6 @@ struct Fields { std::uint32_t filterMode = 0; bool disableDegamma = false; std::uint32_t minLodRaw = 0; - // MAX_LOD is 12 bits of unsigned 4.8 fixed point: 0xc00 is LOD 12. std::uint32_t maxLodRaw = 0xc00; std::uint32_t perfMip = 0; std::uint32_t perfZ = 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index be1fe3d42..64116f4a8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -114,8 +114,6 @@ void RunGuestTextureResourceTests() { tileModes.tileModeRaw = 0x09; Require(DecodeTextureResource(pack(tileModes)).tileMode == TextureTileMode::kStandard64KB, "tile mode 0x09 must decode to standard 64KB"); tileModes.tileModeRaw = 0x1b; - // An XOR swizzle folds a pipe/bank XOR into the low base bits; only 64 KiB aligned bases are - // modeled, so the fixture's unaligned base is refused and an aligned one decodes. rejectFields(tileModes, "pipe/bank XOR base"); tileModes.base40 = 0x120000ull; Require(DecodeTextureResource(pack(tileModes)).tileMode == TextureTileMode::RenderTarget64KB, "tile mode 0x1b must decode to render target 64KB"); @@ -220,14 +218,11 @@ void RunGuestTextureResourceTests() { badMsaa.msaaDepth = true; rejectFields(badMsaa, "MSAA"); - // DCC block size overrides only matter to DCC metadata, which is not read; they decode. Fields blockSize = base; blockSize.maxUncompBlkSize = 1; blockSize.maxCompBlkSize = 1; Require(DecodeTextureResource(pack(blockSize)).baseAddress == DecodeTextureResource(pack(base)).baseAddress, "DCC block size overrides changed texture storage"); - // DCC metadata decodes: its address only with metadata compression on, and where alpha sits; the - // pipe alignment, write compression and color transform flags do not change what a read returns. Fields meta = base; meta.metaPipeAligned = true; meta.writeCompress = true; @@ -250,7 +245,6 @@ void RunGuestTextureResourceTests() { badLevels.maxMip = 1; rejectFields(badLevels, "base mip level past its last mip level"); - // A view may stop before the surface's last level: the surface keeps its mip count. Fields partialMips = base; partialMips.lastLevel = 1; partialMips.maxMip = 2; diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 20c919a16..adaaafd60 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -343,8 +343,6 @@ void testMemorySynchronization() { } void testWriteChangedKeepsUntouchedBytes() { - // Write-back stores only the bytes the GPU changed: its other bytes are the snapshot, and the - // CPU may have written them since. alignas(256) static std::uint8_t guest[256]; std::memset(guest, 0, sizeof(guest)); std::vector original(sizeof(guest)), current(sizeof(guest)); diff --git a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp index ff4ec555f..197224a25 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp @@ -1,5 +1,3 @@ -// A selection in a loop with an arm that continues and another target it shares with an enclosing -// selection: its merge is where the other arm goes, so splitting the shared merge terminates. #include "ControlFlow/Structurizer.hpp" #include #include @@ -9,8 +7,6 @@ using namespace ShaderRecompiler; int main() { - // 0 -> 1 (loop header) -> 2; 2: 5 | 3; 3: 5 | 4; 4 -> 7 (continue); 5: 6 (exit) | 7; 6 returns; - // 7 -> 1. Around the back edge every path from 4 passes 5, so 5 post-dominates both selections. const std::vector> successors{{1}, {2}, {5, 3}, {5, 4}, {7}, {6, 7}, {}, {1}}; ControlFlowGraph graph; graph.entryBlock = 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp index 8afa74332..afda8cfc4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp @@ -88,15 +88,11 @@ void RunTextureTilingTests() { { const auto mips = ComputeMipLayout(TextureTileMode::RenderTarget64KB, 56, 257, 129, 1); Require(mips[0].blocksPerRow == 3 && mips[0].tiledSize == 393216, "render target surfaces must pad to complete 128 by 128 blocks for 32-bit pixels"); - // The detiler writes linear rows at the padded pitch (three 128-texel blocks), so the linear - // region spans whole padded rows. Require(mips[0].pitchBytes == 1536 && mips[0].linearSize == 1536u * 129u, "detiled render target rows must span the padded block width"); Require(ComputeSurfaceSize(mips, 6) == 2359296, "render target cube faces must retain the padded guest slice stride"); } for (const auto format : std::array{1, 7, 56, 71, 77}) { const auto mips = ComputeMipLayout(TextureTileMode::RenderTarget64KB, format, 1024, 513, 11); - // Linear offsets follow the tiled ones (the smallest regular level first), with the tail levels - // after the chain, so the ranges are checked for overlap in address order, not level order. std::vector> ranges; bool tailSeen = false; for (const auto& mip : mips) { @@ -113,11 +109,8 @@ void RunTextureTilingTests() { for (std::size_t index = 1; index < ranges.size(); ++index) Require(ranges[index].first >= ranges[index - 1].second, "detiled mip levels must occupy separate ranges"); Require(tailSeen && !mips.front().tail, "render target mip chains must cover both regular blocks and mip tails"); } - // Block-compressed formats are laid out in render target tiling too: a 64 by 64 texel level is one - // guest 64KB block. const auto compressed = ComputeMipLayout(TextureTileMode::RenderTarget64KB, 169, 64, 64, 1); Require(compressed.size() == 1 && compressed[0].tiledSize == 65536 && compressed[0].linearSize != 0, "block compressed render target layout is wrong"); - // Formats 128, 129 and 132 are laid out in render target tiling as well. Require(ComputeMipLayout(TextureTileMode::RenderTarget64KB, 132, 64, 64, 1).size() == 1, "format 132 render target layout is missing"); reject([] { ComputeMipLayout(TextureTileMode::RenderTarget64KB, 74, 64, 64, 1); }, "unsupported bytes per element"); diff --git a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp index 1cdb7a93e..dd73918e3 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp @@ -80,7 +80,6 @@ bool Replay(const char* path) { const auto result = ShaderRecompiler::Recompile(request.request); std::printf(" recompiled: %zu SPIR-V words\n", result.spirv.size()); if (g_spirv) { - // For spirv-val: .spv in the working directory. std::string name(path); name = name.substr(name.find_last_of("/\\") + 1) + ".spv"; std::ofstream(name, std::ios::binary).write(reinterpret_cast(result.spirv.data()), static_cast(result.spirv.size() * sizeof(result.spirv[0]))); diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 13c823926..4d6207075 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -97,7 +97,6 @@ struct Instance { std::uint64_t totalDecodedSamples = 0; SidebandGaplessDecode gapless{}; bool flagsReported = false; - // MP3: the FFmpeg decoder (it keeps the bit reservoir between frames) and the last frame's shape. AVCodecContext* mp3 = nullptr; std::uint32_t mp3Channels = 0; std::uint32_t mp3SampleRate = 0; @@ -246,7 +245,6 @@ std::vector JoinInputs(const JobHeader& job, const AjmBuffer* inpu return input; } -// The job's output buffers, filled in order. struct PcmOutputs { const AjmBuffer* buffers; std::uint32_t count; @@ -275,12 +273,10 @@ struct PcmOutputs { } }; -// Instance flags bits 7..9 select the PCM encoding: 0 16-bit, 1 32-bit integer, 2 float. std::uint32_t PcmEncoding(const Instance& instance) { return static_cast((instance.flags >> 7u) & 7u); } -// Emits one decoded frame of interleaved PCM after the instance's gapless skip and up to its sample limit. void EmitFrame(Instance& instance, PcmOutputs& outputs, const std::uint8_t* pcm, std::size_t samples, std::size_t channels, std::size_t sampleBytes) { std::size_t first = 0; std::size_t count = samples; @@ -298,7 +294,6 @@ void EmitFrame(Instance& instance, PcmOutputs& outputs, const std::uint8_t* pcm, instance.totalDecodedSamples += count; } -// The sidebands a run job asked for, in the SDK's order after its result. void WriteRunSideband(const JobHeader& job, const Instance& instance, std::int32_t result, std::size_t consumed, std::size_t produced, std::uint32_t frames, const SidebandFormat& format) { auto* sideband = static_cast(job.sideband); std::size_t offset = 0; @@ -321,8 +316,6 @@ void WriteRunSideband(const JobHeader& job, const Instance& instance, std::int32 } } -// Decodes AT9 frames from the concatenated inputs into the concatenated outputs in the instance's PCM -// encoding. void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { const auto input = JoinInputs(job, inputs); PcmOutputs pcmOutputs(outputs, job.outputCount); @@ -398,8 +391,6 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, SidebandFormat{static_cast(channels), ChannelMask(channels), static_cast(instance.info.samplingRate), encoding, 0, 0}); } -// An MPEG audio Layer III frame header: the frame's bytes and PCM shape; false when the bytes do not -// start one (free-format bitrates included). struct Mp3Frame { std::size_t bytes; std::uint32_t channels; @@ -437,8 +428,6 @@ void OpenMp3(Instance& instance) { if (!instance.mp3 || avcodec_open2(instance.mp3, codec, nullptr) < 0) throw std::runtime_error("AJM: cannot open the FFmpeg MP3 decoder"); } -// Decodes whole MP3 frames from the concatenated inputs. Like AT9 superframes, a frame is decoded only -// once all its bytes are present: a trailing partial frame stays unconsumed as partial input. void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { const auto input = JoinInputs(job, inputs); PcmOutputs pcmOutputs(outputs, job.outputCount); @@ -467,7 +456,6 @@ void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c if (frames == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } - // Not reference counted: the decoder copies the frame into a padded buffer of its own. packet->data = const_cast(input.data() + consumed); packet->size = static_cast(frame.bytes); if (avcodec_send_packet(instance.mp3, packet.get()) < 0) { @@ -620,7 +608,6 @@ int APS5_VABI sceAjmInstanceCreate(uint32_t context, uint32_t codec, uint64_t fl auto created = std::make_unique(); created->codec = codec; created->flags = flags; - // MP3 has no codec parameters: the stream's frame headers carry them, so it runs without an initialize job. if (codec == CODEC_MP3) { OpenMp3(*created); created->initialized = true; diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index 1740dea13..3e38e5ad2 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -17,7 +17,6 @@ static constexpr char TEMPORARY_MOUNT_POINT[] = "/temp0"; static constexpr char DOWNLOAD_MOUNT_POINT[] = "/download0"; static constexpr uint32_t TEMPORARY_DATA_OPTION_FORMAT = 1; -// Temporary data lives in /temp0 on the host and survives until the title formats it. static std::filesystem::path TemporaryDirectory(const AppContentMountPoint* mount_point) { if (!mount_point || std::strncmp(mount_point->data, TEMPORARY_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX; return ResolvePath_nid_no_patch(TEMPORARY_MOUNT_POINT); @@ -62,7 +61,6 @@ int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMount if (!mount_point || std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX; const std::uint64_t quotaKb = GetAppDownloadDataSizeMiB_nid_postfix() * 1024u; if (quotaKb == 0) throw std::logic_error(std::string(__func__) + ": the title declares no download data"); - // The system mounts the title's download data before boot; on the host it is /download0. const auto directory = ResolvePath_nid_no_patch(DOWNLOAD_MOUNT_POINT); std::filesystem::create_directories(directory); std::uint64_t usedKb = 0; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index c1fccfd6e..04a0fc850 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -331,7 +331,6 @@ int APS5_VABI sceAudioOutOpen(int userId, int type, int index, std::uint32_t len if (type != PORT_TYPE_VIBRATION) { openDevice(port); } - // APS5_TRACE_AUDIOOUT=1 reports the ports a title opens. static const bool trace = std::getenv("APS5_TRACE_AUDIOOUT") != nullptr; if (trace) std::fprintf(stderr, "[audioout] port %d: type %d, %u samples at %u Hz, format %d, device %u\n", i + 1, type, len, freq, static_cast(format), static_cast(port.device)); return i + 1; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp index 86e9524cb..0ee974536 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp @@ -46,12 +46,8 @@ struct AudioOut2Port { std::uint32_t dataFormat = 0; std::uint32_t samplingFreq = 0; std::uint32_t flags = 0; - // Channel count and sample type decoded from dataFormat: float samples, or 16-bit integers. std::uint32_t channels = 0; bool int16 = false; - // The PCM buffer (one grain, interleaved) the title last handed over through the data attribute. - // It is guest memory the title rewrites every tick, so it is read when the context mixes, not - // when it is set. const void* data = nullptr; float volume[AUDIO_OUT2_PORT_CHANNELS_MAX] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; std::uint64_t dataSets = 0; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp index ea2f8a169..2cc479fe1 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp @@ -22,9 +22,6 @@ static constexpr std::uint64_t ATTRIBUTE_TRACE_EVERY = 2000; static constexpr std::uint32_t ATTRIBUTE_DATA = 0; static constexpr std::uint32_t ATTRIBUTE_VOLUME = 1; -// data_format bits 8..11 carry the channel count, bits 0..6 the sample type (0 float, 1 16-bit -// integer) and bit 7 the standard 8-channel order, as Kyty decodes it: the titles open 0x100 (mono -// object ports), 0x200 (stereo), 0x880 (7.1 bed) and 0x201 (a Bink movie's sound). static constexpr std::uint32_t FORMAT_CHANNELS_SHIFT = 8; static constexpr std::uint32_t FORMAT_CHANNELS_MASK = 0xFu; static constexpr std::uint32_t FORMAT_TYPE_MASK = 0x7Fu; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp index a8da0320c..9185d192e 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp @@ -10,7 +10,6 @@ namespace { -// Layout of the params titles pass to sceAudioOut2SpeakerArrayCreate. struct GuestVbapParams { const AudioOut2Position* positions; std::uint32_t numSpeakers; @@ -24,7 +23,6 @@ constexpr std::uint32_t MaxSpeakers = 32; constexpr std::uint32_t AmbisonicsChannelFlag = 0x40; constexpr std::uint32_t MaxAmbisonicsOrder = 5; -// Lives in the memory the title allocates from sceAudioOut2GetSpeakerArrayMemorySize; the handle points at it. struct SpeakerArray { std::uint32_t magic; std::uint32_t numSpeakers; @@ -38,7 +36,6 @@ SpeakerArray& Array(AudioOut2SpeakerArrayHandle handle) { return *array; } -// Real SN3D spherical harmonic for ACN index `acn` (no Condon-Shortley phase), frame x front, y left, z up. float Sn3d(std::uint32_t acn, const AudioOut2Position& direction) { const int n = static_cast(std::sqrt(static_cast(acn))); const int m = static_cast(acn) - n * n - n; @@ -87,12 +84,9 @@ int APS5_VABI sceAudioOut2SpeakerArrayDestroy(AudioOut2SpeakerArrayHandle handle return 0; } -// Basic sampling decoder: speaker gain = sum over channels of Y(speaker) / numSpeakers, so a stereo pair decodes -// first order as two cardioids. Orders the layout cannot resolve (2D rule: 2N+1 speakers) get zero. int APS5_VABI sceAudioOut2GetSpeakerArrayAmbisonicsCoefficients(AudioOut2SpeakerArrayHandle handle, uint32_t ambisonics_channel, float* coefficients, uint32_t num_coefficients) { const auto& array = Array(handle); if (coefficients == nullptr || num_coefficients != array.numSpeakers) APS5_INVALID_ARG_EX; - // ponytail: only the 0x40-flagged encoding seen so far (assumed ACN/SN3D); other encodings throw until seen. const std::uint32_t acn = ambisonics_channel & ~AmbisonicsChannelFlag; if ((ambisonics_channel & AmbisonicsChannelFlag) == 0 || acn >= (MaxAmbisonicsOrder + 1) * (MaxAmbisonicsOrder + 1)) { throw std::invalid_argument("unsupported ambisonics channel " + std::to_string(ambisonics_channel)); diff --git a/core/libs/prx/libSceContentDelete/Export.cpp b/core/libs/prx/libSceContentDelete/Export.cpp index 7d77a1ffb..16dd322fb 100644 --- a/core/libs/prx/libSceContentDelete/Export.cpp +++ b/core/libs/prx/libSceContentDelete/Export.cpp @@ -7,7 +7,6 @@ #include namespace { -// Deletes exported content; the lifecycle is honest, deletion is not implemented. std::atomic g_initialized{false}; } diff --git a/core/libs/prx/libSceContentExport/Export.cpp b/core/libs/prx/libSceContentExport/Export.cpp index a7a90886b..aac09f3d1 100644 --- a/core/libs/prx/libSceContentExport/Export.cpp +++ b/core/libs/prx/libSceContentExport/Export.cpp @@ -7,7 +7,6 @@ #include namespace { -// Exports to the system gallery; the lifecycle is honest, exports themselves are not implemented. std::atomic g_initialized{false}; } diff --git a/core/libs/prx/libSceContentSearch/Export.cpp b/core/libs/prx/libSceContentSearch/Export.cpp index 0bd423b39..cfb70402a 100644 --- a/core/libs/prx/libSceContentSearch/Export.cpp +++ b/core/libs/prx/libSceContentSearch/Export.cpp @@ -7,7 +7,6 @@ #include namespace { -// Searches the system content library; the lifecycle is honest, searches are not implemented. std::atomic g_initialized{false}; } diff --git a/core/libs/prx/libSceGameLiveStreaming/Export.cpp b/core/libs/prx/libSceGameLiveStreaming/Export.cpp index a0401f1bb..be247e398 100644 --- a/core/libs/prx/libSceGameLiveStreaming/Export.cpp +++ b/core/libs/prx/libSceGameLiveStreaming/Export.cpp @@ -6,7 +6,6 @@ extern "C" { int APS5_VABI sceGameLiveStreamingInitialize(size_t heap_size) { - // Nothing is ever broadcast; the library only has to accept its lifecycle. if (heap_size == 0) APS5_INVALID_ARG_EX; return 0; } diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index c4303becc..b9d2bda7d 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -9,8 +9,6 @@ namespace { -// The host keyboard drives the virtual pad (docs/INPUT_MAPPING.md), so no USB keyboard is attached: -// opening the keyboard succeeds, as on a console with none plugged in, and it never reports events. std::mutex g_keyboardMutex; std::set g_openKeyboards; @@ -99,7 +97,6 @@ int APS5_VABI sceImeSetTextGeometry(TextAreaMode mode, const TextGeometry* geome int APS5_VABI sceImeUpdate(EventHandler handler) { if (!handler) APS5_INVALID_ARG_EX; - // Only keyboard events could be pending (sceImeOpen is not implemented), and none are generated. return 0; } diff --git a/core/libs/prx/libSceMsgDialog.native/Export.cpp b/core/libs/prx/libSceMsgDialog.native/Export.cpp index 208106db8..b2b70f0aa 100644 --- a/core/libs/prx/libSceMsgDialog.native/Export.cpp +++ b/core/libs/prx/libSceMsgDialog.native/Export.cpp @@ -6,7 +6,6 @@ #include "prx/libc/include/General.hpp" namespace { -// Common dialog status: 0 none, 1 initialized, 2 running, 3 finished. Nothing is ever opened here, so it never runs. std::atomic g_status{0}; } diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index 8de1356bd..695705242 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -14,7 +14,6 @@ #include #endif -// The base game is the full SKU; the add-ons the account owns come from anyps5-entitlements.ini (below). static constexpr int SCE_NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER = static_cast(0x80558003); static constexpr int SCE_NP_ENTITLEMENT_ACCESS_ERROR_NOT_FOUND = static_cast(0x80558007); static constexpr uint32_t SKU_FLAG_FULL = 3; @@ -33,10 +32,6 @@ std::filesystem::path EntitlementsPath() { #endif } -// The add-on entitlements the account owns: one unified entitlement label per line of -// anyps5-entitlements.ini next to the executable (ANYPS5_ENTITLEMENTS names another file), '#' and ';' -// starting comments. Without the file the account owns no add-on. They are reported as installed -// license-only add-ons (package type 3, download status 4), as Kyty reports them. const std::vector& OwnedAddons() { static const auto owned = [] { std::vector addons; diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index 87d0fd9f8..491eebc0c 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -128,28 +128,24 @@ void APS5_VABI sceNpRegisterGamePresenceCallback(void* callback, void* userdata) int APS5_VABI sceNpRegisterNpReachabilityStateCallback(void* callback, void* userdata) { (void)userdata; - // The signed-out state never changes, so registered callbacks never fire. if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; return 0; } int APS5_VABI sceNpRegisterPlusEventCallback(void* callback, void* userdata) { (void)userdata; - // The signed-out state never changes, so registered callbacks never fire. if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; return 0; } int APS5_VABI sceNpRegisterPremiumEventCallback(void* callback, void* userdata) { (void)userdata; - // The signed-out state never changes, so registered callbacks never fire. if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; return 0; } int APS5_VABI sceNpRegisterStateCallback(void* callback, void* userdata) { (void)userdata; - // The signed-out state never changes, so registered callbacks never fire. if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; return 0; } diff --git a/core/libs/prx/libSceRemoteplay/Export.cpp b/core/libs/prx/libSceRemoteplay/Export.cpp index ca410a355..1c3e5bbad 100644 --- a/core/libs/prx/libSceRemoteplay/Export.cpp +++ b/core/libs/prx/libSceRemoteplay/Export.cpp @@ -5,7 +5,6 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -// A PC has no Remote Play: the library initializes and every user stays disconnected. constexpr int REMOTEPLAY_CONNECTION_STATUS_DISCONNECT = 0; extern "C" { diff --git a/core/libs/prx/libSceSharePlay/Export.cpp b/core/libs/prx/libSceSharePlay/Export.cpp index 4b4f66b2b..d0289f4bb 100644 --- a/core/libs/prx/libSceSharePlay/Export.cpp +++ b/core/libs/prx/libSceSharePlay/Export.cpp @@ -6,7 +6,6 @@ extern "C" { int APS5_VABI sceSharePlayInitialize(void* heap, size_t heap_size) { - // No Share Play session can ever start; the library only has to accept its lifecycle. if (heap == nullptr || heap_size == 0) APS5_INVALID_ARG_EX; return 0; } diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 45104221d..fcc4fa910 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -85,8 +85,6 @@ public: if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; std::lock_guard lock(cfg->mutex); checkConfig(*cfg); - // The limit was waited for by WaitForFlipRoom before the driver took its lock; concurrent - // submitters may overshoot it by their own flips. if (info.index >= 0) { request->buffer = cfg->buffers[info.index]; require(request->buffer.Occupied(), "flip buffer is not registered"); @@ -507,7 +505,6 @@ void VideoOutDriver::presentLoop(std::stop_token token) { } if (current) { current.reset(); - // processFlip released the flip's reservation: a submitter may be waiting for room. flipQueue->changed.notify_all(); } } diff --git a/core/libs/prx/libSceVideoRecordingP/Export.cpp b/core/libs/prx/libSceVideoRecordingP/Export.cpp index 599945ffb..9400810ba 100644 --- a/core/libs/prx/libSceVideoRecordingP/Export.cpp +++ b/core/libs/prx/libSceVideoRecordingP/Export.cpp @@ -10,8 +10,6 @@ #include namespace { -// Recording metadata the title sets for the system recorder (titles, permission levels, guard -// areas); kept as given. Nothing records, so recording itself is not implemented. std::mutex g_infoMutex; std::map> g_info; } diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index 44e6e9967..735f0697e 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -9,8 +9,6 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -// A console with no headset and no voice peers: the library runs its lifecycle and keeps ports and -// their connections, but no voice ever arrives, so output ports stay empty. namespace { constexpr std::int32_t PortInDevice = 0; @@ -70,7 +68,6 @@ int APS5_VABI sceVoiceInit(VoiceInitParam* param, int32_t version) { } int APS5_VABI sceVoiceSetThreadsParams(void* params) { - // Thread priorities and affinities for the voice threads; this library runs none. if (params == nullptr) APS5_INVALID_ARG_EX; return 0; } @@ -176,8 +173,6 @@ int APS5_VABI sceVoiceGetPortInfo(uint32_t port_id, VoicePortInfo* info) { *info = VoicePortInfo{}; info->port_type = port.type; info->state = PortStateReady; - // Titles pace voice by frame (a title divides by the frame length it derives from this). - // ponytail: the codec's real frame layout is unknown; 20 ms frames at the port's bitrate. if (port.bitrate != 0) info->frame_size = (port.bitrate * VoiceFrameMs + 7999u) / 8000u; return 0; } @@ -196,7 +191,6 @@ int APS5_VABI sceVoiceReadFromOPort(uint32_t output_port_id, void* data, uint32_ auto& voice = State(); std::lock_guard lock(voice.mutex); if (IsInput(RequirePort(voice, output_port_id, __func__).type)) Fail(__func__, "port is not an output port"); - // Inputs are the absent microphone or WriteToIPort, which is not implemented: nothing to read. *size = 0; return 0; } diff --git a/core/libs/prx/libSceWebBrowserDialog/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index b26299820..416407839 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -6,7 +6,6 @@ #include "prx/libc/include/General.hpp" namespace { -// Common dialog status: 0 none, 1 initialized, 2 running, 3 finished. Nothing is ever opened here, so it never runs. std::atomic g_status{0}; } diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index d747e6f01..385b01298 100644 --- a/core/libs/prx/libc/include/GuestArena.hpp +++ b/core/libs/prx/libc/include/GuestArena.hpp @@ -5,7 +5,6 @@ #include // Guest virtual memory is placed inside one reserved arena below the PS5 application map limit -// (0xFC_0000_0000), in ascending first-fit order from the system-managed area like the PS5 kernel. Guest code indexes tables by // absolute address and breaks on host addresses outside that range. namespace GuestArena { diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index 6e6d0b18a..19d02b612 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -202,7 +202,6 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo AppendUtf8(utf8, static_cast(args.Next())); output.Value(spec + 's', utf8.c_str()); } else if (conversion == 's' && (length.empty() || length == "h")) { - // FreeBSD's printf, the console's, prints a null string as "(null)". const char* value = args.Next(); if (!value) value = "(null)"; output.Value(spec + 's', value); diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 59d445d6c..744b07805 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -18,7 +18,6 @@ namespace { -// Guest long is 64-bit; MinGW's is 32-bit, so integer "l" conversions need "ll" on the host. std::string HostLength(const std::string& length, char conversion) { #ifdef _WIN32 if (length == "l" && std::strchr("diouxXn", conversion) != nullptr) return "ll"; @@ -28,8 +27,6 @@ std::string HostLength(const std::string& length, char conversion) { return length; } -// Guest scanf, one directive at a time through the host sscanf, so the guest arguments can come from any -// va_list flavor. secure adds the _s rules: %s, %c and %[ take a buffer capacity after the pointer. template int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer nextPointer, NextCapacity nextCapacity) { if (buffer == nullptr || format == nullptr) return EOF; diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 8730c57f8..5d0dc4028 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -53,7 +53,6 @@ std::optional ResolveAlias(const std::string& guestPath) struct WorkingDirectory { std::mutex mutex; const std::filesystem::path root = std::filesystem::canonical(std::filesystem::current_path()); - // A PS5 process starts in /app0, so titles open game files by relative path. std::filesystem::path current = std::filesystem::is_directory(root / "app0") ? root / "app0" : root; }; WorkingDirectory& Directories() { static WorkingDirectory state; return state; } diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index 30a3896c4..8686e99a5 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -37,8 +37,6 @@ std::atomic generation{1}; std::atomic invalidator{nullptr}; std::atomic pinWaiter{nullptr}; -// How long a mutation of a leased range waits for the lease before it is refused: -// APS5_PIN_WAIT_MS, 60 s by default (tests that expect the refusal set it short). std::chrono::milliseconds pinWait() { static const std::chrono::milliseconds value{[] { const char* text = std::getenv("APS5_PIN_WAIT_MS"); @@ -210,9 +208,6 @@ void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* poi // this registry, so waiting with the lock held would deadlock them. The scan restarts after the // lock is retaken, since another mutation may have run meanwhile. Without a waiter the lease is // dropped by another thread (a synchronous draw or dispatch), so yielding suffices. A waiter - // round that finished nothing (the holder is not recorded GPU work) yields: the holder may be a - // driver thread inside a long Vulkan call (importing host memory takes a lease for its whole - // length), so every wait is bounded by the deadline only. auto* state = static_cast(mutation); const auto deadline = std::chrono::steady_clock::now() + pinWait(); int syncedRounds = 0; diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 19066a785..32efc3c02 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -15,10 +15,6 @@ namespace GuestArena { namespace { -// Mappings without an address go to the system-managed area from 0x2_0000_0000 first, as in Kyty and -// shadPS4; the application area from 0x10_0000_0000 is left to fixed mappings until that fills (titles -// map their virtual memory pages at a fixed window from 0x10_0000_0000). The system-reserved range -// between the two is never handed out. constexpr std::uintptr_t PreferredBase = 0x0000000200000000ull; constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull; constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull; diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 0556f6e34..2339acf46 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -98,7 +98,6 @@ void APS5_VABI srand_nid_postfix(unsigned int seed) { } -// Dinkumware float classification codes returned by _FDtest and _FExp. constexpr short FloatDenorm = -2; constexpr short FloatFinite = -1; constexpr short FloatInfinite = 1; @@ -116,7 +115,6 @@ short APS5_VABI _FDtest_nid_postfix(const float* x) { } } -// Stores y * e^(*x) * 2^eoff in *x and classifies the result as 0 (zero or underflow), infinite, or finite. short APS5_VABI _FExp_nid_postfix(float* x, float y, short eoff) { *x = static_cast(std::ldexp(static_cast(y) * std::exp(static_cast(*x)), eoff)); if (*x == 0.0f) return 0; diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index 0f4fc3d5c..794e11600 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -104,7 +104,6 @@ unsigned int APS5_VABI _ZSt14_Random_devicev_nid_postfix() { throw std::runtime_error("_Throw_Cpp_error: C++ thread error " + std::to_string(code)); } -// The title's std::error_category object carries its own Dinkumware vtable and string ABI. const void* APS5_VABI _ZSt16generic_categoryv_nid_postfix() { NotImplemented_nid_no_patch(__func__); return nullptr; diff --git a/core/libs/prx/libc/src/StdioExtras.cpp b/core/libs/prx/libc/src/StdioExtras.cpp index a1b8d21d6..3f82b12fd 100644 --- a/core/libs/prx/libc/src/StdioExtras.cpp +++ b/core/libs/prx/libc/src/StdioExtras.cpp @@ -51,7 +51,6 @@ int APS5_VABI sprintf_s_nid_postfix(char* buffer, size_t size, const char* forma std::abort(); } -// Dinkumware lconv as the title's libc lays it out: monetary strings, CHAR_MAX monetary flags, then numeric and message strings. struct GuestLconv { const char* monetary[7]; char monetaryFlags[14]; diff --git a/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp b/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp index 93a0ebdc8..dc5540138 100644 --- a/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp +++ b/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp @@ -22,7 +22,6 @@ AppTitle GetAppTitle_nid_postfix(); AppTitleId GetAppTitleId_nid_postfix(); bool HasAppIcon_nid_postfix(); AppIconData GetAppIconData_nid_postfix(); -// The download data quota param.json declares, in MiB (0 when none). std::uint64_t GetAppDownloadDataSizeMiB_nid_postfix(); } diff --git a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp index 08b60e434..a52b5da50 100644 --- a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp +++ b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp @@ -8,7 +8,6 @@ struct ParsedParamJson { std::string title; std::string titleId; - // The download data quota in MiB; 0 when the title declares none. std::uint64_t downloadDataSizeMiB = 0; }; diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 8510e7b63..8d53f8bdd 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -187,7 +187,6 @@ int APS5_VABI sceKernelAprResolveFilepathsToIdsAndFileSizes(const char** paths, return 0; } -// The prefix is prepended to every path; titles pass "" with absolute /app0 paths. int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizes(const char* prefix, const char** paths, uint32_t count, uint32_t* ids, uint64_t* sizes, uint32_t* error_index) { if (!prefix || !paths || !ids) return _fail(GUEST_EINVAL); for (uint32_t index = 0; index < count; ++index) { @@ -223,8 +222,6 @@ int APS5_VABI sceKernelAprSubmitCommandBuffer(const Apr::CommandBufferObject* bu return 0; } -// Command buffers run synchronously, so a submission is complete (result 0) before its id is returned -// and waiting on it has nothing left to do. int APS5_VABI sceKernelAprSubmitCommandBufferAndGetId(const Apr::CommandBufferObject* buffer, uint32_t priority, uint32_t* id) { if (!id) return _fail(GUEST_EINVAL); const int result = sceKernelAprSubmitCommandBuffer(buffer, priority); diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 27546e6c4..22ebff5a3 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -219,17 +219,12 @@ void Trace(const char* format, ...) { va_end(args); } -// Direct memory keeps its contents across mappings: titles unmap physical pages and map them again at -// another address (titles move their pools that way) and read back what they wrote through the old one. -// The contents move by copying at unmap and map; a shared section with views (as in Kyty) would alias -// them for real but loses the write watching of the guest arena. struct DirectMapping { std::uintptr_t end; std::uint64_t phys; }; std::mutex g_directLock; std::map g_directMappings; -// Contents of unmapped direct memory by physical page; all-zero pages are left out. std::map> g_physPages; void ProtectOrThrow(std::uintptr_t address, std::size_t len, int nativeProt) { @@ -248,13 +243,8 @@ void SaveContents(std::uintptr_t address, std::size_t len, std::uint64_t phys) { } } -// A physical page mapped at more than one address has the same contents in every view, and every -// view lacks write access while it is shared (g_shared). -// ponytail: a write to a shared page faults; handing the page to the writing view on the fault, with -// the GPU driver's imports of the other views, is the upgrade if a title writes through an alias. std::set g_shared; -// The views of physPage in g_directMappings. std::vector Views(std::uint64_t physPage) { std::vector views; for (const auto& [base, mapping] : g_directMappings) { @@ -273,9 +263,6 @@ int SharedProtection(int nativeProt) { return nativeProt & ~PROT_WRITE; } -// Takes the shared page out of its sharing; the views left keep sharing it, or the last one gets its -// own protection back. The views must still hold the same bytes: a write that reached one of them -// (the GPU writes through its imports of guest memory) would have been lost to the others. void Unshare(std::uintptr_t page, std::uint64_t physPage) { const auto others = Views(physPage); if (others.empty()) throw std::runtime_error("shared direct memory page has no other view"); @@ -293,7 +280,6 @@ void Unshare(std::uintptr_t page, std::uint64_t physPage) { ProtectOrThrow(others.front(), PS5_PAGE_SIZE, last ? prot : SharedProtection(prot)); } -// Takes [start, end) out of the direct mappings; with save its bytes stay as the contents of its pages. void EraseMappings(std::uintptr_t start, std::uintptr_t end, bool save) { auto it = g_directMappings.lower_bound(start); if (it != g_directMappings.begin() && std::prev(it)->second.end > start) --it; @@ -317,7 +303,6 @@ void EraseMappings(std::uintptr_t start, std::uintptr_t end, bool save) { } } -// Records a fresh mapping of phys at address (zero-filled, already protected nativeProt) and brings its contents back. void AddMapping(std::uintptr_t address, std::size_t len, std::uint64_t phys, int nativeProt) { std::vector> shared; for (const auto& [base, mapping] : g_directMappings) { @@ -355,7 +340,6 @@ bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr, const auto address = reinterpret_cast(addr); std::lock_guard lock(g_directLock); EraseMappings(address, address + len, true); - // The range shows the physical pages' contents now, not what was mapped there before. #ifdef _WIN32 if (physStart >= 0 && !VirtualFree(addr, len, MEM_DECOMMIT)) throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualFree decommit failed"); if (!VirtualAlloc(addr, len, MEM_COMMIT, WinProtFromPosix(nativeProtection))) throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualAlloc commit failed"); @@ -443,8 +427,6 @@ void* MapAligned(void* addr, size_t len, int prot, int flags, size_t alignment) return aligned; } -// SCE protections as the title set them: the host page protection keeps only the CPU bits, but titles -// read the GPU and AMPR bits back (a streamer reads straight into memory the APR may write). struct ProtectedRange { std::uintptr_t end; int prot; diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index a71ffc684..73fde6bd6 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -11,9 +11,7 @@ static constexpr size_t PS5_PAGE_SIZE = 0x4000; int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut); void DirectMemoryFree(int64_t start, size_t len); -// Drops what the released physical range held and stops its live mappings from saving into it. void ForgetDirectMemory(int64_t start, size_t len); -// Finds the allocation containing offset, or with findNext the first one above it; false when there is none. bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType); size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment); @@ -21,7 +19,6 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags); int DoMprotect(const void* addr, size_t len, int prot); int DoMunmap(void* addr, size_t len); int DoReserveVirtual(void** addr, size_t len, size_t alignment); -// The SCE protection the title last mapped or protected addr with (GPU and AMPR bits included); false when unknown. bool GuestProtection(uintptr_t addr, int* prot); #endif \ No newline at end of file diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 0f7fabf6c..5a5d435f7 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -279,7 +279,6 @@ int APS5_VABI sceKernelMtypeprotect(const void* addr, size_t len, int type, int return 0; } -// The mapping that contains addr, as sceKernelVirtualQuery reports it without FIND_NEXT. int APS5_VABI sceKernelQueryMemoryProtection(void* addr, void** start, void** end, int* prot) { VirtualQueryInfo info{}; const int result = sceKernelVirtualQuery(addr, 0, &info, sizeof(info)); diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp index 28ab864c9..b6e799e68 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp @@ -6,7 +6,6 @@ int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut); void DirectMemoryFree(int64_t start, size_t len); -// Finds the allocation containing offset, or with findNext the first one above it; false when there is none. bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType); #endif diff --git a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp index 1bbb73f17..00c3b5895 100644 --- a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp +++ b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp @@ -4,15 +4,11 @@ #include #include -// Windows cannot open a directory as a file descriptor, so a guest open() of a directory gets a -// descriptor for NUL that is tracked here with its path and a getdents cursor. namespace File { int OpenDirectoryDescriptor(const std::filesystem::path& path); std::optional DirectoryDescriptorPath(int fd); void ForgetDirectoryDescriptor(int fd); -// Fills FreeBSD dirent records { u32 fileno; u16 reclen; u8 type; u8 namlen; char name[]; } (4-byte aligned). -// Returns the bytes written, or -1 when fd is not a directory descriptor. int ReadDirectoryDescriptor(int fd, char* buf, int nbytes); } diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index e0dbf878e..4e35b2dd9 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -47,9 +47,6 @@ static int NativeFlock(int descriptor, int operation) { if (operation & 4) flags |= LOCKFILE_FAIL_IMMEDIATELY; return ::LockFileEx(handle, flags, 0, MAXDWORD, MAXDWORD, &overlapped) ? 0 : -1; } -// Positioned I/O through OVERLAPPED is atomic, unlike seek + read, which races when streaming threads share a -// descriptor. ponytail: on a synchronous handle it still moves the file pointer to offset + n, which POSIX pread -// does not; a mixed read()/pread() user on one descriptor would notice. static std::int64_t NativePositioned(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset, bool write) { if (nbytes > static_cast(std::numeric_limits::max())) { throw std::runtime_error("NativePositioned: nbytes exceeds platform limit"); diff --git a/core/libs/prx/libkernel/Pthread/Posix/Xthreads.cpp b/core/libs/prx/libkernel/Pthread/Posix/Xthreads.cpp index 5dd4ab78a..ff45a2d56 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Xthreads.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Xthreads.cpp @@ -9,9 +9,6 @@ #include "../../Time/include/Time.hpp" #include "Common.hpp" -// Dinkumware C11 thread support as the title's own libc.prx implements it: each _Mtx_t/_Cnd_t is a -// pointer-sized slot holding the sce object, and results are _Thrd_* codes. - extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); @@ -37,7 +34,6 @@ int initResult(int result) { return result == 0 ? ThrdSuccess : result == SceEnomem ? ThrdNomem : ThrdError; } -// std::_Pad as laid out by the title's libc; _Go() is the first vtable slot. struct GuestPad { unsigned (APS5_VABI* const* vtable)(GuestPad*); PthreadCond cond; @@ -101,7 +97,6 @@ int APS5_VABI _Cnd_broadcast_nid_postfix(PthreadCond* cond) { return ThrdSuccess; } -// xtime has the KernelTimespec layout and is an absolute gettimeofday deadline. int APS5_VABI _Cnd_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* deadline) { KernelUseconds usec = 0; if (!PosixThread::RelativeMicroseconds(GuestRealtimeClock, deadline, &usec)) throw std::runtime_error("_Cnd_timedwait: invalid xtime"); @@ -120,7 +115,6 @@ Pthread APS5_VABI _Thrd_id_nid_postfix() { return scePthreadSelf(); } -// Microseconds since the epoch, not the 100 ns ticks of other Dinkumware ports. std::int64_t APS5_VABI _Xtime_get_ticks_nid_postfix() { KernelTimeval now{}; gettimeofday_nid_postfix(&now, nullptr); diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index 909bd5a44..5a50773e3 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -57,7 +57,6 @@ int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stacksiz if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); if (stacksize < 16384) throw std::runtime_error(std::string(__func__) + ": too small"); #ifdef _WIN32 - // Like libthr, any size is kept as given; thread creation rounds it up to whole pages. if (stacksize > std::numeric_limits::max() - 0xffffu) throw std::runtime_error(std::string(__func__) + ": stack size exceeds the Windows limit"); #endif diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 575feb15a..be6727f14 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -228,7 +228,7 @@ int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_ int APS5_VABI shutdown_nid_postfix(int descriptor, int how) { const auto socket = Lookup(descriptor); if (!socket) return Fail(9); - if (how < 0 || how > 2) return Fail(22); // SHUT_RD/SHUT_WR/SHUT_RDWR share values with the host constants + if (how < 0 || how > 2) return Fail(22); return ::shutdown(socket->value, how) ? Fail(NativeError()) : 0; } int APS5_VABI getsockname_nid_postfix(int descriptor, void* address, std::uint32_t* length) { diff --git a/core/libs/prx/libkernel/System/src/Coredump.cpp b/core/libs/prx/libkernel/System/src/Coredump.cpp index 1daea6429..1efdef414 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -24,9 +24,6 @@ int APS5_VABI sceCoredumpUnregisterCoredumpHandler(void) { return 0; } -// A libc that fails to throw (no handler: the exception is uncaught) reports it here right before -// std::terminate, after its own stderr line; the console keeps it for the core dump, we print it. -// The second argument is 0x400 from the libc that calls it; its meaning is not known. int APS5_VABI sceKernelDebugWriteCppExceptionInfo(const void* exception, uint64_t unknown, const char* typeName, const char* what) { (void)unknown; std::fprintf(stderr, "[coredump] uncaught C++ exception %p of type %s%s%s\n", exception, typeName ? typeName : "(unknown)", what ? ", what(): " : "", what ? what : ""); diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index c8a27e885..32cf4a341 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -62,7 +62,6 @@ static void CheckNamedAndHintedMappings() { Require(sceKernelMunmap(first, length) == 0); } -// Direct memory contents follow the physical pages: unmapped and mapped again elsewhere, they read back. static void CheckDirectMemoryFollowsPhysicalPages() { constexpr std::size_t page = 0x4000; std::int64_t phys = 0; diff --git a/core/relinker/codegen/src/InstructionScanner.cpp b/core/relinker/codegen/src/InstructionScanner.cpp index 94addfff7..ccd03f983 100644 --- a/core/relinker/codegen/src/InstructionScanner.cpp +++ b/core/relinker/codegen/src/InstructionScanner.cpp @@ -35,7 +35,6 @@ std::vector InstructionScanner::ScanCodeSection( try { length = decoder.Decode(cursor, available); } catch (const CodegenException& e) { - // Zero padding at the segment tail decodes as `add [rax], al` pairs; an odd tail leaves one truncated byte. if (std::all_of(cursor, cursor + available, [](const std::uint8_t b) { return b == 0; })) break; throw CodegenException(std::string("Cannot decode instruction: ") + e.what(), codeSectionOffset + i); } diff --git a/core/shader/recompiler/BdaAbi.hpp b/core/shader/recompiler/BdaAbi.hpp index 753a178b9..8632c87b9 100644 --- a/core/shader/recompiler/BdaAbi.hpp +++ b/core/shader/recompiler/BdaAbi.hpp @@ -45,10 +45,6 @@ static_assert(offsetof(Range, deviceAddress) == 16 && offsetof(Range, permission static_assert(std::is_standard_layout_v && std::is_trivially_copyable_v && sizeof(Fault) == 32); static_assert(offsetof(Fault, address) == 8 && offsetof(Fault, instruction) == 24); -// After the Fault record, the fault buffer holds the pages the shader stored to through the table -// (Write permission), for the host to mark GPU-written once the work completed: an overflow word, -// then an open-addressed set of page number + 1 (0 is free), probed WrittenPageProbes slots from a -// multiplicative hash. A store that finds no slot sets the overflow word. inline constexpr std::uint32_t WrittenPageShift = 12; inline constexpr std::uint32_t WrittenPageSlots = 4096; inline constexpr std::uint32_t WrittenPageProbes = 8; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index ee3c35b6e..0588b5da7 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -670,7 +670,6 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writeGraphicsCompileContext(writer, *request.graphics); } writer.WriteBool(request.useCache); - // Version 3: a compute request's partial-group size. if (request.context.compute.has_value()) { for (const std::uint32_t value : request.context.compute->partialThreads) { writer.WriteU32(value); diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index 8b7f2259f..9a138a090 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -288,7 +288,6 @@ bool canReachBefore(const ControlFlowGraph& graph, std::uint32_t start, std::uin return false; } -// Whether `start` reaches `target` without passing the loop's continue or merge block. bool reachesWithinIteration(const ControlFlowGraph& graph, const NaturalLoop& loop, std::uint32_t start, std::uint32_t target) { std::vector pending = {start}; std::vector visited(graph.blocks.size(), false); @@ -345,9 +344,6 @@ std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock if (isLoopControlGateway(graph, *loop, falseTarget) && graph.Dominates(block.id, falseTarget) && isInsideLoopConstruct(graph, *loop, trueTarget)) { return falseTarget; } - // An arm that reaches the post-dominator only in a later iteration (around the back edge) - // leaves the selection by continuing; the merge is where the other arm goes. Splitting the - // post-dominator off would leave it where it is, and the split repeats without end. if (globalMerge != InvalidControlFlowId) { const bool trueJoins = reachesWithinIteration(graph, *loop, trueTarget, globalMerge); const bool falseJoins = reachesWithinIteration(graph, *loop, falseTarget, globalMerge); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp index 227915e95..7ea1c1823 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp @@ -49,8 +49,6 @@ struct IrBindingLayout { std::uint32_t pushDataStartDword = PushData::NoStart; std::uint32_t memoryOffsetDword = 0; std::uint32_t memoryOffsetCount = 0; - // Three dwords after the memory offsets hold the partial-group dispatch size (ShaderInfo:: - // dispatchThreadLimit). bool dispatchThreadLimit = false; std::vector userDataRegisters; std::vector descriptors; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp index 5b4bed6d5..f5020e2a8 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp @@ -45,11 +45,7 @@ struct MemoryInfo { bool idxen = false; bool offen = false; bool planningOnly = false; - // A buffer load or store with GLC or DLC: it bypasses the caches another workgroup's stores - // may be stale in (a look-back polling a flag another workgroup publishes). bool coherent = false; - // A buffer access whose V# the program computes on the GPU (loaded at a runtime offset, say), - // so no binding can hold it: the shader decodes the V# and goes through the BDA page table. bool gpuDescriptor = false; bool operator==(const MemoryInfo& other) const = default; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 98d92896f..5d20bc6ac 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -25,10 +25,6 @@ struct BufferResource { bool atomic = false; bool formatted = false; bool scalar = false; - // Specialized: the V# has no records or is at address 0 (a null V#), so every access is out of - // bounds: loads read zero (or the format's default), stores and atomics are dropped. For no - // records that is the hardware's behavior; at address 0 an access would fault on hardware, so a - // working title never reaches such a slot. bool empty = false; bool operator==(const BufferResource& other) const = default; @@ -54,8 +50,6 @@ struct ImageResource { bool depthCompare = false; bool cube = false; bool r128 = false; - // Specialized: a 32-bit integer read of a depth plane (IsDepthBitsTexture), bound as the float - // depth view; the texel bits are the result, the swizzle's constants integers. bool depthBits = false; std::uint32_t indirectRoot = NoIndirectImage; std::uint32_t indirectMappingOffset = 0; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp index 6a53521ce..23fe56c39 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp @@ -32,10 +32,7 @@ struct ShaderInfo { bool instanceOffsetConflict = false; bool hasBitwiseXor = false; bool usesDma = false; - // Some access stores through the BDA page table (MemoryInfo::gpuDescriptor): the pages it - // writes are noted in the fault buffer (BdaAbi::WrittenPageSlots). bool bdaWrites = false; - // The entry reads the partial-group dispatch size (StageInputKind::DispatchThreadLimit). bool dispatchThreadLimit = false; bool operator==(const ShaderInfo& other) const = default; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp index bb5972d64..ec74234c2 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp @@ -33,8 +33,6 @@ enum class StageInputKind { LocalInvocationId, LocalInvocationIndex, GlobalInvocationId, - // Not an input: a partial-group dispatch's size in threads, from shader data - // (IrBindingLayout::DispatchThreadLimitDword). DispatchThreadLimit, Parameter, }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index bb5d6f791..bac6a047a 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp @@ -9,7 +9,6 @@ namespace ShaderRecompiler { class DescriptorBindingBuilder { public: - // `partialThreads`: ShaderComputeStageInfo::partialThreads (zero outside compute). void Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array& partialThreads) const; void Populate(BindingAllocationResult& allocation, const ShaderInfo& info, IrShaderStage stage, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 97afa4e9a..44757fdc8 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -148,7 +148,6 @@ struct ShaderVertexInputInfo { struct ShaderComputeInputInfo: ShaderWorkgroupInputInfo { bool groupId[3] = {false, false, false}; - // ShaderComputeStageInfo::PartialGroups: threads past the dispatch size start inactive. bool partialGroups = false; int threadIdsNum = 0; int workgroupRegister = 0; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index a431d1f9a..4fb6ca805 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -507,7 +507,6 @@ void buildResourceSpecialization(const IrResourcePlan& plan, ResourceSnapshot& s entry.packedStride = packedStride; entry.descriptorFormat = buffer.formatted ? decoded.Format() : IrBufferFormat::Invalid; entry.descriptorSwizzle = buffer.formatted ? decoded.DstSelXYZW() : DstSel(4, 5, 6, 7); - // A V# at address 0 is a slot the shader does not reach (an access faults on hardware). entry.empty = decoded.GetSize() == 0u || decoded.Base48() == 0u; result.buffers.push_back(entry); } diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 23a5a76f0..c7ced39e6 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -524,10 +524,6 @@ private: source = InternSource(descriptor); } - // A raw dword load or store (1-4 dwords, or a scalar dword load) through a V# the SRT walker - // cannot compute: the program loads it at a GPU-computed offset (a descriptor heap) or builds - // it. No binding holds such a V#, so the shader decodes it and accesses memory through the BDA - // page table (MemoryInfo::gpuDescriptor). Other accesses keep failing in GetHandle. bool TakeGpuDescriptor(IrValue& inst, std::uint32_t memoryIndex) { const IrValue* handle = inst.Argument(0)->Resolve(); if (handle->Opcode() != IrOpcode::GetBufferResource || handle->ArgumentCount() != 4u) { @@ -540,7 +536,6 @@ private: return false; } const auto op = inst.Opcode(); - // A scalar buffer load (s_buffer_load) reads one dword per ReadConstBuffer. const bool load = op == IrOpcode::LoadBufferU32 || op == IrOpcode::LoadBufferU32x2 || op == IrOpcode::LoadBufferU32x3 || op == IrOpcode::LoadBufferU32x4 || op == IrOpcode::ReadConstBuffer; const bool store = op == IrOpcode::StoreBufferU32 || op == IrOpcode::StoreBufferU32x2 || op == IrOpcode::StoreBufferU32x3 || op == IrOpcode::StoreBufferU32x4; auto& memory = m_program.Resources().memoryInfo[memoryIndex]; @@ -690,7 +685,6 @@ private: void Collect(IrValue& inst) { const auto op = inst.Opcode(); if (op == IrOpcode::ImageBvhIntersectRay) { - // A ray query reads its node through the BDA page table (its T# is a run-time value). m_info.usesDma = true; return; } diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp index 12e355631..f043c6c55 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp @@ -20,9 +20,6 @@ enum class ImageType : std::uint32_t { [[nodiscard]] bool IsFmaskTextureFormat(IrBufferFormat format); [[nodiscard]] IrTextureNumericClass SampledTextureNumericClass(IrBufferFormat format); [[nodiscard]] IrBufferFormat RemapTextureFormat(IrBufferFormat format); -// A sampled T# (dwords 1 and 3) of a 32-bit integer format over a depth layout (a Z swizzle mode): -// the raw bits of a D32 depth plane. A host cannot view a depth image with an integer format, so -// the image binds its float depth view and the shader takes the texel bits (ImageResource::depthBits). [[nodiscard]] bool IsDepthBitsTexture(std::uint32_t word1, std::uint32_t word3); } diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp index eecd30628..e8d2f594c 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp @@ -123,7 +123,6 @@ IrBufferFormat RemapTextureFormat(IrBufferFormat format) { bool IsDepthBitsTexture(std::uint32_t word1, std::uint32_t word3) { const auto format = static_cast((word1 >> 20u) & 0x1ffu); - // SW_4KB_Z, SW_64KB_Z, SW_64KB_Z_T, SW_4KB_Z_X, SW_64KB_Z_X. const auto swizzle = (word3 >> 20u) & 0x1fu; const bool depthLayout = swizzle == 4u || swizzle == 8u || swizzle == 16u || swizzle == 20u || swizzle == 24u; return depthLayout && (format == IrBufferFormat::Format32UInt || format == IrBufferFormat::Format32SInt); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp index c9499b82e..5d0aaf657 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp @@ -86,8 +86,6 @@ void validateFlags(std::uint32_t flags) { if ((flags & RdnaImageSampleFlagCd) != 0u) { throw std::runtime_error("unsupported image coarse derivative (_cd) address layout"); } - // The _a sample aliases (OPM set) keep the plain address layout: the adjustment only touches reserved - // bits of the sampler's dword 3, which the resource tracker canonicalizes (as in Kyty). const auto lodModes = flags & (RdnaImageSampleFlagLod | RdnaImageSampleFlagBias | RdnaImageSampleFlagDerivative | RdnaImageSampleFlagLevelZero); if (std::popcount(lodModes) > 1) { throw std::runtime_error("conflicting image LOD modes"); @@ -229,9 +227,6 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span> 18u) & 0x7Fu) | ((word0 & 1u) << 7u); const auto& info = lookupOpcode(opcode); - // Word0 bit 12 is UNORM, unnormalized addressing, which only a sampler reads: loads, stores and - // atomics address texels by integer anyway (the ISA wants it set on stores and atomics, and the - // RTIP ray queries require it). On a sample or gather it is not implemented. const auto reservedWord0 = info.sample || info.gather ? 0x000350C0u : 0x000340C0u; if ((word0 & reservedWord0) != 0u || (word1 & 0x3C000000u) != 0u) { char message[96]; @@ -262,8 +257,6 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span groupIdEnable; bool tgSizeEnable; std::uint32_t threadIdComponentCount; - // A dispatch sized in threads (USE_THREAD_DIMENSIONS) that is no whole number of workgroups: - // its size per axis, the last workgroup partial; zero otherwise. Variants key on whether it - // is set only; the size reaches the shader as shader data. std::array partialThreads; [[nodiscard]] bool PartialGroups() const { @@ -327,9 +324,6 @@ struct RecompileResult { SharedSpirv spirv; std::vector bindings; std::vector pushConstants; - // The shader data dword of the guest buffers' byte offsets, one byte per GuestBuffers element - // (four per dword), zero as populated: a driver binding an element below its view (the storage - // buffer offset alignment) stores the difference there, the shader adds it to every access. std::uint32_t memoryOffsetDword = 0; std::uint32_t bdaAbiVersion = 0; std::vector vertexAttributes; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp index 53eefef93..c28e6ce6f 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp @@ -18,7 +18,6 @@ struct SpirvRequirements { bool functionScratch = false; bool pixelValidMask = false; bool bufferInt64Atomics = false; - // A buffer load or store is coherent (MemoryInfo::coherent): the guest buffers are Coherent. bool coherentBuffers = false; std::vector capabilities; std::vector extensions; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp index f39119ea9..f7c278915 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp @@ -20,14 +20,10 @@ void ValidateBdaTarget(const IrProgram& program, const SpirvTargetOptions& targe bool BdaInvocationsMayStop(const IrProgram& program); void StopBdaInvocationIf(SpirvEmitterState& state, std::uint32_t condition); std::uint32_t EmitBdaRead(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t bits); -// Stores a dword at a 4-aligned guest address through a range with Write permission and notes its -// page in the fault buffer's written set; a failure records its fault and stores nothing. void EmitBdaWrite(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t value); // Reads the dwords of a 1-4 dword load at address + offset that the program extracts, with one // table lookup for the whole span; the per-byte lookups of EmitBdaRead remain the fallback (and // the only path under APS5_BDA_BYTE_READS=1). Dwords the program never extracts are neither read -// nor fault-checked, as when every dword was its own instruction; `everyDword` reads them all (the -// dwords are not the instruction's result). std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword = false); bool BdaByteReadsForced(); std::uint32_t AddBdaAddress(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, bool subtract); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 8e555b598..497824dbf 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -48,7 +48,6 @@ struct ImageSampleLayout { std::uint32_t bias = NoImageComponent; std::uint32_t coord = 0; std::uint32_t lod = NoImageComponent; - // The _cl minimum level, after the coordinates. std::uint32_t clamp = NoImageComponent; std::uint32_t gradX = NoImageComponent; std::uint32_t gradY = NoImageComponent; @@ -67,7 +66,6 @@ struct MemoryResourceAccess { std::uint32_t indexOffset = 0; std::uint32_t byteOffset = 0; bool addIndexOffset = false; - // Memory operands of the loads and stores: Volatile for a coherent access (MemoryInfo::coherent). std::uint32_t memoryAccess = 0; }; @@ -101,8 +99,6 @@ struct SpirvEmitterState { // The lookup without fault recording that wide reads try first (see EmitBdaDwordReads); 0 when // every read takes the byte path. std::uint32_t bdaProbeFunction = 0; - // The lookup requiring Write and the written-page note of stores through the table (0 unless - // ShaderInfo::bdaWrites). std::uint32_t bdaWritePointerFunction = 0; std::uint32_t bdaNoteWriteFunction = 0; // False for programs with workgroup barriers: faulting BDA accesses then continue (see BdaInvocationsMayStop). diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 4fd16c7db..467811218 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -31,10 +31,8 @@ SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program) { throw std::runtime_error("buffer operation has invalid memory metadata"); } const auto& memory = program.Resources().memoryInfo.at(memoryIndex); - // Through the BDA table a coherent access is a Volatile load or store (BdaAccessMask). requirements.coherentBuffers = requirements.coherentBuffers || (memory.coherent && !memory.gpuDescriptor); if (memory.gpuDescriptor) { - // The V#'s ADD_TID is known at run time only. if (program.Resources().stage != IrShaderStage::Compute) { throw std::runtime_error("GPU-selected buffer descriptors outside compute shaders are not implemented"); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp index 2ff55d064..62a7a4289 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp @@ -18,9 +18,6 @@ void ReturnBdaZeroIf(SpirvEmitterState& state, std::uint32_t condition) { } // (u64 address, u32 bytes, u32 instruction) -> u64 device address of the range holding all the bytes, -// or 0; the range must grant `permission`. With recordFaults the failure is published first; -// without, the caller retries byte-wise, and the fault is recorded there if the bytes are unmapped -// for real. std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool recordFaults, std::uint32_t permission = BdaAbi::Read) { const auto u32 = TypeU32(state); const auto u64 = TypeScalarU64(state); @@ -119,8 +116,6 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r } -// (u64 address) -> void: notes the page of a store through the table in the fault buffer's written -// set (see BdaAbi::WrittenPageSlots). static std::uint32_t DefineBdaNoteWrite(SpirvEmitterState& state) { const auto u32 = TypeU32(state); const auto boolean = TypeBool(state); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index 93957c152..c42a895e1 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -16,8 +16,6 @@ void EmitBdaOverflowCheck(SpirvEmitterState& state, std::uint32_t address, std:: StopBdaInvocationIf(state, overflow); } -// The memory operands of a BDA load or store: Volatile for a coherent access (MemoryInfo::coherent), -// which must see what other workgroups store while the dispatch runs. std::uint32_t BdaAccessMask(const SpirvEmitterState& state, const IrValue& inst) { const auto index = inst.Flags().index; const auto& memory = state.program.Resources().memoryInfo; @@ -148,7 +146,6 @@ void EmitBdaWrite(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), address, BdaConstant(state, 3u)), BdaConstant(state, 0u)); EmitIfCondition(state, unaligned, [&] { RecordBdaFault(state, address, ConstantU32(state, 4u), instruction, BdaAbi::FaultReason::Unaligned); }); EmitIfCondition(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), [&] { - // A failed lookup (unmapped, or a range without Write) has recorded its fault. const auto physical = state.module.AllocateId(); state.module.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), physical, state.bdaWritePointerFunction, address, ConstantU32(state, 4u), instruction); EmitIfCondition(state, Binary(state, spv::OpINotEqual, TypeBool(state), physical, BdaConstant(state, 0u)), [&] { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 2158fc5c9..8162a4551 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -637,9 +637,6 @@ void EmitControlFlow(SpirvValueEmitContext& context, StructuredFunctionState& fu if (info == nullptr) { context.Fail("structured control flow block has no terminator metadata"); } - // A continue construct must reach its back edge (the back-edge block post-dominates the - // continue target), so a BDA fault in the continue target's instructions does not stop the - // invocation; the fault is recorded all the same. const bool stops = state.bdaStopsInvocations; state.bdaStopsInvocations = stops && !IsContinueTarget(program, info->id); EmitStructuredBlock(context, functionState, block); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 3792dd278..d02d65c3b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -301,8 +301,6 @@ std::uint32_t ResultVector(SpirvValueEmitContext& ctx, const ImageEmitAccess& ac component[index] = index == 0u ? F32BitsToU32(ctx, value) : ConstantU32(state, 0); continue; } - // Through a depth view read for its bits, only the depth channel (X) is a texel: the view - // gives 1.0 where the integer view gives 1 (ONE, or W of the single channel). const auto selector = (access.image.shaderSwizzle >> (index * 3u)) & 7u; if (access.image.depthBits && !gather && selector != 4u) { component[index] = ConstantU32(state, selector == 1u || selector == 7u ? 1u : 0u); @@ -789,7 +787,6 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con if (std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityMinLod)) == capabilities.end()) ctx.Fail(access.inst, "clamps its LOD, which needs the device's shaderResourceMinLod"); const auto clamp = AddressF32(ctx, access, setup.layout.clamp); if ((operandMask & spv::ImageOperandsLodMask) != 0u) { - // An explicit level takes no MinLod operand: the level itself is clamped. const auto clamped = state.module.AllocateId(); state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450FMax, operands.back(), clamp); operands.back() = clamped; @@ -957,11 +954,6 @@ void EmitImageAtomicXor32(SpirvValueEmitContext& ctx, const IrValue& inst) { EmitImage(ctx, inst); } -// image_bvh_intersect_ray tests one ray against one BVH node; the guest shader owns the traversal -// and its stack. This is the GFX10 (RT IP 1.1) node test as AMD's GPURT software fallback defines it -// (IntersectCommon.hlsl), with the node layouts of GPURT's gfx10 TriangleNode1_0 and BoxNode1_0, as -// SharpEmu ports it. The node is read through the BDA page table; an inactive lane, a null BVH, an -// out-of-range node or another node type returns four invalid dwords. void EmitImageBvhIntersectRay(SpirvValueEmitContext& ctx, const IrValue& inst) { auto& state = ctx.state; const auto u32 = TypeU32(state); @@ -999,10 +991,6 @@ void EmitImageBvhIntersectRay(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto component = [&](std::uint32_t index) { return ctx.Arg(*address, index); }; const bool a16 = (ctx.Memory(inst).imageSampleFlags & RdnaImageSampleFlagA16) != 0u; - // The node pointer: its byte offset divided by eight above bits 2:0, which hold the type - // (0-1 triangle, 4 float16 box, 5 float32 box). The T#: base_address[39:0] in 256-byte - // units, box_grow_value[62:55], box_sort_en[63], size[105:64] (the last valid 64-byte - // node), triangle_return_mode[120]. const auto node = component(0); const auto type = op(spv::OpBitwiseAnd, u32, node, uint(7u)); const auto nodeIndex = op(spv::OpShiftRightLogical, u64, widen(node), BdaConstant(state, 3u)); @@ -1016,7 +1004,6 @@ void EmitImageBvhIntersectRay(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto valid = both(present, op(spv::OpULessThanEqual, boolean, nodeIndex, lastNode)); const auto isTriangle = both(valid, op(spv::OpULessThanEqual, boolean, type, uint(1u))); const auto isBox16 = both(valid, op(spv::OpIEqual, boolean, type, uint(4u))); - // A float32 box node spans two 64-byte units. const auto isBox32 = both(both(present, op(spv::OpULessThan, boolean, nodeIndex, lastNode)), op(spv::OpIEqual, boolean, type, uint(5u))); const auto extent = asFloat(component(1)); @@ -1030,7 +1017,6 @@ void EmitImageBvhIntersectRay(SpirvValueEmitContext& ctx, const IrValue& inst) { inverse[axis] = asFloat(component(8u + axis)); continue; } - // Six halves in three dwords: the direction, then the inverse direction. const auto half = [&](std::uint32_t index) { const auto packed = component(5u + index / 2u); return EmitF16BitsToF32(state, index % 2u == 0u ? op(spv::OpBitwiseAnd, u32, packed, uint(0xffffu)) : op(spv::OpShiftRightLogical, u32, packed, uint(16u))); @@ -1039,10 +1025,6 @@ void EmitImageBvhIntersectRay(SpirvValueEmitContext& ctx, const IrValue& inst) { inverse[axis] = half(3u + axis); } - // The node's first `count` dwords (a multiple of four). A node lies in one range, so one - // probe of its span and direct loads read it; a node the table does not map as one aligned - // range records an Unmapped fault and reads as zeros. APS5_BDA_BYTE_READS=1 (no probe) - // reads it with the per-byte lookups. const auto nodeDwords = [&](std::uint32_t count) { std::vector dwords; if (state.bdaProbeFunction == 0) { @@ -1088,10 +1070,6 @@ void EmitImageBvhIntersectRay(SpirvValueEmitContext& ctx, const IrValue& inst) { return op(spv::OpFAdd, f32, op(spv::OpFAdd, f32, op(spv::OpFMul, f32, a[0], b[0]), op(spv::OpFMul, f32, a[1], b[1])), op(spv::OpFMul, f32, a[2], b[2])); }; - // fast_intersect_triangle and SwizzleBarycentrics: type 0 tests (v0, v1, v2), type 1 tests - // (v1, v3, v2). A miss returns t_num = +inf over t_denom = 1. The barycentric return mode - // yields the i/j numerators the builder's rotation maps back; the other mode yields the - // triangle id and a hit flag. const auto triangle = [&] { const auto d = nodeDwords(16u); const auto second = op(spv::OpIEqual, boolean, type, uint(1u)); @@ -1131,12 +1109,8 @@ void EmitImageBvhIntersectRay(SpirvValueEmitContext& ctx, const IrValue& inst) { return compose({asUint(tNum), asUint(tDenom), selectU(barycentrics, asUint(barycentric(0u)), triangleId), selectU(barycentrics, asUint(barycentric(2u)), selectU(hit, uint(1u), uint(0u)))}); }; - // IntersectNodeBvh4 with fast_intersect_bbox: a slab test clipped to [0, extent], a NaN - // interval misses, box_grow_value widens the exit time by that many 2^-24 steps, and the - // optional sort orders the hit children by entry time. const auto boxes = [&](bool fp16) { const auto d = nodeDwords(fp16 ? 16u : 28u); - // Bound 0-2 is the minimum, 3-5 the maximum of one child box, from dword 4. const auto bound = [&](std::uint32_t child, std::uint32_t index) { const auto position = child * 6u + index; if (!fp16) { @@ -1145,7 +1119,6 @@ void EmitImageBvhIntersectRay(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto packed = d[4u + position / 2u]; return EmitF16BitsToF32(state, position % 2u == 0u ? op(spv::OpBitwiseAnd, u32, packed, uint(0xffffu)) : op(spv::OpShiftRightLogical, u32, packed, uint(16u))); }; - // HLSL max3/min3: a NaN operand propagates to the NaN check. const auto nanMax = [&](std::uint32_t a, std::uint32_t b) { return selectF(either(isNan(a), op(spv::OpFOrdGreaterThan, boolean, a, b)), a, b); }; const auto nanMin = [&](std::uint32_t a, std::uint32_t b) { return selectF(either(isNan(a), op(spv::OpFOrdLessThan, boolean, a, b)), a, b); }; const auto zero = real(0u); @@ -1171,7 +1144,6 @@ void EmitImageBvhIntersectRay(SpirvValueEmitContext& ctx, const IrValue& inst) { children[child] = selectU(hit, d[child], invalid); keys[child] = minT; } - // The hardware sorting network; an invalid child always sinks. auto sorted = children; for (const auto [a, b] : std::array, 5>{{{0, 2}, {1, 3}, {0, 1}, {2, 3}, {1, 2}}}) { const auto bValid = op(spv::OpINotEqual, boolean, sorted[b], invalid); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index 068400061..1292d8e47 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -109,11 +109,9 @@ MemoryResourceAccess PrepareStorageBufferResourceAccess(SpirvEmitterState& state access.objectPointer = state.module.AllocateId(); state.module.AddFunction(spv::OpAccessChain, pointerType, access.objectPointer, variable, ConstantU32(state, arrayIndex)); access.byteOffset = state.memoryByteOffsets.at(arrayIndex); - // A GLC/DLC access polls or publishes what other workgroups see: never cached or combined. access.memoryAccess = mem.coherent ? spv::MemoryAccessVolatileMask : 0u; access.length = state.module.AllocateId(); state.module.AddFunction(spv::OpArrayLength, TypeU32(state), access.length, access.objectPointer, 0u); - // A null V# binds a placeholder the shader must not touch: nothing is in bounds. if (mem.resource < state.program.Info().buffers.size() && state.program.Info().buffers[mem.resource].empty) { access.length = ConstantU32(state, 0u); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 39c27768a..110176243 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -357,8 +357,6 @@ void DefineDescriptors(SpirvEmitterState& state) { state.module.AddAnnotation(spv::OpDecorate, state.storageBufferU64Variable, spv::DecorationAliased); } if (state.requirements.coherentBuffers) { - // A coherent load polls what other workgroups store (RDNA stores reach L2 without - // GLC too): every alias of the buffers takes part in device-scope visibility. state.module.AddAnnotation(spv::OpDecorate, state.storageBufferVariable, spv::DecorationCoherent); if (state.storageBufferU64Variable != 0u) state.module.AddAnnotation(spv::OpDecorate, state.storageBufferU64Variable, spv::DecorationCoherent); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index 6383809f0..1a9c0cf82 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -173,8 +173,6 @@ std::uint32_t DeviceAddressFromWords(SpirvEmitterState& state, std::uint32_t low return Binary(state, spv::OpBitwiseOr, TypeScalarU64(state), low64, high64); } -// The byte offset of a buffer access from a V# known only at run time (stride and swizzle are ids), -// as BufferByteAddress computes it from a specialized one: {offset + immediate, full byte offset}. std::pair RuntimeBufferByteAddress(SpirvEmitterState& state, std::uint32_t index, std::uint32_t offset, std::uint32_t soffset, std::uint32_t immediate, std::uint32_t stride, std::uint32_t swizzle, std::uint32_t indexStride) { const auto u32 = TypeU32(state); const auto add = [&](std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, spv::OpIAdd, u32, lhs, rhs); }; @@ -191,9 +189,6 @@ std::pair RuntimeBufferByteAddress(SpirvEmitterSta return {offset, add(Select(state, u32, swizzle, swizzled, linear), soffset)}; } -// Visits each dword of a raw access through a GPU-selected V# (MemoryInfo::gpuDescriptor, the four -// dwords are the handle's arguments) with its guest address and whether RDNA2 bounds checking -// (OOB_SELECT, per dword for raw vectors) lets it through; a V# of format 0 lets nothing through. template void ForEachGpuDescriptorDword(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t components, TFunction&& function) { auto& state = ctx.state; @@ -224,8 +219,6 @@ void ForEachGpuDescriptorDword(SpirvValueEmitContext& ctx, const IrValue& inst, const auto rawIndexInBounds = Binary(state, spv::OpULessThan, boolean, index, rawRecords); for (std::uint32_t component = 0; component < components; component++) { const auto [offset, byte] = RuntimeBufferByteAddress(state, index, ctx.Arg(inst, 2), soffset, mem.offset + component * 4u, stride, swizzle, field(word3, 21u, 2u)); - // OOB_SELECT 0: index and offset against the stride; 1: index only; 2: NUM_RECORDS nonzero; - // 3: raw, NUM_RECORDS reduced by SOFFSET first. const auto structured = AndCondition(state, indexInBounds, Binary(state, spv::OpULessThan, boolean, offset, stride)); const auto raw = AndCondition(state, scalarInBounds, Select(state, boolean, swizzle, AndCondition(state, rawIndexInBounds, hasDword(offset, stride)), hasDword(offset, rawRecords))); auto inBounds = Select(state, boolean, Binary(state, spv::OpIEqual, boolean, mode, ConstantU32(state, 0u)), structured, indexInBounds); @@ -661,7 +654,6 @@ const MemoryInfo& BufferMemory(SpirvValueEmitContext& ctx, const IrValue& inst) return mem; } -// A raw dword load or store (1-4 dwords) through a GPU-selected V#; false for a bound buffer. bool EmitGpuDescriptorAccess(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t components, bool store) { if (!ctx.Memory(inst).gpuDescriptor) { return false; @@ -1040,9 +1032,6 @@ void EmitReadConstBuffer(SpirvValueEmitContext& ctx, const IrValue& inst) { auto& state = ctx.state; const auto address = Binary(state, spv::OpIAdd, TypeU32(state), ctx.Arg(inst, 1), ConstantU32(state, mem.offset)); if (mem.gpuDescriptor) { - // Through a GPU-selected V# (MemoryInfo::gpuDescriptor): the dword at the base plus the - // dword-aligned offset, zero past the buffer's STRIDE * NUM_RECORDS bytes (NUM_RECORDS for - // stride 0). const IrValue* handle = inst.Argument(0)->Resolve(); if (handle->Opcode() != IrOpcode::GetBufferResource || handle->ArgumentCount() != 4u) { ctx.Fail(inst, "has no GPU-selected V#"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index f82fedb04..f82cfba1a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -196,7 +196,6 @@ std::uint32_t ExportRawComponent(SpirvValueEmitContext& ctx, std::uint32_t vecto std::uint32_t ExportVector(SpirvValueEmitContext& ctx, std::uint32_t data, const ExportInfo& exp, bool uintOutput) { auto& state = ctx.state; if (exp.compr && !uintOutput) { - // SPI_SHADER_COL_FORMAT: 5 UNORM16_ABGR, 6 SNORM16_ABGR, otherwise FP16_ABGR. const auto mode = MrtOutputMode(state, exp); const auto unpack = mode == 5u ? GLSLstd450UnpackUnorm2x16 : mode == 6u ? GLSLstd450UnpackSnorm2x16 : GLSLstd450UnpackHalf2x16; std::array f32 {ConstantF32(state, 0u), ConstantF32(state, 0u), ConstantF32(state, 0u), ConstantF32(state, 0x3f800000u)}; @@ -226,7 +225,6 @@ std::uint32_t ExportVector(SpirvValueEmitContext& ctx, std::uint32_t data, const ConstantU32(state, 0u), ConstantU32(state, uintOutput ? 1u : 0x3f800000u), }; - // SPI_SHADER_COL_FORMAT 32_R, 32_GR and 32_AR export only those components; the rest read as 0, alpha 1. const auto mode = MrtOutputMode(state, exp); const auto exported = exp.en & (mode == 1u ? 0x1u : mode == 2u ? 0x3u : mode == 3u ? 0x9u : 0xfu); if (exp.compr) { diff --git a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp index 9080ff69b..c7a76848b 100644 --- a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp @@ -77,7 +77,6 @@ void TranslationContext::addU32(const RdnaInstruction& inst, bool vector, bool u ir.SetScc(carryOut.Value()); return; } - // A vector carry-in is the mask the third source names: VCC for VOP2, any SGPR pair for VOP3. const IrU1 carryIn = !vector ? IrU1(ir.GetScc()) : inst.sourceCount >= 3u ? readMask(sourceAt(inst, 2u)) : IrU1(ir.GetVcc()); const IrU32 carryInU32(ir.Select(carryIn.Value(), ir.Constant(1u), ir.Constant(0u))); IrValue& secondAdd = ir.Emit(IrOpcode::IAddCarry32, IrType::U32x2, {&sum.Value(), &carryInU32.Value()}); diff --git a/core/shader/recompiler/Translation/src/DispatchInstructions.cpp b/core/shader/recompiler/Translation/src/DispatchInstructions.cpp index f43a2bb10..6bfc43eca 100644 --- a/core/shader/recompiler/Translation/src/DispatchInstructions.cpp +++ b/core/shader/recompiler/Translation/src/DispatchInstructions.cpp @@ -13,9 +13,6 @@ void DispatchInstruction(IrBuilder& builder, const RdnaInstruction& instruction, } void TranslationContext::TranslateInstruction(const RdnaInstruction& decoded) { - // DPP masks the instruction's VGPR write as well as its source read (row and bank masks, lanes - // with no source lane without bound_ctrl keep their value): the destination takes the source's - // DPP fields for writeRawU32. RdnaInstruction instruction = decoded; instruction.destination = destinationOperand(decoded); currentOpcode = instruction.op; diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 4cfc32819..73813b189 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -200,8 +200,6 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO initialExec = &entryIr.ULessThan(builtin(StageInputKind::LocalInvocationIndex), entryIr.Constant(totalThreads)); } } - // A dispatch sized in threads launches a partial last workgroup; the host rounds it up to - // whole groups, so threads past the dispatch size start inactive. if (options.stage == ShaderStageKind::Compute && options.inputInfo.compute->partialGroups) { for (std::uint32_t axis = 0; axis < 3u; axis++) { initialExec = &entryIr.LogicalAnd(*initialExec, entryIr.ULessThan(builtin(StageInputKind::GlobalInvocationId, axis), builtin(StageInputKind::DispatchThreadLimit, axis))); diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp index c24dc1ef6..da921ca2e 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp @@ -41,9 +41,6 @@ MemoryInfo imageMemoryInfoFromInstruction(const RdnaInstruction& inst) { } bool TranslationContext::imageBvhIntersectRay(const RdnaInstruction& inst) { - // The node test only: the guest shader owns the traversal and its stack. The T# is read at run - // time and the node through the BDA page table, so the access is address-based (kind Global); - // imageSampleFlags carries A16 (the direction and inverse direction as halves). if (inst.dataDwordCount != 4u || inst.imageD16) { throw std::runtime_error("image_bvh_intersect_ray returns four dwords"); } diff --git a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp index 1bda28419..135cc4281 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp @@ -36,7 +36,6 @@ RdnaOperand TranslationContext::destinationOperand(const RdnaInstruction& inst) return destination; } -// The destination read as a mac accumulator: its own value, which the instruction's DPP does not move. RdnaOperand TranslationContext::accumulatorOperand(const RdnaInstruction& inst) { RdnaOperand accumulator = inst.destination; accumulator.dpp = false; From 03465e769cf544dea1c0c97604845f59bcf672b2 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 21:28:49 +0300 Subject: [PATCH 32/40] test(kernel): a second view of direct memory shares the page Since direct memory mapped at two addresses is shared read-only instead of refused, the guest memory test expected an exception that no longer comes. It now maps the second view, reads the first view's bytes through it, and writes through the first view once the second is gone, which faults unless its protection came back. Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestMemory.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 32cf4a341..4222fa2e5 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -70,10 +70,11 @@ static void CheckDirectMemoryFollowsPhysicalPages() { Require(sceKernelMapDirectMemory(&first, page * 2, 3, 0, phys, 0) == 0); static_cast(first)[0] = 11; static_cast(first)[page + 5] = 22; - bool aliased = false; void* alias = nullptr; - try { sceKernelMapDirectMemory(&alias, page, 3, 0, phys + page, 0); } catch (const std::exception&) { aliased = true; } - Require(aliased); + Require(sceKernelMapDirectMemory(&alias, page, 3, 0, phys + page, 0) == 0); + Require(alias != first && static_cast(alias)[5] == 22); + Require(sceKernelMunmap(alias, page) == 0); + static_cast(first)[page + 5] = 22; Require(sceKernelMunmap(first, page * 2) == 0); void* filler = nullptr; Require(sceKernelMapFlexibleMemory(&filler, page * 2, 3, 0) == 0); From d252a3c72ce7f1a04e675c8d359ea8d74ff33f26 Mon Sep 17 00:00:00 2001 From: STL1te Date: Wed, 30 Sep 2026 09:34:56 +0300 Subject: [PATCH 33/40] fix(build): FFmpeg links into the Linux prx and without Windows libraries there The MinGW FFmpeg package needs bcrypt, ole32 and user32; linking them unconditionally broke libSceAjm.native.prx on Linux. The Linux package's x86 assembly references its constants PC32, which ld refuses in a shared object unless the archives' symbols stay local: --exclude-libs,ALL keeps them out of the prx's dynamic symbols. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f75e9677..b17426dec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,7 +36,11 @@ add_subdirectory(3rdparty/SDL2) set(CMAKE_EXE_LINKER_FLAGS "${ANYPS5_EXE_LINKER_FLAGS}") set(ARCHITECTURE x86_64) add_subdirectory(3rdparty/ffmpeg-core) -target_link_libraries(ffmpeg INTERFACE bcrypt ole32 user32) +if(MINGW) + target_link_libraries(ffmpeg INTERFACE bcrypt ole32 user32) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(ffmpeg INTERFACE LINKER:--exclude-libs,ALL) +endif() set(FT_DISABLE_ZLIB ON CACHE BOOL "" FORCE) set(FT_DISABLE_BZIP2 ON CACHE BOOL "" FORCE) From 5cf8ffa09077c550d18719b526d6740f520177e7 Mon Sep 17 00:00:00 2001 From: STL1te Date: Wed, 30 Sep 2026 09:34:56 +0300 Subject: [PATCH 34/40] ci(windows): the WinLibs CMake gets a CA bundle for the FFmpeg download WinLibs puts its own cmake.exe (4.3, OpenSSL) first in PATH and ships no CA certificates, so file(DOWNLOAD) of the FFmpeg package failed with curl error 60. SSL_CERT_FILE points it at the bundle of Git for Windows. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c40f7ec45..4fc884a5b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,6 +43,7 @@ jobs: env: WINLIBS_RELEASE: 15.2.0posix-14.0.0-ucrt-r7 WINLIBS_ZIP: winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip + SSL_CERT_FILE: C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt defaults: run: shell: bash From ee942805cd189edbb60c2d3a84e6a6fc02f116e7 Mon Sep 17 00:00:00 2001 From: STL1te Date: Wed, 30 Sep 2026 09:34:57 +0300 Subject: [PATCH 35/40] test(libc): guest_formatting runs only on Windows It checks the Windows formatter (the guest va_list must stay untouched). On Linux the guest variadic path hands the call to glibc, which advances the va_list, and snprintf_nid_postfix with arguments on the stack crashes; upstream never registered this test. Co-Authored-By: Claude Opus 5.5 --- core/libs/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 3aeac5508..f44ae80e3 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -462,8 +462,8 @@ if(BUILD_TESTING) target_include_directories(guest_formatting_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_formatting_tests PRIVATE libc) configure_windows_unwind(guest_formatting_tests) - add_test(NAME guest_formatting COMMAND guest_formatting_tests) if(WIN32) + add_test(NAME guest_formatting COMMAND guest_formatting_tests) set_tests_properties(guest_formatting PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() From abb9c85266820b4fa4a2a68cb18d9e59089115c3 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Wed, 30 Sep 2026 09:03:22 +0300 Subject: [PATCH 36/40] fix(kernel): share direct memory mappings and track physical offsets --- .../libSceAgcDriver/tests/BdaResources.cpp | 12 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 16 +- core/libs/prx/libc/include/GuestArena.hpp | 5 + .../libs/prx/libc/include/WindowsMappings.hpp | 152 +++++++++++ core/libs/prx/libc/src/GuestArena.cpp | 27 +- core/libs/prx/libc/src/GuestHeap.cpp | 4 +- .../libkernel/DirectMemory/DirectMemory.cpp | 251 +++++++++--------- .../libkernel/DirectMemory/DirectMemory.hpp | 2 + .../prx/libkernel/DirectMemory/Export.cpp | 16 +- .../prx/libkernel/DirectMemory/MemoryPool.cpp | 8 +- core/libs/tests/GuestMemory.cpp | 93 +++++++ 11 files changed, 438 insertions(+), 148 deletions(-) create mode 100644 core/libs/prx/libc/include/WindowsMappings.hpp diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index ea44c3f7a..82d4cbb8e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -40,7 +40,7 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) { constexpr std::size_t bytes = 2 * 65536; void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536); #ifdef _WIN32 - Require(VirtualAlloc(block, bytes, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot commit the heap mirror block"); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); #endif auto* guest = static_cast(block); std::memset(block, 0x11, bytes); @@ -108,7 +108,7 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) { { constexpr std::size_t half = 2 * 65536; void* pair = GuestArena::GuestArenaAllocate_nid_postfix(2 * half, 65536); - Require(VirtualAlloc(pair, 2 * half, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot commit the heap mirror pair"); + GuestArena::GuestArenaCommit_nid_postfix(pair, 2 * half, PAGE_READWRITE, 2 * half); std::memset(pair, 0x11, 2 * half); auto* first = static_cast(pair); auto* second = first + half; @@ -150,7 +150,7 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) { Require(firstDevice() == device && access.addressBytes(device)[5] == std::byte{0x66}, "an interrupted build left a changed heap block marked current"); registerPair(false); sweep(); - VirtualFree(pair, 2 * half, MEM_DECOMMIT); + GuestArena::GuestArenaReset_nid_postfix(pair, 2 * half); GuestArena::GuestArenaRelease_nid_postfix(pair, 2 * half); } #endif @@ -158,7 +158,7 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) { { constexpr std::size_t size = 2 * 65536; void* raw = GuestArena::GuestArenaAllocate_nid_postfix(size, 65536); - Require(VirtualAlloc(raw, size, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot commit the aliased range"); + GuestArena::GuestArenaCommit_nid_postfix(raw, size, PAGE_READWRITE, size); auto* bytes8 = static_cast(raw); std::memset(raw, 0x11, size); const auto base = reinterpret_cast(raw); @@ -209,12 +209,12 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) { sweep(); Require(VirtualProtect(reinterpret_cast(page), 4096, PAGE_READWRITE, &previous) != 0, "cannot restore the aliased page"); GuestAllocations::GuestAllocationsInvalidate_nid_postfix(page, 4096); - VirtualFree(raw, size, MEM_DECOMMIT); + GuestArena::GuestArenaReset_nid_postfix(raw, size); GuestArena::GuestArenaRelease_nid_postfix(raw, size); } #endif #ifdef _WIN32 - VirtualFree(block, bytes, MEM_DECOMMIT); + GuestArena::GuestArenaReset_nid_postfix(block, bytes); #endif GuestArena::GuestArenaRelease_nid_postfix(block, bytes); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 98eb4a645..8679cf508 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -343,7 +343,7 @@ void unchangedSinceTests() { constexpr std::size_t bytes = 65536; void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, bytes); #ifdef _WIN32 - Require(VirtualAlloc(block, bytes, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot commit the arena block"); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); #endif auto* words = static_cast(block); const auto address = reinterpret_cast(block); @@ -371,7 +371,7 @@ void unchangedSinceTests() { CollectWritesUncached(address, 4); Require(!UnchangedSinceCollected(address, 4, generation3), "(6) a CPU write collected by another caller is not seen"); #ifdef _WIN32 - VirtualFree(block, bytes, MEM_DECOMMIT); + GuestArena::GuestArenaReset_nid_postfix(block, bytes); #endif GuestArena::GuestArenaRelease_nid_postfix(block, bytes); } @@ -389,13 +389,13 @@ void keyProofTests(const Device& device, Recorder& recorder) { constexpr std::size_t bytes = 65536; void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, bytes); #ifdef _WIN32 - Require(VirtualAlloc(block, bytes, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot commit the arena block"); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); #endif struct Release { void* block; ~Release() { #ifdef _WIN32 - VirtualFree(block, bytes, MEM_DECOMMIT); + GuestArena::GuestArenaReset_nid_postfix(block, bytes); #endif GuestArena::GuestArenaRelease_nid_postfix(block, bytes); } @@ -476,7 +476,7 @@ void closeRaceTests(const Device& device, Recorder& recorder) { constexpr std::size_t bytes = 1u << 20; void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536); #ifdef _WIN32 - Require(VirtualAlloc(block, bytes, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot commit the arena block"); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); #endif const auto base = reinterpret_cast(block); // Late in the range, so the walker is usually still ahead of the page when the store lands. @@ -510,7 +510,7 @@ void closeRaceTests(const Device& device, Recorder& recorder) { recorder.Sync(); Require(!Recorder::LookupLabel(label, 4, 5).has_value(), "(8) the entry outlived its batch"); #ifdef _WIN32 - VirtualFree(block, bytes, MEM_DECOMMIT); + GuestArena::GuestArenaReset_nid_postfix(block, bytes); #endif GuestArena::GuestArenaRelease_nid_postfix(block, bytes); } @@ -722,13 +722,13 @@ void unitShadowTests(const Device& device, Recorder& recorder) { void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536); Require(block != nullptr, "cannot allocate the shadow test block"); #ifdef _WIN32 - Require(VirtualAlloc(block, bytes, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot commit the shadow test block"); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); #endif struct Release { void* block; ~Release() { #ifdef _WIN32 - VirtualFree(block, bytes, MEM_DECOMMIT); + GuestArena::GuestArenaReset_nid_postfix(block, bytes); #endif GuestArena::GuestArenaRelease_nid_postfix(block, bytes); } diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index 385b01298..c7225a2f6 100644 --- a/core/libs/prx/libc/include/GuestArena.hpp +++ b/core/libs/prx/libc/include/GuestArena.hpp @@ -18,6 +18,11 @@ void GuestArenaRelease_nid_postfix(const void* pointer, std::size_t bytes); // The reserved range, and whether it was reserved with page write watching (Windows MEM_WRITE_WATCH). void GuestArenaRange_nid_postfix(std::uintptr_t* base, std::size_t* bytes); bool GuestArenaWriteWatched_nid_postfix(); +#ifdef _WIN32 +void GuestArenaCommit_nid_postfix(void* pointer, std::size_t bytes, std::uint32_t protection, std::size_t granule); +void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes); +void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection); +#endif } diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp new file mode 100644 index 000000000..8feb591cb --- /dev/null +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -0,0 +1,152 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSMAPPINGS_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSMAPPINGS_HPP + +#ifdef _WIN32 +#include +#include +#include +#include +#include +#include +#include + +namespace GuestArena { + +class WindowsMappings { +public: + static WindowsMappings& Get() { + static WindowsMappings mappings; + return mappings; + } + + void* Reserve(void* address, std::size_t bytes) { + return allocate(GetCurrentProcess(), address, bytes, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, nullptr, 0); + } + + void Commit(void* address, std::size_t bytes, DWORD protection, std::size_t granule, bool watched) { + std::lock_guard lock(mutex); + const auto end = reinterpret_cast(address) + bytes; + for (auto cursor = reinterpret_cast(address); cursor < end;) { + const auto memory = query(cursor); + const auto stop = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); + if (memory.State == MEM_RESERVE) { + const auto limit = std::min(end, cursor + granule); + auto placeholderEnd = stop; + while (placeholderEnd < limit) { + const auto next = query(placeholderEnd); + if (next.State != MEM_RESERVE) break; + placeholderEnd = reinterpret_cast(next.BaseAddress) + next.RegionSize; + } + const auto size = std::min(limit, placeholderEnd) - cursor; + reset(cursor, size); + const DWORD flags = MEM_RESERVE | MEM_COMMIT | MEM_REPLACE_PLACEHOLDER | (watched ? MEM_WRITE_WATCH : 0); + if (!allocate(GetCurrentProcess(), reinterpret_cast(cursor), size, flags, protection, nullptr, 0)) fail("replace guest placeholder with private memory"); + cursor += size; + } else { + if (memory.State != MEM_COMMIT) throw std::runtime_error("guest memory is not committed"); + DWORD previous; + if (!VirtualProtect(reinterpret_cast(cursor), stop - cursor, protection, &previous)) fail("protect guest memory"); + cursor = stop; + } + } + } + + void Reset(void* address, std::size_t bytes) { + std::lock_guard lock(mutex); + reset(reinterpret_cast(address), bytes); + } + + void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection) { + std::lock_guard lock(mutex); + auto cursor = reinterpret_cast(address); + reset(cursor, bytes); + for (std::size_t done = 0; done < bytes; done += pageBytes) { + split(cursor + done, pageBytes); + void* page = reinterpret_cast(cursor + done); + if (!map(section, GetCurrentProcess(), page, offset + done, pageBytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map shared guest page"); + DWORD previous; + if (!VirtualProtect(page, pageBytes, protection, &previous)) fail("protect shared guest page"); + } + } + +private: + static constexpr std::size_t pageBytes = 0x4000; + using AllocateFunction = PVOID (WINAPI*)(HANDLE, PVOID, SIZE_T, ULONG, ULONG, MEM_EXTENDED_PARAMETER*, ULONG); + using MapFunction = PVOID (WINAPI*)(HANDLE, HANDLE, PVOID, ULONG64, SIZE_T, ULONG, ULONG, MEM_EXTENDED_PARAMETER*, ULONG); + using UnmapFunction = BOOL (WINAPI*)(HANDLE, PVOID, ULONG); + + WindowsMappings() { + const auto module = GetModuleHandleW(L"KernelBase.dll"); + if (!module) fail("load Windows memory API"); + allocate = reinterpret_cast(GetProcAddress(module, "VirtualAlloc2")); + map = reinterpret_cast(GetProcAddress(module, "MapViewOfFile3")); + unmap = reinterpret_cast(GetProcAddress(module, "UnmapViewOfFile2")); + if (!allocate || !map || !unmap) throw std::runtime_error("Windows placeholder memory APIs are required"); + } + + [[noreturn]] static void fail(const char* operation) { + throw std::system_error(static_cast(GetLastError()), std::system_category(), operation); + } + + static MEMORY_BASIC_INFORMATION query(std::uintptr_t address) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) != sizeof(memory)) fail("query guest memory"); + return memory; + } + + static void split(std::uintptr_t address, std::size_t bytes) { + auto memory = query(address); + memory = query(reinterpret_cast(memory.AllocationBase)); + if (memory.State != MEM_RESERVE) throw std::runtime_error("guest mapping requires a placeholder"); + const auto base = reinterpret_cast(memory.BaseAddress); + if (address != base) { + if (!VirtualFree(reinterpret_cast(base), address - base, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("split guest placeholder prefix"); + memory = query(address); + } + if (memory.RegionSize < bytes) throw std::runtime_error("guest placeholder is too small"); + if (memory.RegionSize != bytes && !VirtualFree(reinterpret_cast(address), bytes, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("split guest placeholder suffix"); + } + + void reset(std::uintptr_t address, std::size_t bytes) { + const auto end = address + bytes; + for (auto cursor = address; cursor < end;) { + const auto memory = query(cursor); + if (memory.State == MEM_RESERVE) { + cursor = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); + continue; + } + if (reinterpret_cast(memory.AllocationBase) != cursor) throw std::runtime_error("cannot release part of a host allocation"); + auto allocationEnd = cursor; + do { + const auto part = query(allocationEnd); + if (part.AllocationBase != memory.AllocationBase) break; + allocationEnd = reinterpret_cast(part.BaseAddress) + part.RegionSize; + } while (allocationEnd < end); + if (allocationEnd > end || query(allocationEnd).AllocationBase == memory.AllocationBase) throw std::runtime_error("guest release truncates a host allocation"); + if (memory.Type == MEM_MAPPED) { + if (!unmap(GetCurrentProcess(), reinterpret_cast(cursor), MEM_PRESERVE_PLACEHOLDER)) fail("unmap shared guest page"); + } else if (memory.Type == MEM_PRIVATE) { + if (!VirtualFree(reinterpret_cast(cursor), allocationEnd - cursor, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release private guest memory"); + } else { + throw std::runtime_error("unsupported guest mapping type"); + } + cursor = allocationEnd; + } + const auto last = query(reinterpret_cast(query(end - 1).AllocationBase)); + const auto lastBase = reinterpret_cast(last.BaseAddress); + if (lastBase + last.RegionSize > end) split(lastBase, end - lastBase); + const auto first = query(address); + split(address, std::min(bytes, reinterpret_cast(first.BaseAddress) + first.RegionSize - address)); + if (query(address).RegionSize != bytes && !VirtualFree(reinterpret_cast(address), bytes, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) fail("coalesce guest placeholders"); + } + + std::mutex mutex; + AllocateFunction allocate = nullptr; + MapFunction map = nullptr; + UnmapFunction unmap = nullptr; +}; + +} +#endif + +#endif diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 32efc3c02..6e725b2ac 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/WindowsMappings.hpp" #include #include #include @@ -91,13 +92,8 @@ private: for (std::uintptr_t base = PreferredBase; base + MinimumSize <= MapAreaEnd; base += MinimumSize) { for (std::size_t size = MaximumSize; size >= MinimumSize; size /= 2) { if (base + size > MapAreaEnd) continue; - // Write watching lets the GPU driver learn which pages the CPU wrote instead of comparing - // whole resources; the plain reservation is the fallback. Debug aid: APS5_NO_WRITE_WATCH=1 - // skips it, to measure what the write faults it re-arms cost the game's threads. - static const bool noWriteWatch = std::getenv("APS5_NO_WRITE_WATCH") != nullptr; - void* reserved = noWriteWatch ? nullptr : VirtualAlloc(reinterpret_cast(base), size, MEM_RESERVE | MEM_WRITE_WATCH, PAGE_NOACCESS); - _writeWatched = reserved != nullptr; - if (!reserved) reserved = VirtualAlloc(reinterpret_cast(base), size, MEM_RESERVE, PAGE_NOACCESS); + _writeWatched = std::getenv("APS5_NO_WRITE_WATCH") == nullptr; + void* reserved = WindowsMappings::Get().Reserve(reinterpret_cast(base), size); if (!reserved) continue; _base = reinterpret_cast(reserved); _end = _base + size; @@ -150,6 +146,23 @@ void GuestArenaRange_nid_postfix(std::uintptr_t* base, std::size_t* bytes) { *bytes = Arena::Get().Size(); } +#ifdef _WIN32 +void GuestArenaCommit_nid_postfix(void* pointer, std::size_t bytes, std::uint32_t protection, std::size_t granule) { + if (!Arena::Get().Contains(pointer, bytes)) throw std::invalid_argument("commit outside the guest arena"); + WindowsMappings::Get().Commit(pointer, bytes, protection, granule, Arena::Get().WriteWatched()); +} + +void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes) { + if (!Arena::Get().Contains(pointer, bytes)) throw std::invalid_argument("reset outside the guest arena"); + WindowsMappings::Get().Reset(pointer, bytes); +} + +void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection) { + if (!Arena::Get().Contains(pointer, bytes)) throw std::invalid_argument("shared mapping outside the guest arena"); + WindowsMappings::Get().Map(pointer, bytes, section, offset, protection); +} +#endif + bool GuestArenaWriteWatched_nid_postfix() { return Arena::Get().WriteWatched(); } diff --git a/core/libs/prx/libc/src/GuestHeap.cpp b/core/libs/prx/libc/src/GuestHeap.cpp index f47016d02..9d5f0d017 100644 --- a/core/libs/prx/libc/src/GuestHeap.cpp +++ b/core/libs/prx/libc/src/GuestHeap.cpp @@ -107,7 +107,7 @@ private: static void commit(void* pointer, std::size_t bytes) { #ifdef _WIN32 - if (!VirtualAlloc(pointer, bytes, MEM_COMMIT, PAGE_READWRITE)) throw std::bad_alloc(); + GuestArena::GuestArenaCommit_nid_postfix(pointer, bytes, PAGE_READWRITE, bytes); #else (void)pointer; (void)bytes; @@ -165,7 +165,7 @@ private: static void release(void* raw, std::size_t bytes) { #ifdef _WIN32 - if (!VirtualFree(raw, bytes, MEM_DECOMMIT)) throw std::system_error(static_cast(GetLastError()), std::system_category(), "guest heap decommit failed"); + GuestArena::GuestArenaReset_nid_postfix(raw, bytes); GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(raw), bytes); #endif GuestArena::GuestArenaRelease_nid_postfix(raw, bytes); diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 22ebff5a3..805078db4 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -1,4 +1,4 @@ -#include "DirectMemory.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" #include @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -19,6 +18,7 @@ #if defined(__linux__) #include +#include #include #include #include @@ -57,8 +57,7 @@ struct KernelArena { }; static void CommitArenaRange(void* addr, size_t len, DWORD winProt) { - if (!VirtualAlloc(addr, len, MEM_COMMIT, winProt)) - throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualAlloc commit failed"); + GuestArena::GuestArenaCommit_nid_postfix(addr, len, winProt, PS5_PAGE_SIZE); } static void* mmap_aligned(size_t len, int prot, size_t alignment) { @@ -99,9 +98,9 @@ static void* mmap(void* addr, size_t len, int prot, int flags, int, int) { } static int munmap(void* addr, size_t len) { - if (!VirtualFree(addr, len, MEM_DECOMMIT)) - throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualFree decommit failed"); auto& arena = KernelArena::Get(); + if (arena.Contains(addr, len)) GuestArena::GuestArenaReset_nid_postfix(addr, len); + else if (!VirtualFree(addr, len, MEM_DECOMMIT)) throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualFree decommit failed"); if (arena.Contains(addr, len)) arena.Release(addr, len); return 0; } @@ -219,114 +218,108 @@ void Trace(const char* format, ...) { va_end(args); } +class PhysicalBacking { +public: + explicit PhysicalBacking(std::size_t bytes, int memoryType) : memoryType(memoryType) { +#ifdef _WIN32 + const auto size = static_cast(bytes); + section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, static_cast(size >> 32), static_cast(size), nullptr); + if (!section) throw std::system_error(static_cast(GetLastError()), std::system_category(), "create direct memory backing"); +#else + file = std::tmpfile(); + if (!file) throw std::system_error(errno, std::generic_category(), "create direct memory backing"); + if (ftruncate(fileno(file), static_cast(bytes)) != 0) { + const int error = errno; + std::fclose(file); + throw std::system_error(error, std::generic_category(), "size direct memory backing"); + } +#endif + } + + int MemoryType() const { return memoryType; } + + ~PhysicalBacking() { +#ifdef _WIN32 + CloseHandle(section); +#else + std::fclose(file); +#endif + } + + PhysicalBacking(const PhysicalBacking&) = delete; + PhysicalBacking& operator=(const PhysicalBacking&) = delete; + + void Map(std::uintptr_t address, std::size_t bytes, std::uint64_t offset, int protection) const { +#ifdef _WIN32 + GuestArena::GuestArenaMap_nid_postfix(reinterpret_cast(address), bytes, section, offset, WinProtFromPosix(protection)); +#else + if (::mmap(reinterpret_cast(address), bytes, protection, MAP_SHARED | MAP_FIXED, fileno(file), static_cast(offset)) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map direct memory backing"); +#endif + } + +private: + int memoryType; +#ifdef _WIN32 + HANDLE section = nullptr; +#else + std::FILE* file = nullptr; +#endif +}; + +struct PhysicalPage { + std::shared_ptr backing; + std::uint64_t offset; +}; + +std::mutex g_directLock; +std::map g_physPages; + struct DirectMapping { std::uintptr_t end; std::uint64_t phys; + int memoryType; }; -std::mutex g_directLock; + std::map g_directMappings; -std::map> g_physPages; -void ProtectOrThrow(std::uintptr_t address, std::size_t len, int nativeProt) { - if (mprotect(reinterpret_cast(address), len, nativeProt) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); -} - -void SaveContents(std::uintptr_t address, std::size_t len, std::uint64_t phys) { - static const unsigned char zero[PS5_PAGE_SIZE] = {}; - ProtectOrThrow(address, len, PROT_READ); - for (std::size_t offset = 0; offset < len; offset += PS5_PAGE_SIZE) { - const auto* page = reinterpret_cast(address + offset); - if (std::memcmp(page, zero, PS5_PAGE_SIZE) == 0) continue; - auto& copy = g_physPages[phys + offset]; - copy.reset(new unsigned char[PS5_PAGE_SIZE]); - std::memcpy(copy.get(), page, PS5_PAGE_SIZE); - } -} - -std::set g_shared; - -std::vector Views(std::uint64_t physPage) { - std::vector views; - for (const auto& [base, mapping] : g_directMappings) { - if (physPage >= mapping.phys && physPage < mapping.phys + (mapping.end - base)) views.push_back(base + (physPage - mapping.phys)); - } - return views; -} - -int NativeProtection(std::uintptr_t view) { - int prot = 0; - if (!GuestProtection(view, &prot)) throw std::runtime_error("direct memory view has no recorded protection"); - return LinuxProtFromSce(prot); -} - -int SharedProtection(int nativeProt) { - return nativeProt & ~PROT_WRITE; -} - -void Unshare(std::uintptr_t page, std::uint64_t physPage) { - const auto others = Views(physPage); - if (others.empty()) throw std::runtime_error("shared direct memory page has no other view"); - ProtectOrThrow(page, PS5_PAGE_SIZE, PROT_READ); - ProtectOrThrow(others.front(), PS5_PAGE_SIZE, PROT_READ); - if (std::memcmp(reinterpret_cast(page), reinterpret_cast(others.front()), PS5_PAGE_SIZE) != 0) { - char message[160]; - std::snprintf(message, sizeof(message), "direct memory 0x%llx mapped at 0x%llx and 0x%llx was written through one view; aliased writes are not implemented", static_cast(physPage), - static_cast(page), static_cast(others.front())); - throw std::runtime_error(message); - } - const bool last = others.size() == 1; - if (last) g_shared.erase(others.front()); - const auto prot = NativeProtection(others.front()); - ProtectOrThrow(others.front(), PS5_PAGE_SIZE, last ? prot : SharedProtection(prot)); -} - -void EraseMappings(std::uintptr_t start, std::uintptr_t end, bool save) { +void EraseMappings(std::uintptr_t start, std::uintptr_t end) { auto it = g_directMappings.lower_bound(start); if (it != g_directMappings.begin() && std::prev(it)->second.end > start) --it; while (it != g_directMappings.end() && it->first < end) { const auto base = it->first; const auto mapping = it->second; it = g_directMappings.erase(it); - const auto cutStart = std::max(base, start); - const auto cutEnd = std::min(mapping.end, end); - if (base < start) g_directMappings.emplace(base, DirectMapping{start, mapping.phys}); - if (mapping.end > end) it = g_directMappings.emplace(end, DirectMapping{mapping.end, mapping.phys + (end - base)}).first; - if (g_shared.empty()) { - if (save) SaveContents(cutStart, cutEnd - cutStart, mapping.phys + (cutStart - base)); - continue; - } - for (auto page = cutStart; page < cutEnd; page += PS5_PAGE_SIZE) { - const auto physPage = mapping.phys + (page - base); - if (g_shared.erase(page) != 0) Unshare(page, physPage); - else if (save) SaveContents(page, PS5_PAGE_SIZE, physPage); + if (base < start) g_directMappings.emplace(base, DirectMapping{start, mapping.phys, mapping.memoryType}); + if (mapping.end > end) it = g_directMappings.emplace(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType}).first; + } +} + + +void ValidatePhysicalRange(std::uint64_t phys, std::size_t len) { + for (std::size_t offset = 0; offset < len; offset += PS5_PAGE_SIZE) { + if (!g_physPages.contains(phys + offset)) { + char message[160]; + std::snprintf(message, sizeof(message), "direct memory range 0x%llx+0x%zx references unallocated physical page 0x%llx", static_cast(phys), len, static_cast(phys + offset)); + throw std::invalid_argument(message); } } } void AddMapping(std::uintptr_t address, std::size_t len, std::uint64_t phys, int nativeProt) { - std::vector> shared; - for (const auto& [base, mapping] : g_directMappings) { - const auto physEnd = std::min(phys + len, mapping.phys + (mapping.end - base)); - for (auto page = std::max(phys, mapping.phys); page < physEnd; page += PS5_PAGE_SIZE) shared.emplace_back(page - phys, base + (page - mapping.phys)); + ValidatePhysicalRange(phys, len); + EraseMappings(address, address + len); + for (std::size_t offset = 0; offset < len;) { + const auto& page = g_physPages.at(phys + offset); + std::size_t bytes = PS5_PAGE_SIZE; + while (offset + bytes < len) { + const auto& next = g_physPages.at(phys + offset + bytes); + if (next.backing != page.backing || next.offset != page.offset + bytes) break; + bytes += PS5_PAGE_SIZE; + } + page.backing->Map(address + offset, bytes, page.offset, nativeProt); + g_directMappings.emplace(address + offset, DirectMapping{address + offset + bytes, phys + offset, page.backing->MemoryType()}); + offset += bytes; } - g_directMappings.emplace(address, DirectMapping{address + len, phys}); - auto it = g_physPages.lower_bound(phys); - const auto last = g_physPages.lower_bound(phys + len); - if (it == last && shared.empty()) return; - ProtectOrThrow(address, len, PROT_READ | PROT_WRITE); - for (; it != last; it = g_physPages.erase(it)) std::memcpy(reinterpret_cast(address + (it->first - phys)), it->second.get(), PS5_PAGE_SIZE); - for (const auto& [offset, view] : shared) { - ProtectOrThrow(view, PS5_PAGE_SIZE, PROT_READ); - std::memcpy(reinterpret_cast(address + offset), reinterpret_cast(view), PS5_PAGE_SIZE); - ProtectOrThrow(view, PS5_PAGE_SIZE, SharedProtection(NativeProtection(view))); - g_shared.insert(view); - } - ProtectOrThrow(address, len, nativeProt); - for (const auto& [offset, view] : shared) { - ProtectOrThrow(address + offset, PS5_PAGE_SIZE, SharedProtection(nativeProt)); - g_shared.insert(address + offset); - } - if (!shared.empty()) Trace("alias %p+0x%zx shares %zu physical pages with older views", reinterpret_cast(address), len, shared.size()); } bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr, size_t len, int prot, int flags, int64_t physStart = -1) { @@ -339,15 +332,17 @@ bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr, mutation.Protect(addr, len, (prot & 3) != 0, (prot & 2) != 0, [&] { const auto address = reinterpret_cast(addr); std::lock_guard lock(g_directLock); - EraseMappings(address, address + len, true); + if (physStart >= 0) { + AddMapping(address, len, static_cast(physStart), nativeProtection); + } else { + EraseMappings(address, address + len); #ifdef _WIN32 - if (physStart >= 0 && !VirtualFree(addr, len, MEM_DECOMMIT)) throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualFree decommit failed"); - if (!VirtualAlloc(addr, len, MEM_COMMIT, WinProtFromPosix(nativeProtection))) throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualAlloc commit failed"); + GuestArena::GuestArenaReset_nid_postfix(addr, len); + if (nativeProtection != PROT_NONE) CommitArenaRange(addr, len, WinProtFromPosix(nativeProtection)); #else - if (physStart >= 0 && madvise(addr, len, MADV_DONTNEED) != 0) throw std::system_error(errno, std::generic_category(), "madvise failed"); + if (::mmap(addr, len, nativeProtection, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "remap anonymous memory"); #endif - if (mprotect(addr, len, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); - if (physStart >= 0) AddMapping(address, len, static_cast(physStart), nativeProtection); + } }); return true; } @@ -475,11 +470,13 @@ int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, } if (*addr != nullptr && (flags & GuestMapFixedFlag) != 0) mutation.RequireAvailable(*addr, len); std::lock_guard lock(g_directLock); - void* mapped = MapAligned(*addr, len, LinuxProtFromSce(prot), flags, alignment); + ValidatePhysicalRange(static_cast(physStart), len); + void* mapped = MapAligned(*addr, len, PROT_NONE, flags, alignment); try { - mutation.Add(mapped, len, (prot & 3) != 0, (prot & 2) != 0); AddMapping(reinterpret_cast(mapped), len, static_cast(physStart), LinuxProtFromSce(prot)); + mutation.Add(mapped, len, (prot & 3) != 0, (prot & 2) != 0); } catch (...) { + EraseMappings(reinterpret_cast(mapped), reinterpret_cast(mapped) + len); Unmap(mapped, len); throw; } @@ -535,8 +532,6 @@ int DoMprotect(const void* addr, size_t len, int prot) { #endif mutation.Protect(pointer, bytes, (prot & 3) != 0, (prot & 2) != 0, [&] { if (mprotect(const_cast(pointer), bytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); - std::lock_guard lock(g_directLock); - for (auto it = g_shared.lower_bound(first); it != g_shared.end() && *it < end; ++it) ProtectOrThrow(*it, PS5_PAGE_SIZE, SharedProtection(nativeProtection)); }); RecordProtection(pointer, bytes, prot); return 0; @@ -547,11 +542,8 @@ int DoMunmap(void* addr, size_t len) { if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0 || !addr) return SCE_KERNEL_ERROR_EINVAL; GuestAllocations::Mutation mutation; mutation.Unmap(addr, len, [&](const void* allocation, bool last) { - { - const auto address = reinterpret_cast(addr); - std::lock_guard lock(g_directLock); - EraseMappings(address, address + len, true); - } + std::lock_guard lock(g_directLock); + EraseMappings(reinterpret_cast(addr), reinterpret_cast(addr) + len); #if defined(__linux__) Unmap(addr, len); #else @@ -580,17 +572,25 @@ int DoReserveVirtual(void** addr, size_t len, size_t alignment) { return 0; } +void CreateDirectMemoryBacking(int64_t start, size_t len, int memoryType) { + std::lock_guard lock(g_directLock); + const auto first = static_cast(start); + for (std::size_t offset = 0; offset < len; offset += PS5_PAGE_SIZE) { + if (g_physPages.contains(first + offset)) throw std::runtime_error("physical allocation overlaps live direct memory"); + } + const auto backing = std::make_shared(len, memoryType); + std::map pages; + for (std::size_t offset = 0; offset < len; offset += PS5_PAGE_SIZE) pages.emplace(first + offset, PhysicalPage{backing, offset}); + g_physPages.merge(pages); + Trace("allocate physical 0x%llx+0x%zx", static_cast(first), len); +} + void ForgetDirectMemory(int64_t start, size_t len) { const auto first = static_cast(start); - const auto end = first + len; std::lock_guard lock(g_directLock); - g_physPages.erase(g_physPages.lower_bound(first), g_physPages.lower_bound(end)); - std::vector> released; - for (const auto& [base, mapping] : g_directMappings) { - const auto mappedEnd = mapping.phys + (mapping.end - base); - if (mapping.phys < end && first < mappedEnd) released.emplace_back(base + (std::max(first, mapping.phys) - mapping.phys), base + (std::min(end, mappedEnd) - mapping.phys)); - } - for (const auto& [from, to] : released) EraseMappings(from, to, false); + ValidatePhysicalRange(first, len); + g_physPages.erase(g_physPages.lower_bound(first), g_physPages.lower_bound(first + len)); + Trace("release physical 0x%llx+0x%zx", static_cast(first), len); } bool GuestProtection(uintptr_t addr, int* prot) { @@ -602,3 +602,16 @@ bool GuestProtection(uintptr_t addr, int* prot) { *prot = containing->second.prot; return true; } + +bool QueryDirectMapping(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end, std::uint64_t* offset, int* memoryType) { + std::lock_guard lock(g_directLock); + const auto next = g_directMappings.upper_bound(address); + if (next == g_directMappings.begin()) return false; + const auto it = std::prev(next); + if (address >= it->second.end) return false; + *start = it->first; + *end = it->second.end; + *offset = it->second.phys; + *memoryType = it->second.memoryType; + return true; +} diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index 73fde6bd6..3dfdcfee6 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -11,6 +11,8 @@ static constexpr size_t PS5_PAGE_SIZE = 0x4000; int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut); void DirectMemoryFree(int64_t start, size_t len); +void CreateDirectMemoryBacking(int64_t start, size_t len, int memoryType); +bool QueryDirectMapping(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end, std::uint64_t* offset, int* memoryType); void ForgetDirectMemory(int64_t start, size_t len); bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType); size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 5a5d435f7..7b0c1bf8a 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -4,7 +4,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestAllocations.hpp" -#include "DirectMemory.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #define NOMINMAX @@ -218,9 +218,21 @@ int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInf info->protection = (best->readable ? 1 : 0) | (best->writable ? 2 : 0) | (!best->releasable ? 4 : 0); int recorded = 0; if (GuestProtection(std::max(address, info->start), &recorded)) info->protection = recorded; - info->is_direct = best->releasable ? 1u : 0u; + std::uintptr_t directStart = 0; + std::uintptr_t directEnd = 0; + std::uint64_t physicalOffset = 0; + int memoryType = 0; + const bool direct = QueryDirectMapping(std::max(address, info->start), &directStart, &directEnd, &physicalOffset, &memoryType); + info->is_direct = direct ? 1u : 0u; + info->is_flexible = !direct && best->releasable ? 1u : 0u; + if (direct) { + info->start = std::max(info->start, directStart); + info->end = std::min(info->end, directEnd); + info->memory_type = memoryType; + } info->is_committed = 1; ApplyRangeName(std::max(address, info->start), info); + if (direct) info->offset = physicalOffset + info->start - directStart; return 0; } // Memory the registry does not know (the title's own heap blocks, stacks): the host's committed diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index 2bf37398b..1175ada11 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -1,6 +1,5 @@ -#define _GLIBCXX_HAS_GTHREADS 0 -#include "MemoryPool.hpp" -#include "DirectMemory.hpp" +#include "prx/libkernel/DirectMemory/MemoryPool.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include #include @@ -20,6 +19,7 @@ struct PhysicalMemoryPool { uint64_t cur = (start + align - 1) & ~(align - 1); while (cur + len <= end && cur + len <= DIRECT_MEMORY_SIZE) { if (_isFree(cur, len)) { + CreateDirectMemoryBacking(static_cast(cur), len, memoryType); _mark(cur, len, true); _blocks[cur] = {cur + len, memoryType}; *physOut = static_cast(cur); @@ -32,6 +32,7 @@ struct PhysicalMemoryPool { void Free(uint64_t start, size_t len) { std::lock_guard lock(_mutex); + ForgetDirectMemory(static_cast(start), len); _mark(start, len, false); const uint64_t end = start + len; auto it = _blocks.upper_bound(start); @@ -99,7 +100,6 @@ int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t } void DirectMemoryFree(int64_t start, size_t len) { - ForgetDirectMemory(start, len); PhysicalMemoryPool::Instance().Free(static_cast(start), len); } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 4222fa2e5..8ca361130 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestHeap.hpp" #include "SceTypes.hpp" #include #include @@ -20,6 +21,7 @@ int APS5_VABI sceKernelSetVirtualRangeName(const void*, std::uint64_t, const cha int APS5_VABI sceKernelClearVirtualRangeName(const void*, std::uint64_t); int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); +int APS5_VABI sceKernelMprotect(const void*, std::size_t, int); int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); int APS5_VABI sceKernelReserveVirtualRange(void**, std::size_t, int, std::size_t); } @@ -73,6 +75,16 @@ static void CheckDirectMemoryFollowsPhysicalPages() { void* alias = nullptr; Require(sceKernelMapDirectMemory(&alias, page, 3, 0, phys + page, 0) == 0); Require(alias != first && static_cast(alias)[5] == 22); + static_cast(alias)[5] = 37; + Require(static_cast(first)[page + 5] == 37); + static_cast(first)[page + 6] = 48; + Require(static_cast(alias)[6] == 48); + Require(sceKernelMprotect(alias, page, 1) == 0); + static_cast(first)[page + 5] = 59; + Require(static_cast(alias)[5] == 59); + Require(sceKernelMprotect(alias, page, 3) == 0); + static_cast(alias)[5] = 22; + Require(static_cast(first)[page + 5] == 22); Require(sceKernelMunmap(alias, page) == 0); static_cast(first)[page + 5] = 22; Require(sceKernelMunmap(first, page * 2) == 0); @@ -99,9 +111,90 @@ static void CheckDirectMemoryFollowsPhysicalPages() { Require(sceKernelReleaseDirectMemory(again, page * 2) == 0); } +static void CheckSharedDirectMemoryLifecycle() { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 3, 0, 0, &phys) == 0); + void* first = nullptr; + void* second = nullptr; + Require(sceKernelMapDirectMemory(&first, page * 3, 3, 0, phys, 0) == 0); + Require(sceKernelMapDirectMemory(&second, page * 3, 3, 0, phys, 0) == 0); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(second, 0, &info, sizeof(info)) == 0); + Require(info.is_direct && !info.is_flexible && info.offset == static_cast(phys)); + auto* left = static_cast(first); + auto* right = static_cast(second); + left[0] = 31; + right[page] = 47; + left[page * 2] = 63; + Require(right[0] == 31 && left[page] == 47 && right[page * 2] == 63); + void* inaccessible = nullptr; + Require(sceKernelMapDirectMemory(&inaccessible, page, 0, 0, phys + page, 0) == 0); + left[page] = 48; + Require(sceKernelMprotect(inaccessible, page, 1) == 0); + Require(static_cast(inaccessible)[0] == 48); + Require(sceKernelMunmap(inaccessible, page) == 0); + Require(sceKernelMunmap(left + page, page) == 0); + right[page] = 79; + Require(left[0] == 31 && left[page * 2] == 63); + void* middle = left + page; + Require(sceKernelMapDirectMemory(&middle, page, 3, 0x10, phys + page, 0) == 0); + Require(left[page] == 79); + Require(sceKernelVirtualQuery(middle, 0, &info, sizeof(info)) == 0); + Require(info.offset == static_cast(phys) + page && info.start == reinterpret_cast(middle)); + Require(sceKernelMprotect(second, page * 3, 0) == 0); + left[page] = 95; + Require(sceKernelMprotect(second, page * 3, 1) == 0); + Require(right[page] == 95); + Require(sceKernelMunmap(first, page) == 0); + Require(sceKernelMunmap(left + page * 2, page) == 0); + Require(sceKernelMunmap(middle, page) == 0); + Require(sceKernelMprotect(second, page * 3, 3) == 0); + right[page * 2] = 111; + void* reserved = nullptr; + Require(sceKernelReserveVirtualRange(&reserved, page * 3, 0, 0) == 0); + void* fixed = static_cast(reserved) + page; + Require(sceKernelMapDirectMemory(&fixed, page, 3, 0x10, phys + page * 2, 0) == 0); + Require(static_cast(fixed)[0] == 111); + static_cast(fixed)[0] = 127; + Require(right[page * 2] == 127); + Require(sceKernelMapFlexibleMemory(&fixed, page, 3, 0x10) == 0); + Require(static_cast(fixed)[0] == 0); + Require(sceKernelVirtualQuery(fixed, 0, &info, sizeof(info)) == 0); + Require(!info.is_direct && info.is_flexible && info.offset == 0); + static_cast(fixed)[0] = 143; + Require(right[page * 2] == 127); + Require(sceKernelMunmap(reserved, page * 3) == 0); + Require(sceKernelMunmap(second, page * 3) == 0); + Require(sceKernelReleaseDirectMemory(phys + page, page) == 0); + std::int64_t replacement = 0; + Require(sceKernelAllocateDirectMemory(phys + page, phys + page * 2, page, 0, 0, &replacement) == 0); + Require(replacement == phys + page); + void* mixed = nullptr; + Require(sceKernelMapDirectMemory(&mixed, page * 3, 3, 0, phys, 0) == 0); + const auto* data = static_cast(mixed); + Require(data[0] == 31 && data[page] == 0 && data[page * 2] == 127); + Require(sceKernelMunmap(mixed, page * 3) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0); +} + +static void CheckHeapAfterMappingReuse() { + constexpr std::size_t bytes = 0x30000; + auto* pointer = static_cast(GuestHeap::GuestHeapAllocate_nid_postfix(bytes)); + std::memset(pointer, 0x5a, bytes); + Require(pointer[0] == 0x5a && pointer[bytes - 1] == 0x5a); + GuestHeap::GuestHeapFree_nid_postfix(pointer); + pointer = static_cast(GuestHeap::GuestHeapAllocate_nid_postfix(bytes)); + std::memset(pointer, 0xa5, bytes); + Require(pointer[0] == 0xa5 && pointer[bytes - 1] == 0xa5); + GuestHeap::GuestHeapFree_nid_postfix(pointer); +} + int main() { CheckNamedAndHintedMappings(); CheckDirectMemoryFollowsPhysicalPages(); + CheckSharedDirectMemoryLifecycle(); + CheckHeapAfterMappingReuse(); constexpr std::size_t page = 0x4000; const auto failed = reinterpret_cast(static_cast(-1)); const auto reject = [&](std::size_t length, int protection, int flags, int fd, From f7c53a44810deaa4e9e2ab984382dcd1929cffde Mon Sep 17 00:00:00 2001 From: boykopovar Date: Wed, 30 Sep 2026 09:18:52 +0300 Subject: [PATCH 37/40] fix(memory): restore write-watch tracking for shared mappings --- .../Execution/src/GuestMemory.cpp | 16 +-- core/libs/prx/libc/include/GuestArena.hpp | 4 + .../libs/prx/libc/include/WindowsMappings.hpp | 114 ++++++++++++++++++ core/libs/prx/libc/src/GuestArena.cpp | 16 +++ .../src/specifics/windows/CrashReport.cpp | 3 + .../libkernel/DirectMemory/DirectMemory.cpp | 8 +- 6 files changed, 151 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index c17e16f00..354e53311 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -506,7 +506,9 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) { const auto base = reinterpret_cast(memory.BaseAddress); if (memory.RegionSize > std::numeric_limits::max() - base || base + memory.RegionSize <= cursor) return false; const auto regionEnd = base + memory.RegionSize; - const auto protection = memory.Protect & 0xffu; + std::uint32_t logicalProtection = memory.Protect; + GuestArena::GuestArenaProtection_nid_postfix(cursor, &logicalProtection); + const auto protection = logicalProtection & 0xffu; const bool committed = memory.State == MEM_COMMIT && (memory.Protect & (PAGE_GUARD | PAGE_NOACCESS)) == 0; const bool readable = committed && readableProtection(protection); const bool writable = readable && writableProtection(protection); @@ -762,9 +764,9 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi static const bool singlePass = std::getenv("APS5_NO_SINGLE_PASS_COLLECT") == nullptr; bool dirty = false; while (cursor < stop) { - ULONG_PTR count = tracker.pages.size(); - DWORD granularity = 0; - if (GetWriteWatch(singlePass ? WRITE_WATCH_FLAG_RESET : 0, reinterpret_cast(cursor), static_cast(stop - cursor), tracker.pages.data(), &count, &granularity) != 0) { + std::size_t count = tracker.pages.size(); + DWORD granularity = 4096; + if (!GuestArena::GuestArenaCollectWrites_nid_postfix(cursor, static_cast(stop - cursor), tracker.pages.data(), &count, singlePass)) { // Uncommitted pages inside the range make the call fail; such ranges are compared instead. return 0; } @@ -782,9 +784,9 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi // The resetting pass reports pages again, so a write racing the first pass is stamped too. cursor = address & ~(page - 1); while (cursor < stop) { - ULONG_PTR count = tracker.pages.size(); - DWORD granularity = 0; - if (GetWriteWatch(WRITE_WATCH_FLAG_RESET, reinterpret_cast(cursor), static_cast(stop - cursor), tracker.pages.data(), &count, &granularity) != 0) return 0; + std::size_t count = tracker.pages.size(); + DWORD granularity = 4096; + if (!GuestArena::GuestArenaCollectWrites_nid_postfix(cursor, static_cast(stop - cursor), tracker.pages.data(), &count, true)) return 0; for (ULONG_PTR i = 0; i < count; ++i) { const auto block = (reinterpret_cast(tracker.pages[i]) - tracker.base) / WriteBlockBytes; tracker.blocks[block] = tracker.generation; diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index c7225a2f6..894dd0012 100644 --- a/core/libs/prx/libc/include/GuestArena.hpp +++ b/core/libs/prx/libc/include/GuestArena.hpp @@ -19,6 +19,10 @@ void GuestArenaRelease_nid_postfix(const void* pointer, std::size_t bytes); void GuestArenaRange_nid_postfix(std::uintptr_t* base, std::size_t* bytes); bool GuestArenaWriteWatched_nid_postfix(); #ifdef _WIN32 +void GuestArenaSetProtection_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uint32_t protection); +bool GuestArenaHandleWrite_nid_postfix(std::uintptr_t address); +bool GuestArenaProtection_nid_postfix(std::uintptr_t address, std::uint32_t* protection); +bool GuestArenaCollectWrites_nid_postfix(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear); void GuestArenaCommit_nid_postfix(void* pointer, std::size_t bytes, std::uint32_t protection, std::size_t granule); void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes); void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection); diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 8feb591cb..aff457adc 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -7,6 +7,9 @@ #include #include #include +#include +#include +#include #include #include @@ -44,6 +47,11 @@ public: cursor += size; } else { if (memory.State != MEM_COMMIT) throw std::runtime_error("guest memory is not committed"); + const auto mapped = views.find(cursor & ~(pageBytes - 1)); + if (mapped != views.end()) { + mapped->second.protection = protection; + mapped->second.armed = false; + } DWORD previous; if (!VirtualProtect(reinterpret_cast(cursor), stop - cursor, protection, &previous)) fail("protect guest memory"); cursor = stop; @@ -66,11 +74,110 @@ public: if (!map(section, GetCurrentProcess(), page, offset + done, pageBytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map shared guest page"); DWORD previous; if (!VirtualProtect(page, pageBytes, protection, &previous)) fail("protect shared guest page"); + const auto key = std::make_pair(reinterpret_cast(section), offset + done); + auto shared = physical[key].lock(); + if (!shared) { + shared = std::make_shared(); + physical[key] = shared; + } + const auto base = cursor + done; + shared->aliases.push_back(base); + views.emplace(base, View{shared, protection, 0, false}); } } + void SetProtection(std::uintptr_t address, std::size_t bytes, DWORD protection) { + std::lock_guard lock(mutex); + for (auto it = views.lower_bound(address); it != views.end() && it->first < address + bytes; ++it) { + it->second.protection = protection; + it->second.armed = false; + } + } + + bool HandleWrite(std::uintptr_t address) { + std::lock_guard lock(mutex); + const auto base = address & ~(pageBytes - 1); + const auto found = views.find(base); + if (found == views.end() || !writable(found->second.protection)) return false; + auto& view = found->second; + ++view.page->generation; + DWORD previous; + if (!VirtualProtect(reinterpret_cast(base), pageBytes, view.protection, &previous)) fail("resume shared memory write"); + view.armed = false; + return true; + } + + bool Protection(std::uintptr_t address, std::uint32_t* protection) { + std::lock_guard lock(mutex); + const auto found = views.find(address & ~(pageBytes - 1)); + if (found == views.end()) return false; + *protection = found->second.protection; + return true; + } + + bool Collect(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear) { + std::lock_guard lock(mutex); + const auto capacity = *count; + *count = 0; + const auto end = address + bytes; + for (auto cursor = address; cursor < end;) { + const auto base = cursor & ~(pageBytes - 1); + const auto found = views.find(base); + if (found != views.end()) { + auto& view = found->second; + const auto stop = std::min(end, base + pageBytes); + if (view.protection == PAGE_NOACCESS) return false; + if (view.seen != view.page->generation) { + const auto needed = (stop - cursor + 4095) / 4096; + if (needed > capacity - *count) { + for (auto at = cursor; *count < capacity; at += 4096) pages[(*count)++] = reinterpret_cast(at); + return true; + } + for (auto at = cursor; at < stop; at += 4096) pages[(*count)++] = reinterpret_cast(at); + } + if (clear) { + for (const auto alias : view.page->aliases) { + auto& other = views.at(alias); + if (!writable(other.protection) || other.armed) continue; + DWORD previous; + const DWORD protection = other.protection == PAGE_EXECUTE_READWRITE ? PAGE_EXECUTE_READ : PAGE_READONLY; + if (!VirtualProtect(reinterpret_cast(alias), pageBytes, protection, &previous)) fail("arm shared memory write tracking"); + other.armed = true; + } + view.seen = view.page->generation; + } + cursor = stop; + } else { + const auto memory = query(cursor); + if (memory.State != MEM_COMMIT || memory.Type != MEM_PRIVATE) return false; + const auto stop = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); + ULONG_PTR available = capacity - *count; + if (available == 0) return true; + DWORD granularity = 0; + if (GetWriteWatch(clear ? WRITE_WATCH_FLAG_RESET : 0, reinterpret_cast(cursor), stop - cursor, pages + *count, &available, &granularity) != 0) fail("collect private guest writes"); + *count += available; + if (*count == capacity) return true; + cursor = stop; + } + } + return true; + } + private: static constexpr std::size_t pageBytes = 0x4000; + struct SharedPage { + std::uint64_t generation = 1; + std::vector aliases; + }; + struct View { + std::shared_ptr page; + DWORD protection; + std::uint64_t seen; + bool armed; + }; + static bool writable(DWORD protection) { + return protection == PAGE_READWRITE || protection == PAGE_EXECUTE_READWRITE; + } using AllocateFunction = PVOID (WINAPI*)(HANDLE, PVOID, SIZE_T, ULONG, ULONG, MEM_EXTENDED_PARAMETER*, ULONG); using MapFunction = PVOID (WINAPI*)(HANDLE, HANDLE, PVOID, ULONG64, SIZE_T, ULONG, ULONG, MEM_EXTENDED_PARAMETER*, ULONG); using UnmapFunction = BOOL (WINAPI*)(HANDLE, PVOID, ULONG); @@ -125,6 +232,11 @@ private: if (allocationEnd > end || query(allocationEnd).AllocationBase == memory.AllocationBase) throw std::runtime_error("guest release truncates a host allocation"); if (memory.Type == MEM_MAPPED) { if (!unmap(GetCurrentProcess(), reinterpret_cast(cursor), MEM_PRESERVE_PLACEHOLDER)) fail("unmap shared guest page"); + const auto found = views.find(cursor); + if (found != views.end()) { + std::erase(found->second.page->aliases, cursor); + views.erase(found); + } } else if (memory.Type == MEM_PRIVATE) { if (!VirtualFree(reinterpret_cast(cursor), allocationEnd - cursor, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release private guest memory"); } else { @@ -140,6 +252,8 @@ private: if (query(address).RegionSize != bytes && !VirtualFree(reinterpret_cast(address), bytes, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) fail("coalesce guest placeholders"); } + std::map views; + std::map, std::weak_ptr> physical; std::mutex mutex; AllocateFunction allocate = nullptr; MapFunction map = nullptr; diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 6e725b2ac..68892f438 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -147,6 +147,22 @@ void GuestArenaRange_nid_postfix(std::uintptr_t* base, std::size_t* bytes) { } #ifdef _WIN32 +void GuestArenaSetProtection_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uint32_t protection) { + WindowsMappings::Get().SetProtection(address, bytes, protection); +} + +bool GuestArenaHandleWrite_nid_postfix(std::uintptr_t address) { + return WindowsMappings::Get().HandleWrite(address); +} + +bool GuestArenaProtection_nid_postfix(std::uintptr_t address, std::uint32_t* protection) { + return WindowsMappings::Get().Protection(address, protection); +} + +bool GuestArenaCollectWrites_nid_postfix(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear) { + return WindowsMappings::Get().Collect(address, bytes, pages, count, clear); +} + void GuestArenaCommit_nid_postfix(void* pointer, std::size_t bytes, std::uint32_t protection, std::size_t granule) { if (!Arena::Get().Contains(pointer, bytes)) throw std::invalid_argument("commit outside the guest arena"); WindowsMappings::Get().Commit(pointer, bytes, protection, granule, Arena::Get().WriteWatched()); diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 5359fcd33..a4f388e1e 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -1,5 +1,6 @@ #ifdef _WIN32 #include +#include "prx/libc/include/GuestArena.hpp" #include #include #include @@ -235,6 +236,8 @@ bool HandleSse4a(EXCEPTION_POINTERS* info) { LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { static std::atomic reported{false}; + const auto* fault = info->ExceptionRecord; + if (fault->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && fault->NumberParameters >= 2 && fault->ExceptionInformation[0] == 1 && GuestArena::GuestArenaHandleWrite_nid_postfix(fault->ExceptionInformation[1])) return EXCEPTION_CONTINUE_EXECUTION; if (HandleWatch(info)) return EXCEPTION_CONTINUE_EXECUTION; if (HandleSse4a(info)) return EXCEPTION_CONTINUE_EXECUTION; const auto* record = info->ExceptionRecord; diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 805078db4..a1440a0f5 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -112,6 +112,7 @@ static int munmap_release(void* addr) { } static int mprotect(void* addr, size_t len, int prot) { + GuestArena::GuestArenaSetProtection_nid_postfix(reinterpret_cast(addr), len, WinProtFromPosix(prot)); if (prot != PROT_NONE && KernelArena::Get().Contains(addr, len)) CommitArenaRange(addr, len, WinProtFromPosix(prot)); auto cursor = reinterpret_cast(addr); const auto end = cursor + len; @@ -278,6 +279,7 @@ struct DirectMapping { std::uintptr_t end; std::uint64_t phys; int memoryType; + std::shared_ptr backing; }; std::map g_directMappings; @@ -289,8 +291,8 @@ void EraseMappings(std::uintptr_t start, std::uintptr_t end) { const auto base = it->first; const auto mapping = it->second; it = g_directMappings.erase(it); - if (base < start) g_directMappings.emplace(base, DirectMapping{start, mapping.phys, mapping.memoryType}); - if (mapping.end > end) it = g_directMappings.emplace(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType}).first; + if (base < start) g_directMappings.emplace(base, DirectMapping{start, mapping.phys, mapping.memoryType, mapping.backing}); + if (mapping.end > end) it = g_directMappings.emplace(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType, mapping.backing}).first; } } @@ -317,7 +319,7 @@ void AddMapping(std::uintptr_t address, std::size_t len, std::uint64_t phys, int bytes += PS5_PAGE_SIZE; } page.backing->Map(address + offset, bytes, page.offset, nativeProt); - g_directMappings.emplace(address + offset, DirectMapping{address + offset + bytes, phys + offset, page.backing->MemoryType()}); + g_directMappings.emplace(address + offset, DirectMapping{address + offset + bytes, phys + offset, page.backing->MemoryType(), page.backing}); offset += bytes; } } From 5e08eb4cce020167018d42535b114b87b41140a0 Mon Sep 17 00:00:00 2001 From: boykopovar Date: Wed, 30 Sep 2026 09:27:36 +0300 Subject: [PATCH 38/40] perf(memory): skip redundant page scans in shared write tracking --- .../libs/prx/libc/include/WindowsMappings.hpp | 44 +++++++++++++- core/libs/tests/GuestMemory.cpp | 57 +++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index aff457adc..9f008ffad 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -51,6 +51,7 @@ public: if (mapped != views.end()) { mapped->second.protection = protection; mapped->second.armed = false; + invalidate(*mapped->second.page); } DWORD previous; if (!VirtualProtect(reinterpret_cast(cursor), stop - cursor, protection, &previous)) fail("protect guest memory"); @@ -83,6 +84,7 @@ public: const auto base = cursor + done; shared->aliases.push_back(base); views.emplace(base, View{shared, protection, 0, false}); + invalidate(*shared); } } @@ -91,6 +93,7 @@ public: for (auto it = views.lower_bound(address); it != views.end() && it->first < address + bytes; ++it) { it->second.protection = protection; it->second.armed = false; + invalidate(*it->second.page); } } @@ -100,7 +103,7 @@ public: const auto found = views.find(base); if (found == views.end() || !writable(found->second.protection)) return false; auto& view = found->second; - ++view.page->generation; + invalidate(*view.page); DWORD previous; if (!VirtualProtect(reinterpret_cast(base), pageBytes, view.protection, &previous)) fail("resume shared memory write"); view.armed = false; @@ -121,6 +124,14 @@ public: *count = 0; const auto end = address + bytes; for (auto cursor = address; cursor < end;) { + const auto nextClean = cleanRanges.upper_bound(cursor); + if (nextClean != cleanRanges.begin()) { + const auto clean = std::prev(nextClean); + if (cursor < clean->second) { + cursor = std::min(end, clean->second); + continue; + } + } const auto base = cursor & ~(pageBytes - 1); const auto found = views.find(base); if (found != views.end()) { @@ -145,6 +156,7 @@ public: other.armed = true; } view.seen = view.page->generation; + rememberClean(base, base + pageBytes); } cursor = stop; } else { @@ -175,6 +187,34 @@ private: std::uint64_t seen; bool armed; }; + void forgetClean(std::uintptr_t start, std::uintptr_t end) { + auto it = cleanRanges.lower_bound(start); + if (it != cleanRanges.begin() && std::prev(it)->second > start) --it; + while (it != cleanRanges.end() && it->first < end) { + const auto first = it->first; + const auto last = it->second; + it = cleanRanges.erase(it); + if (first < start) cleanRanges.emplace(first, start); + if (last > end) it = cleanRanges.emplace(end, last).first; + } + } + + void rememberClean(std::uintptr_t start, std::uintptr_t end) { + auto it = cleanRanges.lower_bound(start); + if (it != cleanRanges.begin() && std::prev(it)->second >= start) --it; + while (it != cleanRanges.end() && it->first <= end) { + start = std::min(start, it->first); + end = std::max(end, it->second); + it = cleanRanges.erase(it); + } + cleanRanges.emplace(start, end); + } + + void invalidate(SharedPage& page) { + ++page.generation; + for (const auto alias : page.aliases) forgetClean(alias, alias + pageBytes); + } + static bool writable(DWORD protection) { return protection == PAGE_READWRITE || protection == PAGE_EXECUTE_READWRITE; } @@ -216,6 +256,7 @@ private: void reset(std::uintptr_t address, std::size_t bytes) { const auto end = address + bytes; + forgetClean(address, end); for (auto cursor = address; cursor < end;) { const auto memory = query(cursor); if (memory.State == MEM_RESERVE) { @@ -252,6 +293,7 @@ private: if (query(address).RegionSize != bytes && !VirtualFree(reinterpret_cast(address), bytes, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) fail("coalesce guest placeholders"); } + std::map cleanRanges; std::map views; std::map, std::weak_ptr> physical; std::mutex mutex; diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 8ca361130..0d3233446 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -1,6 +1,8 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestHeap.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include #include "SceTypes.hpp" #include #include @@ -190,11 +192,66 @@ static void CheckHeapAfterMappingReuse() { GuestHeap::GuestHeapFree_nid_postfix(pointer); } +static void CheckSharedWriteTracking() { +#ifdef _WIN32 + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 3, 0, 0, &phys) == 0); + void* first = nullptr; + void* second = nullptr; + Require(sceKernelMapDirectMemory(&first, page * 3, 3, 0, phys, 0) == 0); + Require(sceKernelMapDirectMemory(&second, page * 3, 3, 0, phys, 0) == 0); + const auto collect = [](void* address, std::size_t bytes, bool clear = true) { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(address), bytes, pages.data(), &count, clear)); + return count; + }; + Require(collect(first, page * 3) == 12); + Require(collect(second, page * 3) == 12); + Require(collect(first, page * 3) == 0); + Require(collect(second, page * 3) == 0); + auto* left = static_cast(first); + auto* right = static_cast(second); + left[page + 5] = 21; + Require(right[page + 5] == 21); + Require(collect(first, page * 3, false) == 4); + Require(collect(first, page * 3, false) == 4); + Require(collect(first, page * 3) == 4); + Require(collect(second, page * 3) == 4); + Require(collect(first, page * 3) == 0); + right[page * 2] = 42; + Require(collect(first, page * 3) == 4); + Require(collect(second, page * 3) == 4); + Require(collect(second, page * 3) == 0); + Require(sceKernelMprotect(first, page * 3, 1) == 0); + collect(first, page * 3); + collect(second, page * 3); + right[0] = 63; + Require(left[0] == 63 && collect(first, page * 3) == 4); + Require(sceKernelMprotect(first, page * 3, 3) == 0); + collect(first, page * 3); + left[0] = 84; + Require(right[0] == 84 && collect(second, page * 3) != 0); + void* third = nullptr; + Require(sceKernelMapDirectMemory(&third, page, 3, 0, phys + page, 0) == 0); + collect(first, page * 3); + collect(second, page * 3); + static_cast(third)[0] = 105; + Require(collect(first, page * 3) == 4 && collect(second, page * 3) == 4); + Require(sceKernelMunmap(third, page) == 0); + Require(sceKernelMunmap(first, page * 3) == 0); + Require(sceKernelMunmap(second, page * 3) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0); +#endif +} + int main() { CheckNamedAndHintedMappings(); CheckDirectMemoryFollowsPhysicalPages(); CheckSharedDirectMemoryLifecycle(); CheckHeapAfterMappingReuse(); + CheckSharedWriteTracking(); constexpr std::size_t page = 0x4000; const auto failed = reinterpret_cast(static_cast(-1)); const auto reject = [&](std::size_t length, int protection, int flags, int fd, From 9cfdbff17cd0d1ccf83261aacdd5d517f98c84dc Mon Sep 17 00:00:00 2001 From: STL1te <1242562+STL1te@users.noreply.github.com> Date: Wed, 30 Sep 2026 10:56:03 +0300 Subject: [PATCH 39/40] refactor(libc, kernel): drop the libc exports the title takes from its own libc.prx The C11 thread layer (_Mtx_*, _Cnd_*, _Thrd_*, _Xtime_get_ticks, std::_Pad) and the libc additions _FNan, _FDtest, _FExp, __udivti3, _Throw_Cpp_error, generic_category, localeconv, wcsncpy_s and the aligned operator new came from the --skip-sce-module route, where our libc stood in for the title's. With the title's sce_module loaded, its libc.prx exports every one of these NIDs and the relinker lists the guest modules ahead of the host prx, so the imports never reach our copies. Checked both by the NID tables (the executable imports them from libc, the guest libc.prx exports them) and by making the four std::_Pad functions throw: the title boots, reaches the menu and loads story mode without calling them. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libc/src/Math.cpp | 28 ---- core/libs/prx/libc/src/RuntimeSupport.cpp | 9 -- core/libs/prx/libc/src/StdioExtras.cpp | 25 --- core/libs/prx/libc/src/Strings.cpp | 19 --- core/libs/prx/libkernel/CMakeLists.txt | 1 - .../prx/libkernel/Pthread/Posix/Xthreads.cpp | 151 ------------------ 6 files changed, 233 deletions(-) delete mode 100644 core/libs/prx/libkernel/Pthread/Posix/Xthreads.cpp diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 2339acf46..2aca8a4e5 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -97,32 +97,4 @@ void APS5_VABI srand_nid_postfix(unsigned int seed) { g_randState = seed; } - -constexpr short FloatDenorm = -2; -constexpr short FloatFinite = -1; -constexpr short FloatInfinite = 1; -constexpr short FloatNan = 2; - -std::uint32_t _FNan_nid_postfix[4] = {0x7FC00000u, 0, 0, 0}; - -short APS5_VABI _FDtest_nid_postfix(const float* x) { - switch (std::fpclassify(*x)) { - case FP_NAN: return FloatNan; - case FP_INFINITE: return FloatInfinite; - case FP_ZERO: return 0; - case FP_SUBNORMAL: return FloatDenorm; - default: return FloatFinite; - } -} - -short APS5_VABI _FExp_nid_postfix(float* x, float y, short eoff) { - *x = static_cast(std::ldexp(static_cast(y) * std::exp(static_cast(*x)), eoff)); - if (*x == 0.0f) return 0; - return std::isinf(*x) ? FloatInfinite : FloatFinite; -} - -unsigned __int128 APS5_VABI __udivti3_nid_postfix(unsigned __int128 dividend, unsigned __int128 divisor) { - return dividend / divisor; -} - } diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index 794e11600..bf1ada3eb 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -100,15 +100,6 @@ unsigned int APS5_VABI _ZSt14_Random_devicev_nid_postfix() { throw std::runtime_error("_Throw_C_error: C11 thread error " + std::to_string(code)); } -[[noreturn]] void APS5_VABI _ZSt16_Throw_Cpp_errori_nid_postfix(int code) { - throw std::runtime_error("_Throw_Cpp_error: C++ thread error " + std::to_string(code)); -} - -const void* APS5_VABI _ZSt16generic_categoryv_nid_postfix() { - NotImplemented_nid_no_patch(__func__); - return nullptr; -} - void APS5_VABI _Lockfilelock_nid_postfix(FileStream* stream) { if (stream == nullptr) throw std::invalid_argument("_Lockfilelock: null stream"); #ifdef _WIN32 diff --git a/core/libs/prx/libc/src/StdioExtras.cpp b/core/libs/prx/libc/src/StdioExtras.cpp index 3f82b12fd..d2aabc81d 100644 --- a/core/libs/prx/libc/src/StdioExtras.cpp +++ b/core/libs/prx/libc/src/StdioExtras.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -51,26 +50,6 @@ int APS5_VABI sprintf_s_nid_postfix(char* buffer, size_t size, const char* forma std::abort(); } -struct GuestLconv { - const char* monetary[7]; - char monetaryFlags[14]; - const char* decimalPoint; - const char* numeric[4]; - const char* falseName; - const char* trueName; - const char* no; - const char* yes; -}; -static_assert(sizeof(GuestLconv) == 0x90); - -const GuestLconv* APS5_VABI localeconv_nid_postfix() { - static const GuestLconv lconv{ - {"", "", "", "", "", "", ""}, - {CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX, CHAR_MAX}, - ".", {"", "", "", ""}, "false", "true", "", ""}; - return &lconv; -} - // Only the "C" locale exists. const char* APS5_VABI setlocale_nid_postfix(int category, const char* locale) { (void)category; @@ -122,10 +101,6 @@ void APS5_VABI _ZdaPvm_nid_postfix(void* pointer, std::size_t) { if (pointer != nullptr) ApplicationHeapFree_nid_no_patch(pointer); } -void* APS5_VABI _ZnwmSt11align_val_t_nid_postfix(std::size_t size, std::size_t alignment) { - return GuestHeap::GuestHeapAlign_nid_postfix(alignment, size); -} - void APS5_VABI _ZdlPvSt11align_val_t_nid_postfix(void* pointer, std::size_t alignment) { (void)alignment; GuestHeap::GuestHeapFree_nid_postfix(pointer); diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 3ba35d3c0..1f70ddc44 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -188,25 +188,6 @@ int APS5_VABI strncpy_s_nid_postfix(char* dest, size_t destsz, const char* src, return 0; } -int APS5_VABI wcsncpy_s_nid_postfix(wchar_t* dest, size_t destsz, const wchar_t* src, size_t count) { - constexpr int GuestEinval = 22; - constexpr int GuestErange = 34; - if (!dest || destsz == 0) return GuestEinval; - if (!src) { - dest[0] = L'\0'; - return GuestEinval; - } - size_t length = 0; - while (length < count && src[length] != L'\0') ++length; - if (length >= destsz) { - dest[0] = L'\0'; - return GuestErange; - } - std::wmemcpy(dest, src, length); - dest[length] = L'\0'; - return 0; -} - int APS5_VABI strcpy_s_nid_postfix(char* dest, size_t destsz, const char* src) { return strncpy_s_nid_postfix(dest, destsz, src, static_cast(-1)); } diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 6585bc086..dc6eaa05b 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -29,7 +29,6 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Once.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Sem.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Xthreads.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Socket.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Udp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/AddressConversion.cpp diff --git a/core/libs/prx/libkernel/Pthread/Posix/Xthreads.cpp b/core/libs/prx/libkernel/Pthread/Posix/Xthreads.cpp deleted file mode 100644 index ff45a2d56..000000000 --- a/core/libs/prx/libkernel/Pthread/Posix/Xthreads.cpp +++ /dev/null @@ -1,151 +0,0 @@ -#include -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" -#include "../include/Pthread.hpp" -#include "../include/Mutex.hpp" -#include "../include/Cond.hpp" -#include "../../Time/include/Time.hpp" -#include "Common.hpp" - -extern "C" { -int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); -int APS5_VABI scePthreadJoin(Pthread thread, void** retval); -Pthread APS5_VABI scePthreadSelf(); -} - -namespace { - -constexpr int ThrdSuccess = 0; -constexpr int ThrdNomem = 1; -constexpr int ThrdTimedout = 2; -constexpr int ThrdBusy = 3; -constexpr int ThrdError = 4; -constexpr int MtxPlain = 0x1; -constexpr int MtxRecursive = 0x100; -constexpr int SceMutexRecursive = 2; -constexpr int SceEdeadlk = static_cast(0x8002000Bu); -constexpr int SceEnomem = static_cast(0x8002000Cu); -constexpr int SceEtimedout = static_cast(0x8002003Cu); -constexpr int GuestRealtimeClock = 0; - -int initResult(int result) { - return result == 0 ? ThrdSuccess : result == SceEnomem ? ThrdNomem : ThrdError; -} - -struct GuestPad { - unsigned (APS5_VABI* const* vtable)(GuestPad*); - PthreadCond cond; - PthreadMutex mutex; - bool started; -}; -static_assert(offsetof(GuestPad, cond) == 0x8 && offsetof(GuestPad, mutex) == 0x10 && offsetof(GuestPad, started) == 0x18); - -void* APS5_VABI padEntry(void* arg) { - auto* pad = static_cast(arg); - pad->vtable[0](pad); - return nullptr; -} - -void require(int result, const char* what) { - if (result != ThrdSuccess) throw std::runtime_error(what); -} - -} - -extern "C" { - -int APS5_VABI _Mtx_init_nid_postfix(PthreadMutex* mutex, int type) { - *mutex = nullptr; - PthreadMutexattr attr{}; - const int attrResult = scePthreadMutexattrInit(&attr); - if (attrResult != 0) return initResult(attrResult); - if ((type & MtxRecursive) != 0 && scePthreadMutexattrSettype(&attr, SceMutexRecursive) != 0) { - scePthreadMutexattrDestroy(&attr); - return ThrdError; - } - const int result = scePthreadMutexInit(mutex, &attr, nullptr); - scePthreadMutexattrDestroy(&attr); - return initResult(result); -} - -void APS5_VABI _Mtx_destroy_nid_postfix(PthreadMutex* mutex) { - scePthreadMutexDestroy(mutex); -} - -int APS5_VABI _Mtx_lock_nid_postfix(PthreadMutex* mutex) { - const int result = scePthreadMutexLock(mutex); - return result == 0 ? ThrdSuccess : result == SceEdeadlk ? ThrdBusy : ThrdError; -} - -int APS5_VABI _Mtx_unlock_nid_postfix(PthreadMutex* mutex) { - scePthreadMutexUnlock(mutex); - return ThrdSuccess; -} - -int APS5_VABI _Cnd_init_nid_postfix(PthreadCond* cond) { - return initResult(scePthreadCondInit(cond, nullptr, nullptr)); -} - -void APS5_VABI _Cnd_destroy_nid_postfix(PthreadCond* cond) { - scePthreadCondDestroy(cond); -} - -int APS5_VABI _Cnd_broadcast_nid_postfix(PthreadCond* cond) { - scePthreadCondBroadcast(cond); - return ThrdSuccess; -} - -int APS5_VABI _Cnd_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* deadline) { - KernelUseconds usec = 0; - if (!PosixThread::RelativeMicroseconds(GuestRealtimeClock, deadline, &usec)) throw std::runtime_error("_Cnd_timedwait: invalid xtime"); - const int result = scePthreadCondTimedwait(cond, mutex, usec); - return result == 0 ? ThrdSuccess : result == SceEtimedout ? ThrdTimedout : ThrdError; -} - -int APS5_VABI _Thrd_join_nid_postfix(Pthread thread, int* result) { - void* value = nullptr; - if (scePthreadJoin(thread, &value) != 0) return ThrdError; - if (result) *result = static_cast(reinterpret_cast(value)); - return ThrdSuccess; -} - -Pthread APS5_VABI _Thrd_id_nid_postfix() { - return scePthreadSelf(); -} - -std::int64_t APS5_VABI _Xtime_get_ticks_nid_postfix() { - KernelTimeval now{}; - gettimeofday_nid_postfix(&now, nullptr); - return now.tv_sec * 1000000 + now.tv_usec; -} - -void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(GuestPad* pad) { - require(_Cnd_init_nid_postfix(&pad->cond), "std::_Pad: condition variable init failed"); - require(_Mtx_init_nid_postfix(&pad->mutex, MtxPlain), "std::_Pad: mutex init failed"); - pad->started = false; - require(_Mtx_lock_nid_postfix(&pad->mutex), "std::_Pad: mutex lock failed"); -} - -void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(GuestPad* pad) { - _Mtx_unlock_nid_postfix(&pad->mutex); - _Mtx_destroy_nid_postfix(&pad->mutex); - _Cnd_destroy_nid_postfix(&pad->cond); -} - -void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, Pthread* thread) { - if (scePthreadCreate(thread, nullptr, padEntry, pad, nullptr) != 0) throw std::runtime_error("std::_Pad: thread start failed"); - while (!pad->started) { - if (scePthreadCondWait(&pad->cond, &pad->mutex) != 0) throw std::runtime_error("std::_Pad: launch wait failed"); - } -} - -void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad) { - require(_Mtx_lock_nid_postfix(&pad->mutex), "std::_Pad: mutex lock failed"); - pad->started = true; - scePthreadCondSignal(&pad->cond); - _Mtx_unlock_nid_postfix(&pad->mutex); -} - -} From 3a76b18646b37a433eb3e6b1a7a7a467154ef67a Mon Sep 17 00:00:00 2001 From: STL1te <1242562+STL1te@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:15:29 +0300 Subject: [PATCH 40/40] fix(libc): the process starts in the root directory again Starting in app0 when the folder exists made the working directory, and with it the tests, depend on the layout next to the executable. The title opens its files by absolute /app0 paths, so the fallback was never used: the route to story mode runs the same without it. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libc/src/General.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 5d0dc4028..5a3364b95 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -53,7 +53,7 @@ std::optional ResolveAlias(const std::string& guestPath) struct WorkingDirectory { std::mutex mutex; const std::filesystem::path root = std::filesystem::canonical(std::filesystem::current_path()); - std::filesystem::path current = std::filesystem::is_directory(root / "app0") ? root / "app0" : root; + std::filesystem::path current = root; }; WorkingDirectory& Directories() { static WorkingDirectory state; return state; } std::filesystem::path Resolve(WorkingDirectory& state, const char* path) {