mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-11-06 13:30:00 +00:00
Merge main and preserve kernel thread tests
This commit is contained in:
commit
cd3e37795d
46 files changed
+3019
-183
No files matched your search
@@ -377,6 +377,16 @@ if(BUILD_TESTING)
|
||||
add_test(NAME guest_kernel_sys_threads_${case}_patched COMMAND guest_kernel_sys_threads_tests ${PATCHED_DIR} patched ${case})
|
||||
set_tests_properties(guest_kernel_sys_threads_${case} guest_kernel_sys_threads_${case}_patched PROPERTIES TIMEOUT 20)
|
||||
endforeach()
|
||||
|
||||
add_test_executable(guest_gnm_neo_packets_tests tests/GuestGnmNeoPackets.cpp)
|
||||
target_include_directories(guest_gnm_neo_packets_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_gnm_neo_packets_tests PRIVATE libSceGnmDriverForNeoMode libc)
|
||||
configure_windows_unwind(guest_gnm_neo_packets_tests)
|
||||
add_test(NAME guest_gnm_neo_packets COMMAND guest_gnm_neo_packets_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_gnm_neo_packets PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceGnmDriverForNeoMode>")
|
||||
endif()
|
||||
if(APPLE)
|
||||
add_subdirectory(tests/write_watch)
|
||||
add_subdirectory(tests/guest_arena)
|
||||
|
||||
@@ -703,15 +703,13 @@ void testDefaults() {
|
||||
expectFailure([] { Agc::Command::GetRegisterDefaults(0xffffffffu, true, __func__); });
|
||||
}
|
||||
|
||||
struct DefaultsType {
|
||||
std::uint32_t key;
|
||||
std::uint32_t bankIndex;
|
||||
};
|
||||
constexpr std::size_t PairEntryBytes = 8;
|
||||
constexpr std::size_t CompactEntryBytes = 12;
|
||||
|
||||
std::uint32_t colourTargetBlock(const void* root) {
|
||||
std::uint32_t colourTargetBlock(const void* root, std::size_t entryBytes) {
|
||||
const auto* bytes = static_cast<const unsigned char*>(root);
|
||||
ShaderRegister** contextBlocks = nullptr;
|
||||
const DefaultsType* types = nullptr;
|
||||
const unsigned char* types = nullptr;
|
||||
std::uint32_t count = 0;
|
||||
std::memcpy(&contextBlocks, bytes, sizeof(contextBlocks));
|
||||
std::memcpy(&types, bytes + 0x30, sizeof(types));
|
||||
@@ -721,12 +719,17 @@ std::uint32_t colourTargetBlock(const void* root) {
|
||||
std::uint32_t matches = 0;
|
||||
std::uint32_t block = 0;
|
||||
for (std::uint32_t entry = 0; entry < count; ++entry) {
|
||||
if ((types[entry].bankIndex & 3u) == 0)
|
||||
std::uint32_t key = 0;
|
||||
std::uint32_t bankIndex = 0;
|
||||
std::memcpy(&key, types + entry * entryBytes, sizeof(key));
|
||||
std::memcpy(&bankIndex, types + entry * entryBytes + 4, sizeof(bankIndex));
|
||||
bankIndex &= 0x3ffu;
|
||||
if ((bankIndex & 3u) == 0)
|
||||
++contexts;
|
||||
if (types[entry].key != 0x38e92c91u)
|
||||
if (key != 0x38e92c91u)
|
||||
continue;
|
||||
check((types[entry].bankIndex & 3u) == 0, "MRT0 defaults are not in the context bank");
|
||||
block = types[entry].bankIndex >> 2;
|
||||
check((bankIndex & 3u) == 0, "MRT0 defaults are not in the context bank");
|
||||
block = bankIndex >> 2;
|
||||
++matches;
|
||||
}
|
||||
check(matches == 1 && block < contexts && contextBlocks[block] != nullptr, "MRT0 defaults key is missing, repeated or outside the context blocks");
|
||||
@@ -739,7 +742,11 @@ std::uint32_t colourTargetBlock(const void* root) {
|
||||
void testDefaultsTypes() {
|
||||
const auto* legacy = sceAgcGetRegisterDefaults();
|
||||
check(legacy != sceAgcGetRegisterDefaults2(13), "legacy register defaults reused the compact versioned table");
|
||||
check(colourTargetBlock(legacy) == 78, "MRT0 defaults are not block 78 of the 137-entry table");
|
||||
check(colourTargetBlock(legacy, PairEntryBytes) == 78, "MRT0 defaults are not block 78 of the 137-entry table");
|
||||
check(colourTargetBlock(sceAgcGetRegisterDefaults2(13), CompactEntryBytes) == 78, "versioned MRT0 defaults are not block 78 of the compact 137-entry table");
|
||||
check(colourTargetBlock(sceAgcGetRegisterDefaults2(10), CompactEntryBytes) == 73, "versioned MRT0 defaults are not block 73 of the compact 128-entry table");
|
||||
for (std::uint32_t version = 4; version < 14; ++version)
|
||||
colourTargetBlock(sceAgcGetRegisterDefaults2(version), CompactEntryBytes);
|
||||
|
||||
const auto checkCompactTypes = [](const void* root, std::uint32_t count, std::uint32_t key,
|
||||
std::uint32_t packedType, std::uint32_t extra) {
|
||||
|
||||
@@ -337,6 +337,7 @@ set(agcDriverSources
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ShaderResources.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ShaderAddressResources.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/GuestBufferMemory.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ResidentMirror.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/BdaResources.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/BufferAddress.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ShaderValidation.cpp
|
||||
@@ -2560,12 +2561,13 @@ if(BUILD_TESTING)
|
||||
add_test(NAME agc_heap_mirror_unwatch_no_space_cache COMMAND agc_driver_graphics_tests --heap-mirror-unwatch)
|
||||
add_test(NAME agc_sparse_heap_mirror_unwatch COMMAND agc_driver_graphics_tests --sparse-heap-mirror-unwatch)
|
||||
add_test(NAME agc_sparse_heap_mirror_unwatch_no_space_cache COMMAND agc_driver_graphics_tests --sparse-heap-mirror-unwatch)
|
||||
add_test(NAME agc_sparse_heap_mirror_budget COMMAND agc_driver_graphics_tests --sparse-heap-mirror-budget)
|
||||
add_test(NAME agc_heap_mirror_run_unwatch COMMAND agc_driver_graphics_tests --heap-mirror-run-unwatch)
|
||||
add_test(NAME agc_heap_mirror_run_unwatch_no_space_cache COMMAND agc_driver_graphics_tests --heap-mirror-run-unwatch)
|
||||
set_tests_properties(agc_heap_mirror_unwatch_no_space_cache agc_sparse_heap_mirror_unwatch_no_space_cache agc_heap_mirror_run_unwatch_no_space_cache PROPERTIES ENVIRONMENT "APS5_NO_ADDRESS_SPACE_CACHE=1")
|
||||
set_tests_properties(agc_heap_mirror_unwatch agc_heap_mirror_unwatch_no_space_cache agc_sparse_heap_mirror_unwatch agc_sparse_heap_mirror_unwatch_no_space_cache agc_heap_mirror_run_unwatch agc_heap_mirror_run_unwatch_no_space_cache PROPERTIES TIMEOUT 30)
|
||||
set_tests_properties(agc_heap_mirror_unwatch agc_heap_mirror_unwatch_no_space_cache agc_sparse_heap_mirror_unwatch agc_sparse_heap_mirror_unwatch_no_space_cache agc_heap_mirror_run_unwatch agc_heap_mirror_run_unwatch_no_space_cache agc_sparse_heap_mirror_budget PROPERTIES TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_heap_mirror_unwatch agc_heap_mirror_unwatch_no_space_cache agc_sparse_heap_mirror_unwatch agc_sparse_heap_mirror_unwatch_no_space_cache agc_heap_mirror_run_unwatch agc_heap_mirror_run_unwatch_no_space_cache PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
set_tests_properties(agc_heap_mirror_unwatch agc_heap_mirror_unwatch_no_space_cache agc_sparse_heap_mirror_unwatch agc_sparse_heap_mirror_unwatch_no_space_cache agc_heap_mirror_run_unwatch agc_heap_mirror_run_unwatch_no_space_cache agc_sparse_heap_mirror_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
else()
|
||||
add_test(NAME agc_heap_mirror_unwatched_import COMMAND agc_driver_graphics_tests --heap-mirror-unwatched-import)
|
||||
set_tests_properties(agc_heap_mirror_unwatched_import PROPERTIES TIMEOUT 30)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "UserDataUse.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
@@ -11,6 +12,7 @@
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include "prx/libc/include/general/AtomicSharedPtr.hpp"
|
||||
@@ -45,6 +47,7 @@ struct DrawDecode {
|
||||
void ReadUserWords(const QueueState& queue, DrawProgram& program, bool staticAbi);
|
||||
void InitializeMerged(const QueueState& queue, DrawProgram& program, std::uint32_t pointerBase, bool pointerRequired, bool staticAbi);
|
||||
void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const ShaderRegistry& registry, bool staticAbi, bool includeFragment);
|
||||
ShaderRecompiler::ShaderVertexStageInfo DecodeOrderedVertexStageInfo(std::span<const std::byte> header, std::uint64_t headerAddress, std::span<const std::uint32_t> userData, std::vector<Graphics::DecodeRead>& reads, std::uint32_t hiddenUserWords);
|
||||
|
||||
void AppendProgramMemory(std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<DrawProgram>& programs);
|
||||
std::vector<std::pair<std::size_t, std::size_t>> UserDataWords(const std::vector<DrawProgram>& programs, const std::vector<ShaderRecompiler::ProgramRole>& roles, std::uint32_t location);
|
||||
|
||||
@@ -24,6 +24,11 @@ class DrawPipeline {
|
||||
public:
|
||||
using Commit = std::function<void()>;
|
||||
using Range = std::pair<std::uint64_t, std::uint64_t>;
|
||||
struct Watch {
|
||||
std::uint64_t retired = 0;
|
||||
std::size_t items = 0;
|
||||
std::vector<Range> writes;
|
||||
};
|
||||
enum class DrainReason : std::uint8_t { Packet, Flush, Labels, Capture, Indirect, Submission, Count };
|
||||
|
||||
static DrawPipeline& Queue0();
|
||||
@@ -43,6 +48,11 @@ public:
|
||||
std::size_t Outstanding() const { return outstanding.load(std::memory_order_acquire); }
|
||||
bool Overlaps(std::uint64_t address, std::size_t bytes);
|
||||
bool OverlapsAny(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges);
|
||||
Watch WatchWrites();
|
||||
bool WrittenSince(const Watch& watch, const std::vector<Range>& reads);
|
||||
std::uint64_t Retired() const { return retiredItems.load(std::memory_order_acquire); }
|
||||
void NoteDecodeDrain() { decodeDrains.fetch_add(1, std::memory_order_relaxed); }
|
||||
std::uint64_t DecodeDrains() const { return decodeDrains.load(std::memory_order_relaxed); }
|
||||
std::optional<std::uint64_t> PendingLabel(std::uint64_t address, std::size_t bytes);
|
||||
static bool HeldReads(std::uint64_t address, std::size_t bytes);
|
||||
void ReleaseRunningReads();
|
||||
@@ -73,6 +83,9 @@ private:
|
||||
std::vector<Item> retired;
|
||||
std::exception_ptr failure;
|
||||
std::atomic<std::size_t> outstanding{0};
|
||||
std::atomic<std::uint64_t> retiredItems{0};
|
||||
std::atomic<std::uint64_t> decodeDrains{0};
|
||||
std::uint64_t reportedDecodeDrains = 0;
|
||||
std::uint32_t idleWaiters = 0;
|
||||
std::size_t resumeAt = 0;
|
||||
bool committerWaiting = false;
|
||||
|
||||
@@ -221,7 +221,7 @@ inline Registers BuildInitialContextRegisters() {
|
||||
Registers result{
|
||||
{0x200, 0}, {0x201, 0}, {0x202, 0xcc0010}, {0x203, 0},
|
||||
{0x204, 0}, {0x205, 0}, {0x206, 1087}, {0x207, 0},
|
||||
{0x0, 0}, {0x2, 0}, {0x3, 0}, {0x4, 0}, {0x8, 0}, {0x9, 0x3f800000}, {0xa, 0}, {0xb, 0},
|
||||
{0x0, 0}, {0x2, 0}, {0x3, 0}, {0x4, 0}, {0x8, 0}, {0x9, 0x3f800000}, {0xa, 0}, {0xb, 0}, {0x10, 0}, {0x11, 0},
|
||||
{0x80, 0}, {0x83, 0xffff}, {0x8c, 0xaa99aaaa}, {0x8d, 0}, {0x8e, 0}, {0x8f, 0},
|
||||
{0xc, 0}, {0xd, 0x40004000}, {0x81, 0x80000000}, {0x82, 0x40004000},
|
||||
{0x90, 0x80000000}, {0x91, 0x40004000},
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
@@ -35,6 +36,18 @@ struct RecordedDispatch;
|
||||
// The module, layout and pipeline a compute dispatch binds (VulkanDevice::createComputePipeline).
|
||||
struct ComputePipelineObjects;
|
||||
|
||||
struct DeviceQueuePlan {
|
||||
std::uint32_t mainCount = 1;
|
||||
bool settle = false;
|
||||
std::uint32_t settleFamily = 0;
|
||||
std::uint32_t settleIndex = 0;
|
||||
bool sparseBinding = false;
|
||||
std::uint32_t sparseFamily = VK_QUEUE_FAMILY_IGNORED;
|
||||
std::uint32_t sparseIndex = 0;
|
||||
};
|
||||
|
||||
DeviceQueuePlan PlanDeviceQueues(std::span<const VkQueueFamilyProperties> families, std::uint32_t family, bool settles, bool sparseBinding);
|
||||
|
||||
class VulkanDevice {
|
||||
public:
|
||||
explicit VulkanDevice(const PresentationWindow* window = nullptr);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
|
||||
#include "Optimization/ResourceProgram.hpp"
|
||||
#include "prx/libc/include/HostThreadLocal.hpp"
|
||||
#include <atomic>
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
@@ -20,6 +21,7 @@
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace AgcDriver::DriverDetail {
|
||||
|
||||
@@ -191,6 +193,17 @@ DrawFinisher::Pending heldWrites(const DrawTail& tail) {
|
||||
return held;
|
||||
}
|
||||
|
||||
bool EmptyIndirectSkip() {
|
||||
static const bool enabled = std::getenv("APS5_NO_EMPTY_INDIRECT_SKIP") == nullptr;
|
||||
return enabled;
|
||||
}
|
||||
|
||||
struct CpuIndirectProgramsStorage {};
|
||||
|
||||
std::unordered_map<std::uint64_t, bool>& CpuIndirectPrograms() {
|
||||
return HostThreadLocal<std::unordered_map<std::uint64_t, bool>, CpuIndirectProgramsStorage>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packet, const Submission& submission, std::string& rejected) {
|
||||
@@ -386,6 +399,25 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
|
||||
programs = decode->programs;
|
||||
for (const auto& patch : userPatches) programs[patch.program].userData[patch.index] = patch.value;
|
||||
if (const auto skipped = SkippedShader(); skipped != 0 && std::any_of(programs.begin(), programs.end(), [skipped](const DrawProgram& program) { return program.binary.codeAddress == skipped; })) return drawn();
|
||||
const bool emptyIndirectSkip = EmptyIndirectSkip();
|
||||
auto& cpuIndirectPrograms = CpuIndirectPrograms();
|
||||
const auto recordDraws = [&](const Pm4::DrawArguments& arguments) { return arguments.count != 0 && arguments.instances != 0 && (!drawParameters.indexed || arguments.firstVertexOrIndex < drawParameters.indexCount); };
|
||||
if (emptyIndirectSkip && drawParameters.indirect && !programs.empty()) {
|
||||
const auto known = cpuIndirectPrograms.find(programs.front().binary.codeAddress);
|
||||
if (known != cpuIndirectPrograms.end() && known->second) {
|
||||
const auto& indirect = *drawParameters.indirect;
|
||||
recordQueuedLabelsBeforeRead(submission.queue);
|
||||
const auto readStart = std::chrono::steady_clock::now();
|
||||
const auto count = std::min(indirect.countIndirect ? Pm4::ReadDrawCount(indirect) : indirect.count, indirect.count);
|
||||
bool any = false;
|
||||
for (std::uint32_t record = 0; record < count && !any; ++record) any = recordDraws(Pm4::ReadDrawArguments(indirect, record));
|
||||
if (!any) {
|
||||
Graphics::CountEmptyIndirectDraw(std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - readStart).count());
|
||||
timing.Mark("empty_indirect_skipped");
|
||||
return drawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) {
|
||||
if (!graphics.stages.mesh) return;
|
||||
auto& words = programs.front().userData;
|
||||
@@ -448,8 +480,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
|
||||
const auto decodeVertexInfo = [&](std::size_t i) {
|
||||
const auto& program = programs[i];
|
||||
if (program.binary.stage == Stage::Fragment || roles[i] == Role::GeometryBack) return;
|
||||
decodeReads[i].clear();
|
||||
vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i], false, 8u - program.firstUserSgpr);
|
||||
vertexInfos[i] = DecodeOrderedVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, decodeReads[i], 8u - program.firstUserSgpr);
|
||||
if (vertexInfos[i]) {
|
||||
vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207), program.tailContinuation.get()) | graphics.layerExports;
|
||||
vertexInfos[i]->depthRemapMode = graphics.depthRemap.mode;
|
||||
@@ -756,6 +787,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
|
||||
const auto& result = *programResults[i];
|
||||
if (i == 0 && drawParameters.indirect) {
|
||||
indirectCpu = ClassifyIndirectDraw(result, graphics, programs.front(), localDevice, drawParameters, traceIndirect);
|
||||
if (EmptyIndirectSkip()) CpuIndirectPrograms()[programs.front().binary.codeAddress] = indirectCpu.has_value();
|
||||
} else if (i == 0) {
|
||||
fold(result, drawParameters);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include <algorithm>
|
||||
#include <spirv/unified1/spirv.hpp>
|
||||
@@ -30,6 +31,25 @@ void ReadUserWords(const QueueState& queue, DrawProgram& program, bool staticAbi
|
||||
if ((program.binary.stage == ShaderRecompiler::ShaderStage::Vertex || program.binary.stage == ShaderRecompiler::ShaderStage::Fragment) && program.snapshot->header.empty() && program.snapshot->userDataTailRegister) program.tailContinuation = ReadUserDataTailContinuation(program.binary.code, program.firstUserSgpr, program.userData);
|
||||
}
|
||||
|
||||
ShaderRecompiler::ShaderVertexStageInfo DecodeOrderedVertexStageInfo(std::span<const std::byte> header, std::uint64_t headerAddress, std::span<const std::uint32_t> userData, std::vector<Graphics::DecodeRead>& reads, std::uint32_t hiddenUserWords) {
|
||||
const auto decode = [&] {
|
||||
reads.clear();
|
||||
return Graphics::DecodeVertexStageInfo(header, headerAddress, userData, &reads, false, hiddenUserWords);
|
||||
};
|
||||
if (!DrawPipeline::Active()) return decode();
|
||||
auto& queue = DrawPipeline::Queue0();
|
||||
const auto watch = queue.WatchWrites();
|
||||
auto info = decode();
|
||||
if (watch.items == 0 && !queue.Busy() && queue.Retired() == watch.retired) return info;
|
||||
std::vector<DrawPipeline::Range> ranges;
|
||||
ranges.reserve(reads.size());
|
||||
for (const auto& read : reads) ranges.emplace_back(read.address, read.address + read.size);
|
||||
if (!queue.WrittenSince(watch, ranges)) return info;
|
||||
queue.NoteDecodeDrain();
|
||||
queue.Drain(DrawPipeline::DrainReason::Capture);
|
||||
return decode();
|
||||
}
|
||||
|
||||
void InitializeMerged(const QueueState& queue, DrawProgram& program, std::uint32_t pointerBase, bool pointerRequired, bool staticAbi) {
|
||||
program.merged = true;
|
||||
program.mergedPointer = pointerBase;
|
||||
|
||||
@@ -359,8 +359,10 @@ void DrawPipeline::report(std::chrono::steady_clock::time_point now) {
|
||||
const auto perCommit = commits == 0 ? 0.0 : static_cast<double>(commitNs) / 1e3 / static_cast<double>(commits);
|
||||
const auto perDraw = enqueued == 0 ? 0.0 : static_cast<double>(fullWaitNs) / 1e3 / static_cast<double>(enqueued);
|
||||
const auto queued = enqueued == 0 ? 0.0 : static_cast<double>(depthSum) / static_cast<double>(enqueued);
|
||||
char reads[64];
|
||||
std::snprintf(reads, sizeof(reads), " %llu read ranges, %llu stores held", static_cast<unsigned long long>(heldReadRanges.load(std::memory_order_relaxed)), static_cast<unsigned long long>(heldReadHits.exchange(0, std::memory_order_relaxed)));
|
||||
const auto decodeDrainCount = decodeDrains.load(std::memory_order_relaxed);
|
||||
char reads[96];
|
||||
std::snprintf(reads, sizeof(reads), " %llu read ranges, %llu stores held, %llu decode drains", static_cast<unsigned long long>(heldReadRanges.load(std::memory_order_relaxed)), static_cast<unsigned long long>(heldReadHits.exchange(0, std::memory_order_relaxed)), static_cast<unsigned long long>(decodeDrainCount - reportedDecodeDrains));
|
||||
reportedDecodeDrains = decodeDrainCount;
|
||||
drainText += reads;
|
||||
std::fprintf(stderr, "[draw] pipelined (10 s, depth %zu): %llu commits at %.1f us each on the committer, %llu failed; %llu enqueued behind %.2f on average, waiting %.1f us per draw for room, %.2f us for the lock; %llu committer wakes;%s; drains:%s\n", Depth(), static_cast<unsigned long long>(commits), perCommit, static_cast<unsigned long long>(commitErrors), static_cast<unsigned long long>(enqueued), queued, perDraw, enqueued == 0 ? 0.0 : static_cast<double>(lockWaitNs) / 1e3 / static_cast<double>(enqueued), static_cast<unsigned long long>(wakes), reads, drainText.c_str());
|
||||
commits = commitNs = commitErrors = enqueued = fullWaitNs = depthSum = wakes = lockWaitNs = 0;
|
||||
@@ -410,6 +412,7 @@ void DrawPipeline::run() {
|
||||
countWrites(items.front().writes, false);
|
||||
retired.push_back(std::move(items.front()));
|
||||
items.pop_front();
|
||||
retiredItems.fetch_add(1, std::memory_order_release);
|
||||
++commits;
|
||||
commitNs += spent;
|
||||
if (failed) ++commitErrors;
|
||||
@@ -517,6 +520,28 @@ void DrawPipeline::countWrites(const std::vector<Range>& writes, bool added) {
|
||||
}
|
||||
}
|
||||
|
||||
DrawPipeline::Watch DrawPipeline::WatchWrites() {
|
||||
if (!Busy()) return {Retired(), 0, {}};
|
||||
std::lock_guard lock(mutex);
|
||||
Watch watch{retiredItems.load(std::memory_order_relaxed), items.size(), {}};
|
||||
for (const auto& item : items) watch.writes.insert(watch.writes.end(), item.writes.begin(), item.writes.end());
|
||||
return watch;
|
||||
}
|
||||
|
||||
bool DrawPipeline::WrittenSince(const Watch& watch, const std::vector<Range>& reads) {
|
||||
const auto overlaps = [&reads](const std::vector<Range>& writes) {
|
||||
return std::any_of(reads.begin(), reads.end(), [&writes](const Range& read) {
|
||||
return std::any_of(writes.begin(), writes.end(), [&read](const Range& write) { return read.first < write.second && write.first < read.second; });
|
||||
});
|
||||
};
|
||||
if (DrawFinisher::Overlap() && DrawFinisher::Get().PendingOverlapsAny(reads)) return true;
|
||||
std::lock_guard lock(mutex);
|
||||
if (std::any_of(items.begin(), items.end(), [&](const Item& item) { return overlaps(item.writes); })) return true;
|
||||
const auto retiredNow = retiredItems.load(std::memory_order_relaxed);
|
||||
if (retiredNow == watch.retired) return false;
|
||||
return retiredNow - watch.retired > watch.items || overlaps(watch.writes);
|
||||
}
|
||||
|
||||
bool DrawPipeline::Busy() const {
|
||||
return outstanding.load(std::memory_order_acquire) != 0 || (finisherProducer && DrawFinisher::Busy() && DrawFinisher::Overlap());
|
||||
}
|
||||
|
||||
@@ -81,6 +81,44 @@
|
||||
#include <utility>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
DeviceQueuePlan PlanDeviceQueues(std::span<const VkQueueFamilyProperties> families, std::uint32_t family, bool settles, bool sparseBinding) {
|
||||
DeviceQueuePlan plan;
|
||||
const auto count = static_cast<std::uint32_t>(families.size());
|
||||
if (settles) {
|
||||
if (family < count && families[family].queueCount >= 2) {
|
||||
plan.mainCount = 2;
|
||||
plan.settle = true;
|
||||
plan.settleFamily = family;
|
||||
plan.settleIndex = 1;
|
||||
} else {
|
||||
const auto copies = [&](std::uint32_t index) { return index != family && families[index].queueCount != 0 && (families[index].queueFlags & (VK_QUEUE_TRANSFER_BIT | VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT)) != 0; };
|
||||
for (std::uint32_t i = 0; i < count && !plan.settle; ++i) {
|
||||
if (copies(i) && (families[i].queueFlags & (VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT)) == 0) plan.settle = true, plan.settleFamily = i;
|
||||
}
|
||||
for (std::uint32_t i = 0; i < count && !plan.settle; ++i) {
|
||||
if (copies(i)) plan.settle = true, plan.settleFamily = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!sparseBinding) return plan;
|
||||
const auto taken = [&](std::uint32_t index) { return plan.settle && index == plan.settleFamily ? plan.settleIndex + 1 : 0u; };
|
||||
const auto binds = [&](std::uint32_t index) { return index != family && families[index].queueCount > taken(index) && (families[index].queueFlags & VK_QUEUE_SPARSE_BINDING_BIT) != 0; };
|
||||
for (std::uint32_t i = 0; i < count && plan.sparseFamily == VK_QUEUE_FAMILY_IGNORED; ++i) {
|
||||
if (binds(i) && families[i].queueFlags == VK_QUEUE_SPARSE_BINDING_BIT) plan.sparseFamily = i;
|
||||
}
|
||||
for (std::uint32_t i = 0; i < count && plan.sparseFamily == VK_QUEUE_FAMILY_IGNORED; ++i) {
|
||||
if (binds(i)) plan.sparseFamily = i;
|
||||
}
|
||||
if (plan.sparseFamily != VK_QUEUE_FAMILY_IGNORED) {
|
||||
plan.sparseIndex = taken(plan.sparseFamily);
|
||||
plan.sparseBinding = true;
|
||||
} else {
|
||||
plan.sparseBinding = family < count && (families[family].queueFlags & VK_QUEUE_SPARSE_BINDING_BIT) != 0;
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
void check(VkResult result, const char* operation) {
|
||||
@@ -378,8 +416,11 @@ struct VulkanDevice::State {
|
||||
bool drawInputsInPlace = true;
|
||||
bool dmaBufImport = false;
|
||||
bool sparseBinding = false;
|
||||
bool sparseResidencyBuffer = false;
|
||||
bool sparseResidencyAliased = false;
|
||||
std::uint32_t queueFamily = 0;
|
||||
std::uint32_t sparseFamily = VK_QUEUE_FAMILY_IGNORED;
|
||||
std::uint32_t sparseIndex = 0;
|
||||
VkQueue sparseQueue = VK_NULL_HANDLE;
|
||||
std::mutex sparseQueueMutex;
|
||||
VkQueue settleQueue = VK_NULL_HANDLE;
|
||||
@@ -1614,35 +1655,18 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
||||
queueInfo.queueCount = 1;
|
||||
queueInfo.pQueuePriorities = &priority;
|
||||
const float priorities[2]{1.0f, 1.0f};
|
||||
bool settleQueue = false;
|
||||
std::uint32_t settleFamily = family;
|
||||
std::uint32_t settleIndex = 0;
|
||||
std::vector<VkQueueFamilyProperties> queues;
|
||||
#ifdef _WIN32
|
||||
const bool settles = true;
|
||||
#else
|
||||
const bool settles = state->dmaBufImport;
|
||||
#endif
|
||||
if (settles) {
|
||||
std::vector<VkQueueFamilyProperties> queues;
|
||||
{
|
||||
std::uint32_t families = 0;
|
||||
auto getFamilies = state->InstanceFunction<PFN_vkGetPhysicalDeviceQueueFamilyProperties>("vkGetPhysicalDeviceQueueFamilyProperties");
|
||||
getFamilies(selected, &families, nullptr);
|
||||
queues.resize(families);
|
||||
getFamilies(selected, &families, queues.data());
|
||||
if (family < families && queues[family].queueCount >= 2) {
|
||||
queueInfo.queueCount = 2;
|
||||
queueInfo.pQueuePriorities = priorities;
|
||||
settleQueue = true;
|
||||
settleIndex = 1;
|
||||
} else {
|
||||
const auto copies = [&](std::uint32_t index) { return index != family && queues[index].queueCount != 0 && (queues[index].queueFlags & (VK_QUEUE_TRANSFER_BIT | VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT)) != 0; };
|
||||
for (std::uint32_t i = 0; i < families && !settleQueue; ++i) {
|
||||
if (copies(i) && (queues[i].queueFlags & (VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT)) == 0) settleFamily = i, settleQueue = true;
|
||||
}
|
||||
for (std::uint32_t i = 0; i < families && !settleQueue; ++i) {
|
||||
if (copies(i)) settleFamily = i, settleQueue = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
VkDeviceCreateInfo deviceInfo{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
|
||||
deviceInfo.queueCreateInfoCount = 1;
|
||||
@@ -1677,50 +1701,38 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
||||
// Guest shaders routinely read past descriptor ranges; robust access turns that into zeros
|
||||
// instead of a GPU fault that loses the device.
|
||||
enabled.robustBufferAccess = available.robustBufferAccess;
|
||||
const auto plan = PlanDeviceQueues(queues, family, settles, available.sparseBinding == VK_TRUE);
|
||||
const bool settleQueue = plan.settle;
|
||||
const auto settleFamily = plan.settleFamily;
|
||||
const auto settleIndex = plan.settleIndex;
|
||||
VkDeviceQueueCreateInfo queueInfos[3]{queueInfo, {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}, {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}};
|
||||
if (available.sparseBinding && state->dmaBufImport) {
|
||||
std::uint32_t families = 0;
|
||||
auto getFamilies = state->InstanceFunction<PFN_vkGetPhysicalDeviceQueueFamilyProperties>("vkGetPhysicalDeviceQueueFamilyProperties");
|
||||
getFamilies(selected, &families, nullptr);
|
||||
std::vector<VkQueueFamilyProperties> queues(families);
|
||||
getFamilies(selected, &families, queues.data());
|
||||
const auto binds = [&](std::uint32_t index) { return index != family && queues[index].queueCount != 0 && (queues[index].queueFlags & VK_QUEUE_SPARSE_BINDING_BIT) != 0; };
|
||||
std::uint32_t sparseFamily = families;
|
||||
for (std::uint32_t i = 0; i < families && sparseFamily == families; ++i) {
|
||||
if (binds(i) && queues[i].queueFlags == VK_QUEUE_SPARSE_BINDING_BIT) sparseFamily = i;
|
||||
queueInfos[0].queueCount = plan.mainCount;
|
||||
queueInfos[0].pQueuePriorities = plan.mainCount == 2 ? priorities : &priority;
|
||||
deviceInfo.queueCreateInfoCount = 1;
|
||||
const auto addQueue = [&](std::uint32_t queueFamily, std::uint32_t index) {
|
||||
for (std::uint32_t info = 1; info < deviceInfo.queueCreateInfoCount; ++info) {
|
||||
if (queueInfos[info].queueFamilyIndex != queueFamily) continue;
|
||||
queueInfos[info].queueCount = std::max(queueInfos[info].queueCount, index + 1);
|
||||
queueInfos[info].pQueuePriorities = queueInfos[info].queueCount == 2 ? priorities : &priority;
|
||||
return;
|
||||
}
|
||||
for (std::uint32_t i = 0; i < families && sparseFamily == families; ++i) {
|
||||
if (binds(i)) sparseFamily = i;
|
||||
}
|
||||
if (sparseFamily != families) {
|
||||
queueInfos[1].queueFamilyIndex = sparseFamily;
|
||||
queueInfos[1].queueCount = 1;
|
||||
queueInfos[1].pQueuePriorities = &priority;
|
||||
deviceInfo.queueCreateInfoCount = 2;
|
||||
state->sparseFamily = sparseFamily;
|
||||
state->sparseBinding = true;
|
||||
} else {
|
||||
state->sparseBinding = family < families && (queues[family].queueFlags & VK_QUEUE_SPARSE_BINDING_BIT) != 0;
|
||||
}
|
||||
enabled.sparseBinding = state->sparseBinding ? VK_TRUE : VK_FALSE;
|
||||
}
|
||||
auto& added = queueInfos[deviceInfo.queueCreateInfoCount++];
|
||||
added = {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
|
||||
added.queueFamilyIndex = queueFamily;
|
||||
added.queueCount = index + 1;
|
||||
added.pQueuePriorities = added.queueCount == 2 ? priorities : &priority;
|
||||
};
|
||||
if (plan.settle && plan.settleFamily != family) addQueue(plan.settleFamily, plan.settleIndex);
|
||||
if (plan.sparseFamily != VK_QUEUE_FAMILY_IGNORED) addQueue(plan.sparseFamily, plan.sparseIndex);
|
||||
deviceInfo.pQueueCreateInfos = queueInfos;
|
||||
if (settleQueue && settleFamily != family) {
|
||||
if (deviceInfo.queueCreateInfoCount == 2 && queueInfos[1].queueFamilyIndex == settleFamily) {
|
||||
settleQueue = queues[settleFamily].queueCount >= 2;
|
||||
if (settleQueue) {
|
||||
queueInfos[1].queueCount = 2;
|
||||
queueInfos[1].pQueuePriorities = priorities;
|
||||
settleIndex = 1;
|
||||
}
|
||||
} else {
|
||||
auto& settleInfo = queueInfos[deviceInfo.queueCreateInfoCount++];
|
||||
settleInfo = {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
|
||||
settleInfo.queueFamilyIndex = settleFamily;
|
||||
settleInfo.queueCount = 1;
|
||||
settleInfo.pQueuePriorities = &priority;
|
||||
}
|
||||
}
|
||||
state->sparseFamily = plan.sparseFamily;
|
||||
state->sparseIndex = plan.sparseIndex;
|
||||
state->sparseBinding = plan.sparseBinding;
|
||||
enabled.sparseBinding = state->sparseBinding ? VK_TRUE : VK_FALSE;
|
||||
enabled.sparseResidencyBuffer = state->sparseBinding ? available.sparseResidencyBuffer : VK_FALSE;
|
||||
enabled.sparseResidencyAliased = state->sparseBinding ? available.sparseResidencyAliased : VK_FALSE;
|
||||
state->sparseResidencyBuffer = enabled.sparseResidencyBuffer == VK_TRUE;
|
||||
state->sparseResidencyAliased = enabled.sparseResidencyAliased == VK_TRUE;
|
||||
// Indirect draw records with a non-zero first instance, and several records per call.
|
||||
enabled.drawIndirectFirstInstance = available.drawIndirectFirstInstance;
|
||||
enabled.multiDrawIndirect = available.multiDrawIndirect;
|
||||
@@ -2042,7 +2054,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
||||
state->settleFamily = settleFamily;
|
||||
state->DeviceFunction<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(state->device, settleFamily, settleIndex, &state->settleQueue);
|
||||
}
|
||||
if (state->sparseFamily != VK_QUEUE_FAMILY_IGNORED) state->DeviceFunction<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(state->device, state->sparseFamily, 0, &state->sparseQueue);
|
||||
if (state->sparseFamily != VK_QUEUE_FAMILY_IGNORED) state->DeviceFunction<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(state->device, state->sparseFamily, state->sparseIndex, &state->sparseQueue);
|
||||
if (checkpoints) state->checkpointMarker = state->DeviceFunction<PFN_vkCmdSetCheckpointNV>("vkCmdSetCheckpointNV");
|
||||
if (bufferMarkers) state->CreateMarkerBuffer();
|
||||
if (deviceFault || checkpoints || bufferMarkers) {
|
||||
@@ -3849,6 +3861,8 @@ Graphics::Context VulkanDevice::buildContext() const {
|
||||
context.individuallyFreeDescriptorSets = state->individuallyFreeDescriptorSets;
|
||||
context.dmaBufImport = state->dmaBufImport;
|
||||
context.sparseBinding = state->sparseBinding;
|
||||
context.sparseResidencyBuffer = state->sparseResidencyBuffer;
|
||||
context.sparseResidencyAliased = state->sparseResidencyAliased;
|
||||
context.queueFamily = state->queueFamily;
|
||||
context.sparseFamily = state->sparseFamily;
|
||||
context.sparseQueue = state->sparseQueue;
|
||||
|
||||
@@ -182,6 +182,8 @@ struct Context {
|
||||
bool drawInputsInPlace = true;
|
||||
bool dmaBufImport = false;
|
||||
bool sparseBinding = false;
|
||||
bool sparseResidencyBuffer = false;
|
||||
bool sparseResidencyAliased = false;
|
||||
std::uint32_t queueFamily = 0;
|
||||
std::uint32_t sparseFamily = VK_QUEUE_FAMILY_IGNORED;
|
||||
VkQueue sparseQueue = VK_NULL_HANDLE;
|
||||
|
||||
@@ -94,6 +94,8 @@ bool DrawRecipes();
|
||||
enum class IndirectDrawPath : std::uint8_t { Gpu, NotFolded, FetchUnknown, NonVertexPath, IndxOffset, DrawIndex, VertexRange, FeatureGap, PendingImage, PendingLabelOrCopy, NotImported, Disabled, IndexRange, Count };
|
||||
void CountIndirectDraw(IndirectDrawPath path, double readMs, bool rewritten = false);
|
||||
std::array<std::uint64_t, static_cast<std::size_t>(IndirectDrawPath::Count)> IndirectDrawCounts();
|
||||
void CountEmptyIndirectDraw(double readMs);
|
||||
std::uint64_t EmptyIndirectDrawCount();
|
||||
|
||||
struct DrawPassCounts {
|
||||
std::uint64_t draws = 0;
|
||||
|
||||
@@ -248,6 +248,11 @@ struct MirrorStats {
|
||||
};
|
||||
MirrorStats MirrorCounters();
|
||||
std::uint64_t SnapshotMismatches();
|
||||
struct HeapMirrorHold {
|
||||
std::uint64_t mirrors = 0;
|
||||
std::uint64_t bytes = 0;
|
||||
};
|
||||
HeapMirrorHold HeapMirrorsWithin(std::uint64_t begin, std::uint64_t end);
|
||||
void ClearSnapshotCopies(VkDevice device);
|
||||
std::vector<std::uint64_t> AddressTableKey(std::span<const std::pair<std::uint64_t, std::uint64_t>> writes, std::span<const ShaderRecompiler::BdaAbi::Range> own);
|
||||
void ClearImageMirrors(VkDevice device);
|
||||
@@ -492,6 +497,7 @@ private:
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> writes;
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> readsOverBase;
|
||||
std::vector<std::pair<std::uint64_t, std::vector<std::byte>>> heapReferences;
|
||||
std::vector<std::pair<std::uint64_t, std::shared_ptr<Buffer>>> heapReadbacks;
|
||||
// UploadPrepare ran (regions are frozen); `uploaded` once UploadFinish ran.
|
||||
bool prepared = false;
|
||||
bool uploaded = false;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_RESIDENTMIRROR_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_RESIDENTMIRROR_HPP
|
||||
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <span>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver::Graphics {
|
||||
|
||||
class Buffer;
|
||||
class ResidentMemory;
|
||||
|
||||
using GuestRuns = std::vector<std::pair<std::uint64_t, std::uint64_t>>;
|
||||
|
||||
GuestRuns GuestNonzeroBlocks(const GuestRuns& candidates);
|
||||
bool GuestBytesZero(std::uint64_t begin, std::uint64_t end);
|
||||
void SetResidentAddressSpaceLimit(std::uint64_t bytes);
|
||||
|
||||
class ResidentBuffer : public std::enable_shared_from_this<ResidentBuffer> {
|
||||
public:
|
||||
static constexpr std::uint64_t BlockBytes = 65536;
|
||||
|
||||
static std::shared_ptr<ResidentBuffer> Create(const Context& context, std::uint64_t base, std::uint64_t bytes, const GuestRuns& backed);
|
||||
~ResidentBuffer();
|
||||
ResidentBuffer(const ResidentBuffer&) = delete;
|
||||
ResidentBuffer& operator=(const ResidentBuffer&) = delete;
|
||||
|
||||
VkBuffer Handle() const { return buffer; }
|
||||
VkDeviceAddress DeviceAddress() const { return address + lead; }
|
||||
std::uint64_t Origin() const { return origin; }
|
||||
std::uint64_t BoundBytes() const { return bound * BlockBytes; }
|
||||
std::uint32_t MemoryType() const { return type; }
|
||||
std::byte* Block(std::uint64_t guest) const;
|
||||
std::uint64_t UnboundBytes(const GuestRuns& runs) const;
|
||||
void Bind(const GuestRuns& runs);
|
||||
void MarkChanged(std::uint64_t begin, std::uint64_t end);
|
||||
void Read(std::uint64_t guest, std::span<std::byte> out) const;
|
||||
void Store(std::uint64_t guest, std::span<const std::byte> bytes);
|
||||
std::shared_ptr<Buffer> RecordReadback(VkCommandBuffer commands, std::uint64_t guest, std::uint64_t bytes) const;
|
||||
void ReadDevice(std::uint64_t guest, std::span<std::byte> out);
|
||||
static void FlushChanged(const Context& context);
|
||||
|
||||
private:
|
||||
struct Slot {
|
||||
void* slab = nullptr;
|
||||
std::uint32_t index = 0;
|
||||
std::byte* host = nullptr;
|
||||
};
|
||||
ResidentBuffer(const Context& context, std::uint64_t base, std::uint64_t bytes, const GuestRuns& backed);
|
||||
std::size_t blockIndex(std::uint64_t guest) const;
|
||||
void submit(const std::vector<VkSparseMemoryBind>& binds);
|
||||
void flush();
|
||||
std::shared_ptr<Buffer> readbackBatch(std::uint64_t guest, std::uint64_t bytes);
|
||||
void release() noexcept;
|
||||
|
||||
Context context;
|
||||
std::shared_ptr<ResidentMemory> memory;
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
VkDeviceAddress address = 0;
|
||||
std::uint32_t type = 0;
|
||||
std::uint64_t origin = 0;
|
||||
std::uint64_t lead = 0;
|
||||
std::uint64_t size = 0;
|
||||
std::uint64_t bound = 0;
|
||||
std::vector<Slot> slots;
|
||||
std::mutex changedMutex;
|
||||
GuestRuns changed;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -468,6 +468,7 @@ struct DrawProfile {
|
||||
// with a constant, and the time of the CPU record reads (their syncs).
|
||||
std::array<std::uint64_t, IndirectPathCount> indirect{};
|
||||
std::uint64_t indirectRewritten = 0;
|
||||
std::uint64_t indirectEmpty = 0;
|
||||
double indirectReadUs = 0;
|
||||
// Draw packets that drew nothing, by DrawSkip, and their time.
|
||||
std::array<std::uint64_t, static_cast<std::size_t>(DrawSkip::Count)> skips{};
|
||||
@@ -565,12 +566,12 @@ void reportDraw(const std::array<double, PhaseCount>& us, const ShaderResources:
|
||||
if (room()) n += std::snprintf(line + n, sizeof(line) - static_cast<std::size_t>(n), "; hook waits inside draws %.1f ms (max %.1f; the draws' own syncs, %.1f ms, not counted)", profile.hookWaitUs / 1000.0, profile.maxHookWaitUs / 1000.0, profile.ownSyncUs / 1000.0);
|
||||
std::uint64_t indirectCpu = 0;
|
||||
for (std::size_t i = 1; i < IndirectPathCount; ++i) indirectCpu += profile.indirect[i];
|
||||
if ((profile.indirect[0] != 0 || indirectCpu != 0) && room()) {
|
||||
if ((profile.indirect[0] != 0 || indirectCpu != 0 || profile.indirectEmpty != 0) && room()) {
|
||||
n += std::snprintf(line + n, sizeof(line) - static_cast<std::size_t>(n), "; indirect: gpu-side %llu (%llu rewritten), cpu-side %llu (", static_cast<unsigned long long>(profile.indirect[0]), static_cast<unsigned long long>(profile.indirectRewritten), static_cast<unsigned long long>(indirectCpu));
|
||||
for (std::size_t i = 1; i < IndirectPathCount && room(); ++i) {
|
||||
if (profile.indirect[i] != 0) n += std::snprintf(line + n, sizeof(line) - static_cast<std::size_t>(n), " %s %llu", IndirectDrawPathNames[i], static_cast<unsigned long long>(profile.indirect[i]));
|
||||
}
|
||||
if (room()) n += std::snprintf(line + n, sizeof(line) - static_cast<std::size_t>(n), "), argument reads %.1f ms", profile.indirectReadUs / 1000.0);
|
||||
if (room()) n += std::snprintf(line + n, sizeof(line) - static_cast<std::size_t>(n), "), empty skipped before their stages %llu, argument reads %.1f ms", static_cast<unsigned long long>(profile.indirectEmpty), profile.indirectReadUs / 1000.0);
|
||||
}
|
||||
if (room()) n += std::snprintf(line + n, sizeof(line) - static_cast<std::size_t>(n), "; skipped packets:");
|
||||
for (std::size_t i = 0; i < profile.skips.size() && room(); ++i) {
|
||||
@@ -901,6 +902,21 @@ std::array<std::uint64_t, IndirectPathCount> IndirectDrawCounts() {
|
||||
return stats.indirect;
|
||||
}
|
||||
|
||||
void CountEmptyIndirectDraw(double readMs) {
|
||||
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
|
||||
if (!profile) return;
|
||||
auto& stats = Profile();
|
||||
std::lock_guard lock(stats.mutex);
|
||||
++stats.indirectEmpty;
|
||||
stats.indirectReadUs += readMs * 1000.0;
|
||||
}
|
||||
|
||||
std::uint64_t EmptyIndirectDrawCount() {
|
||||
auto& stats = Profile();
|
||||
std::lock_guard lock(stats.mutex);
|
||||
return stats.indirectEmpty;
|
||||
}
|
||||
|
||||
std::array<DrawPassCounts, 2> DrawPassTotals() {
|
||||
auto& stats = Profile();
|
||||
std::lock_guard lock(stats.mutex);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ResidentMirror.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
@@ -74,6 +75,9 @@ struct ImageMirror {
|
||||
std::vector<std::uint64_t> generations;
|
||||
std::weak_ptr<const GuestAllocations::Range> range;
|
||||
std::shared_ptr<Buffer> buffer;
|
||||
std::shared_ptr<ResidentBuffer> resident;
|
||||
std::uint64_t accounted = 0;
|
||||
bool listed = false;
|
||||
// Writable ranges only: the guest bytes the mirror holds, compared on refresh and at write-back.
|
||||
std::vector<std::byte> shadow;
|
||||
// Identity for the life of this mirror (see ImageMirrorSerial); the top bit keeps mirror serials
|
||||
@@ -1655,7 +1659,11 @@ const HostImport* importJoined(const Context& context, HostImports& state, std::
|
||||
auto run = joinedRun(lease, begin, end);
|
||||
if (run.ranges.empty()) return nullptr;
|
||||
if (const auto failed = state.failedJoins.find(run.base); failed != state.failedJoins.end() && sameRun(failed->second, run)) return nullptr;
|
||||
return importRunAt(context, state, std::move(run), true, begin, lease, end, true, ImportRequester::Join);
|
||||
const auto existing = state.imports.find(run.base);
|
||||
const bool existed = existing != state.imports.end() && sameRun(existing->second, run);
|
||||
const auto* entry = importRunAt(context, state, std::move(run), true, begin, lease, end, true, ImportRequester::Join);
|
||||
if (entry != nullptr && !existed) ++state.epoch;
|
||||
return entry;
|
||||
}
|
||||
|
||||
const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict, ImportRequester requester) {
|
||||
@@ -2122,25 +2130,104 @@ std::uint64_t heapMirrorBudget() {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::string heapMirrorError(const GuestAllocations::Range& range, std::uint64_t held, std::string_view reason) {
|
||||
std::string heapMirrorError(std::uint64_t address, std::uint64_t bytes, std::uint64_t held, std::string_view reason) {
|
||||
constexpr std::string_view prefix = "AGC graphics: ";
|
||||
if (reason.starts_with(prefix)) reason.remove_prefix(prefix.size());
|
||||
char where[64];
|
||||
std::snprintf(where, sizeof(where), "heap mirror of 0x%llx+0x%llx: ", static_cast<unsigned long long>(range.address), static_cast<unsigned long long>(range.bytes));
|
||||
std::snprintf(where, sizeof(where), "heap mirror of 0x%llx+0x%llx: ", static_cast<unsigned long long>(address), static_cast<unsigned long long>(bytes));
|
||||
char holding[96];
|
||||
std::snprintf(holding, sizeof(holding), "; heap mirrors hold %llu MiB of %llu MiB (APS5_HEAP_MIRROR_MIB)", static_cast<unsigned long long>(held >> 20u), static_cast<unsigned long long>(heapMirrorBudget() >> 20u));
|
||||
return std::string(prefix) + where + std::string(reason) + holding;
|
||||
}
|
||||
|
||||
std::uint64_t heapMirrorsBeside(const GuestAllocations::Range& range) {
|
||||
auto& state = Mirrors();
|
||||
std::lock_guard lock(state.mutex);
|
||||
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;
|
||||
return held;
|
||||
void sweepMirrors();
|
||||
|
||||
void unlistMirror(ImageMirrors& state, ImageMirror& mirror) {
|
||||
if (!mirror.listed) return;
|
||||
state.heapBytes -= mirror.accounted;
|
||||
mirror.listed = false;
|
||||
}
|
||||
|
||||
void sweepMirrors();
|
||||
std::uint64_t blockBytes(const GuestRuns& runs) {
|
||||
constexpr std::uint64_t block = ResidentBuffer::BlockBytes;
|
||||
std::uint64_t blocks = 0;
|
||||
std::uint64_t last = 0;
|
||||
bool any = false;
|
||||
for (const auto& [first, end] : runs) {
|
||||
if (first >= end) continue;
|
||||
auto index = first / block;
|
||||
if (any && index == last) ++index;
|
||||
if (index <= (end - 1) / block) blocks += (end - 1) / block - index + 1;
|
||||
last = (end - 1) / block;
|
||||
any = true;
|
||||
}
|
||||
return blocks * block;
|
||||
}
|
||||
|
||||
void reserveHeapBytes(std::uint64_t address, std::uint64_t bytes, std::uint64_t need) {
|
||||
auto& state = Mirrors();
|
||||
for (int attempt = 0;; ++attempt) {
|
||||
{
|
||||
std::lock_guard lock(state.mutex);
|
||||
std::uint64_t held = state.heapBytes;
|
||||
if (const auto found = state.entries.find(address); found != state.entries.end() && found->second->heap && found->second->listed) held -= found->second->accounted;
|
||||
if (held + need <= heapMirrorBudget()) {
|
||||
state.heapBytes += need;
|
||||
return;
|
||||
}
|
||||
if (attempt != 0) throw std::runtime_error(heapMirrorError(address, bytes, held, "past the budget"));
|
||||
}
|
||||
sweepMirrors();
|
||||
}
|
||||
}
|
||||
|
||||
void growResident(ImageMirror& mirror, const GuestRuns& runs) {
|
||||
if (mirror.resident == nullptr) return;
|
||||
const auto need = mirror.resident->UnboundBytes(runs);
|
||||
if (need == 0) return;
|
||||
auto& state = Mirrors();
|
||||
bool counted = false;
|
||||
for (int attempt = 0; !counted; ++attempt) {
|
||||
{
|
||||
std::lock_guard lock(state.mutex);
|
||||
if (!mirror.listed) break;
|
||||
if (state.heapBytes + need <= heapMirrorBudget()) {
|
||||
state.heapBytes += need;
|
||||
mirror.accounted += need;
|
||||
counted = true;
|
||||
break;
|
||||
}
|
||||
if (attempt != 0) throw std::runtime_error(heapMirrorError(mirror.base, mirror.reserved, state.heapBytes, "past the budget"));
|
||||
}
|
||||
sweepMirrors();
|
||||
}
|
||||
try {
|
||||
mirror.resident->Bind(runs);
|
||||
} catch (const std::runtime_error& error) {
|
||||
std::uint64_t held = 0;
|
||||
{
|
||||
std::lock_guard lock(state.mutex);
|
||||
if (counted && mirror.listed) {
|
||||
state.heapBytes -= need;
|
||||
mirror.accounted -= need;
|
||||
}
|
||||
held = state.heapBytes;
|
||||
}
|
||||
throw std::runtime_error(heapMirrorError(mirror.base, mirror.reserved, held, error.what()));
|
||||
}
|
||||
}
|
||||
|
||||
std::uint64_t mirrorStart(const ImageMirror& mirror) {
|
||||
return mirror.resident != nullptr ? mirror.resident->Origin() : mirror.base;
|
||||
}
|
||||
|
||||
VkBuffer mirrorHandle(const ImageMirror& mirror) {
|
||||
return mirror.resident != nullptr ? mirror.resident->Handle() : mirror.buffer->Handle();
|
||||
}
|
||||
|
||||
VkDeviceAddress mirrorAddress(const ImageMirror& mirror) {
|
||||
return mirror.resident != nullptr ? mirror.resident->DeviceAddress() : mirror.buffer->DeviceAddress();
|
||||
}
|
||||
|
||||
bool sameRange(const std::weak_ptr<const GuestAllocations::Range>& mirrored, const std::shared_ptr<const GuestAllocations::Range>& range) {
|
||||
return !mirrored.owner_before(range) && !range.owner_before(mirrored);
|
||||
@@ -2162,6 +2249,16 @@ constexpr std::uint8_t SparseZero = 2;
|
||||
bool compareBlock(const RefreshBlock& block) {
|
||||
const auto offset = static_cast<std::size_t>(block.address - block.mirror->base);
|
||||
const auto* guest = reinterpret_cast<const std::byte*>(block.address);
|
||||
if (block.mirror->resident != nullptr) {
|
||||
auto* target = block.mirror->resident->Block(block.address);
|
||||
if (block.fill == SparseZero) {
|
||||
if (target != nullptr) std::memset(target, 0, block.length);
|
||||
return true;
|
||||
}
|
||||
if (target != nullptr) std::memcpy(target, guest, block.length);
|
||||
if (block.fill == 0) block.mirror->generations[static_cast<std::size_t>(block.address / 65536 - block.mirror->base / 65536)] = block.generation;
|
||||
return target != nullptr;
|
||||
}
|
||||
if (block.fill != 0) {
|
||||
auto* target = block.mirror->buffer->Bytes().data() + offset;
|
||||
if (block.fill == SparseZero) std::memset(target, 0, block.length);
|
||||
@@ -2313,9 +2410,33 @@ void appendBlocks(std::vector<RefreshBlock>& blocks, ImageMirror& mirror, std::u
|
||||
}
|
||||
|
||||
// Compares the gathered blocks (of one or many mirrors) and counts them.
|
||||
void bindHeapBlocks(std::span<const RefreshBlock> blocks) {
|
||||
std::vector<std::pair<ImageMirror*, GuestRuns>> wanted;
|
||||
for (const auto& block : blocks) {
|
||||
if (block.mirror->resident == nullptr || block.fill == SparseZero) continue;
|
||||
if (block.mirror->resident->Block(block.address) == nullptr && GuestBytesZero(block.address, block.address + block.length)) continue;
|
||||
if (wanted.empty() || wanted.back().first != block.mirror) {
|
||||
const auto found = std::find_if(wanted.begin(), wanted.end(), [&](const auto& entry) { return entry.first == block.mirror; });
|
||||
if (found == wanted.end()) wanted.emplace_back(block.mirror, GuestRuns{});
|
||||
else std::rotate(found, std::next(found), wanted.end());
|
||||
}
|
||||
auto& runs = wanted.back().second;
|
||||
if (!runs.empty() && runs.back().second == block.address) runs.back().second = block.address + block.length;
|
||||
else runs.emplace_back(block.address, block.address + block.length);
|
||||
}
|
||||
for (auto& [mirror, runs] : wanted) {
|
||||
std::sort(runs.begin(), runs.end());
|
||||
growResident(*mirror, runs);
|
||||
}
|
||||
}
|
||||
|
||||
void compareBlocks(std::span<const RefreshBlock> blocks) {
|
||||
if (blocks.empty()) return;
|
||||
bindHeapBlocks(blocks);
|
||||
const auto copied = RefreshPool::Get().Run(blocks);
|
||||
for (const auto& block : blocks) {
|
||||
if (block.mirror->resident != nullptr && block.mirror->resident->Block(block.address) != nullptr) block.mirror->resident->MarkChanged(block.address, block.address + block.length);
|
||||
}
|
||||
auto& state = Mirrors();
|
||||
state.blocksCompared += blocks.size();
|
||||
state.blocksCopied += copied;
|
||||
@@ -2542,6 +2663,7 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
|
||||
{
|
||||
std::lock_guard lock(state.mutex);
|
||||
if (state.device != context.device) {
|
||||
for (auto& [base, entry] : state.entries) entry->listed = false;
|
||||
state.entries.clear();
|
||||
state.failed.clear();
|
||||
state.heapBytes = 0;
|
||||
@@ -2574,6 +2696,7 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
|
||||
mirror->writable = range->writable;
|
||||
mirror->heap = heap;
|
||||
mirror->range = range;
|
||||
GuestRuns backed;
|
||||
if (heap) {
|
||||
std::uint64_t generation = 0;
|
||||
const auto serial = GuestMemory::UnwatchSerial();
|
||||
@@ -2591,7 +2714,7 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
|
||||
std::lock_guard lock(state.mutex);
|
||||
++state.heapUnwatched;
|
||||
if (const auto found = state.entries.find(range->address); found != state.entries.end() && found->second->heap) {
|
||||
state.heapBytes -= found->second->bytes;
|
||||
unlistMirror(state, *found->second);
|
||||
stale = std::move(found->second);
|
||||
state.entries.erase(found);
|
||||
++state.version;
|
||||
@@ -2599,15 +2722,64 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
|
||||
return nullptr;
|
||||
}
|
||||
mirror->watchedSerial.store(serial, std::memory_order_relaxed);
|
||||
if (heapMirrorsBeside(*range) + mirror->bytes > heapMirrorBudget()) {
|
||||
sweepMirrors();
|
||||
if (const auto held = heapMirrorsBeside(*range); held + mirror->bytes > heapMirrorBudget()) throw std::runtime_error(heapMirrorError(*range, held, "past the budget"));
|
||||
if (context.sparseBinding && context.sparseResidencyBuffer && context.sparseResidencyAliased) backed = GuestNonzeroBlocks(sparse ? clipPieces(pieces, mirror->base, mirror->base + mirror->bytes) : GuestRuns{{mirror->base, mirror->base + mirror->bytes}});
|
||||
auto need = blockBytes(backed);
|
||||
reserveHeapBytes(range->address, range->bytes, need);
|
||||
try {
|
||||
mirror->resident = ResidentBuffer::Create(context, mirror->base, mirror->bytes, backed);
|
||||
} catch (const std::runtime_error& error) {
|
||||
std::uint64_t held = 0;
|
||||
{
|
||||
std::lock_guard lock(state.mutex);
|
||||
state.heapBytes -= need;
|
||||
held = state.heapBytes;
|
||||
}
|
||||
throw std::runtime_error(heapMirrorError(range->address, range->bytes, held, error.what()));
|
||||
}
|
||||
if (mirror->resident == nullptr) {
|
||||
{
|
||||
std::lock_guard lock(state.mutex);
|
||||
state.heapBytes -= need;
|
||||
}
|
||||
need = mirror->bytes;
|
||||
reserveHeapBytes(range->address, range->bytes, need);
|
||||
}
|
||||
mirror->accounted = need;
|
||||
}
|
||||
if (mirror->resident != nullptr) {
|
||||
{
|
||||
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::MirrorRefresh);
|
||||
for (const auto& [first, last] : backed) {
|
||||
for (auto at = first; at < last;) {
|
||||
const auto next = std::min(last, (at / ResidentBuffer::BlockBytes + 1) * ResidentBuffer::BlockBytes);
|
||||
GuestMemory::Read(at, std::span<std::byte>(mirror->resident->Block(at), static_cast<std::size_t>(next - at)));
|
||||
at = next;
|
||||
}
|
||||
mirror->resident->MarkChanged(first / ResidentBuffer::BlockBytes * ResidentBuffer::BlockBytes, (last + ResidentBuffer::BlockBytes - 1) / ResidentBuffer::BlockBytes * ResidentBuffer::BlockBytes);
|
||||
}
|
||||
}
|
||||
mirror->committed = std::move(pieces);
|
||||
std::lock_guard lock(state.mutex);
|
||||
++state.rebuilds;
|
||||
mirror->serial = (1ull << 63u) | ++state.serials;
|
||||
if (const auto found = state.entries.find(range->address); found != state.entries.end() && found->second->heap) unlistMirror(state, *found->second);
|
||||
mirror->listed = true;
|
||||
state.entries[range->address] = mirror;
|
||||
++state.version;
|
||||
return mirror;
|
||||
}
|
||||
try {
|
||||
mirror->buffer = std::make_shared<Buffer>(context, static_cast<std::size_t>(mirror->bytes), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
|
||||
} catch (const std::runtime_error& error) {
|
||||
if (heap) throw std::runtime_error(heapMirrorError(*range, heapMirrorsBeside(*range), error.what()));
|
||||
if (heap) {
|
||||
std::uint64_t held = 0;
|
||||
{
|
||||
std::lock_guard lock(state.mutex);
|
||||
state.heapBytes -= mirror->accounted;
|
||||
held = state.heapBytes;
|
||||
}
|
||||
throw std::runtime_error(heapMirrorError(range->address, range->bytes, held, error.what()));
|
||||
}
|
||||
std::fprintf(stderr, "[gpu] image mirror of 0x%llx+0x%llx failed: %s; falling back to copies\n", static_cast<unsigned long long>(range->address), static_cast<unsigned long long>(range->bytes), error.what());
|
||||
std::lock_guard lock(state.mutex);
|
||||
state.failed.insert(range->address);
|
||||
@@ -2634,8 +2806,8 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
|
||||
++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;
|
||||
if (const auto found = state.entries.find(range->address); found != state.entries.end() && found->second->heap) unlistMirror(state, *found->second);
|
||||
mirror->listed = heap;
|
||||
state.entries[range->address] = mirror;
|
||||
++state.version;
|
||||
return mirror;
|
||||
@@ -2671,7 +2843,7 @@ void sweepMirrors() {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
if (it->second->heap) state.heapBytes -= it->second->bytes;
|
||||
if (it->second->heap) unlistMirror(state, *it->second);
|
||||
it = state.entries.erase(it);
|
||||
++state.version;
|
||||
}
|
||||
@@ -2688,9 +2860,9 @@ void releaseImportedMirrors(const Context& context, const std::vector<std::uint6
|
||||
for (const auto base : imported) {
|
||||
const auto found = state.entries.find(base);
|
||||
if (found == state.entries.end() || !found->second->heap) continue;
|
||||
state.heapBytes -= found->second->bytes;
|
||||
state.heapImportedBytes += found->second->accounted;
|
||||
unlistMirror(state, *found->second);
|
||||
++state.heapImported;
|
||||
state.heapImportedBytes += found->second->bytes;
|
||||
released.push_back(std::move(found->second));
|
||||
state.entries.erase(found);
|
||||
}
|
||||
@@ -3013,6 +3185,18 @@ std::uint64_t SnapshotMismatches() {
|
||||
return Snapshots().mismatched.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
HeapMirrorHold HeapMirrorsWithin(std::uint64_t begin, std::uint64_t end) {
|
||||
auto& state = Mirrors();
|
||||
std::lock_guard lock(state.mutex);
|
||||
HeapMirrorHold hold;
|
||||
for (auto it = state.entries.lower_bound(begin); it != state.entries.end() && it->first < end; ++it) {
|
||||
if (!it->second->heap || it->second->range.expired()) continue;
|
||||
++hold.mirrors;
|
||||
if (it->second->listed) hold.bytes += it->second->accounted;
|
||||
}
|
||||
return hold;
|
||||
}
|
||||
|
||||
std::vector<std::uint64_t> AddressTableKey(std::span<const std::pair<std::uint64_t, std::uint64_t>> writes, std::span<const ShaderRecompiler::BdaAbi::Range> own) {
|
||||
std::vector<std::uint64_t> key;
|
||||
key.reserve(writes.size() * 2u + 1u + own.size() * 4u);
|
||||
@@ -3041,6 +3225,7 @@ void ClearImageMirrors(VkDevice device) {
|
||||
std::lock_guard lock(state.mutex);
|
||||
if (state.device != device) return;
|
||||
entries.swap(state.entries);
|
||||
for (auto& [base, entry] : entries) entry->listed = false;
|
||||
state.failed.clear();
|
||||
state.heapBytes = 0;
|
||||
state.device = VK_NULL_HANDLE;
|
||||
@@ -3983,7 +4168,7 @@ bool GuestBufferMemory::bindSparseMirror(Region& region, const GuestAllocations:
|
||||
refreshHeapMirrors(whole, blocks);
|
||||
}
|
||||
compareBlocks(blocks);
|
||||
if (mirror == nullptr || !mirror->heap || region.begin < mirror->base || (region.begin - mirror->base) % context.limits.minStorageBufferOffsetAlignment != 0) return false;
|
||||
if (mirror == nullptr || !mirror->heap || region.begin < mirror->base || (region.begin - mirrorStart(*mirror)) % context.limits.minStorageBufferOffsetAlignment != 0) return false;
|
||||
region.mirror = std::move(mirror);
|
||||
if (!servesView(region, std::max(region.lastView, region.begin))) {
|
||||
region.mirror = nullptr;
|
||||
@@ -4689,6 +4874,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) {
|
||||
if (heapsEnd > heapsBegin) StorageTexture::PublishShadowsOnly(heapsBegin, static_cast<std::size_t>(heapsEnd - heapsBegin), PublishScope::Whole, "address-based heaps");
|
||||
if (!gpuCopies.empty()) recordGpuCopies(gpuCopies, addressable);
|
||||
takeHeapReferences();
|
||||
ResidentBuffer::FlushChanged(context);
|
||||
if (!profile) return;
|
||||
const auto loopUs = microsecondsSince(loopStart);
|
||||
readUs.fetch_add(refreshUs, std::memory_order_relaxed);
|
||||
@@ -4923,16 +5109,36 @@ void GuestBufferMemory::RecordCopyBacks(Recorder& recorder) {
|
||||
VkCommandBuffer commands = VK_NULL_HANDLE;
|
||||
auto timing = Recorder::NoTiming;
|
||||
std::uint64_t copiedBytes = 0;
|
||||
if (!writes.empty()) {
|
||||
auto sorted = writes;
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
for (const auto& range : sorted) {
|
||||
if (!merged.empty() && range.first < merged.back().second) merged.back().second = std::max(merged.back().second, range.second);
|
||||
else merged.push_back(range);
|
||||
}
|
||||
}
|
||||
for (const auto& [begin, end] : merged) {
|
||||
const auto* found = owner(begin, end);
|
||||
if (found == nullptr || found->mirror == nullptr || !found->mirror->heap || !found->mirror->writable || found->mirror->resident == nullptr) continue;
|
||||
const auto& region = *found;
|
||||
{
|
||||
const auto from = begin;
|
||||
const auto to = std::min(end, servedEnd(region));
|
||||
if (from >= to || std::any_of(heapReadbacks.begin(), heapReadbacks.end(), [&](const auto& entry) { return entry.first == from; })) continue;
|
||||
if (!recording) {
|
||||
commands = recorder.Commands();
|
||||
timing = recorder.BeginGpuTiming(StagingCopyBackKey);
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
|
||||
Recorder::CountBarriers(Recorder::CommandClass::StagingOut);
|
||||
recording = true;
|
||||
}
|
||||
heapReadbacks.emplace_back(from, region.mirror->resident->RecordReadback(commands, from, to - from));
|
||||
recorder.Keep(heapReadbacks.back().second);
|
||||
copiedBytes += to - from;
|
||||
}
|
||||
}
|
||||
for (auto& region : regions) {
|
||||
if (!region.gpuCopy || region.copiedBack) continue;
|
||||
if (merged.empty() && !writes.empty()) {
|
||||
auto sorted = writes;
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
for (const auto& range : sorted) {
|
||||
if (!merged.empty() && range.first < merged.back().second) merged.back().second = std::max(merged.back().second, range.second);
|
||||
else merged.push_back(range);
|
||||
}
|
||||
}
|
||||
for (const auto& [begin, end] : merged) {
|
||||
const auto from = std::max(begin, region.begin);
|
||||
const auto to = std::min(end, region.end);
|
||||
@@ -4989,7 +5195,7 @@ VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std:
|
||||
return std::string(text);
|
||||
};
|
||||
if (!(address >= region.begin && address + bytes <= region.end && (region.buffer != nullptr || region.direct != nullptr || region.mirror != nullptr))) Require(false, describe("exceeds"));
|
||||
const auto base = region.direct != nullptr ? region.direct->base - region.direct->lead : region.mirror != nullptr ? region.mirror->base : region.begin;
|
||||
const auto base = region.direct != nullptr ? region.direct->base - region.direct->lead : region.mirror != nullptr ? mirrorStart(*region.mirror) : region.begin;
|
||||
const auto offset = address - base;
|
||||
Require(context.limits.minStorageBufferOffsetAlignment != 0, "no storage buffer offset alignment");
|
||||
Require(base % 4 == 0, "a guest buffer view in a GPU owner that does not start at a DWORD boundary is not implemented");
|
||||
@@ -5003,7 +5209,7 @@ VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std:
|
||||
}
|
||||
if (address - adjustment + range > end) Require(false, describe("rounded up to its view range exceeds"));
|
||||
Require(range <= 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();
|
||||
const auto handle = region.direct != nullptr ? region.direct->buffer : region.mirror != nullptr ? mirrorHandle(*region.mirror) : region.buffer->Handle();
|
||||
return {handle, offset - adjustment, range};
|
||||
}
|
||||
|
||||
@@ -5016,7 +5222,7 @@ std::uint64_t GuestBufferMemory::ViewBytes(std::uint64_t bytes, std::uint32_t ad
|
||||
|
||||
ShaderRecompiler::BdaAbi::Range GuestBufferMemory::addressRange(const Region& region) {
|
||||
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();
|
||||
const auto address = region.direct != nullptr ? region.direct->address + (region.begin - region.direct->base) : region.mirror != nullptr ? mirrorAddress(*region.mirror) + (region.begin - region.mirror->base) : region.buffer->DeviceAddress();
|
||||
Require(region.end - region.begin <= std::numeric_limits<std::uint64_t>::max() - address, "GPU address range overflow");
|
||||
const auto permissions = ShaderRecompiler::BdaAbi::Read | (region.direct != nullptr && region.writable ? ShaderRecompiler::BdaAbi::Write : 0u);
|
||||
return {region.begin, region.direct != nullptr ? inPlaceEnd(region) : servedEnd(region), address, permissions, 0};
|
||||
@@ -5279,15 +5485,19 @@ std::vector<std::pair<std::uint64_t, std::uint64_t>> mergeWrites(std::vector<std
|
||||
|
||||
void GuestBufferMemory::takeHeapReferences() {
|
||||
heapReferences.clear();
|
||||
heapReadbacks.clear();
|
||||
for (const auto& [begin, end] : mergeWrites(writes)) {
|
||||
const auto* found = owner(begin, end);
|
||||
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;
|
||||
auto& mirror = *found->mirror;
|
||||
const auto served = std::min(end, servedEnd(*found));
|
||||
if (begin >= served) continue;
|
||||
const auto bytes = mirror.buffer->Bytes().subspan(static_cast<std::size_t>(begin - mirror.base), static_cast<std::size_t>(served - begin));
|
||||
heapReferences.emplace_back(begin, std::vector<std::byte>(bytes.begin(), bytes.end()));
|
||||
growResident(mirror, {{begin, served}});
|
||||
std::vector<std::byte> bytes(static_cast<std::size_t>(served - begin));
|
||||
if (mirror.resident != nullptr) mirror.resident->Read(begin, bytes);
|
||||
else std::memcpy(bytes.data(), mirror.buffer->Bytes().data() + (begin - mirror.base), bytes.size());
|
||||
heapReferences.emplace_back(begin, std::move(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5317,14 +5527,28 @@ void GuestBufferMemory::WriteBack() {
|
||||
const auto served = std::min(end, servedEnd(region));
|
||||
if (mirror.heap && begin >= served) continue;
|
||||
const auto length = static_cast<std::size_t>((mirror.heap ? served : end) - begin);
|
||||
const auto current = mirror.buffer->Bytes().subspan(offset, length);
|
||||
if (mirror.heap) {
|
||||
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");
|
||||
if (mirror.resident == nullptr) {
|
||||
const auto current = mirror.buffer->Bytes().subspan(offset, length);
|
||||
if (mirror.sparse) storeSparse(mirror, begin, current, reference->second);
|
||||
else storeChanged(begin, current, reference->second);
|
||||
continue;
|
||||
}
|
||||
std::vector<std::byte> current(length);
|
||||
if (const auto readback = std::find_if(heapReadbacks.begin(), heapReadbacks.end(), [&](const auto& entry) { return entry.first == begin; }); readback != heapReadbacks.end()) {
|
||||
Require(readback->second->Bytes().size() == length, "heap mirror readback does not match its write-back");
|
||||
std::memcpy(current.data(), readback->second->Bytes().data(), length);
|
||||
} else {
|
||||
mirror.resident->ReadDevice(begin, current);
|
||||
}
|
||||
mirror.resident->Store(begin, current);
|
||||
if (mirror.sparse) storeSparse(mirror, begin, current, reference->second);
|
||||
else storeChanged(begin, current, reference->second);
|
||||
continue;
|
||||
}
|
||||
const auto current = mirror.buffer->Bytes().subspan(offset, length);
|
||||
GuestMemory::WriteChanged(begin, current, std::span<const std::byte>(mirror.shadow).subspan(offset, length));
|
||||
std::memcpy(mirror.shadow.data() + offset, current.data(), length);
|
||||
continue;
|
||||
@@ -5375,6 +5599,7 @@ void GuestBufferMemory::WriteBack() {
|
||||
lease.clear();
|
||||
space.reset();
|
||||
heapReferences.clear();
|
||||
heapReadbacks.clear();
|
||||
}
|
||||
|
||||
std::optional<std::vector<std::pair<std::uint64_t, std::uint64_t>>> GuestBufferMemory::DirectRegions() const {
|
||||
@@ -5471,6 +5696,7 @@ std::vector<std::pair<std::uint64_t, std::uint64_t>> GuestBufferMemory::DeviceRe
|
||||
}
|
||||
|
||||
void GuestBufferMemory::RecordStagingCopies(Recorder& recorder) {
|
||||
ResidentBuffer::FlushChanged(context);
|
||||
if (!uploaded || committed) return;
|
||||
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
|
||||
std::vector<Region*> copies;
|
||||
|
||||
@@ -0,0 +1,659 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ResidentMirror.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#ifndef _WIN32
|
||||
#include <fcntl.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
|
||||
namespace AgcDriver::Graphics {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint64_t BlockSize = ResidentBuffer::BlockBytes;
|
||||
constexpr std::uint64_t SlabBytes = std::uint64_t{16} << 20u;
|
||||
constexpr std::uint32_t SlabBlocks = static_cast<std::uint32_t>(SlabBytes / BlockSize);
|
||||
constexpr std::uint64_t ZeroBytes = SlabBytes;
|
||||
constexpr VkAccessFlags TransferredAccess = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_INDIRECT_COMMAND_READ_BIT;
|
||||
|
||||
[[noreturn]] void fail(const char* step, VkResult result) {
|
||||
char text[160];
|
||||
std::snprintf(text, sizeof(text), "%s failed (%d)", step, static_cast<int>(result));
|
||||
throw std::runtime_error(text);
|
||||
}
|
||||
|
||||
struct TransientBuffer {
|
||||
TransientBuffer(const Context& context, VkBuffer buffer) : context(context), buffer(buffer) {}
|
||||
~TransientBuffer() { context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(context.device, buffer, nullptr); }
|
||||
TransientBuffer(const TransientBuffer&) = delete;
|
||||
TransientBuffer& operator=(const TransientBuffer&) = delete;
|
||||
Context context;
|
||||
VkBuffer buffer;
|
||||
};
|
||||
|
||||
template<typename Work>
|
||||
void recordTransfer(const Context& context, Work&& work) {
|
||||
GuestMemory::AssertGpuLockHeld("heap mirror transfer");
|
||||
std::vector<std::shared_ptr<void>> kept;
|
||||
if (auto* recorder = Recorder::Active(); recorder != nullptr) {
|
||||
const auto commands = recorder->Commands();
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
work(commands, kept);
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, TransferredAccess);
|
||||
for (auto& object : kept) recorder->Keep(std::move(object));
|
||||
recorder->MarkCovered(TransferredAccess);
|
||||
return;
|
||||
}
|
||||
CommandBatch batch(context);
|
||||
RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
work(batch.Handle(), kept);
|
||||
RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, TransferredAccess);
|
||||
batch.SubmitAndWait();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ResidentMemory {
|
||||
public:
|
||||
struct Slab {
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
std::unique_ptr<std::byte[]> host;
|
||||
std::vector<std::uint32_t> free;
|
||||
};
|
||||
|
||||
ResidentMemory(const Context& context, std::uint32_t type) : context(context), type(type) {
|
||||
zero = allocate(ZeroBytes, "vkAllocateMemory of the heap mirrors' zero block");
|
||||
}
|
||||
|
||||
~ResidentMemory() {
|
||||
const auto freeMemory = context.Function<PFN_vkFreeMemory>("vkFreeMemory");
|
||||
for (auto& slab : slabs) freeMemory(context.device, slab->memory, nullptr);
|
||||
freeMemory(context.device, zero, nullptr);
|
||||
}
|
||||
|
||||
ResidentMemory(const ResidentMemory&) = delete;
|
||||
ResidentMemory& operator=(const ResidentMemory&) = delete;
|
||||
|
||||
VkDeviceMemory Zero() const { return zero; }
|
||||
bool Zeroed() const { return zeroed.load(std::memory_order_acquire); }
|
||||
|
||||
void FillZero(VkCommandBuffer commands, std::vector<std::shared_ptr<void>>& kept) {
|
||||
if (Zeroed()) return;
|
||||
VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
||||
info.size = ZeroBytes;
|
||||
info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
if (const auto result = context.Function<PFN_vkCreateBuffer>("vkCreateBuffer")(context.device, &info, nullptr, &buffer); result != VK_SUCCESS) fail("vkCreateBuffer of the heap mirrors' zero fill", result);
|
||||
auto transient = std::make_shared<TransientBuffer>(context, buffer);
|
||||
VkMemoryRequirements requirements{};
|
||||
context.Function<PFN_vkGetBufferMemoryRequirements>("vkGetBufferMemoryRequirements")(context.device, buffer, &requirements);
|
||||
if ((requirements.memoryTypeBits & (1u << type)) == 0 || requirements.size > ZeroBytes) throw std::runtime_error("the heap mirrors' zero block memory cannot back a transfer buffer to fill it");
|
||||
if (const auto result = context.Function<PFN_vkBindBufferMemory>("vkBindBufferMemory")(context.device, buffer, zero, 0); result != VK_SUCCESS) fail("vkBindBufferMemory of the heap mirrors' zero fill", result);
|
||||
context.Function<PFN_vkCmdFillBuffer>("vkCmdFillBuffer")(commands, buffer, 0, VK_WHOLE_SIZE, 0);
|
||||
kept.push_back(std::move(transient));
|
||||
zeroed.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
std::tuple<Slab*, std::uint32_t> Take() {
|
||||
std::lock_guard lock(mutex);
|
||||
for (auto& slab : slabs) {
|
||||
if (slab->free.empty()) continue;
|
||||
const auto index = slab->free.back();
|
||||
slab->free.pop_back();
|
||||
return {slab.get(), index};
|
||||
}
|
||||
auto slab = std::make_unique<Slab>();
|
||||
slab->memory = allocate(SlabBytes, "vkAllocateMemory of a heap mirror slab");
|
||||
slab->host = std::unique_ptr<std::byte[]>(new std::byte[SlabBytes]);
|
||||
for (std::uint32_t index = SlabBlocks; index-- > 1;) slab->free.push_back(index);
|
||||
slabs.push_back(std::move(slab));
|
||||
return {slabs.back().get(), 0u};
|
||||
}
|
||||
|
||||
void Put(Slab* slab, std::uint32_t index) noexcept {
|
||||
std::lock_guard lock(mutex);
|
||||
slab->free.push_back(index);
|
||||
if (slab->free.size() != SlabBlocks) return;
|
||||
const auto found = std::find_if(slabs.begin(), slabs.end(), [&](const auto& held) { return held.get() == slab; });
|
||||
context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, slab->memory, nullptr);
|
||||
slabs.erase(found);
|
||||
}
|
||||
|
||||
private:
|
||||
VkDeviceMemory allocate(std::uint64_t bytes, const char* operation) {
|
||||
const VkMemoryAllocateFlagsInfo flags{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO, nullptr, VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT, 0};
|
||||
VkMemoryAllocateInfo info{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, context.bufferDeviceAddress ? &flags : nullptr};
|
||||
info.allocationSize = bytes;
|
||||
info.memoryTypeIndex = type;
|
||||
if (context.bufferPool != nullptr) return context.bufferPool->AllocateMemory(info, operation);
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
if (const auto result = context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &info, nullptr, &memory); result != VK_SUCCESS) fail(operation, result);
|
||||
return memory;
|
||||
}
|
||||
|
||||
Context context;
|
||||
std::uint32_t type;
|
||||
VkDeviceMemory zero = VK_NULL_HANDLE;
|
||||
std::atomic<bool> zeroed{false};
|
||||
std::mutex mutex;
|
||||
std::vector<std::unique_ptr<Slab>> slabs;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
struct ResidentDevices {
|
||||
std::mutex mutex;
|
||||
std::map<std::tuple<VkDevice, std::uint32_t>, std::weak_ptr<ResidentMemory>> memories;
|
||||
std::map<VkDevice, std::uint64_t> addressSpace;
|
||||
std::uint64_t limit = 0;
|
||||
std::mutex pendingMutex;
|
||||
std::vector<std::weak_ptr<ResidentBuffer>> pending;
|
||||
};
|
||||
|
||||
ResidentDevices& Devices() {
|
||||
static ResidentDevices devices;
|
||||
return devices;
|
||||
}
|
||||
|
||||
std::shared_ptr<ResidentMemory> residentMemory(const Context& context, std::uint32_t type) {
|
||||
auto& devices = Devices();
|
||||
std::lock_guard lock(devices.mutex);
|
||||
auto& slot = devices.memories[{context.device, type}];
|
||||
if (auto held = slot.lock()) return held;
|
||||
auto made = std::make_shared<ResidentMemory>(context, type);
|
||||
slot = made;
|
||||
return made;
|
||||
}
|
||||
|
||||
struct Candidate {
|
||||
std::uint64_t begin = 0;
|
||||
std::uint64_t end = 0;
|
||||
bool readable = true;
|
||||
};
|
||||
|
||||
bool allZero(std::uint64_t begin, std::uint64_t end) {
|
||||
const auto* at = reinterpret_cast<const unsigned char*>(begin);
|
||||
const auto* stop = reinterpret_cast<const unsigned char*>(end);
|
||||
while (at < stop && reinterpret_cast<std::uintptr_t>(at) % sizeof(std::uint64_t) != 0) {
|
||||
if (*at++ != 0) return false;
|
||||
}
|
||||
while (stop - at >= static_cast<std::ptrdiff_t>(sizeof(std::uint64_t))) {
|
||||
std::uint64_t word = 0;
|
||||
std::memcpy(&word, at, sizeof(word));
|
||||
if (word != 0) return false;
|
||||
at += sizeof(word);
|
||||
}
|
||||
while (at < stop) {
|
||||
if (*at++ != 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
struct Mapping {
|
||||
std::uint64_t begin = 0;
|
||||
std::uint64_t end = 0;
|
||||
bool readable = false;
|
||||
bool privateAnonymous = false;
|
||||
};
|
||||
|
||||
std::vector<Mapping> mappings(std::uint64_t begin, std::uint64_t end) {
|
||||
std::ifstream maps("/proc/self/maps");
|
||||
Require(static_cast<bool>(maps), "cannot read /proc/self/maps for a heap mirror's pages");
|
||||
std::vector<Mapping> found;
|
||||
std::string line;
|
||||
while (std::getline(maps, line)) {
|
||||
std::istringstream fields(line);
|
||||
std::string span, permissions, offset, device;
|
||||
unsigned long long inode = 0;
|
||||
fields >> span >> permissions >> offset >> device >> inode;
|
||||
const auto dash = span.find('-');
|
||||
Require(dash != std::string::npos && permissions.size() == 4, "unexpected /proc/self/maps line: " + line);
|
||||
const auto first = std::stoull(span.substr(0, dash), nullptr, 16);
|
||||
const auto last = std::stoull(span.substr(dash + 1), nullptr, 16);
|
||||
if (last <= begin || first >= end) continue;
|
||||
found.push_back({std::max<std::uint64_t>(first, begin), std::min<std::uint64_t>(last, end), permissions[0] == 'r', inode == 0 && permissions[3] == 'p'});
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
void fileData(std::vector<Candidate>& maybe, const Mapping& mapping, int file, std::uint64_t offset) {
|
||||
const auto end = offset + (mapping.end - mapping.begin);
|
||||
auto at = static_cast<off_t>(offset);
|
||||
while (static_cast<std::uint64_t>(at) < end) {
|
||||
const auto data = lseek(file, at, SEEK_DATA);
|
||||
if (data < 0) {
|
||||
Require(errno == ENXIO, "SEEK_DATA on a heap mirror's shared backing failed");
|
||||
return;
|
||||
}
|
||||
if (static_cast<std::uint64_t>(data) >= end) return;
|
||||
const auto hole = lseek(file, data, SEEK_HOLE);
|
||||
Require(hole >= 0, "SEEK_HOLE on a heap mirror's shared backing failed");
|
||||
const auto stop = std::min<std::uint64_t>(static_cast<std::uint64_t>(hole), end);
|
||||
maybe.push_back({mapping.begin + (static_cast<std::uint64_t>(data) - offset), mapping.begin + (stop - offset), mapping.readable});
|
||||
at = static_cast<off_t>(stop);
|
||||
}
|
||||
}
|
||||
|
||||
void populatedPages(std::vector<Candidate>& maybe, const Mapping& mapping) {
|
||||
static const auto page = static_cast<std::uint64_t>(sysconf(_SC_PAGESIZE));
|
||||
const int file = open("/proc/self/pagemap", O_RDONLY | O_CLOEXEC);
|
||||
Require(file >= 0, "cannot open /proc/self/pagemap for a heap mirror's pages");
|
||||
constexpr std::size_t Entries = 4096;
|
||||
std::array<std::uint64_t, Entries> entries{};
|
||||
const auto first = mapping.begin / page;
|
||||
const auto last = (mapping.end + page - 1) / page;
|
||||
for (auto at = first; at < last;) {
|
||||
const auto count = static_cast<std::size_t>(std::min<std::uint64_t>(Entries, last - at));
|
||||
const auto read = pread(file, entries.data(), count * sizeof(std::uint64_t), static_cast<off_t>(at * sizeof(std::uint64_t)));
|
||||
if (read != static_cast<ssize_t>(count * sizeof(std::uint64_t))) {
|
||||
close(file);
|
||||
throw std::runtime_error("reading /proc/self/pagemap for a heap mirror's pages failed");
|
||||
}
|
||||
for (std::size_t index = 0; index < count; ++index) {
|
||||
constexpr std::uint64_t present = std::uint64_t{1} << 63u;
|
||||
constexpr std::uint64_t swapped = std::uint64_t{1} << 62u;
|
||||
if ((entries[index] & (present | swapped)) == 0) continue;
|
||||
const auto address = (at + index) * page;
|
||||
const auto from = std::max(address, mapping.begin);
|
||||
const auto to = std::min(address + page, mapping.end);
|
||||
if (!maybe.empty() && maybe.back().end == from && maybe.back().readable == mapping.readable) maybe.back().end = to;
|
||||
else maybe.push_back({from, to, mapping.readable});
|
||||
}
|
||||
at += count;
|
||||
}
|
||||
close(file);
|
||||
}
|
||||
#endif
|
||||
|
||||
std::vector<Candidate> possiblyNonzero(std::uint64_t begin, std::uint64_t end) {
|
||||
std::vector<Candidate> maybe;
|
||||
#ifdef _WIN32
|
||||
maybe.push_back({begin, end, true});
|
||||
#else
|
||||
for (const auto& mapping : mappings(begin, end)) {
|
||||
int file = -1;
|
||||
std::uint64_t offset = 0;
|
||||
if (GuestArena::GuestArenaSharedBacking_nid_postfix(static_cast<std::uintptr_t>(mapping.begin), static_cast<std::size_t>(mapping.end - mapping.begin), &file, &offset)) {
|
||||
try {
|
||||
fileData(maybe, mapping, file, offset);
|
||||
} catch (...) {
|
||||
close(file);
|
||||
throw;
|
||||
}
|
||||
close(file);
|
||||
} else if (mapping.privateAnonymous) {
|
||||
populatedPages(maybe, mapping);
|
||||
} else {
|
||||
maybe.push_back({mapping.begin, mapping.end, mapping.readable});
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return maybe;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool GuestBytesZero(std::uint64_t begin, std::uint64_t end) {
|
||||
return allZero(begin, end);
|
||||
}
|
||||
|
||||
GuestRuns GuestNonzeroBlocks(const GuestRuns& candidates) {
|
||||
GuestRuns runs;
|
||||
for (const auto& [first, last] : candidates) {
|
||||
if (first >= last) continue;
|
||||
const auto maybe = possiblyNonzero(first, last);
|
||||
std::size_t at = 0;
|
||||
for (auto block = first / BlockSize * BlockSize; block < last; block += BlockSize) {
|
||||
const auto from = std::max(first, block);
|
||||
const auto to = std::min(last, block + BlockSize);
|
||||
while (at < maybe.size() && maybe[at].end <= from) ++at;
|
||||
bool nonzero = false;
|
||||
for (auto next = at; !nonzero && next < maybe.size() && maybe[next].begin < to; ++next) {
|
||||
const auto low = std::max(from, maybe[next].begin);
|
||||
const auto high = std::min(to, maybe[next].end);
|
||||
if (low < high) nonzero = !maybe[next].readable || !allZero(low, high);
|
||||
}
|
||||
if (!nonzero) continue;
|
||||
if (!runs.empty() && runs.back().second == from) runs.back().second = to;
|
||||
else runs.emplace_back(from, to);
|
||||
}
|
||||
}
|
||||
return runs;
|
||||
}
|
||||
|
||||
void SetResidentAddressSpaceLimit(std::uint64_t bytes) {
|
||||
auto& devices = Devices();
|
||||
std::lock_guard lock(devices.mutex);
|
||||
devices.limit = bytes;
|
||||
}
|
||||
|
||||
std::shared_ptr<ResidentBuffer> ResidentBuffer::Create(const Context& context, std::uint64_t base, std::uint64_t bytes, const GuestRuns& backed) {
|
||||
if (!context.sparseBinding || !context.sparseResidencyBuffer || !context.sparseResidencyAliased) return nullptr;
|
||||
const auto first = base / BlockSize * BlockSize;
|
||||
const auto size = (base - first + bytes + BlockSize - 1) / BlockSize * BlockSize;
|
||||
{
|
||||
auto& devices = Devices();
|
||||
std::lock_guard lock(devices.mutex);
|
||||
const auto limit = devices.limit != 0 ? std::min(devices.limit, context.limits.sparseAddressSpaceSize) : context.limits.sparseAddressSpaceSize;
|
||||
auto& used = devices.addressSpace[context.device];
|
||||
if (size > limit || used > limit - size) return nullptr;
|
||||
used += size;
|
||||
}
|
||||
return std::shared_ptr<ResidentBuffer>(new ResidentBuffer(context, base, bytes, backed));
|
||||
}
|
||||
|
||||
ResidentBuffer::ResidentBuffer(const Context& context, std::uint64_t base, std::uint64_t bytes, const GuestRuns& backed) : context(context) {
|
||||
origin = base / BlockSize * BlockSize;
|
||||
lead = base - origin;
|
||||
size = (lead + bytes + BlockSize - 1) / BlockSize * BlockSize;
|
||||
try {
|
||||
VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
||||
info.flags = VK_BUFFER_CREATE_SPARSE_BINDING_BIT | VK_BUFFER_CREATE_SPARSE_ALIASED_BIT;
|
||||
info.size = size;
|
||||
info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
const std::array<std::uint32_t, 2> families{context.queueFamily, context.sparseFamily};
|
||||
if (context.sparseQueue != VK_NULL_HANDLE && context.sparseFamily != context.queueFamily) {
|
||||
info.sharingMode = VK_SHARING_MODE_CONCURRENT;
|
||||
info.queueFamilyIndexCount = static_cast<std::uint32_t>(families.size());
|
||||
info.pQueueFamilyIndices = families.data();
|
||||
}
|
||||
if (const auto result = context.Function<PFN_vkCreateBuffer>("vkCreateBuffer")(context.device, &info, nullptr, &buffer); result != VK_SUCCESS) fail("vkCreateBuffer of a resident heap mirror", result);
|
||||
VkMemoryRequirements requirements{};
|
||||
context.Function<PFN_vkGetBufferMemoryRequirements>("vkGetBufferMemoryRequirements")(context.device, buffer, &requirements);
|
||||
if (requirements.alignment == 0 || requirements.alignment > BlockSize || BlockSize % requirements.alignment != 0) {
|
||||
char text[160];
|
||||
std::snprintf(text, sizeof(text), "the device's sparse buffer block of %llu bytes does not divide a heap mirror's 64 KiB block", static_cast<unsigned long long>(requirements.alignment));
|
||||
throw std::runtime_error(text);
|
||||
}
|
||||
type = context.DeviceMemoryType(requirements.memoryTypeBits, SlabBytes, "a resident heap mirror slab");
|
||||
memory = residentMemory(context, type);
|
||||
slots.resize(static_cast<std::size_t>(size / BlockSize));
|
||||
std::vector<bool> wanted(slots.size(), false);
|
||||
for (const auto& [first, last] : backed) {
|
||||
if (first >= last) continue;
|
||||
for (auto index = blockIndex(first); index <= blockIndex(last - 1); ++index) wanted[index] = true;
|
||||
}
|
||||
std::vector<VkSparseMemoryBind> binds;
|
||||
for (std::size_t index = 0; index < slots.size();) {
|
||||
if (wanted[index]) {
|
||||
auto [slab, at] = memory->Take();
|
||||
slots[index] = {slab, at, slab->host.get() + at * BlockSize};
|
||||
std::memset(slots[index].host, 0, static_cast<std::size_t>(BlockSize));
|
||||
binds.push_back({index * BlockSize, BlockSize, slab->memory, at * BlockSize, 0});
|
||||
++bound;
|
||||
++index;
|
||||
continue;
|
||||
}
|
||||
auto last = index;
|
||||
while (last < slots.size() && !wanted[last] && (last - index + 1) * BlockSize <= ZeroBytes) ++last;
|
||||
binds.push_back({index * BlockSize, (last - index) * BlockSize, memory->Zero(), 0, 0});
|
||||
index = last;
|
||||
}
|
||||
submit(binds);
|
||||
const VkBufferDeviceAddressInfo addressInfo{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, buffer};
|
||||
address = context.Function<PFN_vkGetBufferDeviceAddressKHR>("vkGetBufferDeviceAddressKHR")(context.device, &addressInfo);
|
||||
if (address == 0) fail("vkGetBufferDeviceAddressKHR of a resident heap mirror", VK_ERROR_UNKNOWN);
|
||||
} catch (...) {
|
||||
release();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
ResidentBuffer::~ResidentBuffer() {
|
||||
release();
|
||||
}
|
||||
|
||||
void ResidentBuffer::release() noexcept {
|
||||
if (buffer != VK_NULL_HANDLE) context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(context.device, buffer, nullptr);
|
||||
buffer = VK_NULL_HANDLE;
|
||||
for (auto& slot : slots) {
|
||||
if (slot.host != nullptr) memory->Put(static_cast<ResidentMemory::Slab*>(slot.slab), slot.index);
|
||||
slot = {};
|
||||
}
|
||||
auto& devices = Devices();
|
||||
std::lock_guard lock(devices.mutex);
|
||||
if (const auto found = devices.addressSpace.find(context.device); found != devices.addressSpace.end()) found->second -= std::min(found->second, size);
|
||||
size = 0;
|
||||
}
|
||||
|
||||
std::size_t ResidentBuffer::blockIndex(std::uint64_t guest) const {
|
||||
Require(guest >= origin && guest - origin < size, "guest address outside its resident heap mirror");
|
||||
return static_cast<std::size_t>((guest - origin) / BlockSize);
|
||||
}
|
||||
|
||||
std::byte* ResidentBuffer::Block(std::uint64_t guest) const {
|
||||
const auto& slot = slots[blockIndex(guest)];
|
||||
return slot.host == nullptr ? nullptr : slot.host + (guest - origin) % BlockSize;
|
||||
}
|
||||
|
||||
std::uint64_t ResidentBuffer::UnboundBytes(const GuestRuns& runs) const {
|
||||
std::vector<std::size_t> seen;
|
||||
for (const auto& [first, last] : runs) {
|
||||
if (first >= last) continue;
|
||||
for (auto index = blockIndex(first); index <= blockIndex(last - 1); ++index) {
|
||||
if (slots[index].host == nullptr) seen.push_back(index);
|
||||
}
|
||||
}
|
||||
std::sort(seen.begin(), seen.end());
|
||||
return static_cast<std::uint64_t>(std::unique(seen.begin(), seen.end()) - seen.begin()) * BlockSize;
|
||||
}
|
||||
|
||||
void ResidentBuffer::Bind(const GuestRuns& runs) {
|
||||
std::vector<VkSparseMemoryBind> binds;
|
||||
std::vector<std::size_t> made;
|
||||
for (const auto& [first, last] : runs) {
|
||||
if (first >= last) continue;
|
||||
for (auto index = blockIndex(first); index <= blockIndex(last - 1); ++index) {
|
||||
if (slots[index].host != nullptr) continue;
|
||||
auto [slab, at] = memory->Take();
|
||||
slots[index] = {slab, at, slab->host.get() + at * BlockSize};
|
||||
std::memset(slots[index].host, 0, static_cast<std::size_t>(BlockSize));
|
||||
binds.push_back({index * BlockSize, BlockSize, slab->memory, at * BlockSize, 0});
|
||||
made.push_back(index);
|
||||
}
|
||||
}
|
||||
if (binds.empty()) return;
|
||||
if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->Sync();
|
||||
try {
|
||||
submit(binds);
|
||||
} catch (...) {
|
||||
for (const auto index : made) {
|
||||
memory->Put(static_cast<ResidentMemory::Slab*>(slots[index].slab), slots[index].index);
|
||||
slots[index] = {};
|
||||
}
|
||||
throw;
|
||||
}
|
||||
bound += made.size();
|
||||
for (const auto index : made) MarkChanged(origin + index * BlockSize, origin + (index + 1) * BlockSize);
|
||||
}
|
||||
|
||||
void ResidentBuffer::MarkChanged(std::uint64_t begin, std::uint64_t end) {
|
||||
if (begin >= end) return;
|
||||
bool first = false;
|
||||
{
|
||||
std::lock_guard lock(changedMutex);
|
||||
first = changed.empty();
|
||||
if (!changed.empty() && changed.back().second == begin) changed.back().second = end;
|
||||
else changed.emplace_back(begin, end);
|
||||
}
|
||||
if (!first) return;
|
||||
auto& devices = Devices();
|
||||
std::lock_guard lock(devices.pendingMutex);
|
||||
devices.pending.push_back(weak_from_this());
|
||||
}
|
||||
|
||||
void ResidentBuffer::Read(std::uint64_t guest, std::span<std::byte> out) const {
|
||||
std::size_t done = 0;
|
||||
while (done < out.size()) {
|
||||
const auto at = guest + done;
|
||||
const auto within = static_cast<std::size_t>((at - origin) % BlockSize);
|
||||
const auto length = std::min<std::size_t>(out.size() - done, static_cast<std::size_t>(BlockSize) - within);
|
||||
if (const auto* source = Block(at); source != nullptr) std::memcpy(out.data() + done, source, length);
|
||||
else std::memset(out.data() + done, 0, length);
|
||||
done += length;
|
||||
}
|
||||
}
|
||||
|
||||
void ResidentBuffer::Store(std::uint64_t guest, std::span<const std::byte> bytes) {
|
||||
std::size_t done = 0;
|
||||
while (done < bytes.size()) {
|
||||
const auto at = guest + done;
|
||||
const auto within = static_cast<std::size_t>((at - origin) % BlockSize);
|
||||
const auto length = std::min<std::size_t>(bytes.size() - done, static_cast<std::size_t>(BlockSize) - within);
|
||||
if (auto* target = Block(at); target != nullptr) std::memcpy(target, bytes.data() + done, length);
|
||||
else Require(GuestBytesZero(reinterpret_cast<std::uintptr_t>(bytes.data() + done), reinterpret_cast<std::uintptr_t>(bytes.data() + done + length)), "the GPU wrote a heap mirror block that holds no memory");
|
||||
done += length;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Buffer> ResidentBuffer::RecordReadback(VkCommandBuffer commands, std::uint64_t guest, std::uint64_t bytes) const {
|
||||
Require(guest >= origin && bytes <= size && guest - origin <= size - bytes, "heap mirror readback outside its buffer");
|
||||
auto staging = std::make_shared<Buffer>(context, static_cast<std::size_t>(bytes), VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||
CopyBuffer(context, commands, buffer, guest - origin, staging->Handle(), 0, bytes);
|
||||
return staging;
|
||||
}
|
||||
|
||||
void ResidentBuffer::ReadDevice(std::uint64_t guest, std::span<std::byte> out) {
|
||||
GuestMemory::AssertGpuLockHeld("heap mirror device read");
|
||||
Require(!Recorder::InCompletion(), "heap mirror device read inside a GPU completion: its write-back needs a recorded readback");
|
||||
auto staging = readbackBatch(guest, out.size());
|
||||
std::memcpy(out.data(), staging->Bytes().data(), out.size());
|
||||
}
|
||||
|
||||
std::shared_ptr<Buffer> ResidentBuffer::readbackBatch(std::uint64_t guest, std::uint64_t bytes) {
|
||||
CommandBatch batch(context);
|
||||
RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
|
||||
auto staging = RecordReadback(batch.Handle(), guest, bytes);
|
||||
RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
|
||||
batch.SubmitAndWait();
|
||||
return staging;
|
||||
}
|
||||
|
||||
void ResidentBuffer::flush() {
|
||||
GuestRuns runs;
|
||||
{
|
||||
std::lock_guard lock(changedMutex);
|
||||
runs.swap(changed);
|
||||
}
|
||||
if (runs.empty()) return;
|
||||
try {
|
||||
std::sort(runs.begin(), runs.end());
|
||||
std::vector<VkBufferCopy> regions;
|
||||
std::uint64_t total = 0;
|
||||
std::uint64_t cursor = 0;
|
||||
for (const auto& [first, last] : runs) {
|
||||
for (auto at = std::max(first, cursor); at < last;) {
|
||||
const auto next = std::min(last, (at / BlockSize + 1) * BlockSize);
|
||||
if (slots[blockIndex(at)].host != nullptr) {
|
||||
if (!regions.empty() && regions.back().dstOffset + regions.back().size == at - origin) regions.back().size += next - at;
|
||||
else regions.push_back({total, at - origin, next - at});
|
||||
total += next - at;
|
||||
}
|
||||
at = next;
|
||||
}
|
||||
cursor = std::max(cursor, last);
|
||||
}
|
||||
if (regions.empty()) return;
|
||||
auto staging = std::make_shared<Buffer>(context, static_cast<std::size_t>(total), VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
|
||||
for (const auto& region : regions) {
|
||||
for (VkDeviceSize done = 0; done < region.size;) {
|
||||
const auto at = origin + region.dstOffset + done;
|
||||
const auto length = std::min<VkDeviceSize>(region.size - done, BlockSize - (at - origin) % BlockSize);
|
||||
std::memcpy(staging->Bytes().data() + region.srcOffset + done, Block(at), static_cast<std::size_t>(length));
|
||||
done += length;
|
||||
}
|
||||
}
|
||||
recordTransfer(context, [&](VkCommandBuffer commands, std::vector<std::shared_ptr<void>>& kept) {
|
||||
context.Function<PFN_vkCmdCopyBuffer>("vkCmdCopyBuffer")(commands, staging->Handle(), buffer, static_cast<std::uint32_t>(regions.size()), regions.data());
|
||||
kept.push_back(staging);
|
||||
kept.push_back(shared_from_this());
|
||||
});
|
||||
} catch (...) {
|
||||
{
|
||||
std::lock_guard lock(changedMutex);
|
||||
changed.insert(changed.begin(), runs.begin(), runs.end());
|
||||
}
|
||||
auto& devices = Devices();
|
||||
std::lock_guard lock(devices.pendingMutex);
|
||||
devices.pending.push_back(weak_from_this());
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void ResidentBuffer::FlushChanged(const Context& context) {
|
||||
auto& devices = Devices();
|
||||
std::vector<std::shared_ptr<ResidentMemory>> unzeroed;
|
||||
{
|
||||
std::lock_guard lock(devices.mutex);
|
||||
for (const auto& [key, held] : devices.memories) {
|
||||
if (std::get<0>(key) != context.device) continue;
|
||||
if (auto memory = held.lock(); memory != nullptr && !memory->Zeroed()) unzeroed.push_back(std::move(memory));
|
||||
}
|
||||
}
|
||||
for (auto& memory : unzeroed) recordTransfer(context, [&](VkCommandBuffer commands, std::vector<std::shared_ptr<void>>& kept) { memory->FillZero(commands, kept); });
|
||||
std::vector<std::weak_ptr<ResidentBuffer>> waiting;
|
||||
{
|
||||
std::lock_guard lock(devices.pendingMutex);
|
||||
waiting.swap(devices.pending);
|
||||
}
|
||||
for (std::size_t index = 0; index < waiting.size(); ++index) {
|
||||
const auto held = waiting[index].lock();
|
||||
if (held == nullptr) continue;
|
||||
if (held->context.device != context.device) {
|
||||
std::lock_guard lock(devices.pendingMutex);
|
||||
devices.pending.push_back(waiting[index]);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
held->flush();
|
||||
} catch (...) {
|
||||
std::lock_guard lock(devices.pendingMutex);
|
||||
devices.pending.insert(devices.pending.end(), waiting.begin() + static_cast<std::ptrdiff_t>(index) + 1, waiting.end());
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ResidentBuffer::submit(const std::vector<VkSparseMemoryBind>& binds) {
|
||||
const bool ownQueue = context.sparseQueue != VK_NULL_HANDLE && context.sparseQueueMutex != nullptr;
|
||||
const VkSparseBufferMemoryBindInfo bufferBinds{buffer, static_cast<std::uint32_t>(binds.size()), binds.data()};
|
||||
VkBindSparseInfo bind{VK_STRUCTURE_TYPE_BIND_SPARSE_INFO};
|
||||
bind.bufferBindCount = 1;
|
||||
bind.pBufferBinds = &bufferBinds;
|
||||
const VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
|
||||
VkFence fence = VK_NULL_HANDLE;
|
||||
if (const auto result = context.Function<PFN_vkCreateFence>("vkCreateFence")(context.device, &fenceInfo, nullptr, &fence); result != VK_SUCCESS) fail("vkCreateFence of a resident heap mirror bind", result);
|
||||
VkResult submitted = VK_SUCCESS;
|
||||
VkResult waited = VK_SUCCESS;
|
||||
{
|
||||
std::unique_lock<std::mutex> queueLock;
|
||||
std::unique_lock<GuestMemory::GpuMutexType> gpuLock;
|
||||
if (ownQueue) queueLock = std::unique_lock(*context.sparseQueueMutex);
|
||||
else gpuLock = std::unique_lock(GuestMemory::GpuMutex());
|
||||
submitted = context.Function<PFN_vkQueueBindSparse>("vkQueueBindSparse")(ownQueue ? context.sparseQueue : context.queue, 1, &bind, fence);
|
||||
if (submitted == VK_SUCCESS) waited = context.Function<PFN_vkWaitForFences>("vkWaitForFences")(context.device, 1, &fence, VK_TRUE, UINT64_MAX);
|
||||
}
|
||||
context.Function<PFN_vkDestroyFence>("vkDestroyFence")(context.device, fence, nullptr);
|
||||
if (submitted != VK_SUCCESS) fail("vkQueueBindSparse of a resident heap mirror", submitted);
|
||||
if (waited != VK_SUCCESS) fail("vkWaitForFences of a resident heap mirror bind", waited);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3131,6 +3131,63 @@ void RunSparseHeapMirrorUnwatchTests(const Context& context, const BdaTestAccess
|
||||
std::cout << "an address-based build keeps and refreshes a sparse heap's mirror once its range has left the write watch\n";
|
||||
}
|
||||
|
||||
void RunSparseHeapMirrorBudgetTests(const Context& context, const BdaTestAccess& access) {
|
||||
constexpr std::size_t block = 65536;
|
||||
constexpr std::size_t reserved = 128 * block;
|
||||
constexpr std::size_t committed = 80 * block;
|
||||
#ifdef _WIN32
|
||||
if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) {
|
||||
std::cout << "guest arena unavailable or not write-watched: sparse heaps past the heap mirror budget not tested\n";
|
||||
return;
|
||||
}
|
||||
auto* guest = static_cast<std::byte*>(GuestArena::GuestArenaAllocate_nid_postfix(reserved, block));
|
||||
Require(guest != nullptr, "cannot allocate the sparse heap in the guest arena");
|
||||
GuestArena::GuestArenaCommit_nid_postfix(guest, committed, PAGE_READWRITE, block);
|
||||
#else
|
||||
if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) {
|
||||
std::cout << "write watch unavailable: sparse heaps past the heap mirror budget not tested\n";
|
||||
return;
|
||||
}
|
||||
auto* guest = reserveSparse(reserved, committed);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(guest, reserved);
|
||||
#endif
|
||||
std::memset(guest, 0x5a, committed);
|
||||
const auto base = reinterpret_cast<std::uintptr_t>(guest);
|
||||
const auto registry = [&](bool add) {
|
||||
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
||||
if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, guest, reserved, true, true, true);
|
||||
else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, guest);
|
||||
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
||||
};
|
||||
registry(true);
|
||||
const auto before = MirrorCounters();
|
||||
{
|
||||
GuestBufferMemory memory(context);
|
||||
memory.AddWritable(base, reserved);
|
||||
memory.Upload(false);
|
||||
std::uint32_t adjustment = 0;
|
||||
const auto view = memory.Descriptor(base, reserved, adjustment);
|
||||
const auto bytes = access.bytes(view.buffer);
|
||||
Require(adjustment == 0 && view.range >= committed && bytes[view.offset + committed - 1] == std::byte{0x5a}, "a sparse heap past the heap mirror budget was not copied up to its last committed byte");
|
||||
bytes[view.offset + 8] = std::byte{0x77};
|
||||
memory.WriteBack();
|
||||
}
|
||||
Require(guest[8] == std::byte{0x77}, "a store to a sparse heap copied past the heap mirror budget was not written back");
|
||||
const auto after = MirrorCounters();
|
||||
Require(after.sparseBinds == before.sparseBinds && after.heapMirrors == before.heapMirrors && after.heapBytes == before.heapBytes, "a sparse heap past the heap mirror budget was mirrored or left heap mirror bytes held");
|
||||
registry(false);
|
||||
ClearImageMirrors(context.device);
|
||||
ClearSnapshotCopies(context.device);
|
||||
#ifdef _WIN32
|
||||
GuestArena::GuestArenaReset_nid_postfix(guest, reserved);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(guest, reserved);
|
||||
#else
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(guest, reserved);
|
||||
releaseSparse(guest, reserved);
|
||||
#endif
|
||||
std::cout << "a sparse heap whose mirror would pass the heap mirror budget is copied\n";
|
||||
}
|
||||
|
||||
void RunHeapMirrorRunUnwatchTests(const Context& context, const BdaTestAccess& access) {
|
||||
constexpr std::size_t block = 65536;
|
||||
constexpr std::size_t heapBytes = 32 * block;
|
||||
|
||||
@@ -19,6 +19,7 @@ void RunBdaResourceTests(const AgcDriver::Graphics::Context& context, const BdaT
|
||||
bool RunSparseBufferHoleTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access);
|
||||
void RunHeapMirrorUnwatchTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access, bool throughImport);
|
||||
void RunSparseHeapMirrorUnwatchTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access);
|
||||
void RunSparseHeapMirrorBudgetTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access);
|
||||
void RunHeapMirrorRunUnwatchTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access);
|
||||
void RunGuestAllocationTests();
|
||||
void RunGuestLeaseWaitTests();
|
||||
|
||||
@@ -945,6 +945,57 @@ void msaaEnableClearOneSampleTargetTests() {
|
||||
expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's");
|
||||
}
|
||||
|
||||
void UnwrittenDepthPlaneTests() {
|
||||
using AgcDriver::Graphics::DecodeState;
|
||||
using AgcDriver::Graphics::DrawRejection;
|
||||
const auto fresh = [](std::uint32_t depthControl) {
|
||||
auto written = makeState();
|
||||
written.context[0x200] = depthControl;
|
||||
written.context[0x1b3] = 2;
|
||||
written.context[0x1b4] = 2;
|
||||
AgcDriver::QueueState queue;
|
||||
queue.shader = written.shader;
|
||||
queue.userConfig = written.userConfig;
|
||||
written.context.ForEach([&](std::uint32_t offset, std::uint32_t value) { queue.context[offset] = value; });
|
||||
return queue;
|
||||
};
|
||||
auto queue = fresh(0x777);
|
||||
auto state = DecodeState(queue);
|
||||
Require(!state.depth && !state.depthTest && !state.depthWrite && !state.stencilTest, "depth and stencil tests on unwritten depth and stencil planes were kept (PPSA24040's DB_DEPTH_CONTROL)");
|
||||
auto rejection = DrawRejection(queue, false);
|
||||
Require(rejection.empty(), "the precheck refused depth and stencil tests on unwritten depth and stencil planes: " + rejection);
|
||||
queue = fresh(0x77f);
|
||||
state = DecodeState(queue);
|
||||
Require(!state.depth && !state.depthBoundsTest, "a depth bounds test on unwritten depth and stencil planes was kept");
|
||||
rejection = DrawRejection(queue, false);
|
||||
Require(rejection.empty(), "the precheck refused a depth bounds test on unwritten depth and stencil planes: " + rejection);
|
||||
queue = fresh(0x007007b3);
|
||||
queue.context[0x010] = 0x80000183;
|
||||
queue.context[0x012] = 0x00001000;
|
||||
queue.context[0x014] = 0x00001000;
|
||||
queue.context[0x007] = 0x0001001f;
|
||||
queue.context[0x00b] = 0x3f800000;
|
||||
state = DecodeState(queue);
|
||||
Require(state.depth && state.depthTest && !state.stencilTest && state.renderExtent.width == 32 && state.renderExtent.height == 2, "a depth plane beside an unwritten DB_STENCIL_INFO did not decode as depth only");
|
||||
rejection = DrawRejection(queue, false);
|
||||
Require(rejection.empty(), "the precheck refused a depth plane beside an unwritten DB_STENCIL_INFO: " + rejection);
|
||||
queue = fresh(0x007007b3);
|
||||
queue.context[0x011] = 0x20000181;
|
||||
queue.context[0x013] = 0x00002000;
|
||||
queue.context[0x015] = 0x00002000;
|
||||
queue.context[0x007] = 0x003f003f;
|
||||
queue.context[0x00b] = 0x3f800000;
|
||||
queue.context[0x10c] = 0x01ffff00;
|
||||
queue.context[0x10d] = 0x01ffff00;
|
||||
state = DecodeState(queue);
|
||||
Require(!state.depthTest && state.stencilTest, "a stencil plane beside an unwritten DB_Z_INFO kept the depth test or lost the stencil test");
|
||||
rejection = DrawRejection(queue, false);
|
||||
Require(rejection.empty(), "the precheck refused a stencil plane beside an unwritten DB_Z_INFO: " + rejection);
|
||||
queue.context[0x010] = 0x80000183;
|
||||
queue.ClearContext();
|
||||
for (const auto offset : {0x010u, 0x011u}) Require(queue.context.contains(offset) && queue.context.at(offset) == 0, "CLEAR_STATE did not reset DB_Z_INFO and DB_STENCIL_INFO to their clear-state value 0");
|
||||
}
|
||||
|
||||
void MsaaEnableSingleSampleTests() {
|
||||
using AgcDriver::Graphics::DecodeState;
|
||||
using AgcDriver::Graphics::DrawRejection;
|
||||
@@ -11239,6 +11290,23 @@ int main(int argc, char** argv) {
|
||||
Require(mock.live == 0, "the sparse heap mirror unwatch test leaked Vulkan objects");
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--sparse-heap-mirror-budget") {
|
||||
{
|
||||
mock = MockVulkan{};
|
||||
auto context = mockContext();
|
||||
context.bufferDeviceAddress = true;
|
||||
context.limits.maxStorageBufferRange = 1u << 27;
|
||||
RunSparseHeapMirrorBudgetTests(context, {
|
||||
[](VkBuffer buffer) -> std::span<std::byte> { return mockBufferMemory(buffer); },
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
});
|
||||
}
|
||||
Require(mock.live == 0, "the sparse heap mirror budget test leaked Vulkan objects");
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--heap-mirror-run-unwatch") {
|
||||
{
|
||||
mock = MockVulkan{};
|
||||
@@ -11438,6 +11506,7 @@ int main(int argc, char** argv) {
|
||||
UnwrittenSampleLocationTests();
|
||||
sampleLocationGridFallbackTests();
|
||||
sampleOrderTests();
|
||||
UnwrittenDepthPlaneTests();
|
||||
depthCopyFormatTests();
|
||||
quadPixelMaskTests();
|
||||
ShaderStageTests();
|
||||
|
||||
@@ -192,9 +192,9 @@ void testContextAndBases() {
|
||||
check(state.context.contains(0x280) && state.context.at(0x280) == 0x00080008u && state.context.contains(0x281) && state.context.at(0x281) == 0xffff0000u, "initial point context differs from the SDK defaults");
|
||||
execute(state, makePacket(0x69, {0x280, 0x00180018u, 0x00400008u}));
|
||||
check(state.context.at(0x280) == 0x00180018u && state.context.at(0x281) == 0x00400008u, "explicit point size and limits were not stored");
|
||||
for (const auto offset : {0x10bu, 0x10cu, 0x10du}) execute(state, makePacket(0x69, {offset, 0xffffffffu}));
|
||||
for (const auto offset : {0x010u, 0x011u, 0x10bu, 0x10cu, 0x10du}) execute(state, makePacket(0x69, {offset, 0xffffffffu}));
|
||||
execute(state, makePacket(0x12, {0}));
|
||||
for (const auto offset : {0x10bu, 0x10cu, 0x10du}) check(state.context.at(offset) == 0, "CLEAR_STATE did not restore stencil defaults");
|
||||
for (const auto offset : {0x010u, 0x011u, 0x10bu, 0x10cu, 0x10du}) check(state.context.at(offset) == 0, "CLEAR_STATE did not restore depth and stencil defaults");
|
||||
check(state.context.at(0x280) == 0x00080008u && state.context.at(0x281) == 0xffff0000u, "CLEAR_STATE did not restore point size and limits");
|
||||
execute(state, makePacket(0x69, {0x10, 17}));
|
||||
execute(state, makePacket(0x76, {0x20c, 2}));
|
||||
@@ -1060,8 +1060,9 @@ void testInvalidRegisterListEntries() {
|
||||
check(!AgcDriver::Pm4::IndirectRegistersValid(std::array<std::uint32_t, 2>{0xffffffffu, 0}), "the sentinel was taken as a register offset");
|
||||
check(AgcDriver::Pm4::IndirectRegistersValid(std::array<std::uint32_t, 4>{0x10, 1, 0x70000011u, 2}), "a valid register list was rejected");
|
||||
AgcDriver::QueueState state;
|
||||
const auto initial = state.context;
|
||||
expectFailure([&] { AgcDriver::Pm4::ExecuteIndirectRegisters(packet, pairs, state); }, "extended register");
|
||||
check(!state.context.contains(0x10) && !state.context.contains(0x11), "a rejected register list partially changed state");
|
||||
check(state.context == initial, "a rejected register list partially changed state");
|
||||
AgcDriver::Pm4::ExecuteIndirectRegisters(packet, pairs, state, true);
|
||||
check(state.context.at(0x10) == 41 && state.context.at(0x11) == 42 && !state.context.contains(0x38f), "skipping an invalid entry lost the valid ones or wrote the invalid one");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
|
||||
constexpr VkQueueFlags Graphics = VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT | VK_QUEUE_TRANSFER_BIT;
|
||||
constexpr VkQueueFlags Sparse = VK_QUEUE_SPARSE_BINDING_BIT;
|
||||
constexpr std::uint32_t Own = VK_QUEUE_FAMILY_IGNORED;
|
||||
|
||||
struct Case {
|
||||
const char* name;
|
||||
std::vector<VkQueueFamilyProperties> families;
|
||||
bool settles;
|
||||
std::uint32_t mainCount;
|
||||
bool settle;
|
||||
std::uint32_t settleFamily;
|
||||
std::uint32_t settleIndex;
|
||||
bool sparseBinding;
|
||||
std::uint32_t sparseFamily;
|
||||
std::uint32_t sparseIndex;
|
||||
};
|
||||
|
||||
VkQueueFamilyProperties family(VkQueueFlags flags, std::uint32_t queues) {
|
||||
VkQueueFamilyProperties properties{};
|
||||
properties.queueFlags = flags;
|
||||
properties.queueCount = queues;
|
||||
return properties;
|
||||
}
|
||||
|
||||
std::string describe(const AgcDriver::DeviceQueuePlan& plan) {
|
||||
return "main " + std::to_string(plan.mainCount) + " queues, settle " + (plan.settle ? std::to_string(plan.settleFamily) + ":" + std::to_string(plan.settleIndex) : std::string("none")) + ", sparse " + (!plan.sparseBinding ? std::string("none") : plan.sparseFamily == Own ? std::string("on the main queue") : std::to_string(plan.sparseFamily) + ":" + std::to_string(plan.sparseIndex));
|
||||
}
|
||||
|
||||
void check(const Case& test) {
|
||||
const auto plan = AgcDriver::PlanDeviceQueues(test.families, 0, test.settles, true);
|
||||
const auto dense = AgcDriver::PlanDeviceQueues(test.families, 0, test.settles, false);
|
||||
Require(plan.mainCount == dense.mainCount && plan.settle == dense.settle && plan.settleFamily == dense.settleFamily && plan.settleIndex == dense.settleIndex, std::string("device queue plan, ") + test.name + ": sparse binding changed the queues: " + describe(dense) + " without it, " + describe(plan) + " with it");
|
||||
Require(!dense.sparseBinding && dense.sparseFamily == Own, std::string("device queue plan, ") + test.name + ": a device without sparse binding got a sparse queue: " + describe(dense));
|
||||
const bool expected = plan.mainCount == test.mainCount && plan.settle == test.settle && (!test.settle || (plan.settleFamily == test.settleFamily && plan.settleIndex == test.settleIndex)) && plan.sparseBinding == test.sparseBinding && plan.sparseFamily == test.sparseFamily && (test.sparseFamily == Own || plan.sparseIndex == test.sparseIndex);
|
||||
Require(expected, std::string("device queue plan, ") + test.name + ": got " + describe(plan));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const std::vector<Case> cases{
|
||||
{"one family with one queue", {family(Graphics | Sparse, 1)}, true, 1, false, 0, 0, true, Own, 0},
|
||||
{"one family with one queue, no settles", {family(Graphics | Sparse, 1)}, false, 1, false, 0, 0, true, Own, 0},
|
||||
{"settle family with one queue that also binds", {family(Graphics | Sparse, 1), family(VK_QUEUE_TRANSFER_BIT | Sparse, 1)}, true, 1, true, 1, 0, true, Own, 0},
|
||||
{"settle family with two queues that also binds", {family(Graphics, 1), family(VK_QUEUE_TRANSFER_BIT | Sparse, 2)}, true, 1, true, 1, 0, true, 1, 1},
|
||||
{"settle on the main family", {family(Graphics | Sparse, 2), family(VK_QUEUE_TRANSFER_BIT | Sparse, 1)}, true, 2, true, 0, 1, true, 1, 0},
|
||||
{"sparse-only family preferred", {family(Graphics | Sparse, 16), family(VK_QUEUE_TRANSFER_BIT | Sparse, 2), family(Sparse, 1)}, true, 2, true, 0, 1, true, 2, 0},
|
||||
{"no settles, a second family binds", {family(Graphics | Sparse, 1), family(VK_QUEUE_TRANSFER_BIT | Sparse, 1)}, false, 1, false, 0, 0, true, 1, 0},
|
||||
{"no queue binds", {family(Graphics, 1), family(VK_QUEUE_TRANSFER_BIT, 1)}, true, 1, true, 1, 0, false, Own, 0},
|
||||
{"only the settle queue binds", {family(Graphics, 1), family(VK_QUEUE_TRANSFER_BIT | Sparse, 1)}, true, 1, true, 1, 0, false, Own, 0},
|
||||
};
|
||||
for (const auto& test : cases) check(test);
|
||||
std::puts("device queue plan tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <initializer_list>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
|
||||
constexpr std::byte Kept{0x40};
|
||||
constexpr std::uint32_t BaseVertexLocation = 0x8c;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 20> VertexCode{
|
||||
0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u,
|
||||
0x7e060208u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u,
|
||||
};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 8> PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u};
|
||||
alignas(256) std::array<std::byte, 1024> FirstMemory{};
|
||||
alignas(256) std::array<std::byte, 1024> NoInstancesMemory{};
|
||||
alignas(256) std::array<std::byte, 1024> NoVerticesMemory{};
|
||||
alignas(256) std::array<std::byte, 1024> LastMemory{};
|
||||
alignas(256) std::array<std::uint32_t, 16> Records{3, 1, 0, 0, 3, 0, 0, 0, 0, 1, 0, 0, 3, 1, 0, 0};
|
||||
|
||||
struct Header {
|
||||
Shader shader{};
|
||||
ShaderUserData userData{};
|
||||
};
|
||||
|
||||
alignas(8) Header VertexHeader{};
|
||||
alignas(8) Header PixelHeader{};
|
||||
|
||||
void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) {
|
||||
header.shader.file_header = 0x34333231u;
|
||||
header.shader.version = 0x18u;
|
||||
header.shader.header_size = sizeof(Header);
|
||||
header.shader.shader_size = static_cast<std::uint32_t>(bytes);
|
||||
header.shader.code = code;
|
||||
header.shader.type = type;
|
||||
header.shader.user_data = &header.userData;
|
||||
AgcDriverRegisterShader_nid_postfix(&header.shader);
|
||||
}
|
||||
|
||||
void Append(std::vector<std::uint32_t>& commands, std::uint32_t opcode, std::initializer_list<std::uint32_t> payload) {
|
||||
commands.push_back(0xc0000000u | (static_cast<std::uint32_t>(payload.size() - 1) << 16u) | (opcode << 8u));
|
||||
commands.insert(commands.end(), payload);
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> DrawFour() {
|
||||
const auto vertex = reinterpret_cast<std::uintptr_t>(VertexCode.data());
|
||||
const auto pixel = reinterpret_cast<std::uintptr_t>(PixelCode.data());
|
||||
const auto records = reinterpret_cast<std::uintptr_t>(Records.data());
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>> context{
|
||||
{0x2d5, 0x2000},
|
||||
{0x1b6, 0}, {0x207, 0x01200000}, {0x200, 0}, {0x203, 0x800},
|
||||
{0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0},
|
||||
{0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa},
|
||||
{0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff},
|
||||
{0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240},
|
||||
{0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010},
|
||||
{0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028},
|
||||
{0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u},
|
||||
{0x3b8, 0x9000000}, {0x1e0, 0},
|
||||
{0xc, 0}, {0xd, 0x40040},
|
||||
{0x81, 0x80000000}, {0x82, 0x40040},
|
||||
{0x90, 0x80000000}, {0x91, 0x40040},
|
||||
{0x94, 0x80000000}, {0x95, 0x40040},
|
||||
{0x10f, std::bit_cast<std::uint32_t>(32.0f)}, {0x110, std::bit_cast<std::uint32_t>(32.0f)},
|
||||
{0x111, std::bit_cast<std::uint32_t>(-2.0f)}, {0x112, std::bit_cast<std::uint32_t>(2.0f)},
|
||||
{0x113, std::bit_cast<std::uint32_t>(1.0f)}, {0x114, 0},
|
||||
{0xb4, 0}, {0xb5, std::bit_cast<std::uint32_t>(1.0f)},
|
||||
{0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0},
|
||||
};
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>> shader{
|
||||
{0xc8, static_cast<std::uint32_t>(vertex >> 8u)}, {0xc9, static_cast<std::uint32_t>(vertex >> 40u)}, {0x8b, 1u << 1u},
|
||||
{0x008, static_cast<std::uint32_t>(pixel >> 8u)}, {0x009, static_cast<std::uint32_t>(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast<std::uint32_t>(1.0f)},
|
||||
};
|
||||
std::vector<std::uint32_t> commands;
|
||||
Append(commands, 0x79, {0x242, 4});
|
||||
Append(commands, 0x79, {0x24a, 0});
|
||||
for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value});
|
||||
for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value});
|
||||
Append(commands, 0x11, {1, static_cast<std::uint32_t>(records), static_cast<std::uint32_t>(records >> 32u)});
|
||||
const auto draw = [&](std::array<std::byte, 1024>& memory, std::uint32_t recordOffset) {
|
||||
const auto color = reinterpret_cast<std::uintptr_t>(memory.data());
|
||||
Append(commands, 0x69, {0x318, static_cast<std::uint32_t>(color >> 8u)});
|
||||
Append(commands, 0x69, {0x390, static_cast<std::uint32_t>(color >> 40u)});
|
||||
Append(commands, 0x76, {BaseVertexLocation, 0});
|
||||
Append(commands, 0x24, {recordOffset, BaseVertexLocation, 0, 2});
|
||||
};
|
||||
draw(FirstMemory, 0);
|
||||
draw(NoInstancesMemory, 16);
|
||||
draw(NoVerticesMemory, 32);
|
||||
draw(LastMemory, 48);
|
||||
return commands;
|
||||
}
|
||||
|
||||
std::size_t Covered(const std::array<std::byte, 1024>& memory) {
|
||||
return static_cast<std::size_t>(std::count_if(memory.begin(), memory.end(), [](std::byte value) { return value != Kept; }));
|
||||
}
|
||||
|
||||
void EmptyIndirectDrawsSkipTheirStages() {
|
||||
FirstMemory.fill(Kept);
|
||||
NoInstancesMemory.fill(Kept);
|
||||
NoVerticesMemory.fill(Kept);
|
||||
LastMemory.fill(Kept);
|
||||
Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2);
|
||||
Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1);
|
||||
auto commands = DrawFour();
|
||||
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
|
||||
Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
const auto first = Covered(FirstMemory);
|
||||
Require(first != 0, "the first indirect draw, whose record draws 3 vertices, covered nothing");
|
||||
Require(Covered(NoInstancesMemory) == 0, "the indirect draw whose record has no instances covered " + std::to_string(Covered(NoInstancesMemory)) + " bytes, expected none");
|
||||
Require(Covered(NoVerticesMemory) == 0, "the indirect draw whose record has no vertices covered " + std::to_string(Covered(NoVerticesMemory)) + " bytes, expected none");
|
||||
for (std::size_t index = 0; index < FirstMemory.size(); ++index) {
|
||||
Require(LastMemory[index] == FirstMemory[index], "byte " + std::to_string(index) + " of the indirect draw after the empty ones is " + std::to_string(static_cast<unsigned>(LastMemory[index])) + ", the first indirect draw wrote " + std::to_string(static_cast<unsigned>(FirstMemory[index])));
|
||||
}
|
||||
const auto empty = AgcDriver::Graphics::EmptyIndirectDrawCount();
|
||||
Require(empty == 2, std::to_string(empty) + " of the 2 indirect draws whose records draw nothing were skipped before their stages were built");
|
||||
const auto counts = AgcDriver::Graphics::IndirectDrawCounts();
|
||||
std::uint64_t cpu = 0;
|
||||
for (std::size_t path = 1; path < counts.size(); ++path) cpu += counts[path];
|
||||
Require(counts[0] == 0 && cpu == 2, "the drawing indirect draws took the GPU-side path " + std::to_string(counts[0]) + " times and the CPU path " + std::to_string(cpu) + " times, expected the CPU path twice");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
#ifdef _WIN32
|
||||
_putenv_s("APS5_PROFILE_DRAW", "1");
|
||||
#else
|
||||
setenv("APS5_PROFILE_DRAW", "1", 1);
|
||||
#endif
|
||||
{
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
}
|
||||
EmptyIndirectDrawsSkipTheirStages();
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
std::puts("CPU-path indirect draws whose records draw nothing were skipped before their stages were built");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ResidentMirror.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 64;
|
||||
constexpr std::uint64_t Block = 65536;
|
||||
constexpr std::uint64_t HeapBytes = 8u << 20u;
|
||||
constexpr std::uint64_t JoinedBytes = 16u << 20u;
|
||||
constexpr std::uint64_t ThreadStride = 16384;
|
||||
constexpr std::uint64_t StoreBlock = 3 * Block;
|
||||
constexpr std::uint32_t LateThread = 5;
|
||||
constexpr std::uint64_t ZeroedBlock = 2 * Block;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 10> Code{
|
||||
0x3404008e, 0xdc388000, 0x04000002, 0x34060082, 0xbf8c3f70, 0xe0701000, 0x80010403, 0xe0701000, 0x80020303, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 7> CopyCode{0x34060082, 0xe0301000, 0x80000403, 0xbf8c3f70, 0xe0701000, 0x80010403, 0xbf810000};
|
||||
|
||||
class OutputBlock {
|
||||
public:
|
||||
OutputBlock() {
|
||||
#ifdef _WIN32
|
||||
block = static_cast<std::uint32_t*>(VirtualAlloc(nullptr, Block, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
#else
|
||||
block = static_cast<std::uint32_t*>(std::aligned_alloc(Block, Block));
|
||||
#endif
|
||||
Require(block != nullptr, "resident heap mirror: cannot allocate the output block");
|
||||
GuestAllocations::Mutation().Add(block, Block, true, true, true);
|
||||
}
|
||||
|
||||
~OutputBlock() {
|
||||
GuestAllocations::Mutation().Remove(block);
|
||||
#ifdef _WIN32
|
||||
VirtualFree(block, 0, MEM_RELEASE);
|
||||
#else
|
||||
std::free(block);
|
||||
#endif
|
||||
}
|
||||
|
||||
OutputBlock(const OutputBlock&) = delete;
|
||||
OutputBlock& operator=(const OutputBlock&) = delete;
|
||||
|
||||
std::uint32_t* Data() const { return block; }
|
||||
|
||||
private:
|
||||
std::uint32_t* block = nullptr;
|
||||
};
|
||||
|
||||
class GuestHeap {
|
||||
public:
|
||||
explicit GuestHeap(bool writable, std::uint64_t ranges = 1, std::uint64_t rangeBytes = HeapBytes, void* at = nullptr) : ranges(ranges), rangeBytes(rangeBytes) {
|
||||
const auto bytes = rangeBytes * ranges;
|
||||
#ifdef _WIN32
|
||||
raw = at == nullptr ? GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block)
|
||||
: GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix(reinterpret_cast<std::uintptr_t>(at), bytes, Block);
|
||||
Require(raw != nullptr && (at == nullptr || raw == at), "resident heap mirror: cannot allocate the guest heap");
|
||||
GuestArena::GuestArenaCommit_nid_postfix(raw, bytes, PAGE_READWRITE, bytes);
|
||||
base = static_cast<std::uint8_t*>(raw);
|
||||
#else
|
||||
raw = mmap(at, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | (at != nullptr ? MAP_FIXED_NOREPLACE : 0), -1, 0);
|
||||
Require(raw != MAP_FAILED && (at == nullptr || raw == at), "resident heap mirror: cannot map the guest heap");
|
||||
base = reinterpret_cast<std::uint8_t*>((reinterpret_cast<std::uintptr_t>(raw) + Block - 1) / Block * Block);
|
||||
#endif
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(base, bytes);
|
||||
GuestAllocations::Mutation mutation;
|
||||
for (std::uint64_t range = 0; range < ranges; ++range) mutation.Add(base + range * rangeBytes, rangeBytes, true, writable, true);
|
||||
}
|
||||
|
||||
~GuestHeap() {
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
for (std::uint64_t range = 0; range < ranges; ++range) mutation.Remove(base + range * rangeBytes);
|
||||
}
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(base, rangeBytes * ranges);
|
||||
#ifdef _WIN32
|
||||
GuestArena::GuestArenaReset_nid_postfix(raw, rangeBytes * ranges);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(raw, rangeBytes * ranges);
|
||||
#else
|
||||
munmap(raw, rangeBytes * ranges + Block);
|
||||
#endif
|
||||
}
|
||||
|
||||
GuestHeap(const GuestHeap&) = delete;
|
||||
GuestHeap& operator=(const GuestHeap&) = delete;
|
||||
|
||||
std::uint8_t* Data() const { return base; }
|
||||
void* Raw() const { return raw; }
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(base); }
|
||||
|
||||
void Store(std::uint64_t offset, std::uint32_t value) const { std::memcpy(base + offset, &value, sizeof(value)); }
|
||||
|
||||
std::uint32_t Load(std::uint64_t offset) const {
|
||||
std::uint32_t value = 0;
|
||||
std::memcpy(&value, base + offset, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
private:
|
||||
std::uint64_t ranges = 1;
|
||||
std::uint64_t rangeBytes = HeapBytes;
|
||||
void* raw = nullptr;
|
||||
std::uint8_t* base = nullptr;
|
||||
};
|
||||
|
||||
struct ImportBudget {
|
||||
explicit ImportBudget(std::uint64_t bytes) { AgcDriver::Graphics::SetHostImportBudget(bytes); }
|
||||
~ImportBudget() { AgcDriver::Graphics::SetHostImportBudget(0); }
|
||||
ImportBudget(const ImportBudget&) = delete;
|
||||
ImportBudget& operator=(const ImportBudget&) = delete;
|
||||
};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(std::uint64_t address, std::uint32_t bytes) {
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
|
||||
}
|
||||
|
||||
std::string Hex(std::uint64_t value) {
|
||||
char text[24];
|
||||
std::snprintf(text, sizeof(text), "0x%llx", static_cast<unsigned long long>(value));
|
||||
return text;
|
||||
}
|
||||
|
||||
std::uint32_t Pattern(std::uint32_t thread) {
|
||||
return 0x5a000000u + thread * 0x00010203u + 1u;
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData) {
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> regions{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, regions},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
void Dispatch(AgcDriver::VulkanDevice& device, std::uint64_t read, std::uint64_t written, const OutputBlock& output) {
|
||||
std::fill(output.Data(), output.Data() + Threads, 0xdeadbeefu);
|
||||
std::vector<std::uint32_t> userData(12, 0u);
|
||||
userData[0] = static_cast<std::uint32_t>(read);
|
||||
userData[1] = static_cast<std::uint32_t>(read >> 32u);
|
||||
const auto out = BufferDescriptor(reinterpret_cast<std::uintptr_t>(output.Data()), static_cast<std::uint32_t>(Threads * sizeof(std::uint32_t)));
|
||||
const auto store = BufferDescriptor(written + StoreBlock, static_cast<std::uint32_t>(Threads * sizeof(std::uint32_t)));
|
||||
std::copy(out.begin(), out.end(), userData.begin() + 4);
|
||||
std::copy(store.begin(), store.end(), userData.begin() + 8);
|
||||
Run(device, Code, userData);
|
||||
}
|
||||
|
||||
void Dispatch(AgcDriver::VulkanDevice& device, const GuestHeap& read, const GuestHeap& written, const OutputBlock& output) {
|
||||
Dispatch(device, read.Address(), written.Address(), output);
|
||||
}
|
||||
|
||||
void Copy(AgcDriver::VulkanDevice& device, std::uint64_t source, const OutputBlock& output) {
|
||||
std::fill(output.Data(), output.Data() + Threads, 0xdeadbeefu);
|
||||
std::vector<std::uint32_t> userData(8, 0u);
|
||||
const auto in = BufferDescriptor(source, static_cast<std::uint32_t>(Threads * sizeof(std::uint32_t)));
|
||||
const auto out = BufferDescriptor(reinterpret_cast<std::uintptr_t>(output.Data()), static_cast<std::uint32_t>(Threads * sizeof(std::uint32_t)));
|
||||
std::copy(in.begin(), in.end(), userData.begin());
|
||||
std::copy(out.begin(), out.end(), userData.begin() + 4);
|
||||
Run(device, CopyCode, userData);
|
||||
}
|
||||
|
||||
void CheckReads(const OutputBlock& output, const std::string& step, bool late) {
|
||||
for (std::uint32_t thread = 0; thread < Threads; ++thread) {
|
||||
const auto expected = thread * ThreadStride < Block ? Pattern(thread) : late && thread == LateThread ? Pattern(thread) : 0u;
|
||||
Require(output.Data()[thread] == expected, "resident heap mirror: " + step + ": thread " + std::to_string(thread) + " read " + Hex(output.Data()[thread]) + " at heap offset " + Hex(thread * ThreadStride) + ", expected " + Hex(expected));
|
||||
}
|
||||
}
|
||||
|
||||
void CheckStores(const GuestHeap& written, const std::string& step) {
|
||||
for (std::uint32_t thread = 0; thread < Threads; ++thread) {
|
||||
const auto value = written.Load(StoreBlock + thread * sizeof(std::uint32_t));
|
||||
Require(value == thread * 4u, "resident heap mirror: " + step + ": the GPU store of thread " + std::to_string(thread) + " into a never-written heap block reached the guest as " + Hex(value) + ", expected " + Hex(thread * 4u));
|
||||
}
|
||||
}
|
||||
|
||||
void CheckHeld(std::uint64_t before, std::uint64_t written, const std::string& step) {
|
||||
const auto held = AgcDriver::Graphics::MirrorCounters().heapBytes - before;
|
||||
Require(held == written, "resident heap mirror: " + step + ": the heap mirrors of two " + std::to_string(HeapBytes >> 10u) + " KiB heaps hold " + std::to_string(held >> 10u) + " KiB of device memory; the guest and the GPU wrote " + std::to_string(written >> 10u) + " KiB of them");
|
||||
}
|
||||
|
||||
void ResidentTests(AgcDriver::VulkanDevice& device) {
|
||||
GuestHeap read(false);
|
||||
GuestHeap written(true);
|
||||
const OutputBlock output;
|
||||
for (std::uint32_t thread = 0; thread * ThreadStride < Block; ++thread) read.Store(thread * ThreadStride, Pattern(thread));
|
||||
written.Store(16, 0x77u);
|
||||
const ImportBudget budget(AgcDriver::Graphics::HostImportLiveBytes() + Block);
|
||||
const auto before = AgcDriver::Graphics::MirrorCounters();
|
||||
Dispatch(device, read, written, output);
|
||||
const auto made = AgcDriver::Graphics::MirrorCounters();
|
||||
Require(made.heapMirrors == before.heapMirrors + 2, "resident heap mirror: the two heaps were not heap mirrored (" + std::to_string(made.heapMirrors - before.heapMirrors) + " mirrors made)");
|
||||
CheckHeld(before.heapBytes, 3 * Block, "first dispatch");
|
||||
CheckReads(output, "first dispatch", false);
|
||||
CheckStores(written, "first dispatch");
|
||||
Require(written.Load(16) == 0x77u, "resident heap mirror: the written heap's guest bytes changed");
|
||||
read.Store(LateThread * ThreadStride, Pattern(LateThread));
|
||||
std::memset(read.Data() + ZeroedBlock, 0, Block);
|
||||
Dispatch(device, read, written, output);
|
||||
CheckHeld(before.heapBytes, 4 * Block, "after a guest write into a never-written block and zeros written into another");
|
||||
CheckReads(output, "after a guest write into a never-written block", true);
|
||||
const auto steady = AgcDriver::Graphics::MirrorCounters();
|
||||
Dispatch(device, read, written, output);
|
||||
const auto again = AgcDriver::Graphics::MirrorCounters();
|
||||
Require(again.rebuilds == steady.rebuilds && again.heapBytes == steady.heapBytes, "resident heap mirror: an unchanged dispatch rebuilt or grew the heap mirrors");
|
||||
CheckReads(output, "unchanged dispatch", true);
|
||||
CheckStores(written, "unchanged dispatch");
|
||||
}
|
||||
|
||||
struct AddressSpaceLimit {
|
||||
explicit AddressSpaceLimit(std::uint64_t bytes) { AgcDriver::Graphics::SetResidentAddressSpaceLimit(bytes); }
|
||||
~AddressSpaceLimit() { AgcDriver::Graphics::SetResidentAddressSpaceLimit(0); }
|
||||
AddressSpaceLimit(const AddressSpaceLimit&) = delete;
|
||||
AddressSpaceLimit& operator=(const AddressSpaceLimit&) = delete;
|
||||
};
|
||||
|
||||
void CopiedTests(AgcDriver::VulkanDevice& device) {
|
||||
const AddressSpaceLimit limit(1);
|
||||
GuestHeap read(false);
|
||||
GuestHeap written(true);
|
||||
const OutputBlock output;
|
||||
for (std::uint32_t thread = 0; thread * ThreadStride < Block; ++thread) read.Store(thread * ThreadStride, Pattern(thread));
|
||||
written.Store(16, 0x77u);
|
||||
const ImportBudget budget(AgcDriver::Graphics::HostImportLiveBytes() + Block);
|
||||
const auto before = AgcDriver::Graphics::MirrorCounters();
|
||||
Dispatch(device, read, written, output);
|
||||
const auto made = AgcDriver::Graphics::MirrorCounters();
|
||||
Require(made.heapMirrors == before.heapMirrors + 2, "copied heap mirror: the two heaps were not heap mirrored without sparse address space (" + std::to_string(made.heapMirrors - before.heapMirrors) + " mirrors made)");
|
||||
Require(made.heapBytes - before.heapBytes == 2 * HeapBytes, "copied heap mirror: two " + std::to_string(HeapBytes >> 10u) + " KiB heaps without sparse address space hold " + std::to_string((made.heapBytes - before.heapBytes) >> 10u) + " KiB, expected whole copies");
|
||||
CheckReads(output, "copied, first dispatch", false);
|
||||
CheckStores(written, "copied, first dispatch");
|
||||
Require(written.Load(16) == 0x77u, "copied heap mirror: the written heap's guest bytes changed");
|
||||
read.Store(LateThread * ThreadStride, Pattern(LateThread));
|
||||
Dispatch(device, read, written, output);
|
||||
CheckReads(output, "copied, after a guest write", true);
|
||||
CheckStores(written, "copied, after a guest write");
|
||||
}
|
||||
|
||||
void ReusedAddressTests(AgcDriver::VulkanDevice& device) {
|
||||
GuestHeap written(true);
|
||||
const OutputBlock output;
|
||||
const ImportBudget budget(AgcDriver::Graphics::HostImportLiveBytes() + Block);
|
||||
void* at = nullptr;
|
||||
std::uint64_t address = 0;
|
||||
{
|
||||
GuestHeap first(false);
|
||||
for (std::uint32_t thread = 0; thread * ThreadStride < Block; ++thread) first.Store(thread * ThreadStride, Pattern(thread));
|
||||
Dispatch(device, first, written, output);
|
||||
CheckReads(output, "the first heap", false);
|
||||
at = first.Raw();
|
||||
address = first.Address();
|
||||
}
|
||||
GuestHeap second(false, 1, HeapBytes, at);
|
||||
Require(second.Address() == address, "resident heap mirror: the second heap did not take the first heap's address");
|
||||
for (std::uint32_t thread = 0; thread * ThreadStride < Block; ++thread) second.Store(thread * ThreadStride, ~Pattern(thread));
|
||||
Dispatch(device, second, written, output);
|
||||
for (std::uint32_t thread = 0; thread < Threads; ++thread) {
|
||||
const auto expected = thread * ThreadStride < Block ? ~Pattern(thread) : 0u;
|
||||
Require(output.Data()[thread] == expected, "resident heap mirror: a heap mapped where a mirrored heap was freed: thread " + std::to_string(thread) + " read " + Hex(output.Data()[thread]) + ", expected " + Hex(expected));
|
||||
}
|
||||
const auto held = AgcDriver::Graphics::HeapMirrorsWithin(address, address + HeapBytes);
|
||||
Require(held.mirrors == 1 && held.bytes == Block, "resident heap mirror: a heap mapped where a mirrored heap was freed holds " + std::to_string(held.mirrors) + " live heap mirrors (" + std::to_string(held.bytes >> 10u) + " KiB), expected 1 (64 KiB)");
|
||||
}
|
||||
|
||||
void JoinedImportTests(AgcDriver::VulkanDevice& device) {
|
||||
GuestHeap pair(true, 2, JoinedBytes);
|
||||
const OutputBlock output;
|
||||
const auto begin = pair.Address();
|
||||
const auto end = begin + 2 * JoinedBytes;
|
||||
for (std::uint32_t thread = 0; thread * ThreadStride < Block; ++thread) pair.Store(thread * ThreadStride, Pattern(thread));
|
||||
pair.Store(JoinedBytes - sizeof(std::uint32_t), 0x41u);
|
||||
pair.Store(JoinedBytes, 0x42u);
|
||||
const auto before = AgcDriver::Graphics::HeapMirrorsWithin(begin, end);
|
||||
{
|
||||
const ImportBudget budget(AgcDriver::Graphics::HostImportLiveBytes() + Block);
|
||||
Dispatch(device, begin, begin + JoinedBytes, output);
|
||||
CheckReads(output, "two adjacent heaps", false);
|
||||
}
|
||||
const auto mirrored = AgcDriver::Graphics::HeapMirrorsWithin(begin, end);
|
||||
Require(before.mirrors == 0 && mirrored.mirrors == 2 && mirrored.bytes == 4 * Block, "resident heap mirror: two adjacent heaps hold " + std::to_string(before.mirrors) + " heap mirrors before the first dispatch and " + std::to_string(mirrored.mirrors) + " (" + std::to_string(mirrored.bytes >> 10u) + " KiB) after it, expected 0 and 2 (256 KiB); " + std::to_string(AgcDriver::Graphics::HostImportLiveBytes() >> 10u) + " KiB host imports live");
|
||||
const auto live = AgcDriver::Graphics::HostImportLiveBytes();
|
||||
Copy(device, begin + JoinedBytes - Threads * sizeof(std::uint32_t) / 2, output);
|
||||
Require(output.Data()[Threads / 2 - 1] == 0x41u && output.Data()[Threads / 2] == 0x42u, "resident heap mirror: a view across two adjacent heaps read " + Hex(output.Data()[Threads / 2 - 1]) + " " + Hex(output.Data()[Threads / 2]) + ", expected 0x41 0x42");
|
||||
const auto joined = AgcDriver::Graphics::HostImportLiveBytes() - live;
|
||||
Require(joined >= 2 * JoinedBytes, "resident heap mirror: a view across two adjacent heaps made no joined host import (" + std::to_string(joined >> 10u) + " KiB imported)");
|
||||
Dispatch(device, begin, begin + JoinedBytes, output);
|
||||
CheckReads(output, "after the joined import", false);
|
||||
const auto released = AgcDriver::Graphics::HeapMirrorsWithin(begin, end);
|
||||
Require(released.mirrors == 0 && released.bytes == 0, "resident heap mirror: a joined host import over two heap-mirrored heaps left " + std::to_string(released.mirrors) + " of their " + std::to_string(mirrored.mirrors) + " heap mirrors and " + std::to_string(released.bytes >> 10u) + " of " + std::to_string(mirrored.bytes >> 10u) + " KiB held");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
#ifdef _WIN32
|
||||
const bool watched = GuestArena::GuestArenaWriteWatched_nid_postfix();
|
||||
#else
|
||||
const bool watched = GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix();
|
||||
#endif
|
||||
if (!watched) {
|
||||
std::puts("skipped, no guest write watch: heap mirrors need one");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
ResidentTests(*device);
|
||||
ReusedAddressTests(*device);
|
||||
JoinedImportTests(*device);
|
||||
CopiedTests(*device);
|
||||
std::puts("resident heap mirror tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
|
||||
#include "SceShaders.hpp"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::DriverDetail::DrawPipeline;
|
||||
|
||||
void check(bool condition, const std::string& what) {
|
||||
if (!condition) throw std::runtime_error("vertex table read order: " + what);
|
||||
}
|
||||
|
||||
constexpr std::size_t UserDataOffset = 288;
|
||||
constexpr std::size_t DirectOffset = 352;
|
||||
constexpr std::size_t SemanticOffset = 384;
|
||||
|
||||
alignas(64) std::array<std::uint32_t, 8> BufferTable{0x1000u, 0x20000u, 64u, 0x3000u, 0x7000u, 0x20000u, 32u, 0x3000u};
|
||||
alignas(64) std::array<std::uint32_t, 1> AttribTable{(0x2cu << 5u) | 1u};
|
||||
alignas(64) std::array<std::uint32_t, 4> Elsewhere{};
|
||||
std::vector<std::byte> Header(SemanticOffset + sizeof(ShaderSemantic));
|
||||
|
||||
void BuildHeader() {
|
||||
std::array<std::uint16_t, ShaderRegs::AGC_DIRECT_RESOURCE_TYPE_COUNT> direct{};
|
||||
direct.fill(ShaderRegs::AGC_ILLEGAL_DIRECT_OFFSET);
|
||||
direct[static_cast<std::size_t>(ShaderRegs::AgcDirectResourceType::PtrVertexBufferTable)] = 0;
|
||||
direct[static_cast<std::size_t>(ShaderRegs::AgcDirectResourceType::PtrVertexAttribDescTable)] = 2;
|
||||
std::memcpy(Header.data() + DirectOffset, direct.data(), sizeof(direct));
|
||||
ShaderSemantic semantic{};
|
||||
semantic.size_in_elements = 1;
|
||||
std::memcpy(Header.data() + SemanticOffset, &semantic, sizeof(semantic));
|
||||
ShaderUserData userData{};
|
||||
userData.direct_resource_offset = reinterpret_cast<std::uint16_t*>(Header.data() + DirectOffset);
|
||||
userData.direct_resource_count = ShaderRegs::AGC_DIRECT_RESOURCE_TYPE_COUNT;
|
||||
std::memcpy(Header.data() + UserDataOffset, &userData, sizeof(userData));
|
||||
Shader shader{};
|
||||
shader.user_data = reinterpret_cast<ShaderUserData*>(Header.data() + UserDataOffset);
|
||||
shader.input_semantics = reinterpret_cast<ShaderSemantic*>(Header.data() + SemanticOffset);
|
||||
shader.num_input_semantics = 1;
|
||||
std::memcpy(Header.data(), &shader, sizeof(shader));
|
||||
}
|
||||
|
||||
std::uint64_t Address(const void* data) {
|
||||
return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data));
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> UserWords() {
|
||||
const auto buffer = Address(BufferTable.data());
|
||||
const auto attrib = Address(AttribTable.data());
|
||||
return {static_cast<std::uint32_t>(buffer), static_cast<std::uint32_t>(buffer >> 32u), static_cast<std::uint32_t>(attrib), static_cast<std::uint32_t>(attrib >> 32u)};
|
||||
}
|
||||
|
||||
ShaderRecompiler::ShaderVertexStageInfo Decode(std::vector<AgcDriver::Graphics::DecodeRead>& reads) {
|
||||
const auto userWords = UserWords();
|
||||
return AgcDriver::DriverDetail::DecodeOrderedVertexStageInfo(Header, Address(Header.data()), userWords, reads, 0);
|
||||
}
|
||||
|
||||
struct Gate {
|
||||
std::atomic<bool> release{false};
|
||||
};
|
||||
|
||||
std::vector<std::shared_ptr<Gate>> Gates;
|
||||
|
||||
std::shared_ptr<Gate> Hold(const void* range, std::size_t bytes, std::function<void()> store) {
|
||||
auto gate = Gates.emplace_back(std::make_shared<Gate>());
|
||||
DrawPipeline::Queue0().Enqueue([gate, store = std::move(store)] {
|
||||
while (!gate->release.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
store();
|
||||
}, {{Address(range), Address(range) + bytes}});
|
||||
return gate;
|
||||
}
|
||||
|
||||
void StopQueue() {
|
||||
AgcDriver::GuestMemory::SetReadGuard(nullptr);
|
||||
for (const auto& gate : Gates) gate->release.store(true, std::memory_order_release);
|
||||
DrawPipeline::Active() = false;
|
||||
DrawPipeline::Queue0().Stop();
|
||||
}
|
||||
|
||||
void WaitEmpty() {
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
||||
while (DrawPipeline::QueuedItems() != 0) {
|
||||
check(std::chrono::steady_clock::now() < deadline, "the pipeline never emptied");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Gate> completeAtBufferRead;
|
||||
const void* completeAtRead = BufferTable.data();
|
||||
|
||||
void CompleteQueuedStoreAtBufferRead(std::uint64_t address, std::size_t) {
|
||||
if (address != Address(completeAtRead) || completeAtBufferRead == nullptr) return;
|
||||
std::exchange(completeAtBufferRead, nullptr)->release.store(true, std::memory_order_release);
|
||||
WaitEmpty();
|
||||
}
|
||||
|
||||
ShaderRecompiler::ShaderVertexStageInfo DecodeBehindQueuedStore(const void* range, std::size_t bytes, std::function<void()> store, std::vector<AgcDriver::Graphics::DecodeRead>& reads) {
|
||||
WaitEmpty();
|
||||
auto gate = Hold(range, bytes, std::move(store));
|
||||
std::thread releaser([gate] {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
gate->release.store(true, std::memory_order_release);
|
||||
});
|
||||
ShaderRecompiler::ShaderVertexStageInfo info;
|
||||
try {
|
||||
info = Decode(reads);
|
||||
} catch (...) {
|
||||
releaser.join();
|
||||
throw;
|
||||
}
|
||||
releaser.join();
|
||||
return info;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
BuildHeader();
|
||||
DrawPipeline::Active() = true;
|
||||
std::vector<AgcDriver::Graphics::DecodeRead> reads;
|
||||
|
||||
auto info = Decode(reads);
|
||||
check(info.fetchEmbedded && info.resourcesNum == 1 && info.resources[0].fields[0] == 0x7000u && info.resources[0].fields[2] == 32u, "the tables did not decode to the second vertex buffer");
|
||||
|
||||
info = DecodeBehindQueuedStore(BufferTable.data() + 4, 16, [] {
|
||||
BufferTable[4] = 0x9000u;
|
||||
BufferTable[6] = 128u;
|
||||
}, reads);
|
||||
check(info.resources[0].fields[0] == 0x9000u && info.resources[0].fields[2] == 128u, "the vertex buffer V# was read before the queued store into the vertex buffer table");
|
||||
check(reads.size() == 2 && reads[1].address == Address(BufferTable.data() + 4) && std::memcmp(reads[1].Bytes().data(), BufferTable.data() + 4, 16) == 0, "the recorded V# read is not the stored V#");
|
||||
check(DrawPipeline::QueuedItems() == 0, "the store into the vertex buffer table is still queued");
|
||||
|
||||
info = DecodeBehindQueuedStore(AttribTable.data(), 4, [] { AttribTable[0] = 0x2cu << 5u; }, reads);
|
||||
check(info.resources[0].fields[0] == 0x1000u && info.resources[0].fields[2] == 64u, "the attribute word was read before the queued store into the vertex attribute table");
|
||||
check(reads.size() == 2 && reads[0].address == Address(AttribTable.data()) && reads[1].address == Address(BufferTable.data()), "the decode did not read the stored attribute word and the V# it names");
|
||||
|
||||
WaitEmpty();
|
||||
auto gate = Hold(Elsewhere.data(), sizeof(Elsewhere), [] {});
|
||||
info = Decode(reads);
|
||||
check(info.resources[0].fields[0] == 0x1000u, "the decode next to an unrelated queued store changed");
|
||||
check(DrawPipeline::QueuedItems() == 1, "a decode that overlaps nothing queued drained the pipeline");
|
||||
gate->release.store(true, std::memory_order_release);
|
||||
WaitEmpty();
|
||||
|
||||
completeAtBufferRead = Hold(AttribTable.data(), 4, [] { AttribTable[0] = (0x2cu << 5u) | 1u; });
|
||||
AgcDriver::GuestMemory::SetReadGuard(&CompleteQueuedStoreAtBufferRead);
|
||||
auto drains = DrawPipeline::Queue0().DecodeDrains();
|
||||
info = Decode(reads);
|
||||
AgcDriver::GuestMemory::SetReadGuard(nullptr);
|
||||
check(completeAtBufferRead == nullptr, "the queued attribute store did not complete during the decode");
|
||||
check(info.resources[0].fields[0] == 0x9000u && info.resources[0].fields[2] == 128u, "the decode kept the vertex buffer of an attribute word that a queued store replaced during the decode");
|
||||
check(reads.size() == 2 && reads[1].address == Address(BufferTable.data() + 4), "the recorded reads are not the stored attribute word and the V# it names");
|
||||
check(DrawPipeline::Queue0().DecodeDrains() == drains + 1, "the drain the decode caused was not counted");
|
||||
|
||||
WaitEmpty();
|
||||
auto finished = Hold(BufferTable.data() + 4, 16, [] {
|
||||
BufferTable[4] = 0xb000u;
|
||||
BufferTable[6] = 256u;
|
||||
});
|
||||
finished->release.store(true, std::memory_order_release);
|
||||
WaitEmpty();
|
||||
gate = Hold(Elsewhere.data(), sizeof(Elsewhere), [] {});
|
||||
drains = DrawPipeline::Queue0().DecodeDrains();
|
||||
info = Decode(reads);
|
||||
check(info.resources[0].fields[0] == 0xb000u && info.resources[0].fields[2] == 256u, "the decode did not read the V# a finished item stored");
|
||||
check(DrawPipeline::QueuedItems() == 1 && DrawPipeline::Queue0().DecodeDrains() == drains, "a decode of a table that a finished item wrote drained the pipeline");
|
||||
gate->release.store(true, std::memory_order_release);
|
||||
WaitEmpty();
|
||||
|
||||
completeAtBufferRead = Hold(Elsewhere.data(), sizeof(Elsewhere), [] { Elsewhere[0] = 1u; });
|
||||
completeAtRead = BufferTable.data() + 4;
|
||||
AgcDriver::GuestMemory::SetReadGuard(&CompleteQueuedStoreAtBufferRead);
|
||||
drains = DrawPipeline::Queue0().DecodeDrains();
|
||||
info = Decode(reads);
|
||||
AgcDriver::GuestMemory::SetReadGuard(nullptr);
|
||||
check(completeAtBufferRead == nullptr, "the unrelated queued store did not complete during the decode");
|
||||
check(info.resources[0].fields[0] == 0xb000u && DrawPipeline::Queue0().DecodeDrains() == drains, "a store elsewhere that retired during the decode made the decode drain");
|
||||
|
||||
StopQueue();
|
||||
std::puts("vertex table read order tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
try { StopQueue(); } catch (...) {}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,41 @@
|
||||
add_library(libSceConvertKeycode SHARED EXCLUDE_FROM_ALL Export.cpp)
|
||||
target_include_directories(libSceConvertKeycode PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(libSceConvertKeycode PRIVATE libSceUserService)
|
||||
configure_windows_unwind(libSceConvertKeycode)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(guest_convert_keycode_tests ${LIBS_INCLUDE_DIR}/tests/GuestConvertKeycode.cpp)
|
||||
target_include_directories(guest_convert_keycode_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_convert_keycode_tests PRIVATE libSceConvertKeycode libc)
|
||||
target_link_libraries(guest_convert_keycode_tests PRIVATE libSceConvertKeycode libSceUserService libSceSystemService libc)
|
||||
configure_windows_unwind(guest_convert_keycode_tests)
|
||||
add_test(NAME guest_convert_keycode COMMAND guest_convert_keycode_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_convert_keycode PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceConvertKeycode>")
|
||||
endif()
|
||||
add_test_executable(guest_convert_keycode_title_tests ${LIBS_INCLUDE_DIR}/tests/GuestConvertKeycodeTitle.cpp)
|
||||
target_include_directories(guest_convert_keycode_title_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_convert_keycode_title_tests PRIVATE libSceConvertKeycode libSceUserService libSceSystemService libc)
|
||||
configure_windows_unwind(guest_convert_keycode_title_tests)
|
||||
string(REPEAT "T" 130 convertKeycodeLongTitle)
|
||||
foreach(titleCase IN ITEMS oversized-id oversized-title cusa ppsa malformed-json missing)
|
||||
set(titleRun ${CMAKE_CURRENT_BINARY_DIR}/convert_keycode_title_${titleCase})
|
||||
file(REMOVE_RECURSE ${titleRun})
|
||||
file(MAKE_DIRECTORY ${titleRun})
|
||||
if(titleCase STREQUAL "oversized-id")
|
||||
file(WRITE ${titleRun}/app0/sce_sys/param.json "{\"titleId\": \"CUSA0123456789012345\", \"localizedParameters\": {\"en-US\": {\"titleName\": \"Fixture\"}}}")
|
||||
elseif(titleCase STREQUAL "oversized-title")
|
||||
file(WRITE ${titleRun}/app0/sce_sys/param.json "{\"titleId\": \"PPSA00000\", \"localizedParameters\": {\"en-US\": {\"titleName\": \"${convertKeycodeLongTitle}\"}}}")
|
||||
elseif(titleCase STREQUAL "cusa" OR titleCase STREQUAL "ppsa")
|
||||
string(TOUPPER ${titleCase} titlePrefix)
|
||||
file(WRITE ${titleRun}/app0/sce_sys/param.json "{\"titleId\": \"${titlePrefix}00000\", \"localizedParameters\": {\"en-US\": {\"titleName\": \"Fixture\"}}}")
|
||||
elseif(titleCase STREQUAL "malformed-json")
|
||||
file(WRITE ${titleRun}/app0/sce_sys/param.json "{")
|
||||
endif()
|
||||
add_test(NAME guest_convert_keycode_title_${titleCase} COMMAND guest_convert_keycode_title_tests WORKING_DIRECTORY ${titleRun})
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_convert_keycode_title_${titleCase} PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceConvertKeycode>")
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
@@ -1,12 +1,23 @@
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/ApplicationHeap.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
#include "prx/libSceConvertKeycode/ImeKeyboardType.hpp"
|
||||
#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp"
|
||||
#include <string_view>
|
||||
|
||||
extern "C" {
|
||||
int APS5_VABI sceUserServiceGetKeyboardType(int32_t userId, int32_t* type);
|
||||
int APS5_VABI sceUserServiceGetJapaneseInputType(int32_t userId, int32_t* type);
|
||||
int APS5_VABI sceUserServiceGetTraditionalChineseInputType(int32_t userId, int32_t* type);
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr int kErrInvalidAddress = static_cast<int>(0x80BC0031u);
|
||||
constexpr int kErrInvalidUserId = static_cast<int>(0x80BC0010u);
|
||||
constexpr std::array<std::uint16_t, 256> BaseVirtualKeys = {
|
||||
255, 255, 255, 255, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76,
|
||||
77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 49, 50,
|
||||
@@ -2393,14 +2404,26 @@ char16_t ConvertCharacter(std::uint16_t keycode, std::uint32_t state, std::uint3
|
||||
}
|
||||
return character;
|
||||
}
|
||||
bool Ps4Client() {
|
||||
try {
|
||||
const auto titleId = GetAppTitleId_nid_postfix();
|
||||
return ImeKeyboardType::IsPs4TitleId(std::string_view(titleId.value, strnlen(titleId.value, sizeof(titleId.value))));
|
||||
} catch (const std::runtime_error&) {
|
||||
return false;
|
||||
} catch (const std::length_error&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI sceConvertKeycodeGetImeKeyboardType(int32_t userId, uint32_t* type) {
|
||||
if (type == nullptr) return kErrInvalidAddress;
|
||||
if (userId < 0) return kErrInvalidUserId;
|
||||
throw std::runtime_error("sceConvertKeycodeGetImeKeyboardType: user keyboard language and variant are unavailable");
|
||||
const bool ps4Client = Ps4Client();
|
||||
const auto* process = static_cast<const std::byte*>(ApplicationProcessParameters_nid_no_patch());
|
||||
std::uint32_t sdkVersion;
|
||||
std::memcpy(&sdkVersion, process + 16, sizeof(sdkVersion));
|
||||
return ImeKeyboardType::Resolve(userId, type, sdkVersion, ps4Client, sceUserServiceGetKeyboardType, sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType);
|
||||
}
|
||||
int APS5_VABI sceConvertKeycodeGetVirtualKeycode(std::uint16_t keycode, std::uint32_t keyboardType, std::uint16_t* virtualKeycode) {
|
||||
if (virtualKeycode == nullptr) return kErrInvalidAddress;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#ifndef CORE_LIBS_PRX_LIBSCECONVERTKEYCODE_IMEKEYBOARDTYPE_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCECONVERTKEYCODE_IMEKEYBOARDTYPE_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
// sceConvertKeycodeGetImeKeyboardType: the UserService keyboard type (plus the Japanese and Traditional Chinese
|
||||
// input variants) to IME keyboard type mapping and the SDK compatibility rules, as specified by the repository
|
||||
// owner (see docs/dev/debt/libSceConvertKeycode.md). Header-only so the guest test drives every row and error path
|
||||
// with fake UserService getters.
|
||||
namespace ImeKeyboardType {
|
||||
|
||||
constexpr int kErrInvalidAddress = static_cast<int>(0x80BC0031u);
|
||||
constexpr int kErrInvalidUserId = static_cast<int>(0x80BC0010u);
|
||||
constexpr int kErrGeneric = static_cast<int>(0x80BC00FFu);
|
||||
constexpr int kUserServiceUnknownUser = static_cast<int>(0x80960105u);
|
||||
|
||||
constexpr std::int32_t kJapaneseKeyboard = 37;
|
||||
constexpr std::int32_t kTraditionalChineseKeyboard = 39;
|
||||
|
||||
// Index = UserService keyboard type - 16. Types 37 and 39 are resolved through their variants; 0 = no mapping.
|
||||
constexpr std::int32_t kFirstMappedType = 16;
|
||||
constexpr std::uint32_t kImeTypeByKeyboardType[] = {
|
||||
1, 2, 3, 5, 4, 6, 7, 8, 10, 11, 9, 12, 13, 14, 15, 16, 17, 18, 19, 20, // 16..35
|
||||
21, 0, 24, 0, 25, 30, 31, 33, 32, 34, 35, 36, 37, 38, // 36..49 (37 and 39 are variants)
|
||||
};
|
||||
constexpr std::int32_t kMappedTypeCount = static_cast<std::int32_t>(sizeof(kImeTypeByKeyboardType) / sizeof(kImeTypeByKeyboardType[0]));
|
||||
|
||||
constexpr std::uint32_t LookUp(std::int32_t keyboardType, std::int32_t japaneseVariant, std::int32_t traditionalChineseVariant) {
|
||||
if (keyboardType == kJapaneseKeyboard) {
|
||||
constexpr std::uint32_t japanese[] = {22, 23};
|
||||
return japaneseVariant >= 0 && japaneseVariant < 2 ? japanese[japaneseVariant] : 0;
|
||||
}
|
||||
if (keyboardType == kTraditionalChineseKeyboard) {
|
||||
constexpr std::uint32_t traditionalChinese[] = {26, 27, 28, 29, 39};
|
||||
return traditionalChineseVariant >= 0 && traditionalChineseVariant < 5 ? traditionalChinese[traditionalChineseVariant] : 0;
|
||||
}
|
||||
const std::int32_t index = keyboardType - kFirstMappedType;
|
||||
return index >= 0 && index < kMappedTypeCount ? kImeTypeByKeyboardType[index] : 0;
|
||||
}
|
||||
|
||||
// Applied after the lookup. 38 -> 4 below PS5 SDK 0x06000000 / PS4 SDK 0x10000000; 39 -> 29 below PS5 SDK 0x08000000
|
||||
// and for every PS4 client.
|
||||
constexpr std::uint32_t ApplySdkCompatibility(std::uint32_t imeType, std::uint32_t sdkVersion, bool ps4Client) {
|
||||
if (imeType == 38 && sdkVersion < (ps4Client ? 0x10000000u : 0x06000000u)) return 4;
|
||||
if (imeType == 39 && (ps4Client || sdkVersion < 0x08000000u)) return 29;
|
||||
return imeType;
|
||||
}
|
||||
|
||||
constexpr bool IsPs4TitleId(std::string_view titleId) {
|
||||
return titleId.substr(0, 4) == "CUSA";
|
||||
}
|
||||
|
||||
template <typename GetKeyboardType, typename GetJapaneseInputType, typename GetTraditionalChineseInputType>
|
||||
int Resolve(std::int32_t userId, std::uint32_t* type, std::uint32_t sdkVersion, bool ps4Client, GetKeyboardType getKeyboardType, GetJapaneseInputType getJapaneseInputType, GetTraditionalChineseInputType getTraditionalChineseInputType) {
|
||||
if (type == nullptr) return kErrInvalidAddress;
|
||||
std::int32_t keyboardType = 0;
|
||||
const int result = getKeyboardType(userId, &keyboardType);
|
||||
if (result == kUserServiceUnknownUser) return kErrInvalidUserId;
|
||||
if (result != 0) return kErrGeneric;
|
||||
std::int32_t japaneseVariant = 0;
|
||||
std::int32_t traditionalChineseVariant = 0;
|
||||
if (keyboardType == kJapaneseKeyboard && getJapaneseInputType(userId, &japaneseVariant) != 0) japaneseVariant = 0;
|
||||
if (keyboardType == kTraditionalChineseKeyboard && getTraditionalChineseInputType(userId, &traditionalChineseVariant) != 0) traditionalChineseVariant = 0;
|
||||
*type = ApplySdkCompatibility(LookUp(keyboardType, japaneseVariant, traditionalChineseVariant), sdkVersion, ps4Client);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace ImeKeyboardType
|
||||
|
||||
#endif
|
||||
@@ -31,6 +31,44 @@ constexpr int Failure = static_cast<int>(0x8EEE00FFu);
|
||||
constexpr int NoInterface = static_cast<int>(0x80000000u);
|
||||
constexpr int ValidationNotEnabled = static_cast<int>(0x80D13FFFu);
|
||||
|
||||
constexpr int CommandFailed = -1;
|
||||
constexpr std::uint32_t OpNop = 0x10;
|
||||
constexpr std::uint32_t NeoDrawFlags = 0xe0000000u;
|
||||
constexpr std::uint32_t AutoIndex = 2;
|
||||
|
||||
constexpr std::uint32_t Header(std::uint32_t opcode, std::uint32_t bodyWords, bool compute = false, bool predicated = false) {
|
||||
return 0xc0000000u | ((bodyWords - 1) << 16) | (opcode << 8) | (compute ? 2u : 0u) | (predicated ? 1u : 0u);
|
||||
}
|
||||
|
||||
std::uint32_t* Nop(std::uint32_t* at, std::uint32_t bodyWords) {
|
||||
at[0] = Header(OpNop, bodyWords);
|
||||
at[1] = 0;
|
||||
return at + 1 + bodyWords;
|
||||
}
|
||||
|
||||
constexpr std::uint32_t ShaderStageCount = 7;
|
||||
constexpr std::uint32_t StageVs = 2;
|
||||
constexpr std::uint32_t StageEs = 4;
|
||||
constexpr std::uint32_t StageLs = 6;
|
||||
constexpr std::uint32_t IndirectSgprBase[ShaderStageCount] = {0, 0, 0x4c, 0, 0xcc, 0, 0x14c};
|
||||
constexpr std::uint32_t DrawIndirectArgsBytes = 0x10;
|
||||
constexpr std::uint32_t DrawIndexedIndirectArgsBytes = 0x14;
|
||||
|
||||
std::uint32_t IndirectSgpr(std::uint32_t stage, std::uint32_t offset) {
|
||||
return offset == 0 ? 0 : (offset & 0xffffu) + IndirectSgprBase[stage];
|
||||
}
|
||||
|
||||
bool ValidIndirect(std::uint32_t stage, std::uint32_t vertexSgpr, std::uint32_t instanceSgpr) {
|
||||
return stage < ShaderStageCount && vertexSgpr < 0x10 && instanceSgpr < 0x10;
|
||||
}
|
||||
|
||||
bool IndexedIndirectStage(std::uint32_t stage) {
|
||||
return stage == StageVs || stage == StageEs || stage == StageLs;
|
||||
}
|
||||
|
||||
constexpr std::uint32_t OpSetUconfigReg = 0x79;
|
||||
constexpr std::uint32_t IaMultiVgtParam = 0x40000258u;
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
@@ -40,13 +78,28 @@ int APS5_VABI sceGnmBeginWorkload(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceGnmComputeWaitOnAddress(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
int APS5_VABI sceGnmComputeWaitOnAddress(std::uint32_t* cmdbuf, std::uint32_t size, std::uint64_t address, std::uint32_t mask, std::uint32_t compare, std::uint32_t reference) {
|
||||
if (!cmdbuf || size != 14) return CommandFailed;
|
||||
auto* at = Nop(cmdbuf, 3);
|
||||
const bool memory = address > 0xffff;
|
||||
at[0] = Header(0x3c, 6);
|
||||
at[1] = (memory ? 1u << 4 : 0u) | (compare & 7);
|
||||
at[2] = static_cast<std::uint32_t>(address & (memory ? 0xfffffffcu : 0xffffu));
|
||||
at[3] = static_cast<std::uint32_t>(address >> 32);
|
||||
at[4] = reference;
|
||||
at[5] = mask;
|
||||
at[6] = 10;
|
||||
Nop(at + 7, 2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceGnmComputeWaitSemaphore(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
int APS5_VABI sceGnmComputeWaitSemaphore(std::uint32_t* cmdbuf, std::uint32_t size, std::uint64_t semaphoreAddress, std::uint32_t flags) {
|
||||
if (!cmdbuf || size != 10) return CommandFailed;
|
||||
auto* at = Nop(cmdbuf, 3);
|
||||
at[0] = Header(0x39, 2);
|
||||
at[1] = static_cast<std::uint32_t>(semaphoreAddress);
|
||||
at[2] = (flags == 1 ? 0x100000u : 0u) - 0x20000000u + static_cast<std::uint32_t>(semaphoreAddress >> 32);
|
||||
Nop(at + 3, 2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -73,43 +126,96 @@ int APS5_VABI sceGnmDingDongForWorkload(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceGnmDispatchDirect(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
int APS5_VABI sceGnmDispatchDirect(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t threadsX, std::uint32_t threadsY, std::uint32_t threadsZ, std::uint32_t flags) {
|
||||
if (!cmdbuf || size != 9 || static_cast<std::int32_t>(threadsX | threadsY | threadsZ) < 0) return CommandFailed;
|
||||
cmdbuf[0] = Header(0x15, 4, true, flags & 1);
|
||||
cmdbuf[1] = threadsX;
|
||||
cmdbuf[2] = threadsY;
|
||||
cmdbuf[3] = threadsZ;
|
||||
cmdbuf[4] = (flags & 0x18) + 1;
|
||||
Nop(cmdbuf + 5, 3);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceGnmDispatchIndirect(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
int APS5_VABI sceGnmDispatchIndirect(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t dataOffset, std::uint32_t flags) {
|
||||
if (!cmdbuf || size != 7) return CommandFailed;
|
||||
cmdbuf[0] = Header(0x16, 2, true, flags & 1);
|
||||
cmdbuf[1] = dataOffset;
|
||||
cmdbuf[2] = (flags & 0x18) + 1;
|
||||
Nop(cmdbuf + 3, 3);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceGnmDispatchIndirectOnMec(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
int APS5_VABI sceGnmDispatchIndirectOnMec(std::uint32_t* cmdbuf, std::uint32_t size, std::uint64_t args, std::uint32_t modifier) {
|
||||
if (!cmdbuf || size != 8 || args == 0 || (args & 3) != 0) return CommandFailed;
|
||||
cmdbuf[0] = Header(0x16, 3, true, modifier & 1);
|
||||
cmdbuf[1] = static_cast<std::uint32_t>(args);
|
||||
cmdbuf[2] = static_cast<std::uint32_t>(args >> 32);
|
||||
cmdbuf[3] = (modifier & 0x18) | 1;
|
||||
cmdbuf[4] = Header(OpNop, 3);
|
||||
cmdbuf[5] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceGnmDrawIndex(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
int APS5_VABI sceGnmDrawIndex(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t indexCount, std::uint64_t indexAddress, std::uint32_t flags, std::uint32_t) {
|
||||
if (!cmdbuf || size != 10 || indexAddress == 0 || (indexAddress & 1) != 0 || (flags & 0x1ffffffeu) != 0) return CommandFailed;
|
||||
cmdbuf[0] = Header(0x27, 5);
|
||||
cmdbuf[1] = indexCount;
|
||||
cmdbuf[2] = static_cast<std::uint32_t>(indexAddress);
|
||||
cmdbuf[3] = static_cast<std::uint32_t>(indexAddress >> 32);
|
||||
cmdbuf[4] = indexCount;
|
||||
cmdbuf[5] = flags & NeoDrawFlags;
|
||||
Nop(cmdbuf + 6, 3);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceGnmDrawIndexAuto(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
int APS5_VABI sceGnmDrawIndexAuto(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t indexCount, std::uint32_t flags) {
|
||||
if (!cmdbuf || size != 7 || (flags & 0x1ffffffeu) != 0) return CommandFailed;
|
||||
cmdbuf[0] = Header(0x2d, 2);
|
||||
cmdbuf[1] = indexCount;
|
||||
cmdbuf[2] = (flags & NeoDrawFlags) | AutoIndex;
|
||||
Nop(cmdbuf + 3, 3);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceGnmDrawIndexIndirect(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
int APS5_VABI sceGnmDrawIndexIndirect(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t dataOffset, std::uint32_t stage, std::uint32_t vertexSgpr, std::uint32_t instanceSgpr, std::uint32_t flags) {
|
||||
if (!cmdbuf || size != 9 || !ValidIndirect(stage, vertexSgpr, instanceSgpr)) return CommandFailed;
|
||||
cmdbuf[0] = Header(0x25, 4, false, flags & 1);
|
||||
cmdbuf[1] = dataOffset;
|
||||
cmdbuf[2] = IndirectSgpr(stage, vertexSgpr);
|
||||
cmdbuf[3] = IndirectSgpr(stage, instanceSgpr);
|
||||
cmdbuf[4] = flags & NeoDrawFlags;
|
||||
Nop(cmdbuf + 5, 3);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceGnmDrawIndexIndirectCountMulti(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
int APS5_VABI sceGnmDrawIndexIndirectCountMulti(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t dataOffset, std::uint32_t maxCount, std::uint64_t countAddress, std::uint32_t stage, std::uint32_t vertexSgpr, std::uint32_t instanceSgpr, std::uint32_t flags) {
|
||||
if (!cmdbuf || size != 0x10 || vertexSgpr >= 0x10 || instanceSgpr >= 0x10 || !IndexedIndirectStage(stage)) return CommandFailed;
|
||||
auto* at = Nop(cmdbuf, 2);
|
||||
at[0] = Header(0x9d, 9, false, flags & 1);
|
||||
at[1] = dataOffset;
|
||||
at[2] = IndirectSgpr(stage, vertexSgpr);
|
||||
at[3] = IndirectSgpr(stage, instanceSgpr);
|
||||
at[4] = countAddress != 0 ? 1u << 30 : 0;
|
||||
at[5] = maxCount;
|
||||
at[6] = static_cast<std::uint32_t>(countAddress);
|
||||
at[7] = static_cast<std::uint32_t>(countAddress >> 32);
|
||||
at[8] = DrawIndexedIndirectArgsBytes;
|
||||
at[9] = flags & NeoDrawFlags;
|
||||
Nop(at + 10, 2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceGnmDrawIndexIndirectMulti(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
int APS5_VABI sceGnmDrawIndexIndirectMulti(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t dataOffset, std::uint32_t maxCount, std::uint32_t stage, std::uint32_t vertexSgpr, std::uint32_t instanceSgpr, std::uint32_t flags) {
|
||||
if (!cmdbuf || size != 11 || vertexSgpr >= 0x10 || instanceSgpr >= 0x10 || !IndexedIndirectStage(stage)) return CommandFailed;
|
||||
cmdbuf[0] = Header(0x38, 6, false, flags & 1);
|
||||
cmdbuf[1] = dataOffset;
|
||||
cmdbuf[2] = IndirectSgpr(stage, vertexSgpr);
|
||||
cmdbuf[3] = IndirectSgpr(stage, instanceSgpr);
|
||||
cmdbuf[4] = maxCount;
|
||||
cmdbuf[5] = DrawIndexedIndirectArgsBytes;
|
||||
cmdbuf[6] = flags & NeoDrawFlags;
|
||||
Nop(cmdbuf + 7, 3);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -118,23 +224,55 @@ int APS5_VABI sceGnmDrawIndexMultiInstanced(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceGnmDrawIndexOffset(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
int APS5_VABI sceGnmDrawIndexOffset(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t indexOffset, std::uint32_t indexCount, std::uint32_t flags) {
|
||||
if (!cmdbuf || size != 9) return CommandFailed;
|
||||
cmdbuf[0] = Header(0x35, 4, false, flags & 1);
|
||||
cmdbuf[1] = indexCount;
|
||||
cmdbuf[2] = indexOffset;
|
||||
cmdbuf[3] = indexCount;
|
||||
cmdbuf[4] = flags & NeoDrawFlags;
|
||||
Nop(cmdbuf + 5, 3);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceGnmDrawIndirect(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
int APS5_VABI sceGnmDrawIndirect(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t dataOffset, std::uint32_t stage, std::uint32_t vertexSgpr, std::uint32_t instanceSgpr, std::uint32_t flags) {
|
||||
if (!cmdbuf || size != 9 || !ValidIndirect(stage, vertexSgpr, instanceSgpr)) return CommandFailed;
|
||||
cmdbuf[0] = Header(0x24, 4, false, flags & 1);
|
||||
cmdbuf[1] = dataOffset;
|
||||
cmdbuf[2] = IndirectSgpr(stage, vertexSgpr);
|
||||
cmdbuf[3] = IndirectSgpr(stage, instanceSgpr);
|
||||
cmdbuf[4] = (flags & NeoDrawFlags) | AutoIndex;
|
||||
Nop(cmdbuf + 5, 3);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceGnmDrawIndirectCountMulti(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
int APS5_VABI sceGnmDrawIndirectCountMulti(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t dataOffset, std::uint32_t maxCount, std::uint64_t countAddress, std::uint32_t stage, std::uint32_t vertexSgpr, std::uint32_t instanceSgpr, std::uint32_t flags) {
|
||||
if (!cmdbuf || size != 0x10 || !ValidIndirect(stage, vertexSgpr, instanceSgpr)) return CommandFailed;
|
||||
auto* at = Nop(cmdbuf, 2);
|
||||
at[0] = Header(0x9c, 9, false, flags & 1);
|
||||
at[1] = dataOffset;
|
||||
at[2] = IndirectSgpr(stage, vertexSgpr);
|
||||
at[3] = IndirectSgpr(stage, instanceSgpr);
|
||||
at[4] = countAddress != 0 ? 1u << 30 : 0;
|
||||
at[5] = maxCount;
|
||||
at[6] = static_cast<std::uint32_t>(countAddress);
|
||||
at[7] = static_cast<std::uint32_t>(countAddress >> 32);
|
||||
at[8] = DrawIndirectArgsBytes;
|
||||
at[9] = (flags & NeoDrawFlags) | AutoIndex;
|
||||
Nop(at + 10, 2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceGnmDrawIndirectMulti(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
int APS5_VABI sceGnmDrawIndirectMulti(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t dataOffset, std::uint32_t maxCount, std::uint32_t stage, std::uint32_t vertexSgpr, std::uint32_t instanceSgpr, std::uint32_t flags) {
|
||||
if (!cmdbuf || size != 11 || !ValidIndirect(stage, vertexSgpr, instanceSgpr)) return CommandFailed;
|
||||
cmdbuf[0] = Header(0x2c, 6, false, flags & 1);
|
||||
cmdbuf[1] = dataOffset;
|
||||
cmdbuf[2] = IndirectSgpr(stage, vertexSgpr);
|
||||
cmdbuf[3] = IndirectSgpr(stage, instanceSgpr);
|
||||
cmdbuf[4] = maxCount;
|
||||
cmdbuf[5] = DrawIndirectArgsBytes;
|
||||
cmdbuf[6] = (flags & NeoDrawFlags) | AutoIndex;
|
||||
Nop(cmdbuf + 7, 3);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -321,8 +459,12 @@ int APS5_VABI sceGnmGpuPaDebugLeave(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceGnmInsertDingDongMarker(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
int APS5_VABI sceGnmInsertDingDongMarker(std::uint32_t* cmdbuf, std::uint32_t size) {
|
||||
if (!cmdbuf || size != 4) return CommandFailed;
|
||||
cmdbuf[0] = Header(OpNop, 3);
|
||||
cmdbuf[1] = 0;
|
||||
cmdbuf[2] = 0;
|
||||
cmdbuf[3] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -396,8 +538,11 @@ int APS5_VABI sceGnmRequestFlipAndSubmitDoneForWorkload(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceGnmResetVgtControl(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
int APS5_VABI sceGnmResetVgtControl(std::uint32_t* cmdbuf, std::uint32_t size) {
|
||||
if (!cmdbuf || size != 3) return CommandFailed;
|
||||
cmdbuf[0] = Header(OpSetUconfigReg, 2);
|
||||
cmdbuf[1] = IaMultiVgtParam;
|
||||
cmdbuf[2] = 0x6d007fu;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -469,8 +614,16 @@ int APS5_VABI sceGnmSetSpiEnableSqCountersForUnitInstance(void) {
|
||||
return Failure;
|
||||
}
|
||||
|
||||
int APS5_VABI sceGnmSetVgtControl(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
int APS5_VABI sceGnmSetVgtControl(std::uint32_t* cmdbuf, std::uint32_t size, std::uint32_t primGroupSizeMinusOne, std::uint32_t partialVsWaveMode, std::uint32_t switchOnlyOnEop) {
|
||||
if (!cmdbuf || size != 3 || primGroupSizeMinusOne >= 0x100 || (switchOnlyOnEop | partialVsWaveMode) >= 2) return CommandFailed;
|
||||
const std::uint32_t switchOnEop = static_cast<std::uint32_t>(switchOnlyOnEop != 0) << 20;
|
||||
const std::uint32_t switchOnEoi = static_cast<std::uint32_t>(switchOnlyOnEop == 0) << 19;
|
||||
const std::uint32_t value = switchOnlyOnEop != 0
|
||||
? ((partialVsWaveMode & 1) << 16) | primGroupSizeMinusOne | switchOnEop | switchOnEoi | 0x40000u
|
||||
: (primGroupSizeMinusOne & 0x1cffffu) | switchOnEop | switchOnEoi | 0x50000u;
|
||||
cmdbuf[0] = Header(OpSetUconfigReg, 2);
|
||||
cmdbuf[1] = IaMultiVgtParam;
|
||||
cmdbuf[2] = value | 0x600000u;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp)
|
||||
target_include_directories(libSceLibreSsl PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(libSceLibreSsl PRIVATE mbedcrypto)
|
||||
configure_windows_unwind(libSceLibreSsl)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
@@ -13,4 +14,15 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(guest_libressl_digest PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibreSsl>")
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_libressl_des_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslDes.cpp)
|
||||
target_include_directories(guest_libressl_des_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_libressl_des_tests PRIVATE libSceLibreSsl libc)
|
||||
configure_windows_unwind(guest_libressl_des_tests)
|
||||
add_test(NAME guest_libressl_des COMMAND guest_libressl_des_tests)
|
||||
set_tests_properties(guest_libressl_des PROPERTIES TIMEOUT 20)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_libressl_des PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibreSsl>")
|
||||
endif()
|
||||
endif()
|
||||
@@ -1,7 +1,17 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
#include "mbedtls/des.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t DesScheduleBytes = 128;
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
@@ -301,19 +311,28 @@ int APS5_VABI CRYPTO_set_mem_functions_nid_postfix(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI DES_ecb_encrypt_nid_postfix(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
void APS5_VABI DES_ecb_encrypt_nid_postfix(const unsigned char* input, unsigned char* output, const unsigned char* schedule, int enc) {
|
||||
if (input == nullptr || output == nullptr || schedule == nullptr) APS5_INVALID_ARG_EX;
|
||||
mbedtls_des_context context;
|
||||
mbedtls_des_init(&context);
|
||||
unsigned char block[MBEDTLS_DES_KEY_SIZE];
|
||||
std::memcpy(block, input, sizeof(block));
|
||||
int status = enc ? mbedtls_des_setkey_enc(&context, schedule) : mbedtls_des_setkey_dec(&context, schedule);
|
||||
if (status == 0) status = mbedtls_des_crypt_ecb(&context, block, output);
|
||||
mbedtls_des_free(&context);
|
||||
if (status != 0) throw std::runtime_error("DES_ecb_encrypt: mbedtls failed");
|
||||
}
|
||||
|
||||
int APS5_VABI DES_set_key_nid_postfix(const unsigned char* key, unsigned char* schedule) {
|
||||
if (key == nullptr || schedule == nullptr) APS5_INVALID_ARG_EX;
|
||||
std::memset(schedule, 0, DesScheduleBytes);
|
||||
std::memcpy(schedule, key, MBEDTLS_DES_KEY_SIZE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI DES_set_key_nid_postfix(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI DES_set_odd_parity_nid_postfix(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
void APS5_VABI DES_set_odd_parity_nid_postfix(unsigned char* key) {
|
||||
if (key == nullptr) APS5_INVALID_ARG_EX;
|
||||
mbedtls_des_key_set_parity(key);
|
||||
}
|
||||
|
||||
int APS5_VABI DTLSv1_client_method_nid_postfix(void) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
add_library(libSceUserService SHARED EXCLUDE_FROM_ALL Export.cpp)
|
||||
target_include_directories(libSceUserService PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(libSceUserService PRIVATE libSceSystemService)
|
||||
configure_windows_unwind(libSceUserService)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(guest_user_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserService.cpp)
|
||||
target_include_directories(guest_user_service_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_user_service_tests PRIVATE libSceUserService)
|
||||
target_link_libraries(guest_user_service_tests PRIVATE libSceUserService libSceSystemService libc)
|
||||
configure_windows_unwind(guest_user_service_tests)
|
||||
add_test(NAME guest_user_service COMMAND guest_user_service_tests)
|
||||
if(WIN32)
|
||||
@@ -17,7 +18,7 @@ endif()
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(guest_user_service_user_name_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserServiceUserName.cpp)
|
||||
target_include_directories(guest_user_service_user_name_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_user_service_user_name_tests PRIVATE libSceUserService)
|
||||
target_link_libraries(guest_user_service_user_name_tests PRIVATE libSceUserService libSceSystemService)
|
||||
add_test(NAME guest_user_service_user_name COMMAND guest_user_service_user_name_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_user_service_user_name PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
@@ -28,7 +29,7 @@ endif()
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(guest_user_service_lifecycle_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserServiceLifecycle.cpp)
|
||||
target_include_directories(guest_user_service_lifecycle_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_user_service_lifecycle_tests PRIVATE libSceUserService)
|
||||
target_link_libraries(guest_user_service_lifecycle_tests PRIVATE libSceUserService libSceSystemService)
|
||||
add_test(NAME guest_user_service_lifecycle COMMAND guest_user_service_lifecycle_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_user_service_lifecycle PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
|
||||
@@ -20,6 +20,40 @@ static int ReportAccessibilitySetting(int user_id, int32_t* setting, int32_t val
|
||||
return USER_SERVICE_OK;
|
||||
}
|
||||
|
||||
|
||||
extern "C" int APS5_VABI sceSystemServiceParamGetInt(int param_id, int* value);
|
||||
|
||||
// Per-user keyboard settings. Nothing in AnyPS5 stores a keyboard type or input variant, so every known user reports
|
||||
// the keyboard type of the configured system language and 0 for the input variants (see
|
||||
// docs/dev/debt/libSceUserService.md).
|
||||
static int32_t KeyboardTypeForSystemLanguage() {
|
||||
constexpr int param_id_lang = 1;
|
||||
constexpr int32_t fallback = 20;
|
||||
constexpr int32_t keyboard_types[] = {37, 20, 26, 21, 17, 28, 29, 33, 34, 38, 39, 40, 23, 35, 16,
|
||||
30, 31, 32, 19, 36, 22, 41, 25, 44, 48, 43, 47, 42, 46, 45};
|
||||
int language = -1;
|
||||
if (sceSystemServiceParamGetInt(param_id_lang, &language) != 0 || language < 0 ||
|
||||
language >= static_cast<int>(sizeof(keyboard_types) / sizeof(keyboard_types[0]))) {
|
||||
return fallback;
|
||||
}
|
||||
return keyboard_types[language];
|
||||
}
|
||||
|
||||
static int32_t NoInputVariant() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int ReportKeyboardSetting(int user_id, int32_t* type, int32_t (*value)()) {
|
||||
if (type == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != USER_SERVICE_INITIAL_USER_ID) {
|
||||
return USER_SERVICE_ERROR_UNKNOWN_USER;
|
||||
}
|
||||
*type = value();
|
||||
return USER_SERVICE_OK;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI sceUserServiceGetAccessibilityChatTranscription(int user_id, int32_t* chat_transcription) {
|
||||
@@ -218,6 +252,18 @@ int APS5_VABI sceUserServiceGetUserColor(int user_id, int* color) {
|
||||
return USER_SERVICE_OK;
|
||||
}
|
||||
|
||||
int APS5_VABI sceUserServiceGetKeyboardType(int user_id, int32_t* type) {
|
||||
return ReportKeyboardSetting(user_id, type, KeyboardTypeForSystemLanguage);
|
||||
}
|
||||
|
||||
int APS5_VABI sceUserServiceGetJapaneseInputType(int user_id, int32_t* type) {
|
||||
return ReportKeyboardSetting(user_id, type, NoInputVariant);
|
||||
}
|
||||
|
||||
int APS5_VABI sceUserServiceGetTraditionalChineseInputType(int user_id, int32_t* type) {
|
||||
return ReportKeyboardSetting(user_id, type, NoInputVariant);
|
||||
}
|
||||
|
||||
int APS5_VABI sceUserServiceGetNpAccountId(int user_id, uint64_t* account_id) {
|
||||
if (account_id == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include "ProcessParametersFixture.hpp"
|
||||
#include "prx/libSceConvertKeycode/ImeKeyboardType.hpp"
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <initializer_list>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
extern "C" {
|
||||
int APS5_VABI sceConvertKeycodeGetVirtualKeycode(std::uint16_t, std::uint32_t, std::uint16_t*);
|
||||
@@ -13,7 +20,111 @@ int APS5_VABI sceConvertKeycodeGetCharacter(std::uint16_t, std::uint32_t, std::u
|
||||
|
||||
static void Require(bool value) { if (!value) std::abort(); }
|
||||
|
||||
static void SetLanguage(const char* value) {
|
||||
#ifdef _WIN32
|
||||
Require(_putenv_s("ANYPS5_LANGUAGE", value != nullptr ? value : "") == 0);
|
||||
#else
|
||||
Require((value != nullptr ? ::setenv("ANYPS5_LANGUAGE", value, 1) : ::unsetenv("ANYPS5_LANGUAGE")) == 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct FakeUser {
|
||||
int keyboardResult = 0;
|
||||
std::int32_t keyboardType = 0;
|
||||
int japaneseResult = 0;
|
||||
std::int32_t japaneseVariant = 0;
|
||||
int chineseResult = 0;
|
||||
std::int32_t chineseVariant = 0;
|
||||
int calls = 0;
|
||||
};
|
||||
|
||||
int Resolve(const FakeUser& user, std::uint32_t* type, std::uint32_t sdkVersion = 0xFFFFFFFFu, bool ps4Client = false) {
|
||||
return ImeKeyboardType::Resolve(
|
||||
7, type, sdkVersion, ps4Client,
|
||||
[&](std::int32_t id, std::int32_t* value) { Require(id == 7); *value = user.keyboardType; return user.keyboardResult; },
|
||||
[&](std::int32_t id, std::int32_t* value) { Require(id == 7); *value = user.japaneseVariant; return user.japaneseResult; },
|
||||
[&](std::int32_t id, std::int32_t* value) { Require(id == 7); *value = user.chineseVariant; return user.chineseResult; });
|
||||
}
|
||||
|
||||
std::uint32_t Map(std::int32_t keyboardType, std::int32_t japanese = 0, std::int32_t chinese = 0, std::uint32_t sdkVersion = 0xFFFFFFFFu, bool ps4Client = false) {
|
||||
FakeUser user;
|
||||
user.keyboardType = keyboardType;
|
||||
user.japaneseVariant = japanese;
|
||||
user.chineseVariant = chinese;
|
||||
std::uint32_t type = 0xFFFFFFFFu;
|
||||
Require(Resolve(user, &type, sdkVersion, ps4Client) == 0);
|
||||
return type;
|
||||
}
|
||||
|
||||
void RequireImeKeyboardTypeMapping() {
|
||||
// The owner's table: UserService keyboard type -> IME keyboard type.
|
||||
constexpr std::pair<int, int> table[] = {
|
||||
{16, 1}, {17, 2}, {18, 3}, {19, 5}, {20, 4}, {21, 6}, {22, 7}, {23, 8}, {24, 10}, {25, 11}, {26, 9}, {27, 12},
|
||||
{28, 13}, {29, 14}, {30, 15}, {31, 16}, {32, 17}, {33, 18}, {34, 19}, {35, 20}, {36, 21}, {38, 24}, {40, 25},
|
||||
{41, 30}, {42, 31}, {43, 33}, {44, 32}, {45, 34}, {46, 35}, {47, 36}, {48, 37}, {49, 38}};
|
||||
for (const auto& [keyboard, ime] : table) {
|
||||
// 49 -> 38 is compatibility-sensitive; the table row is checked on a new SDK, the rule separately below.
|
||||
Require(Map(keyboard) == static_cast<std::uint32_t>(ime));
|
||||
}
|
||||
// Unmatched keyboard types succeed with IME type 0 (including 0, the gaps, and out-of-range values).
|
||||
for (int keyboard : {-1, 0, 1, 15, 50, 51, 1000, 0x7FFFFFFF, static_cast<int>(0x80000000u)}) Require(Map(keyboard) == 0);
|
||||
|
||||
// Type 37: Japanese input variant; anything else is unmatched.
|
||||
Require(Map(37, 0) == 22 && Map(37, 1) == 23);
|
||||
Require(Map(37, 2) == 0 && Map(37, -1) == 0);
|
||||
Require(Map(37, 0, 4) == 22); // the Traditional Chinese variant is ignored for 37
|
||||
// Type 39: Traditional Chinese input variant.
|
||||
Require(Map(39, 0, 0) == 26 && Map(39, 0, 1) == 27 && Map(39, 0, 2) == 28 && Map(39, 0, 3) == 29 && Map(39, 0, 4) == 39);
|
||||
Require(Map(39, 0, 5) == 0 && Map(39, 0, -1) == 0);
|
||||
Require(Map(39, 1, 0) == 26); // the Japanese variant is ignored for 39
|
||||
|
||||
// Variant query failure falls back to variant 0 and still succeeds; other types never query the variants.
|
||||
FakeUser user;
|
||||
std::uint32_t type = 0xFFFFFFFFu;
|
||||
user.keyboardType = 37; user.japaneseResult = static_cast<int>(0x80960001u); user.japaneseVariant = 1;
|
||||
Require(Resolve(user, &type) == 0 && type == 22);
|
||||
user = {};
|
||||
type = 0xFFFFFFFFu;
|
||||
user.keyboardType = 39; user.chineseResult = static_cast<int>(0x80960001u); user.chineseVariant = 4;
|
||||
Require(Resolve(user, &type) == 0 && type == 26);
|
||||
user = {};
|
||||
type = 0xFFFFFFFFu;
|
||||
user.keyboardType = 20; user.japaneseResult = 1; user.chineseResult = 1;
|
||||
Require(Resolve(user, &type) == 0 && type == 4);
|
||||
|
||||
// SDK compatibility, applied after the lookup. IME 38 (from 49): PS5 SDK < 0x06000000, PS4 SDK < 0x10000000 -> 4.
|
||||
Require(Map(49, 0, 0, 0x05FFFFFFu, false) == 4);
|
||||
Require(Map(49, 0, 0, 0x06000000u, false) == 38);
|
||||
Require(Map(49, 0, 0, 0x0FFFFFFFu, true) == 4);
|
||||
Require(Map(49, 0, 0, 0x10000000u, true) == 38);
|
||||
// IME 39 (from 39 variant 4): PS5 SDK < 0x08000000 and every PS4 client -> 29.
|
||||
Require(Map(39, 0, 4, 0x07FFFFFFu, false) == 29);
|
||||
Require(Map(39, 0, 4, 0x08000000u, false) == 39);
|
||||
Require(Map(39, 0, 4, 0xFFFFFFFFu, true) == 29);
|
||||
// Other IME types are never rewritten.
|
||||
Require(Map(20, 0, 0, 0, false) == 4 && Map(36, 0, 0, 0, true) == 21 && Map(39, 0, 3, 0, true) == 29);
|
||||
|
||||
// Errors leave the output untouched.
|
||||
type = 0x1234u;
|
||||
user = {};
|
||||
Require(ImeKeyboardType::Resolve(7, nullptr, 0, false, [](std::int32_t, std::int32_t*) { Require(false); return 0; }, [](std::int32_t, std::int32_t*) { return 0; }, [](std::int32_t, std::int32_t*) { return 0; }) == static_cast<int>(0x80BC0031u));
|
||||
user.keyboardResult = static_cast<int>(0x80960105u);
|
||||
Require(Resolve(user, &type) == static_cast<int>(0x80BC0010u) && type == 0x1234u);
|
||||
for (int other : {1, -1, static_cast<int>(0x80960002u), static_cast<int>(0x80960005u), static_cast<int>(0x80BC0010u)}) {
|
||||
user.keyboardResult = other;
|
||||
Require(Resolve(user, &type) == static_cast<int>(0x80BC00FFu) && type == 0x1234u);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
static std::array<std::byte, 64> process{};
|
||||
Require(ProcessParametersFixture::Install(process.data(), process.size()));
|
||||
constexpr std::uint32_t sdkVersion = 0x09000000u;
|
||||
std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion));
|
||||
constexpr int invalidAddress = static_cast<int>(0x80BC0031u);
|
||||
constexpr int invalidUserId = static_cast<int>(0x80BC0010u);
|
||||
|
||||
@@ -41,18 +152,33 @@ int main() {
|
||||
Require(rejected && virtualKey == 0xABCD);
|
||||
}
|
||||
|
||||
// The exported function with the real UserService: unknown users fail, the initial user reports the US-English default (keyboard type 20 -> IME 4).
|
||||
std::uint32_t type = 0xFFFFFFFFu;
|
||||
Require(sceConvertKeycodeGetImeKeyboardType(-1, &type) == invalidUserId);
|
||||
Require(sceConvertKeycodeGetImeKeyboardType(0, &type) == invalidUserId);
|
||||
Require(type == 0xFFFFFFFFu);
|
||||
|
||||
bool rejected = false;
|
||||
try {
|
||||
sceConvertKeycodeGetImeKeyboardType(0, &type);
|
||||
} catch (const std::runtime_error&) {
|
||||
rejected = true;
|
||||
SetLanguage(nullptr);
|
||||
Require(sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == 4);
|
||||
constexpr std::uint32_t imeTypeByLanguage[] = {22, 4, 9, 6, 2, 13, 14, 18, 19, 24, 26, 25, 8, 20, 1, 15, 16, 17, 5, 21, 7, 30, 11, 32, 37, 33, 36, 31, 35, 34};
|
||||
for (int language = 0; language < 30; ++language) {
|
||||
SetLanguage(std::to_string(language).c_str());
|
||||
type = 0xFFFFFFFFu;
|
||||
Require(sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == imeTypeByLanguage[language]);
|
||||
}
|
||||
Require(rejected && type == 0xFFFFFFFFu);
|
||||
SetLanguage("30");
|
||||
Require(sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == 4);
|
||||
SetLanguage(nullptr);
|
||||
|
||||
Require(ImeKeyboardType::IsPs4TitleId("CUSA00000"));
|
||||
Require(ImeKeyboardType::IsPs4TitleId("CUSA"));
|
||||
Require(!ImeKeyboardType::IsPs4TitleId("PPSA00000"));
|
||||
Require(!ImeKeyboardType::IsPs4TitleId(""));
|
||||
Require(!ImeKeyboardType::IsPs4TitleId("CUS"));
|
||||
Require(!ImeKeyboardType::IsPs4TitleId("cusa00000"));
|
||||
Require(!ImeKeyboardType::IsPs4TitleId("XCUSA0000"));
|
||||
Require(!ImeKeyboardType::IsPs4TitleId("HOMEBREW1"));
|
||||
|
||||
RequireImeKeyboardTypeMapping();
|
||||
Require(sceConvertKeycodeGetCharacter(4, 0, 0, nullptr) == invalidAddress);
|
||||
char16_t character = u'?';
|
||||
Require(sceConvertKeycodeGetCharacter(4, 0, 0, &character) == static_cast<int>(0x80BC001Du));
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#include "SceTypes.hpp"
|
||||
#include "ProcessParametersFixture.hpp"
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
extern "C" {
|
||||
int APS5_VABI sceConvertKeycodeGetImeKeyboardType(std::int32_t, std::uint32_t*);
|
||||
}
|
||||
|
||||
int main() {
|
||||
#ifdef _WIN32
|
||||
if (_putenv_s("ANYPS5_LANGUAGE", "") != 0) return 2;
|
||||
#else
|
||||
if (::unsetenv("ANYPS5_LANGUAGE") != 0) return 2;
|
||||
#endif
|
||||
static std::array<std::byte, 64> process{};
|
||||
constexpr std::uint32_t sdkVersion = 0x09000000u;
|
||||
std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion));
|
||||
if (!ProcessParametersFixture::Install(process.data(), process.size())) return 2;
|
||||
std::uint32_t type = 0xFFFFFFFFu;
|
||||
return sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == 4 ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <initializer_list>
|
||||
|
||||
extern "C" {
|
||||
int APS5_VABI sceGnmInsertDingDongMarker(std::uint32_t*, std::uint32_t);
|
||||
int APS5_VABI sceGnmDrawIndexAuto(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t);
|
||||
int APS5_VABI sceGnmDrawIndex(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint64_t, std::uint32_t, std::uint32_t);
|
||||
int APS5_VABI sceGnmDrawIndexOffset(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t);
|
||||
int APS5_VABI sceGnmDispatchDirect(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t);
|
||||
int APS5_VABI sceGnmDispatchIndirect(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t);
|
||||
int APS5_VABI sceGnmDispatchIndirectOnMec(std::uint32_t*, std::uint32_t, std::uint64_t, std::uint32_t);
|
||||
int APS5_VABI sceGnmDrawIndirect(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t);
|
||||
int APS5_VABI sceGnmDrawIndexIndirect(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t);
|
||||
int APS5_VABI sceGnmDrawIndirectMulti(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t);
|
||||
int APS5_VABI sceGnmDrawIndexIndirectMulti(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t);
|
||||
int APS5_VABI sceGnmDrawIndirectCountMulti(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t);
|
||||
int APS5_VABI sceGnmDrawIndexIndirectCountMulti(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t);
|
||||
int APS5_VABI sceGnmComputeWaitSemaphore(std::uint32_t*, std::uint32_t, std::uint64_t, std::uint32_t);
|
||||
int APS5_VABI sceGnmComputeWaitOnAddress(std::uint32_t*, std::uint32_t, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t);
|
||||
int APS5_VABI sceGnmSetVgtControl(std::uint32_t*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t);
|
||||
int APS5_VABI sceGnmResetVgtControl(std::uint32_t*, std::uint32_t);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t Untouched = 0xa5a5a5a5u;
|
||||
constexpr std::uint32_t U = Untouched;
|
||||
|
||||
std::uint32_t g_words[16];
|
||||
|
||||
void Require(bool value) { if (!value) std::abort(); }
|
||||
|
||||
std::uint32_t* Fresh() {
|
||||
for (auto& word : g_words) word = Untouched;
|
||||
return g_words;
|
||||
}
|
||||
|
||||
void RequireWords(std::initializer_list<std::uint32_t> expected) {
|
||||
std::size_t at = 0;
|
||||
for (const auto word : expected) Require(g_words[at++] == word);
|
||||
for (; at < 16; ++at) Require(g_words[at] == Untouched);
|
||||
}
|
||||
|
||||
void RequireRefused(int result) {
|
||||
Require(result == -1);
|
||||
RequireWords({});
|
||||
}
|
||||
|
||||
void CheckDirectDraws() {
|
||||
Require(sceGnmInsertDingDongMarker(Fresh(), 4) == 0);
|
||||
RequireWords({0xc0021000u, 0, 0, 0});
|
||||
RequireRefused(sceGnmInsertDingDongMarker(Fresh(), 3));
|
||||
Require(sceGnmInsertDingDongMarker(nullptr, 4) == -1);
|
||||
|
||||
Require(sceGnmDrawIndexAuto(Fresh(), 7, 36, 0xe0000001u) == 0);
|
||||
RequireWords({0xc0012d00u, 36, 0xe0000002u, 0xc0021000u, 0, U, U});
|
||||
RequireRefused(sceGnmDrawIndexAuto(Fresh(), 7, 36, 2));
|
||||
RequireRefused(sceGnmDrawIndexAuto(Fresh(), 6, 36, 0));
|
||||
|
||||
Require(sceGnmDrawIndex(Fresh(), 10, 99, 0x123456789abcull, 0x20000000u, 0) == 0);
|
||||
RequireWords({0xc0042700u, 99, 0x56789abcu, 0x1234, 99, 0x20000000u, 0xc0021000u, 0, U, U});
|
||||
RequireRefused(sceGnmDrawIndex(Fresh(), 10, 99, 0x1001, 0, 0));
|
||||
RequireRefused(sceGnmDrawIndex(Fresh(), 10, 99, 0, 0, 0));
|
||||
RequireRefused(sceGnmDrawIndex(Fresh(), 10, 99, 0x1000, 0x10, 0));
|
||||
|
||||
Require(sceGnmDrawIndexOffset(Fresh(), 9, 4, 12, 0x40000001u) == 0);
|
||||
RequireWords({0xc0033501u, 12, 4, 12, 0x40000000u, 0xc0021000u, 0, U, U});
|
||||
RequireRefused(sceGnmDrawIndexOffset(Fresh(), 10, 4, 12, 0));
|
||||
}
|
||||
|
||||
void CheckDispatches() {
|
||||
Require(sceGnmDispatchDirect(Fresh(), 9, 8, 4, 2, 0x19) == 0);
|
||||
RequireWords({0xc0031503u, 8, 4, 2, 0x19, 0xc0021000u, 0, U, U});
|
||||
RequireRefused(sceGnmDispatchDirect(Fresh(), 9, 0x80000000u, 1, 1, 0));
|
||||
RequireRefused(sceGnmDispatchDirect(Fresh(), 8, 1, 1, 1, 0));
|
||||
|
||||
Require(sceGnmDispatchIndirect(Fresh(), 7, 0x40, 0x8) == 0);
|
||||
RequireWords({0xc0011602u, 0x40, 9, 0xc0021000u, 0, U, U});
|
||||
RequireRefused(sceGnmDispatchIndirect(Fresh(), 9, 0x40, 0));
|
||||
|
||||
Require(sceGnmDispatchIndirectOnMec(Fresh(), 8, 0x700001000ull, 0x19) == 0);
|
||||
RequireWords({0xc0021603u, 0x1000, 7, 0x19, 0xc0021000u, 0});
|
||||
RequireRefused(sceGnmDispatchIndirectOnMec(Fresh(), 8, 0x700001002ull, 0));
|
||||
RequireRefused(sceGnmDispatchIndirectOnMec(Fresh(), 8, 0, 0));
|
||||
}
|
||||
|
||||
void CheckIndirectDraws() {
|
||||
Require(sceGnmDrawIndirect(Fresh(), 9, 0x20, 2, 3, 5, 0x20000001u) == 0);
|
||||
RequireWords({0xc0032401u, 0x20, 0x4f, 0x51, 0x20000002u, 0xc0021000u, 0, U, U});
|
||||
RequireRefused(sceGnmDrawIndirect(Fresh(), 9, 0x20, 7, 0, 0, 0));
|
||||
RequireRefused(sceGnmDrawIndirect(Fresh(), 9, 0x20, 2, 0x10, 0, 0));
|
||||
|
||||
Require(sceGnmDrawIndexIndirect(Fresh(), 9, 0x60, 4, 1, 0, 0x1) == 0);
|
||||
RequireWords({0xc0032501u, 0x60, 0xcd, 0, 0, 0xc0021000u, 0, U, U});
|
||||
RequireRefused(sceGnmDrawIndexIndirect(Fresh(), 9, 0x60, 4, 0, 0x10, 0));
|
||||
|
||||
Require(sceGnmDrawIndirectMulti(Fresh(), 11, 0x30, 6, 6, 2, 0, 0x80000000u) == 0);
|
||||
RequireWords({0xc0052c00u, 0x30, 0x14e, 0, 6, 0x10, 0x80000002u, 0xc0021000u, 0, U, U});
|
||||
RequireRefused(sceGnmDrawIndirectMulti(Fresh(), 9, 0x30, 6, 6, 2, 0, 0));
|
||||
|
||||
Require(sceGnmDrawIndexIndirectMulti(Fresh(), 11, 0x30, 6, 2, 0, 1, 0x60000000u) == 0);
|
||||
RequireWords({0xc0053800u, 0x30, 0, 0x4d, 6, 0x14, 0x60000000u, 0xc0021000u, 0, U, U});
|
||||
RequireRefused(sceGnmDrawIndexIndirectMulti(Fresh(), 11, 0x30, 6, 0, 0, 0, 0));
|
||||
RequireRefused(sceGnmDrawIndexIndirectMulti(Fresh(), 11, 0x30, 6, 3, 0, 0, 0));
|
||||
|
||||
Require(sceGnmDrawIndirectCountMulti(Fresh(), 16, 0, 5, 0x200000040ull, 2, 1, 1, 1) == 0);
|
||||
RequireWords({0xc0011000u, 0, U, 0xc0089c01u, 0, 0x4d, 0x4d, 0x40000000u, 5, 0x40, 2, 0x10, 2, 0xc0011000u, 0, U});
|
||||
RequireRefused(sceGnmDrawIndirectCountMulti(Fresh(), 15, 0, 5, 0, 2, 1, 1, 0));
|
||||
|
||||
Require(sceGnmDrawIndexIndirectCountMulti(Fresh(), 16, 0x14, 3, 0, 6, 0, 0, 0xe0000000u) == 0);
|
||||
RequireWords({0xc0011000u, 0, U, 0xc0089d00u, 0x14, 0, 0, 0, 3, 0, 0, 0x14, 0xe0000000u, 0xc0011000u, 0, U});
|
||||
RequireRefused(sceGnmDrawIndexIndirectCountMulti(Fresh(), 16, 0, 3, 0, 5, 0, 0, 0));
|
||||
}
|
||||
|
||||
void CheckComputeWaits() {
|
||||
Require(sceGnmComputeWaitSemaphore(Fresh(), 10, 0x123456780ull, 1) == 0);
|
||||
RequireWords({0xc0021000u, 0, U, U, 0xc0013900u, 0x23456780u, 0xe0100001u, 0xc0011000u, 0, U});
|
||||
Require(sceGnmComputeWaitSemaphore(Fresh(), 10, 0x123456780ull, 0) == 0);
|
||||
Require(g_words[6] == 0xe0000001u);
|
||||
RequireRefused(sceGnmComputeWaitSemaphore(Fresh(), 9, 0, 0));
|
||||
|
||||
Require(sceGnmComputeWaitOnAddress(Fresh(), 14, 0x500001003ull, 0xff, 0xb, 7) == 0);
|
||||
RequireWords({0xc0021000u, 0, U, U, 0xc0053c00u, 0x13, 0x1000, 5, 7, 0xff, 10, 0xc0011000u, 0, U});
|
||||
Require(sceGnmComputeWaitOnAddress(Fresh(), 14, 0x1234, 1, 3, 0) == 0);
|
||||
RequireWords({0xc0021000u, 0, U, U, 0xc0053c00u, 3, 0x1234, 0, 0, 1, 10, 0xc0011000u, 0, U});
|
||||
RequireRefused(sceGnmComputeWaitOnAddress(Fresh(), 13, 0x1234, 1, 3, 0));
|
||||
}
|
||||
|
||||
void CheckVgtControl() {
|
||||
Require(sceGnmSetVgtControl(Fresh(), 3, 0x7f, 1, 1) == 0);
|
||||
RequireWords({0xc0017900u, 0x40000258u, 0x75007fu});
|
||||
Require(sceGnmSetVgtControl(Fresh(), 3, 0x10, 1, 0) == 0);
|
||||
RequireWords({0xc0017900u, 0x40000258u, 0x6d0010u});
|
||||
RequireRefused(sceGnmSetVgtControl(Fresh(), 3, 0x100, 0, 0));
|
||||
RequireRefused(sceGnmSetVgtControl(Fresh(), 3, 0, 2, 0));
|
||||
RequireRefused(sceGnmSetVgtControl(Fresh(), 3, 0, 0, 2));
|
||||
RequireRefused(sceGnmSetVgtControl(Fresh(), 4, 0, 0, 0));
|
||||
|
||||
Require(sceGnmResetVgtControl(Fresh(), 3) == 0);
|
||||
RequireWords({0xc0017900u, 0x40000258u, 0x6d007fu});
|
||||
RequireRefused(sceGnmResetVgtControl(Fresh(), 2));
|
||||
Require(sceGnmResetVgtControl(nullptr, 3) == -1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
CheckDirectDraws();
|
||||
CheckDispatches();
|
||||
CheckIndirectDraws();
|
||||
CheckComputeWaits();
|
||||
CheckVgtControl();
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
|
||||
using DesBlock = std::array<unsigned char, 8>;
|
||||
using DesSchedule = std::array<unsigned char, 128>;
|
||||
|
||||
extern "C" {
|
||||
void APS5_VABI DES_ecb_encrypt_nid_postfix(const unsigned char*, unsigned char*, const unsigned char*, int);
|
||||
int APS5_VABI DES_set_key_nid_postfix(const unsigned char*, unsigned char*);
|
||||
void APS5_VABI DES_set_odd_parity_nid_postfix(unsigned char*);
|
||||
}
|
||||
|
||||
static void Require(bool value) { if (!value) std::abort(); }
|
||||
|
||||
template<typename TAction>
|
||||
static void RequireThrows(TAction action) {
|
||||
try { action(); } catch (const std::invalid_argument&) { return; }
|
||||
std::abort();
|
||||
}
|
||||
|
||||
static DesBlock Block(std::uint64_t value) {
|
||||
DesBlock block{};
|
||||
for (int index = 0; index < 8; ++index) block[index] = static_cast<unsigned char>(value >> (56 - 8 * index));
|
||||
return block;
|
||||
}
|
||||
|
||||
static void CheckVector(std::uint64_t key, std::uint64_t plain, std::uint64_t cipher) {
|
||||
const auto keyBlock = Block(key);
|
||||
DesSchedule schedule{};
|
||||
schedule.fill(0xaa);
|
||||
Require(DES_set_key_nid_postfix(keyBlock.data(), schedule.data()) == 0);
|
||||
const auto plainBlock = Block(plain);
|
||||
DesBlock output{};
|
||||
DES_ecb_encrypt_nid_postfix(plainBlock.data(), output.data(), schedule.data(), 1);
|
||||
Require(output == Block(cipher));
|
||||
DES_ecb_encrypt_nid_postfix(output.data(), output.data(), schedule.data(), 0);
|
||||
Require(output == plainBlock);
|
||||
DesBlock again{};
|
||||
DES_ecb_encrypt_nid_postfix(plainBlock.data(), again.data(), schedule.data(), 7);
|
||||
Require(again == Block(cipher));
|
||||
}
|
||||
|
||||
static void CheckParity() {
|
||||
DesBlock key{0x00, 0x01, 0x02, 0x03, 0x12, 0x13, 0xfe, 0xff};
|
||||
DES_set_odd_parity_nid_postfix(key.data());
|
||||
Require(key == (DesBlock{0x01, 0x01, 0x02, 0x02, 0x13, 0x13, 0xfe, 0xfe}));
|
||||
for (int value = 0; value < 256; ++value) {
|
||||
DesBlock single{};
|
||||
single.fill(static_cast<unsigned char>(value));
|
||||
DES_set_odd_parity_nid_postfix(single.data());
|
||||
Require((single[0] & 0xfe) == (value & 0xfe) && std::popcount(single[0]) % 2 == 1);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
CheckVector(0x133457799bbcdff1, 0x0123456789abcdef, 0x85e813540f0ab405);
|
||||
CheckVector(0x0e329232ea6d0d73, 0x8787878787878787, 0x0000000000000000);
|
||||
CheckVector(0x0101010101010101, 0x8000000000000000, 0x95f8a5e5dd31d900);
|
||||
CheckParity();
|
||||
DesSchedule schedule{};
|
||||
const auto weak = Block(0x0101010101010101);
|
||||
Require(DES_set_key_nid_postfix(weak.data(), schedule.data()) == 0);
|
||||
const auto badParity = Block(0x0000000000000000);
|
||||
Require(DES_set_key_nid_postfix(badParity.data(), schedule.data()) == 0);
|
||||
RequireThrows([&] { DES_set_key_nid_postfix(nullptr, schedule.data()); });
|
||||
RequireThrows([&] { DES_ecb_encrypt_nid_postfix(weak.data(), schedule.data(), nullptr, 1); });
|
||||
RequireThrows([] { DES_set_odd_parity_nid_postfix(nullptr); });
|
||||
return 0;
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
extern "C" {
|
||||
int APS5_VABI sceUserServiceGetUserNumber(int user_id, int32_t* number);
|
||||
@@ -10,6 +12,9 @@ int APS5_VABI sceUserServiceGetPlatformPrivacyWs1(int32_t user_id, int32_t* valu
|
||||
int APS5_VABI sceUserServiceGetForegroundUser(int* user_id);
|
||||
int APS5_VABI sceUserServiceGetRegisteredUserIdList(UserServiceRegisteredUserIdList* user_id_list);
|
||||
int APS5_VABI sceUserServiceGetUserColor(int user_id, int* color);
|
||||
int APS5_VABI sceUserServiceGetKeyboardType(int user_id, std::int32_t* type);
|
||||
int APS5_VABI sceUserServiceGetJapaneseInputType(int user_id, std::int32_t* type);
|
||||
int APS5_VABI sceUserServiceGetTraditionalChineseInputType(int user_id, std::int32_t* type);
|
||||
int APS5_VABI sceUserServiceGetNpAccountId(int user_id, std::uint64_t* account_id);
|
||||
int APS5_VABI sceUserServiceGetAgeLevel(int user_id, std::uint32_t* age_level);
|
||||
int APS5_VABI sceUserServiceGetAccessibilityChatTranscription(int user_id, std::int32_t* value);
|
||||
@@ -26,6 +31,14 @@ namespace {
|
||||
|
||||
void Require(bool value) { if (!value) std::abort(); }
|
||||
|
||||
void SetLanguage(const char* value) {
|
||||
#ifdef _WIN32
|
||||
Require(_putenv_s("ANYPS5_LANGUAGE", value != nullptr ? value : "") == 0);
|
||||
#else
|
||||
Require((value != nullptr ? ::setenv("ANYPS5_LANGUAGE", value, 1) : ::unsetenv("ANYPS5_LANGUAGE")) == 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
@@ -57,6 +70,48 @@ int main() {
|
||||
Require(sceUserServiceGetUserColor(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT);
|
||||
Require(sceUserServiceGetUserColor(123, &color) == USER_SERVICE_ERROR_INVALID_ARGUMENT);
|
||||
Require(sceUserServiceGetUserColor(USER_SERVICE_INITIAL_USER_ID, &color) == USER_SERVICE_OK);
|
||||
|
||||
SetLanguage(nullptr);
|
||||
using KeyboardSetting = decltype(&sceUserServiceGetKeyboardType);
|
||||
const std::pair<KeyboardSetting, std::int32_t> keyboardSettings[] = {
|
||||
{sceUserServiceGetKeyboardType, 20}, {sceUserServiceGetJapaneseInputType, 0}, {sceUserServiceGetTraditionalChineseInputType, 0}};
|
||||
for (const auto& [setting, expected] : keyboardSettings) {
|
||||
std::int32_t value = 0x5A5A;
|
||||
Require(setting(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT);
|
||||
Require(setting(123, &value) == USER_SERVICE_ERROR_UNKNOWN_USER && value == 0x5A5A);
|
||||
Require(setting(USER_SERVICE_USER_ID_INVALID, &value) == USER_SERVICE_ERROR_UNKNOWN_USER && value == 0x5A5A);
|
||||
Require(setting(USER_SERVICE_INITIAL_USER_ID, &value) == USER_SERVICE_OK && value == expected);
|
||||
}
|
||||
constexpr std::int32_t keyboardTypeByLanguage[] = {37, 20, 26, 21, 17, 28, 29, 33, 34, 38, 39, 40, 23, 35, 16, 30, 31, 32, 19, 36, 22, 41, 25, 44, 48, 43, 47, 42, 46, 45};
|
||||
for (int language = 0; language < 30; ++language) {
|
||||
SetLanguage(std::to_string(language).c_str());
|
||||
std::int32_t value = 0x5A5A;
|
||||
Require(sceUserServiceGetKeyboardType(USER_SERVICE_INITIAL_USER_ID, &value) == USER_SERVICE_OK && value == keyboardTypeByLanguage[language]);
|
||||
Require(sceUserServiceGetKeyboardType(123, &value) == USER_SERVICE_ERROR_UNKNOWN_USER && value == keyboardTypeByLanguage[language]);
|
||||
for (const auto variant : {sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType}) {
|
||||
value = 0x5A5A;
|
||||
Require(variant(USER_SERVICE_INITIAL_USER_ID, &value) == USER_SERVICE_OK && value == 0);
|
||||
}
|
||||
}
|
||||
SetLanguage("30");
|
||||
std::int32_t fallback = 0x5A5A;
|
||||
Require(sceUserServiceGetKeyboardType(USER_SERVICE_INITIAL_USER_ID, &fallback) == USER_SERVICE_OK && fallback == 20);
|
||||
for (const char* malformed : {"arabic", "-1", "31", "1x", ""}) {
|
||||
SetLanguage(malformed);
|
||||
std::int32_t untouched = 0x5A5A;
|
||||
bool rejected = false;
|
||||
try {
|
||||
sceUserServiceGetKeyboardType(USER_SERVICE_INITIAL_USER_ID, &untouched);
|
||||
} catch (const std::runtime_error& error) {
|
||||
rejected = std::string(error.what()).find("ANYPS5_LANGUAGE") != std::string::npos;
|
||||
}
|
||||
const bool configured = malformed[0] != '\0';
|
||||
Require(rejected == configured && untouched == (configured ? 0x5A5A : 20));
|
||||
untouched = 0x5A5A;
|
||||
Require(sceUserServiceGetKeyboardType(123, &untouched) == USER_SERVICE_ERROR_UNKNOWN_USER && untouched == 0x5A5A);
|
||||
Require(sceUserServiceGetJapaneseInputType(USER_SERVICE_INITIAL_USER_ID, &untouched) == USER_SERVICE_OK && untouched == 0);
|
||||
}
|
||||
SetLanguage(nullptr);
|
||||
Require(color == 0);
|
||||
|
||||
std::uint64_t account_id = 0xdeadbeefu;
|
||||
|
||||
@@ -224,6 +224,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti
|
||||
| `APS5_NO_DRAW_RESOURCE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | builds every draw's shader resources instead of reusing a cached identical build |
|
||||
| `APS5_NO_DRAW_SRT_ENTRIES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | disables the draw cache's SRT entries, so every draw decodes its programs |
|
||||
| `APS5_NO_DRAW_STAGE_DATA_HITS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | disables draw-cache hits that differ only in a stage's ShaderData words |
|
||||
| `APS5_NO_EMPTY_INDIRECT_SKIP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | | builds the stages of an indirect draw whose front program last took the CPU path even when none of its records draws anything, instead of reading its records first and skipping it |
|
||||
| `APS5_NO_EPOCH_COLLECT_MAP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | drops the per-epoch map of completed collects, leaving only the fixed-size thread memo to answer a repeated collect |
|
||||
| `APS5_NO_EPOCH_REVALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | runs every Revalidate's per-element and per-region checks instead of the epoch gate |
|
||||
| `APS5_NO_FAST_REVALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | always repeats the resource lookups instead of using the fast proof |
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
- [sceAgcDcbSetCfRegisterDirect, sceAgcDcbSetCfRegisterRangeDirect](../../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - signatures follow [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Agc/SceAgc.cs), which agree; the SET_CONFIG_REG (0x68) packet is OpenAGC's. The driver does not execute SET_CONFIG_REG, so a submitted packet throws
|
||||
- [Command buffer cursors](../../../core/libs/prx/libSceAgc/Command/src/Packet.cpp) (libSceAgc) - a null `cursor_down` is taken as a buffer without downward allocations, whose limit is `top`. PPSA16064 builds one-packet buffers of 8 dwords that way; the console's handling is unconfirmed
|
||||
- [sceAgcGetRegisterDefaultsInternal](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table while `sceAgcGetRegisterDefaults` now returns the version 13 public one; which internal table a console returns is not known. [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns the table of the version passed to `sceAgcInit` instead, and null before it
|
||||
- [sceAgcGetRegisterDefaults](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 13 public table (137 keyed entries over 84 context blocks) because the console returns the running system software's table: [ps5-vulkan](https://github.com/mpereiraesaa/ps5-vulkan/blob/4bb0c8b1423698a7369efc22b829bf16a99e7128/src/color_runtime_defaults.h) measured 128 entries over 79 blocks on 10.x and 137 over 84 on 12.02 and 13.x, the shapes of the version 10 and version 11/13 tables here, and PPSA99011 refuses the version 0 table. The [type table](../../../core/libs/prx/libSceAgc/Command/src/RegisterDefaults.cpp) is handed out as 8-byte `{key, index << 2 | bank}` pairs, the layout ps5-vulkan and the homebrew sharing its `ps5_agc_registers.h` read; the compact 12-byte entries also pack the block's register count and stride above bit 10, and those bits are dropped. No console dump of the runtime type table was compared, and AnyPS5 does not choose 10.x or 12.02 by the system software a title targets
|
||||
- [sceAgcGetRegisterDefaults](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 13 public table (137 keyed entries over 84 context blocks) because the console returns the running system software's table: [ps5-vulkan](https://github.com/mpereiraesaa/ps5-vulkan/blob/4bb0c8b1423698a7369efc22b829bf16a99e7128/src/color_runtime_defaults.h) measured 128 entries over 79 blocks on 10.x and 137 over 84 on 12.02 and 13.x, the shapes of the version 10 and version 11/13 tables here, and PPSA99011 refuses the version 0 table. The [type table](../../../core/libs/prx/libSceAgc/Command/src/RegisterDefaults.cpp) is handed out as 8-byte `{key, index << 2 | bank}` pairs, the layout ps5-vulkan and the homebrew sharing its `ps5_agc_registers.h` read; the compact 12-byte entries also pack the block's register count and stride above bit 10, and those bits are dropped. `sceAgcGetRegisterDefaults2`, `sceAgcGetRegisterDefaults2Internal`, `sceAgcGetRegisterDefaultsInternal` and `sceAgcInit` hand out the compact 12-byte entries (`RegisterDefaultsTypeLayout::Compact`): SDK-built code walks the tables of the first two with a 12-byte stride, reads the key at +0 and the bank and index from the low 10 bits of the word at +4, and keeps an all-ones sentinel for a key it does not find. Which layout the legacy internal call returns on a console is not known. No console dump of the runtime type table was compared, and AnyPS5 does not choose 10.x or 12.02 by the system software a title targets
|
||||
- [sceAgcInit](../../../core/libs/prx/libSceAgc/DcbState/src/Init.cpp) (libSceAgc) - versions 0–13 select their register-default tables and return 0; pointer-shaped or otherwise out-of-range values (>=14) return 0 without a register-default lookup. Console probe on firmware 13.60 ([#4808](https://github.com/boykopovar/AnyPS5/issues/4808#issuecomment-6149448503)): `sceAgcInit(&buf, 8)` (NID `kW3GLb7QfPg`) returns 0 and writes nothing; scalar versions 2–13 return `0x8A6C0004` there, while AnyPS5 still returns 0 for those scalars. The probe does not show that table 13 is selected for pointer-shaped calls. [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns `0x80890001` above 12 for `sceAgcInit`, and falls back to version 11 in `sceAgcGetRegisterDefaults2`. PPSA99011, PPSA99995, [ps5-vulkan](https://github.com/mpereiraesaa/ps5-vulkan) and [prospero-win](https://github.com/mpereiraesaa/prospero-win) call that NID with a pointer-shaped first argument. `sceAgcInit_0090` (NID `23LRUSvYu1M`) checks the state pointer and then calls `sceAgcInit`, as OpenAGC does
|
||||
- [sceAgcSetSubmitMode](../../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted
|
||||
- [sceAgcGetGsOversubscription](../../../core/libs/prx/libSceAgc/Shader/src/GsOversubscription.cpp) (libSceAgc) - signature and occupancy formula (base and expanded vertex/export capacities, budget shift, `GE_PC_ALLOC` / `SPI_SHADER_PGM_RSRC4_GS` encoding) follow KytyPS5 only, not confirmed on hardware or a title; a negative or NaN factor that gives a negative target, a missing GS context register or a zero `GE_MAX_OUTPUT_PER_SUBGROUP` throws
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
|
||||
- [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. Planet Coaster (PPSA01736 v01.017) enables stencil tests without writing the control register.
|
||||
- [Multisampled DCC comp-to-single clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (AGC driver) - RADV's multisampled comp-to-single clear dispatch ([`radv_clear_dcc_comp_to_single`](https://github.com/mihawk-99/PS5_Mesa/blob/1ba9874f478657ccf6da72ce55f0260164cc0bd0/src/amd/vulkan/meta/radv_meta_clear.c#L954)) is recognized by its code and not run. Its clear color (user data 5 to 8, which the shader stores as the four dwords of sample 0's texel) is recorded for the surface its image descriptor names, and DCC keys of 0x10 on a multisampled target of that surface, pipe-bank XOR, size and sample count import as a clear to it, for texels of 8 to 128 bits. A record is dropped when an unmap or allocation removal covers its surface's base. The guest's color memory doesn't get the color at the blocks' first pixels, and the recorded color is assumed to be the one the keys refer to: RADV writes 0x10 keys only in a fast clear that runs this dispatch for the same image right after, and initializes DCC to 0x00 or 0xff on an image's first use ([`radv_init_color_image_metadata`](https://github.com/mihawk-99/PS5_Mesa/blob/1ba9874f478657ccf6da72ce55f0260164cc0bd0/src/amd/vulkan/radv_cmd_buffer.c#L16894)). A new image on memory reused within one mapping, with the same address, XOR, size and sample count, whose clear runs a comp-to-single shader other than the recognized one, would still import the old color.
|
||||
- [Initial depth and stencil planes](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) - the console's reset values of `DB_Z_INFO` and `DB_STENCIL_INFO` (context `0x010`-`0x011`) are not measured. Both start, and return after `CLEAR_STATE`, at zero (formats `INVALID`), the value of Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h). The public [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) write `0x80000180` and `0x20000180` instead, also with formats `INVALID`; of their other bits, only `TILE_STENCIL_DISABLE` would change a result: the HTILE layout of a multisampled depth plane whose `DB_STENCIL_INFO` was never written.
|
||||
- [Type-0 and type-1 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 or 1 in dword 3) with a nonzero base are bound as null images, like a zero base. TYPE 0 comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault). TYPE 1 comes from a gfx1010 (RDNA1) measurement in #4139 and #5828 (`image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` return zero, no fault) with format 22 only; TYPE 1 is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). Neither type was measured on the console's gfx1013.
|
||||
- [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. The next target, sampled or storage lookup removes retired cache owners; recorded work and sampled views keep their images alive until their last reference is released. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported.
|
||||
- [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` replays the low eight bits of the values proved from constant, select and phi exports, with an export proved on every kept path. An export the prover cannot bound replays all 256 eight-bit values, one pass per value per triangle (logged once per process). It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions, since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware.
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
### Functional
|
||||
|
||||
- [sceConvertKeycodeGetVirtualKeycode](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) and `sceConvertKeycodeGetCharacter`: layouts 31–39 throw until the guest platform and compiled SDK version are exposed. `GetVirtualKeycode` also throws for version-dependent Y/Z mappings in layouts 2, 3 and 12. Mapping tables and modifier behavior are recovered; no SDK version is guessed.
|
||||
- [sceConvertKeycodeGetImeKeyboardType](../../../core/libs/prx/libSceConvertKeycode/Export.cpp): user keyboard language and Japanese/Chinese keyboard variants are not exposed by the current UserService implementation. Valid-user queries throw instead of returning a fabricated keyboard type. The confirmed language/variant mapping and SDK compatibility rules still need integration with those settings.
|
||||
- [sceConvertKeycodeGetImeKeyboardType](../../../core/libs/prx/libSceConvertKeycode/Export.cpp): implemented exactly as the repository owner (boykopovar) specified the UserService keyboard type (plus the Japanese variant for type 37 and the Traditional Chinese variant for type 39) to IME keyboard type table, the SDK compatibility rules (38 becomes 4 for PS5 SDK below 0x06000000 or PS4 SDK below 0x10000000; 39 becomes 29 for PS5 SDK below 0x08000000 and for every PS4 client), the error codes and the unmatched-type result (success, IME type 0). The PS4/PS5 client rule reads the title ID prefix from `param.sfo` or `param.json` (`CUSA` is PS4); a missing, malformed or oversized ID or title, as in homebrew, is treated as PS5. Reliable detection inspects the original `eboot.bin`, which only the relinker reads, so it is not done. UserService reports the keyboard type of the configured system language, so the function returns the owner's IME type for that language (see libSceUserService.md).
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
### Unknown function info
|
||||
|
||||
- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 65 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`.
|
||||
- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 48 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`.
|
||||
|
||||
### Functional
|
||||
|
||||
- [retail-only answers](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - the debug resets, the resource and owner registration calls, the SDMA calls and the SQ counter calls return `0x8EEE00FF`, the five `sceGnmDriverInternalRetrieveGnmInterface*` calls return `0x80000000`, `sceGnmGetDbgGcHandle` returns -1, `sceGnmDriverTraceInProgress` and `sceGnmValidateOnSubmitEnabled` return 0 and the other validation calls return `0x80D13FFF`. These are the values [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/gnmdriver/gnmdriver.cpp) returns for calls it marks as not available in retail firmware; they are not measured on a console, and the arguments are not read
|
||||
- [default state packets](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - `sceGnmDrawInitDefaultHardwareState`, `sceGnmDrawInitDefaultHardwareState200`, `sceGnmDrawInitDefaultHardwareState350`, `sceGnmDrawInitToDefaultContextState` and `sceGnmDrawInitToDefaultContextState400` write the sequences in [InitSequences.hpp](../../../core/libs/prx/libSceGnmDriverForNeoMode/include/InitSequences.hpp), taken from [shadPS4's gnmdriver_init.h](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/gnmdriver/gnmdriver_init.h): its Neo sequences, except the plain hardware state, which shadPS4 also writes unchanged in Neo mode. The size checks and returned sizes are shadPS4's. Not compared with console output, and nothing in AnyPS5 consumes a GNM command buffer yet
|
||||
- [draw, dispatch, wait and VGT packets](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) (libSceGnmDriverForNeoMode) - the signatures, size checks, PM4 words and refusals (-1) of the 17 command writers follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/gnmdriver/gnmdriver.cpp) on its Neo-mode path, since this module is the Neo-mode driver: draw initiators keep flag bits 29 to 31, and `sceGnmSetVgtControl` and `sceGnmResetVgtControl` write `IA_MULTI_VGT_PARAM` through `SET_UCONFIG_REG` (shadPS4's `UseNeoCompatSequences` is false). The packets are not compared with console output, and nothing in AnyPS5 consumes a GNM command buffer yet
|
||||
@@ -2,5 +2,6 @@
|
||||
|
||||
### Unknown function info
|
||||
|
||||
- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 377 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`.
|
||||
- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 374 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`.
|
||||
- [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); the module's LibreSSL version is unknown and nothing is checked on the console. Behaviour that differs between LibreSSL versions throws: `SHA256` with a null digest buffer (older versions return a static buffer) and `SHA512_Final` with the SHA-512/224 or SHA-512/256 digest length. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw
|
||||
- [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults
|
||||
@@ -8,3 +8,4 @@
|
||||
- [sceUserServiceGetUserName](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - `0x80960105`, which a PS5 (firmware 13.60) returns for a user id it does not know, has no known name; it is `USER_SERVICE_ERROR_UNKNOWN_USER` here. On the console a registered user who is not logged in still gets their name; here only the initial user exists
|
||||
- [sceUserServiceGetAgeLevel](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - on a PS5 (firmware 13.60) the value is the age set in the user's parental restriction (0, 7, 12 and 15 were measured) and `0xFFFFFFFF` without one; a user who is not logged in returns `0x80960009` with or without an output. Parental restrictions are not modelled, so the initial user is always reported as unrestricted. PPSA12544 (Unity) only stores the value for its user profile
|
||||
- [sceUserServiceGetAccessibilityChatTranscription, PressAndHoldDelay, TriggerEffect, Vibration, ZoomEnabled, ZoomFollowFocus](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - report 0, 0, 1, 1, 0 and 1, what a PS5 (firmware 13.60) reports for a user whose accessibility settings are all at their defaults; the console reports the user's own settings (a changed vibration intensity read 3), which are not modelled. What each number means is unknown. A user who is not logged in returns `0x80960009` with or without an output
|
||||
- [sceUserServiceGetKeyboardType, sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - AnyPS5 stores no per-user keyboard setting, so the initial user reports the keyboard type of the configured system language (`ANYPS5_LANGUAGE`, English US by default) and 0 for the two input variants, and any other user id returns `USER_SERVICE_ERROR_UNKNOWN_USER`. The language-to-keyboard-type table is the repository owner's (languages 0 to 29); a valid language outside it, such as Ukrainian (30), reports 20 (US English). A value that is not a console language number is rejected by `sceSystemServiceParamGetInt` with an error naming `ANYPS5_LANGUAGE`; the call throws it and leaves the output untouched. The input variant is 0 because the owner's table gives 0 for Japanese and Traditional Chinese, the only languages needing one. `sceConvertKeycodeGetImeKeyboardType` consumes these values.
|
||||
Reference in new issue
Block a user