mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-22 00:22:57 -05:00
Merge main into fix/recompiler-once-per-pixel-sample-inputs
This commit is contained in:
commit
2848253fe4
115 files changed
+5013
-417
No files matched your search
@@ -97,6 +97,17 @@ if(BUILD_TESTING)
|
||||
add_test(NAME request_serializer COMMAND request_serializer_tests)
|
||||
set_tests_properties(request_serializer PROPERTIES TIMEOUT 20)
|
||||
find_package(Python3 QUIET COMPONENTS Interpreter)
|
||||
add_test_executable(shader_ir_use_rewrite_tests
|
||||
core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp
|
||||
core/shader/recompiler/IntermediateRepresentation/tests/UseRewriteAllocation.cpp
|
||||
core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp
|
||||
core/shader/recompiler/IntermediateRepresentation/src/IrBlock.cpp
|
||||
core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp
|
||||
core/shader/recompiler/IntermediateRepresentation/src/GuestRegister.cpp)
|
||||
target_include_directories(shader_ir_use_rewrite_tests PRIVATE
|
||||
core/shader/recompiler/IntermediateRepresentation/include)
|
||||
add_test(NAME shader_ir_use_rewrite COMMAND shader_ir_use_rewrite_tests)
|
||||
set_tests_properties(shader_ir_use_rewrite PROPERTIES TIMEOUT 20)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME shader_cache_native_paths COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/test_shader_cache_native_paths.py
|
||||
|
||||
@@ -185,6 +185,7 @@ set(agcDriverSources
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Synchronization/WaitStatistics.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/WorkerSampler.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceTrace.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceTraceThread.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/TraceFile.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ThreadPriority.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/GuestMemory.cpp
|
||||
@@ -494,6 +495,7 @@ add_test_executable(agc_shader_memory_tests EXCLUDE_FROM_ALL
|
||||
tests/ShaderMemory.cpp
|
||||
Execution/src/ShaderMemory.cpp
|
||||
Execution/src/GuestMemory.cpp
|
||||
Execution/src/PerformanceTraceThread.cpp
|
||||
)
|
||||
target_include_directories(agc_shader_memory_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_shader_memory_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
@@ -509,6 +511,34 @@ target_link_libraries(agc_vertex_input_tests PRIVATE shader_recompiler_agc_drive
|
||||
set_target_properties(agc_vertex_input_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_vertex_input_tests)
|
||||
|
||||
add_test_executable(agc_guarded_memory_loads_tests EXCLUDE_FROM_ALL tests/GuardedMemoryLoads.cpp)
|
||||
target_link_libraries(agc_guarded_memory_loads_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
set_target_properties(agc_guarded_memory_loads_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_guarded_memory_loads_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_guarded_memory_loads_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_guarded_memory_loads COMMAND agc_guarded_memory_loads_tests)
|
||||
set_tests_properties(agc_guarded_memory_loads PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_guarded_memory_loads PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_lds_lock_budget_tests EXCLUDE_FROM_ALL tests/LdsLockBudget.cpp)
|
||||
target_link_libraries(agc_lds_lock_budget_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
set_target_properties(agc_lds_lock_budget_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_lds_lock_budget_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_lds_lock_budget_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_lds_lock_budget COMMAND agc_lds_lock_budget_tests)
|
||||
set_tests_properties(agc_lds_lock_budget PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_lds_lock_budget PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_shared_write_tests EXCLUDE_FROM_ALL tests/SharedWrites.cpp)
|
||||
target_include_directories(agc_shared_write_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_shared_write_tests PRIVATE libSceAgcDriver libkernel libc)
|
||||
@@ -518,6 +548,7 @@ configure_windows_unwind(agc_shared_write_tests)
|
||||
add_test_executable(agc_write_tracking_tests EXCLUDE_FROM_ALL
|
||||
tests/WriteTracking.cpp
|
||||
Execution/src/GuestMemory.cpp
|
||||
Execution/src/PerformanceTraceThread.cpp
|
||||
)
|
||||
target_include_directories(agc_write_tracking_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_write_tracking_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
@@ -1239,8 +1270,10 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(agc_shadowed_key_stores PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
add_test(NAME agc_sample_dumps COMMAND agc_driver_recorder_tests --sample-dumps-only)
|
||||
set_tests_properties(agc_sample_dumps PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
add_test(NAME agc_draw_descriptors COMMAND agc_driver_recorder_tests --draw-descriptors-only)
|
||||
set_tests_properties(agc_draw_descriptors PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_pending_key_stores agc_shadowed_key_stores agc_sample_dumps PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
set_tests_properties(agc_pending_key_stores agc_shadowed_key_stores agc_sample_dumps agc_draw_descriptors PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
@@ -1564,6 +1597,8 @@ target_include_directories(agc_driver_swap_alt_blend_tests PRIVATE $<TARGET_PROP
|
||||
|
||||
target_include_directories(agc_driver_packed_float_color_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
target_link_libraries(agc_driver_job_manager_compute_tests PRIVATE libSceJobManager libSceAgc libkernel)
|
||||
|
||||
target_include_directories(agc_driver_packed_unorm_color_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
target_include_directories(agc_driver_color1555_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
@@ -1724,6 +1759,13 @@ if(BUILD_TESTING)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_sparse_buffer_holes PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_driver_lds_lock_budget COMMAND agc_driver_lds_mixed_width_atomics_tests --lock-budget)
|
||||
set_tests_properties(agc_driver_lds_lock_budget PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_lds_lock_budget PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
|
||||
add_test(NAME agc_driver_emulated_color_compare_srgb_decode COMMAND agc_driver_emulated_color_compare_tests)
|
||||
set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES
|
||||
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SRGB_SHADER_DECODE=1"
|
||||
@@ -2591,6 +2633,19 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_constant_select_tests EXCLUDE_FROM_ALL tests/ConstantSelect.cpp)
|
||||
target_link_libraries(agc_constant_select_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
set_target_properties(agc_constant_select_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_constant_select_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_constant_select_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_constant_select COMMAND agc_constant_select_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_constant_select PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
get_directory_property(agcRegisteredTests TESTS)
|
||||
list(FILTER agcRegisteredTests EXCLUDE REGEX "^(agc_pipeline_journal|agc_driver_pipeline_journal)$")
|
||||
|
||||
@@ -26,6 +26,7 @@ struct Commitment {
|
||||
bool whole = false;
|
||||
};
|
||||
Commitment DescribeCommitted(std::uint64_t address, std::size_t bytes, bool writable = false);
|
||||
void CopyGuestBytes(void* destination, const void* source, std::size_t bytes);
|
||||
void Read(std::uint64_t address, std::span<std::byte> destination, std::size_t alignment = 1);
|
||||
// Variants for resources in heaps the guest commits on demand (partially resident textures, GPU
|
||||
// scratch heaps): uncommitted pages read as zeros, are not compared, and are never stored.
|
||||
|
||||
@@ -21,6 +21,13 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
class TraceThread;
|
||||
}
|
||||
|
||||
extern "C" bool AgcDriverTraceRecording_nid_postfix();
|
||||
extern "C" AgcDriver::TraceThread* AgcDriverTraceThread_nid_postfix();
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
class VulkanDevice;
|
||||
@@ -532,23 +539,26 @@ public:
|
||||
}
|
||||
|
||||
static bool Recording() {
|
||||
return recording.load(std::memory_order_relaxed);
|
||||
return AgcDriverTraceRecording_nid_postfix();
|
||||
}
|
||||
|
||||
static TraceThread* Thread() {
|
||||
return local;
|
||||
return AgcDriverTraceThread_nid_postfix();
|
||||
}
|
||||
|
||||
static void StopOutput();
|
||||
|
||||
private:
|
||||
friend bool ::AgcDriverTraceRecording_nid_postfix();
|
||||
friend TraceThread* ::AgcDriverTraceThread_nid_postfix();
|
||||
|
||||
static void OpenInterval(std::chrono::steady_clock::time_point now, std::uint64_t present);
|
||||
static void FlushInterval(std::chrono::steady_clock::time_point now, std::uint32_t width, std::uint32_t height, const VulkanDevice* device);
|
||||
|
||||
inline static std::atomic<bool> recording{false};
|
||||
static std::atomic<bool> recording;
|
||||
inline static std::atomic<bool> buffering{false};
|
||||
inline static std::atomic<std::uint64_t> presentCount{0};
|
||||
inline static thread_local TraceThread* local = nullptr;
|
||||
static thread_local TraceThread* local;
|
||||
inline static std::mutex gpuMutex;
|
||||
inline static std::vector<TraceGpuRange> gpuRanges;
|
||||
inline static std::uint64_t gpuLost = 0;
|
||||
|
||||
@@ -73,8 +73,7 @@ bool StackHoldsImport() {
|
||||
const auto low = bounds.low;
|
||||
const auto high = bounds.high;
|
||||
#endif
|
||||
const volatile unsigned char marker = 0;
|
||||
const auto frame = reinterpret_cast<std::uintptr_t>(&marker);
|
||||
const auto frame = reinterpret_cast<std::uintptr_t>(__builtin_frame_address(0));
|
||||
return frame >= low && frame < high && frame - low >= InlineImportStackBytes;
|
||||
}
|
||||
|
||||
|
||||
@@ -658,8 +658,7 @@ bool onOwnLiveStack(std::uintptr_t address, std::size_t bytes) {
|
||||
}
|
||||
};
|
||||
thread_local const Bounds bounds;
|
||||
const volatile unsigned char marker = 0;
|
||||
const auto frame = reinterpret_cast<std::uintptr_t>(&marker);
|
||||
const auto frame = reinterpret_cast<std::uintptr_t>(__builtin_frame_address(0));
|
||||
return bounds.high != 0 && frame >= bounds.low && frame < bounds.high && address >= frame && address < bounds.high && bytes <= bounds.high - address;
|
||||
}
|
||||
|
||||
@@ -2101,9 +2100,26 @@ void Read(std::uint64_t address, std::span<std::byte> destination, std::size_t a
|
||||
if (trace) std::fprintf(stderr, "[unreadable] read from +0x%llx\n", ModuleOffset(__builtin_return_address(0)));
|
||||
throw;
|
||||
}
|
||||
std::memcpy(destination.data(), source, destination.size());
|
||||
CopyGuestBytes(destination.data(), source, destination.size());
|
||||
}
|
||||
|
||||
#ifdef ANYPS5_ENABLE_ASAN
|
||||
[[gnu::no_sanitize("address")]] void CopyGuestBytes(void* destination, const void* source, std::size_t bytes) {
|
||||
auto* to = static_cast<unsigned char*>(destination);
|
||||
const auto* from = static_cast<const unsigned char*>(source);
|
||||
std::size_t index = 0;
|
||||
for (; index + sizeof(std::uint64_t) <= bytes; index += sizeof(std::uint64_t)) {
|
||||
const std::uint64_t word = *reinterpret_cast<const volatile std::uint64_t*>(from + index);
|
||||
std::memcpy(to + index, &word, sizeof(word));
|
||||
}
|
||||
for (; index < bytes; ++index) to[index] = static_cast<const volatile unsigned char*>(from)[index];
|
||||
}
|
||||
#else
|
||||
void CopyGuestBytes(void* destination, const void* source, std::size_t bytes) {
|
||||
std::memcpy(destination, source, bytes);
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
void copyCommitted(std::uint64_t address, std::span<std::byte> destination) {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp"
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
std::atomic<bool> PerformanceTrace::recording{false};
|
||||
thread_local TraceThread* PerformanceTrace::local = nullptr;
|
||||
|
||||
}
|
||||
|
||||
extern "C" bool AgcDriverTraceRecording_nid_postfix() {
|
||||
return AgcDriver::PerformanceTrace::recording.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
extern "C" AgcDriver::TraceThread* AgcDriverTraceThread_nid_postfix() {
|
||||
return AgcDriver::PerformanceTrace::local;
|
||||
}
|
||||
@@ -219,7 +219,7 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val
|
||||
const auto index = static_cast<std::size_t>((address % PageBytes) / sizeof(*value));
|
||||
if (!page.valid.Test(index) && page.lazy) {
|
||||
const auto first = index / BlockWords * BlockWords;
|
||||
std::memcpy(&page.words[first], reinterpret_cast<const void*>((address & ~static_cast<std::uint64_t>(PageBytes - 1)) + first * sizeof(std::uint32_t)), BlockWords * sizeof(std::uint32_t));
|
||||
GuestMemory::CopyGuestBytes(&page.words[first], reinterpret_cast<const void*>((address & ~static_cast<std::uint64_t>(PageBytes - 1)) + first * sizeof(std::uint32_t)), BlockWords * sizeof(std::uint32_t));
|
||||
page.valid.SetBlock(index);
|
||||
}
|
||||
if (!page.valid.Test(index)) {
|
||||
|
||||
@@ -303,6 +303,7 @@ struct VulkanDevice::State {
|
||||
bool float16DenormPreserve = false;
|
||||
bool demoteToHelperInvocation = false;
|
||||
bool earlyFragmentSampleMaskTestBeforeSampleCounting = false;
|
||||
bool robustBufferAccess2 = false;
|
||||
// VK_EXT_descriptor_indexing with non-uniform image array indexing (bindless image tables in
|
||||
// graphics stages, and compute workgroups wider than a wave).
|
||||
bool descriptorIndexing = false;
|
||||
@@ -1836,9 +1837,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
||||
VkPhysicalDeviceFeatures2 queried{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &robustness2};
|
||||
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &queried);
|
||||
}
|
||||
if (robustness2.nullDescriptor == VK_TRUE) {
|
||||
state->robustBufferAccess2 = robustness2.robustBufferAccess2 == VK_TRUE && enabled.robustBufferAccess == VK_TRUE;
|
||||
if (robustness2.nullDescriptor == VK_TRUE || state->robustBufferAccess2) {
|
||||
const bool nullDescriptor = robustness2.nullDescriptor == VK_TRUE;
|
||||
robustness2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT};
|
||||
robustness2.nullDescriptor = VK_TRUE;
|
||||
robustness2.nullDescriptor = nullDescriptor ? VK_TRUE : VK_FALSE;
|
||||
robustness2.robustBufferAccess2 = state->robustBufferAccess2 ? VK_TRUE : VK_FALSE;
|
||||
robustness2.pNext = bdaFeatures.pNext;
|
||||
bdaFeatures.pNext = &robustness2;
|
||||
deviceExtensions.push_back(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME);
|
||||
@@ -3494,6 +3498,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const {
|
||||
target.spannedWaves = state->spannedWaves;
|
||||
target.fixedPushSlots = state->graphicsPipelineLibrary;
|
||||
target.registrationModules = state->graphicsPipelineLibrary;
|
||||
target.robustBufferAccess2 = state->robustBufferAccess2;
|
||||
target.srgbDecodeFormats = state->srgbDecodeFormats;
|
||||
target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0;
|
||||
if (state->meshShader) {
|
||||
|
||||
@@ -4928,7 +4928,8 @@ std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindi
|
||||
for (std::size_t index = 0; index < selected.size(); ++index) snapshotIndices[selected[index]] = index;
|
||||
auto nextWrite = writes.begin();
|
||||
for (const auto& binding : bindings) {
|
||||
while (nextWrite != writes.end() && nextWrite->dstBinding == binding.layout.binding) {
|
||||
const auto original = setOf(binding) == 0 ? _set : fragment.set;
|
||||
while (nextWrite != writes.end() && nextWrite->dstBinding == binding.layout.binding && nextWrite->dstSet == original) {
|
||||
auto& write = *nextWrite++;
|
||||
write.dstSet = setOf(binding) == 0 ? result->allocation.set : result->fragmentAllocation.set;
|
||||
if (binding.allocations.empty()) continue;
|
||||
|
||||
@@ -102,6 +102,7 @@ public:
|
||||
context.bufferDeviceAddress = true;
|
||||
context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue);
|
||||
VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||
pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||
pool.queueFamilyIndex = family;
|
||||
Check(context.Function<PFN_vkCreateCommandPool>("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool");
|
||||
} catch (...) {
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
#include "IntermediateRepresentation/IrBuilder.hpp"
|
||||
#include "Optimization/ConstantFolder.hpp"
|
||||
#include "Optimization/DeadCodeEliminator.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace ShaderRecompiler;
|
||||
|
||||
namespace {
|
||||
|
||||
void Require(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
struct Program {
|
||||
IrProgram program;
|
||||
IrBuilder builder{program};
|
||||
IrValue* lane = nullptr;
|
||||
IrValue* condition = nullptr;
|
||||
IrValue* other = nullptr;
|
||||
std::uint32_t values = 0;
|
||||
|
||||
Program() {
|
||||
auto& block = program.CreateBlock();
|
||||
program.BlockOrder().push_back(&block);
|
||||
BlockInfo info;
|
||||
info.id = 0;
|
||||
info.terminator.kind = TerminatorKind::Return;
|
||||
program.Metadata().blockInfo.push_back(info);
|
||||
program.SetEntryBlock(block);
|
||||
builder.SetInsertionPoint(block);
|
||||
lane = &builder.Emit(IrOpcode::LaneId, IrType::U32, {});
|
||||
condition = &builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {lane, &builder.Constant(16u)});
|
||||
other = &builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {lane, &builder.Constant(8u)});
|
||||
}
|
||||
|
||||
IrValue& value() {
|
||||
return builder.Emit(IrOpcode::IAdd32, IrType::U32, {lane, &builder.Constant(++values)});
|
||||
}
|
||||
|
||||
IrValue& select(IrValue& selector, IrValue& onTrue, IrValue& onFalse) {
|
||||
return builder.Emit(IrOpcode::SelectU32, IrType::U32, {&selector, &onTrue, &onFalse});
|
||||
}
|
||||
|
||||
const IrValue* folded(IrValue& result) {
|
||||
auto& reference = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&result});
|
||||
ValidateProgram(program, true);
|
||||
ConstantFolder{}.Fold(program);
|
||||
DeadCodeEliminator{}.RemoveIdentities(program);
|
||||
DeadCodeEliminator{}.Eliminate(program);
|
||||
ValidateProgram(program, true);
|
||||
return reference.Argument(0)->Resolve();
|
||||
}
|
||||
};
|
||||
|
||||
bool Selects(const IrValue* value, const IrValue* selector, const IrValue* onTrue, const IrValue* onFalse) {
|
||||
return value->Opcode() == IrOpcode::SelectU32 && value->Argument(0)->Resolve() == selector && value->Argument(1)->Resolve() == onTrue && value->Argument(2)->Resolve() == onFalse;
|
||||
}
|
||||
|
||||
void CheckFalseArm() {
|
||||
Program p;
|
||||
auto& kept = p.value();
|
||||
auto& dropped = p.value();
|
||||
auto& fallback = p.value();
|
||||
auto& inner = p.select(*p.condition, dropped, fallback);
|
||||
const auto* result = p.folded(p.select(*p.condition, kept, inner));
|
||||
Require(Selects(result, p.condition, &kept, &fallback), "a select in the false arm of a select on the same condition was not folded to its false arm");
|
||||
Require(!inner.HasUses(), "the folded inner select is still used");
|
||||
}
|
||||
|
||||
void CheckTrueArm() {
|
||||
Program p;
|
||||
auto& kept = p.value();
|
||||
auto& dropped = p.value();
|
||||
auto& fallback = p.value();
|
||||
const auto* result = p.folded(p.select(*p.condition, p.select(*p.condition, kept, dropped), fallback));
|
||||
Require(Selects(result, p.condition, &kept, &fallback), "a select in the true arm of a select on the same condition was not folded to its true arm");
|
||||
}
|
||||
|
||||
void CheckChain() {
|
||||
Program p;
|
||||
auto& last = p.value();
|
||||
auto& fallback = p.value();
|
||||
auto* chain = &p.select(*p.condition, p.value(), fallback);
|
||||
chain = &p.select(*p.condition, p.value(), *chain);
|
||||
const auto* result = p.folded(p.select(*p.condition, last, *chain));
|
||||
Require(Selects(result, p.condition, &last, &fallback), "a chain of selects on one condition did not fold to the last write and the fallback");
|
||||
}
|
||||
|
||||
void CheckEqualArms() {
|
||||
Program p;
|
||||
auto& same = p.value();
|
||||
const auto* result = p.folded(p.select(*p.condition, same, p.select(*p.condition, p.value(), same)));
|
||||
Require(result == &same, "a select whose arms become equal after folding was not replaced by that value");
|
||||
}
|
||||
|
||||
void CheckOtherCondition() {
|
||||
Program p;
|
||||
auto& kept = p.value();
|
||||
auto& inner = p.select(*p.other, p.value(), p.value());
|
||||
const auto* result = p.folded(p.select(*p.condition, kept, inner));
|
||||
Require(Selects(result, p.condition, &kept, &inner), "a select nested under a different condition was folded");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
CheckFalseArm();
|
||||
CheckTrueArm();
|
||||
CheckChain();
|
||||
CheckEqualArms();
|
||||
CheckOtherCondition();
|
||||
std::puts("constant select tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
#include "IntermediateRepresentation/IrProgram.hpp"
|
||||
#include "Optimization/DenormalFlushEliminator.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <set>
|
||||
#include <span>
|
||||
#include <spirv/unified1/spirv.hpp>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace ShaderRecompiler;
|
||||
|
||||
@@ -80,6 +89,46 @@ bool run(const char* name, const std::function<bool()>& test) {
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> Recompiled(std::span<const std::uint32_t> code) {
|
||||
const std::array<std::uint32_t, 4> userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu};
|
||||
const std::array<std::uint32_t, 2> capabilities{1u, 61u};
|
||||
const std::array<std::string_view, 1> extensions{"SPV_KHR_storage_buffer_storage_class"};
|
||||
RecompileRequest request{};
|
||||
request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}};
|
||||
request.context.waveSize = 32;
|
||||
request.context.userDataBaseRegister = 0;
|
||||
request.context.userData = userData;
|
||||
request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
|
||||
request.target.vulkanVersion = 0x00403000u;
|
||||
request.target.spirvVersion = 0x00010600u;
|
||||
request.target.subgroupSize = 32u;
|
||||
request.target.bdaAbiVersion = 1;
|
||||
request.target.supportedCapabilities = capabilities;
|
||||
request.target.supportedExtensions = extensions;
|
||||
request.target.maxWorkgroupSize = {1024u, 1024u, 64u};
|
||||
request.target.maxWorkgroupInvocations = 1024;
|
||||
request.target.maxWorkgroupSharedMemoryBytes = 49152;
|
||||
request.layout = {0, 0, 0, 128};
|
||||
request.useCache = false;
|
||||
const auto& words = Recompile(request).spirv.Words();
|
||||
return {words.begin(), words.end()};
|
||||
}
|
||||
|
||||
std::size_t ExponentMasks(std::span<const std::uint32_t> words) {
|
||||
if (words.size() < 5 || words[0] != spv::MagicNumber) throw std::runtime_error("the recompiler returned no SPIR-V module");
|
||||
std::set<std::uint32_t> exponents;
|
||||
std::size_t masks = 0;
|
||||
for (std::size_t offset = 5; offset < words.size();) {
|
||||
const auto count = words[offset] >> spv::WordCountShift;
|
||||
if (count == 0 || count > words.size() - offset) throw std::runtime_error("the recompiler returned a malformed SPIR-V instruction");
|
||||
const auto opcode = words[offset] & spv::OpCodeMask;
|
||||
if (opcode == spv::OpConstant && count == 4 && words[offset + 3] == 0x7f800000u) exponents.insert(words[offset + 2]);
|
||||
if (opcode == spv::OpBitwiseAnd && count == 5 && (exponents.contains(words[offset + 3]) || exponents.contains(words[offset + 4]))) ++masks;
|
||||
offset += count;
|
||||
}
|
||||
return masks;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
@@ -252,5 +301,79 @@ int main() {
|
||||
return loop(true) && !loop(false);
|
||||
});
|
||||
|
||||
passed &= run("a product behind its round-to-nearest tiny-product guard needs no flush, one behind another guard does", [] {
|
||||
Builder b;
|
||||
const auto value = [&](IrValue& bits) -> IrValue& { return b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&bits}); };
|
||||
const auto bitsOf = [&](IrValue& number) -> IrValue& { return b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&number}); };
|
||||
const auto guarded = [&](IrValue& lhs, IrValue& rhs, IrValue& guardLhs, IrValue& guardRhs, std::uint32_t threshold) -> IrValue& {
|
||||
auto& product = b.emit(IrOpcode::FPMul32, IrType::F32, {&lhs, &rhs});
|
||||
auto& scaled = b.emit(IrOpcode::FPMul32, IrType::F32, {&b.emit(IrOpcode::FPMul32, IrType::F32, {&guardLhs, &b.constant(0x5f800000u)}), &guardRhs});
|
||||
auto& magnitude = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bitsOf(scaled), &b.constant(0x7fffffffu)});
|
||||
auto& tiny = b.emit(IrOpcode::ULessThan32, IrType::Bool, {&b.emit(IrOpcode::ISub32, IrType::U32, {&magnitude, &b.constant(1u)}), &b.constant(threshold)});
|
||||
auto& sign = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.emit(IrOpcode::BitwiseXor32, IrType::U32, {&bitsOf(lhs), &bitsOf(rhs)}), &b.constant(0x80000000u)});
|
||||
return b.flush(bitsOf(b.emit(IrOpcode::SelectF32, IrType::F32, {&tiny, &value(sign), &product})));
|
||||
};
|
||||
auto& x = value(b.flush(b.unknown()));
|
||||
auto& y = value(b.flush(b.unknown()));
|
||||
auto& raw = value(b.unknown());
|
||||
IrValue* removed[] = {&guarded(x, y, x, y, 0x207fffffu), &guarded(x, y, y, x, 0x207fffffu)};
|
||||
IrValue* kept[] = {&guarded(x, y, x, raw, 0x207fffffu), &guarded(x, y, x, y, 0x20800000u), &guarded(x, raw, x, raw, 0x207fffffu)};
|
||||
for (auto* flushed : removed) b.keep(*flushed);
|
||||
for (auto* flushed : kept) b.keep(*flushed);
|
||||
const bool counted = b.eliminate() == 2u;
|
||||
bool flushes = true;
|
||||
for (auto* flushed : removed) flushes &= !flushed->HasUses();
|
||||
for (auto* flushed : kept) flushes &= flushed->HasUses();
|
||||
return counted && flushes;
|
||||
});
|
||||
|
||||
passed &= run("an arm a select takes only when its bits are NaN needs no flush, one under another value's NaN test does", [] {
|
||||
Builder b;
|
||||
auto& x = b.unknown();
|
||||
auto& other = b.unknown();
|
||||
const auto magnitude = [&](IrValue& bits) -> IrValue& { return b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bits, &b.constant(0x7fffffffu)}); };
|
||||
const auto isNan = [&](IrValue& bits) -> IrValue& { return b.emit(IrOpcode::UGreaterThan32, IrType::Bool, {&magnitude(bits), &b.constant(0x7f800000u)}); };
|
||||
const auto chosen = [&](IrValue& condition, IrValue& arm) -> IrValue& { return b.flush(b.emit(IrOpcode::SelectU32, IrType::U32, {&condition, &arm, &b.flush(b.unknown())})); };
|
||||
IrValue* removed[] = {
|
||||
&chosen(isNan(x), x),
|
||||
&chosen(isNan(x), b.emit(IrOpcode::BitwiseOr32, IrType::U32, {&x, &b.constant(0x00400000u)})),
|
||||
&chosen(b.emit(IrOpcode::FPIsNan32, IrType::Bool, {&b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&x})}), x),
|
||||
};
|
||||
IrValue* kept[] = {
|
||||
&chosen(isNan(other), x),
|
||||
&chosen(b.emit(IrOpcode::UGreaterThan32, IrType::Bool, {&b.constant(0x7f800000u), &magnitude(x)}), x),
|
||||
};
|
||||
for (auto* flushed : removed) b.keep(*flushed);
|
||||
for (auto* flushed : kept) b.keep(*flushed);
|
||||
const bool counted = b.eliminate() == 3u;
|
||||
bool flushes = true;
|
||||
for (auto* flushed : removed) flushes &= !flushed->HasUses();
|
||||
for (auto* flushed : kept) flushes &= flushed->HasUses();
|
||||
return counted && flushes;
|
||||
});
|
||||
|
||||
passed &= run("the sign or exponent bits of any value need no flush, a mask that keeps significand bits does", [] {
|
||||
Builder b;
|
||||
auto& sign = b.flush(b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.unknown(), &b.constant(0x80000000u)}));
|
||||
auto& exponent = b.flush(b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.constant(0xff800000u), &b.unknown()}));
|
||||
auto& significand = b.flush(b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.unknown(), &b.constant(0x807fffffu)}));
|
||||
b.keep(sign);
|
||||
b.keep(exponent);
|
||||
b.keep(significand);
|
||||
return b.eliminate() == 2u && !sign.HasUses() && !exponent.HasUses() && significand.HasUses();
|
||||
});
|
||||
|
||||
passed &= run("two chained v_mul_f32 keep only the input flush of the first source", [] {
|
||||
const std::array<std::uint32_t, 5> code{
|
||||
0x10020100u,
|
||||
0x10040301u,
|
||||
0xe0700000u, 0x80000200u,
|
||||
0xbf810000u,
|
||||
};
|
||||
const auto masks = ExponentMasks(Recompiled(code));
|
||||
if (masks != 1u) std::fprintf(stderr, " %zu f32 exponent masks, expected 1\n", masks);
|
||||
return masks == 1u;
|
||||
});
|
||||
|
||||
return passed ? 0 : 1;
|
||||
}
|
||||
@@ -458,8 +458,8 @@ void RunDepthSurfaceReuseTests() {
|
||||
const auto otherWithA = pairing(otherDepth, stencilA);
|
||||
Require(imageCopies.size() == 1 && imageCopies[0] == std::make_tuple(withA, otherWithA, static_cast<VkImageAspectFlags>(VK_IMAGE_ASPECT_STENCIL_BIT)), "a stencil plane paired with another depth plane must carry its stencil over");
|
||||
{
|
||||
constexpr std::uint64_t arrayBase = 0x80000000;
|
||||
constexpr std::uint64_t htile = 0x81000000;
|
||||
constexpr std::uint64_t arrayBase = 0x30000000;
|
||||
constexpr std::uint64_t htile = 0x31000000;
|
||||
constexpr VkExtent2D extent{1024, 512};
|
||||
const auto sliceHtile = HtileMetadataBytes(extent);
|
||||
DepthTarget first{arrayBase, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0};
|
||||
|
||||
@@ -6059,6 +6059,23 @@ void registeredReadableCoverTests() {
|
||||
::operator delete(memory, std::align_val_t{block});
|
||||
}
|
||||
|
||||
std::size_t VariableBuiltIns(const ShaderRecompiler::RecompileResult& result, spv::BuiltIn builtin) {
|
||||
std::size_t count = 0;
|
||||
const auto& words = result.spirv.Words();
|
||||
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
|
||||
if (static_cast<spv::Op>(words[at] & 0xffffu) == spv::OpDecorate && (words[at] >> 16u) == 4u && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == builtin) ++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool MemberBuiltIn(const ShaderRecompiler::RecompileResult& result, spv::BuiltIn builtin) {
|
||||
const auto& words = result.spirv.Words();
|
||||
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
|
||||
if (static_cast<spv::Op>(words[at] & 0xffffu) == spv::OpMemberDecorate && words[at + 3] == spv::DecorationBuiltIn && words[at + 4] == builtin) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void rectListTests() {
|
||||
using namespace ShaderRecompiler;
|
||||
using namespace AgcDriver::Graphics;
|
||||
@@ -6092,6 +6109,21 @@ void rectListTests() {
|
||||
expectFailure([&] { ValidateShaders(shaders, state, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false); }, "stage count");
|
||||
}
|
||||
}
|
||||
{
|
||||
RecompileResult clipped;
|
||||
clipped.spirv = makeModule({.clipDistanceLength = 2});
|
||||
const std::array<std::uint32_t, 3> clipCapabilities{spv::CapabilityShader, spv::CapabilityTessellation, spv::CapabilityClipDistance};
|
||||
auto clipTarget = target;
|
||||
clipTarget.supportedCapabilities = clipCapabilities;
|
||||
auto clippedRect = BuildRectListShaders(clipped, fragment, clipTarget);
|
||||
for (const auto* stage : {&clippedRect.control, &clippedRect.lastVertexControl, &clippedRect.evaluation}) {
|
||||
Require(!MemberBuiltIn(*stage, spv::BuiltInClipDistance) && VariableBuiltIns(*stage, spv::BuiltInClipDistance) == 2u, "a rect-list stage does not take and forward the vertex clip distances through variables of their own, as the vertex shader writes them");
|
||||
}
|
||||
const std::array<CompiledShader, 4> clippedShaders{{{ShaderStage::Vertex, &clipped, 0}, {ShaderStage::TessellationControl, &clippedRect.control, 0}, {ShaderStage::TessellationEvaluation, &clippedRect.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}};
|
||||
auto queue = makeState();
|
||||
queue.userConfig[0x242] = 17;
|
||||
ValidateShaders(clippedShaders, DecodeState(queue), VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false, false, false, false, false, false, true);
|
||||
}
|
||||
vertex.spirv = makeModule({.parameterOutput = true});
|
||||
fragment.spirv = makeModule({.fragment = true, .rectParameters = true});
|
||||
vertex.parameterExports = {0};
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
#define SPV_ENABLE_UTILITY_CODE
|
||||
#include <spirv/unified1/spirv.hpp>
|
||||
#undef SPV_ENABLE_UTILITY_CODE
|
||||
#include "IntermediateRepresentation/IrProgram.hpp"
|
||||
#include "Optimization/ShaderStageInputInfo.hpp"
|
||||
#include "SpirvBackend/SpirvEmitter.hpp"
|
||||
#include "PipelineSpecialization.hpp"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace ShaderRecompiler;
|
||||
|
||||
void require(bool condition, const std::string& message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
struct LoadCase {
|
||||
ResourceKind kind;
|
||||
IrOpcode opcode;
|
||||
IrType type;
|
||||
std::uint32_t bits = 32;
|
||||
std::uint32_t components = 1;
|
||||
};
|
||||
|
||||
std::vector<std::uint32_t> emitLoad(const LoadCase& test, bool active, std::uint32_t offset, std::uint32_t records, bool robust) {
|
||||
IrProgram program;
|
||||
program.SetWaveSize(32);
|
||||
program.Resources().stage = IrShaderStage::Compute;
|
||||
program.Resources().srtPlanComplete = true;
|
||||
program.Resources().resourceTrackingComplete = true;
|
||||
program.Metadata().shaderInfoComplete = true;
|
||||
program.Metadata().bindingLayoutComplete = true;
|
||||
auto& block = program.CreateBlock();
|
||||
program.SetEntryBlock(block);
|
||||
program.BlockOrder().push_back(&block);
|
||||
program.Metadata().blockInfo.emplace_back();
|
||||
const auto constant = [&](std::uint32_t number) -> IrValue& {
|
||||
auto& value = program.CreateValue(IrOpcode::Void, IrType::U32);
|
||||
value.SetImmediateU32(number);
|
||||
return value;
|
||||
};
|
||||
const auto emit = [&](IrOpcode opcode, IrType type, std::initializer_list<IrValue*> arguments) -> IrValue& {
|
||||
auto& value = program.CreateValue(opcode, type);
|
||||
for (auto* argument : arguments) value.AddArgument(argument);
|
||||
block.AppendInstruction(&value);
|
||||
return value;
|
||||
};
|
||||
auto& enabled = program.CreateValue(IrOpcode::Void, IrType::Bool);
|
||||
enabled.SetImmediateBool(active);
|
||||
auto& zero = constant(0);
|
||||
auto& address = constant(offset);
|
||||
MemoryInfo memory;
|
||||
memory.kind = test.kind;
|
||||
memory.dataBits = test.bits;
|
||||
memory.dataDwords = test.components;
|
||||
memory.componentCount = test.components;
|
||||
program.Resources().memoryInfo.push_back(memory);
|
||||
if (test.kind == ResourceKind::Scratch) {
|
||||
program.Info().scratchDwords = 16;
|
||||
auto& handle = emit(IrOpcode::GetScratchResource, IrType::AddressResource, {});
|
||||
emit(test.opcode, test.type, {&handle, &address, &zero, &enabled});
|
||||
} else if (test.kind == ResourceKind::Buffer) {
|
||||
program.Resources().memoryInfo.front().coherent = true;
|
||||
program.Info().buffers.emplace_back().read = true;
|
||||
program.Metadata().bindings.descriptors.push_back({DescriptorBindingKind::Buffers, {0}});
|
||||
program.Metadata().bindings.pushDataStartDword = 0;
|
||||
program.Metadata().bindings.memoryOffsetCount = 1;
|
||||
auto& size = constant(records);
|
||||
auto& handle = emit(IrOpcode::GetBufferResource, IrType::BufferResource, {&zero, &zero, &size, &zero});
|
||||
emit(test.opcode, test.type, {&handle, &zero, &address, &zero, &enabled});
|
||||
} else {
|
||||
if (test.kind == ResourceKind::Gds) program.Metadata().bindings.descriptors.push_back({DescriptorBindingKind::Gds, {0}});
|
||||
emit(test.opcode, test.type, {&address, &enabled});
|
||||
}
|
||||
ShaderComputeInputInfo compute;
|
||||
compute.threadsNum[0] = 32;
|
||||
compute.threadsNum[1] = 1;
|
||||
compute.threadsNum[2] = 1;
|
||||
compute.ldsSizeDwords = 16;
|
||||
compute.scratchSizeDwords = 16;
|
||||
compute.hostSubgroupSize = 32;
|
||||
compute.waveSize = 32;
|
||||
ShaderStageInputInfo inputs;
|
||||
inputs.compute = &compute;
|
||||
CompiledBindingLayout bindings;
|
||||
bindings.layout = program.Metadata().bindings;
|
||||
SpirvTargetOptions target{};
|
||||
target.vulkanVersion = 0x00401000;
|
||||
target.spirvVersion = 0x00010300;
|
||||
target.subgroupSize = 32;
|
||||
target.robustBufferAccess2 = robust;
|
||||
return SpirvEmitter().Emit(program, inputs, bindings, target);
|
||||
}
|
||||
|
||||
class AccessTrace {
|
||||
public:
|
||||
AccessTrace(const std::vector<std::uint32_t>& module, std::uint32_t misalignment = 0, bool valid = true)
|
||||
: words(module), misalignment(misalignment), valid(valid) {
|
||||
require(words.size() >= 5, "SPIR-V module header is missing");
|
||||
for (std::size_t offset = 5; offset < words.size();) {
|
||||
const auto count = words[offset] >> 16;
|
||||
require(count != 0 && count <= words.size() - offset, "SPIR-V instruction is truncated");
|
||||
const auto instruction = std::span(words).subspan(offset, count);
|
||||
const auto opcode = static_cast<spv::Op>(instruction[0] & 0xffff);
|
||||
bool result = false;
|
||||
bool type = false;
|
||||
spv::HasResultAndType(opcode, &result, &type);
|
||||
if (result) definitions.emplace(instruction[type ? 2 : 1], instruction);
|
||||
if (opcode == spv::OpName) names.emplace(instruction[1], std::string(reinterpret_cast<const char*>(instruction.data() + 2)));
|
||||
if (opcode == spv::OpDecorate && instruction[2] == spv::DecorationSpecId) specializations.emplace(instruction[1], instruction[3]);
|
||||
if (opcode == spv::OpLabel) labels.emplace(instruction[1], offset);
|
||||
if (opcode == spv::OpEntryPoint) entryFunction = instruction[2];
|
||||
offset += count;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> Run() {
|
||||
std::size_t cursor = 0;
|
||||
bool entry = false;
|
||||
for (std::size_t offset = 5; offset < words.size(); offset += words[offset] >> 16) {
|
||||
const auto opcode = static_cast<spv::Op>(words[offset] & 0xffff);
|
||||
if (opcode == spv::OpFunction) entry = words[offset + 2] == entryFunction;
|
||||
if (entry && opcode == spv::OpLabel) { cursor = offset; break; }
|
||||
}
|
||||
require(cursor != 0, "SPIR-V entry block is missing");
|
||||
std::vector<std::uint32_t> reads;
|
||||
std::uint32_t previous = 0;
|
||||
std::uint32_t current = 0;
|
||||
for (std::size_t steps = 0; steps < 4096; ++steps) {
|
||||
const auto instruction = std::span(words).subspan(cursor, words[cursor] >> 16);
|
||||
const auto opcode = static_cast<spv::Op>(instruction[0] & 0xffff);
|
||||
if (opcode == spv::OpLabel) current = instruction[1];
|
||||
if (opcode == spv::OpPhi) {
|
||||
bool found = false;
|
||||
for (std::size_t index = 3; index + 1 < instruction.size(); index += 2) {
|
||||
if (instruction[index + 1] == previous) { phiValues[instruction[2]] = instruction[index]; found = true; }
|
||||
}
|
||||
require(found, "SPIR-V phi has no incoming edge for the executed path");
|
||||
}
|
||||
if (opcode == spv::OpLoad && isGuestPointer(instruction[3])) {
|
||||
const auto pointer = definition(instruction[3]);
|
||||
require((pointer[0] & 0xffff) == spv::OpAccessChain, "guest read is not an indexed access");
|
||||
reads.push_back(value(pointer.back()));
|
||||
if (rootName(instruction[3]) == "scratch_dwords") require(pointerStorage(instruction[3]) == spv::StorageClassFunction, "scratch selection accessed non-private storage");
|
||||
if (rootName(instruction[3]).starts_with("buffers")) require(instruction.size() == 5 && (instruction[4] & spv::MemoryAccessVolatileMask), "coherent buffer read lost its volatile access");
|
||||
}
|
||||
if (opcode == spv::OpBranch || opcode == spv::OpBranchConditional) {
|
||||
const auto destination = opcode == spv::OpBranch ? instruction[1] : instruction[value(instruction[1]) ? 2 : 3];
|
||||
previous = current;
|
||||
cursor = labels.at(destination);
|
||||
continue;
|
||||
}
|
||||
if (opcode == spv::OpReturn) return reads;
|
||||
require(opcode != spv::OpFunctionCall && opcode != spv::OpSwitch && opcode != spv::OpLoopMerge, "access fixture unexpectedly requires a call, switch or loop");
|
||||
cursor += instruction.size();
|
||||
require(cursor < words.size(), "SPIR-V execution fell outside the function");
|
||||
}
|
||||
throw std::runtime_error("SPIR-V access trace exceeded its bounded instruction budget");
|
||||
}
|
||||
|
||||
private:
|
||||
std::span<const std::uint32_t> definition(std::uint32_t id) const {
|
||||
const auto found = definitions.find(id);
|
||||
require(found != definitions.end(), "SPIR-V access condition refers to an undefined value");
|
||||
return found->second;
|
||||
}
|
||||
|
||||
std::string rootName(std::uint32_t id) const {
|
||||
const auto instruction = definition(id);
|
||||
if ((instruction[0] & 0xffff) == spv::OpAccessChain) return rootName(instruction[3]);
|
||||
const auto found = names.find(id);
|
||||
return found == names.end() ? std::string() : found->second;
|
||||
}
|
||||
|
||||
std::uint32_t pointerStorage(std::uint32_t id) const {
|
||||
const auto instruction = definition(id);
|
||||
if ((instruction[0] & 0xffff) == spv::OpAccessChain) return pointerStorage(instruction[3]);
|
||||
require((instruction[0] & 0xffff) == spv::OpVariable, "access pointer has no variable root");
|
||||
return instruction[3];
|
||||
}
|
||||
|
||||
bool isGuestPointer(std::uint32_t id) const {
|
||||
const auto name = rootName(id);
|
||||
return name == "lds_dwords" || name == "gds" || name == "scratch_dwords" || name.starts_with("buffers");
|
||||
}
|
||||
|
||||
std::uint32_t value(std::uint32_t id) const {
|
||||
const auto instruction = definition(id);
|
||||
const auto opcode = static_cast<spv::Op>(instruction[0] & 0xffff);
|
||||
const auto argument = [&](std::size_t index) { return value(instruction[index]); };
|
||||
switch (opcode) {
|
||||
case spv::OpConstantTrue: return 1;
|
||||
case spv::OpConstantFalse: case spv::OpConstantNull: return 0;
|
||||
case spv::OpConstant: return instruction[3];
|
||||
case spv::OpSpecConstant: {
|
||||
const auto spec = specializations.at(id);
|
||||
if (spec == PipelineSpecialization::BufferBase) return 0;
|
||||
if (spec == PipelineSpecialization::BufferBase + 1) return 0x30001000;
|
||||
if (spec == PipelineSpecialization::BufferBase + 2) return valid;
|
||||
if (spec == PipelineSpecialization::BufferBase + 3) return misalignment;
|
||||
if (spec == PipelineSpecialization::PushDataOffset) return 0;
|
||||
throw std::runtime_error("unexpected specialization in access fixture");
|
||||
}
|
||||
case spv::OpSelect: return argument(3) ? argument(4) : argument(5);
|
||||
case spv::OpPhi: return value(phiValues.at(id));
|
||||
case spv::OpIAdd: return argument(3) + argument(4);
|
||||
case spv::OpISub: return argument(3) - argument(4);
|
||||
case spv::OpIMul: return argument(3) * argument(4);
|
||||
case spv::OpShiftLeftLogical:
|
||||
require(argument(4) < 32, "undefined shift in access condition");
|
||||
return argument(3) << argument(4);
|
||||
case spv::OpShiftRightLogical:
|
||||
require(argument(4) < 32, "undefined shift in access condition");
|
||||
return argument(3) >> argument(4);
|
||||
case spv::OpBitwiseAnd: return argument(3) & argument(4);
|
||||
case spv::OpBitwiseOr: return argument(3) | argument(4);
|
||||
case spv::OpBitFieldUExtract: {
|
||||
const auto count = argument(5);
|
||||
require(count <= 32 && argument(4) <= 32 - count, "invalid bitfield in access condition");
|
||||
return (argument(3) >> argument(4)) & (count == 32 ? 0xffffffffu : (1u << count) - 1);
|
||||
}
|
||||
case spv::OpIEqual: return argument(3) == argument(4);
|
||||
case spv::OpINotEqual: return argument(3) != argument(4);
|
||||
case spv::OpULessThan: return argument(3) < argument(4);
|
||||
case spv::OpULessThanEqual: return argument(3) <= argument(4);
|
||||
case spv::OpUGreaterThanEqual: return argument(3) >= argument(4);
|
||||
case spv::OpLogicalAnd: return argument(3) && argument(4);
|
||||
case spv::OpLogicalOr: return argument(3) || argument(4);
|
||||
case spv::OpLogicalNot: return !argument(3);
|
||||
case spv::OpArrayLength:
|
||||
require(isGuestPointer(instruction[3]), "unknown array length in access fixture");
|
||||
return 16;
|
||||
case spv::OpLoad:
|
||||
require(!isGuestPointer(instruction[3]), "guest data unexpectedly controls a fixture access");
|
||||
require(rootName(instruction[3]) == "vsharp" || rootName(instruction[3]).find("subgroup") != std::string::npos, "unknown input to access condition");
|
||||
return 0;
|
||||
default: throw std::runtime_error("unsupported opcode in access condition: " + std::to_string(opcode));
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<std::uint32_t>& words;
|
||||
std::uint32_t misalignment;
|
||||
bool valid;
|
||||
std::uint32_t entryFunction = 0;
|
||||
std::map<std::uint32_t, std::span<const std::uint32_t>> definitions;
|
||||
std::map<std::uint32_t, std::string> names;
|
||||
std::map<std::uint32_t, std::uint32_t> specializations;
|
||||
std::map<std::uint32_t, std::size_t> labels;
|
||||
std::map<std::uint32_t, std::uint32_t> phiValues;
|
||||
};
|
||||
|
||||
constexpr std::uint32_t Unreachable = 0xfffffffeu;
|
||||
|
||||
std::uint32_t ldsLength(const std::vector<std::uint32_t>& words) {
|
||||
std::map<std::uint32_t, std::span<const std::uint32_t>> definitions;
|
||||
std::uint32_t variable = 0;
|
||||
for (std::size_t offset = 5; offset < words.size(); offset += words[offset] >> 16) {
|
||||
const auto instruction = std::span(words).subspan(offset, words[offset] >> 16);
|
||||
const auto opcode = static_cast<spv::Op>(instruction[0] & 0xffff);
|
||||
if (opcode == spv::OpName && std::string(reinterpret_cast<const char*>(instruction.data() + 2)) == "lds_dwords") variable = instruction[1];
|
||||
bool result = false;
|
||||
bool type = false;
|
||||
spv::HasResultAndType(opcode, &result, &type);
|
||||
if (result) definitions.emplace(instruction[type ? 2 : 1], instruction);
|
||||
}
|
||||
require(variable != 0, "LDS array is missing");
|
||||
const auto pointer = definitions.at(definitions.at(variable)[1]);
|
||||
const auto array = definitions.at(pointer[3]);
|
||||
require((array[0] & 0xffff) == spv::OpTypeArray, "LDS variable is not an array");
|
||||
return definitions.at(array[3])[3];
|
||||
}
|
||||
|
||||
void expectReads(const LoadCase& test, bool robust, bool active, std::uint32_t offset, std::vector<std::uint32_t> expected, std::uint32_t records = 64, std::uint32_t misalignment = 0, bool valid = true) {
|
||||
const auto module = emitLoad(test, active, offset, records, robust);
|
||||
const auto actual = AccessTrace(module, misalignment, valid).Run();
|
||||
if (actual != expected) {
|
||||
std::string detail = std::string(robust ? "robust" : "plain") + " guest reads:";
|
||||
for (const auto index : actual) detail += " " + std::to_string(index);
|
||||
detail += "; expected:";
|
||||
for (const auto index : expected) detail += " " + std::to_string(index);
|
||||
throw std::runtime_error(detail);
|
||||
}
|
||||
}
|
||||
|
||||
void ldsLoads(IrOpcode opcode, IrType type, std::uint32_t bits, std::uint32_t components) {
|
||||
const LoadCase test{ResourceKind::Lds, opcode, type, bits, components};
|
||||
require(ldsLength(emitLoad(test, true, 8, 64, false)) == 16u, "LDS array declares more than the program's dwords");
|
||||
for (const bool robust : {false, true}) {
|
||||
expectReads(test, robust, false, 8, {});
|
||||
expectReads(test, robust, true, 84, {});
|
||||
std::vector<std::uint32_t> indices;
|
||||
for (std::uint32_t index = 0; index < components; ++index) indices.push_back(2 + index);
|
||||
expectReads(test, robust, true, 8, indices);
|
||||
expectReads(test, robust, true, 60, {15});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
unsigned failures = 0;
|
||||
const auto run = [&](std::string_view name, const auto& test) {
|
||||
try {
|
||||
test();
|
||||
std::cout << "guarded memory loads: " << name << " passed\n";
|
||||
} catch (const std::exception& error) {
|
||||
++failures;
|
||||
std::cout << "guarded memory loads: " << name << ": " << error.what() << '\n';
|
||||
}
|
||||
};
|
||||
run("LDS word keeps its branch", [] { ldsLoads(IrOpcode::LoadSharedU32, IrType::U32, 32, 1); });
|
||||
run("LDS subword keeps its branch", [] {
|
||||
ldsLoads(IrOpcode::LoadSharedU8, IrType::U8, 8, 1);
|
||||
ldsLoads(IrOpcode::LoadSharedU16, IrType::U16, 16, 1);
|
||||
});
|
||||
run("LDS wide keeps its branch", [] { ldsLoads(IrOpcode::LoadSharedU32x4, IrType::U32x4, 32, 4); });
|
||||
run("GDS word", [] {
|
||||
const LoadCase test{ResourceKind::Gds, IrOpcode::LoadSharedU32, IrType::U32, 32, 1};
|
||||
expectReads(test, false, false, 8, {});
|
||||
expectReads(test, false, true, 64, {});
|
||||
expectReads(test, false, true, 8, {2});
|
||||
expectReads(test, true, false, 8, {Unreachable});
|
||||
expectReads(test, true, true, 64, {Unreachable});
|
||||
expectReads(test, true, true, 8, {2});
|
||||
expectReads(test, true, true, 60, {15});
|
||||
});
|
||||
run("coherent buffer bounds", [] {
|
||||
const LoadCase test{ResourceKind::Buffer, IrOpcode::LoadBufferU32, IrType::U32};
|
||||
for (const bool robust : {false, true}) expectReads(test, robust, false, 8, {});
|
||||
expectReads(test, false, true, 64, {}, 128);
|
||||
expectReads(test, false, true, 60, {}, 32);
|
||||
expectReads(test, false, true, 8, {}, 64, 0, false);
|
||||
expectReads(test, true, true, 64, {Unreachable, Unreachable}, 128);
|
||||
expectReads(test, true, true, 60, {Unreachable, Unreachable}, 32);
|
||||
expectReads(test, true, true, 8, {Unreachable, Unreachable}, 64, 0, false);
|
||||
});
|
||||
run("coherent buffer high word", [] {
|
||||
const LoadCase test{ResourceKind::Buffer, IrOpcode::LoadBufferU32, IrType::U32};
|
||||
expectReads(test, false, true, 8, {2});
|
||||
expectReads(test, false, true, 8, {2, 3}, 64, 1);
|
||||
expectReads(test, false, true, 60, {}, 64, 1);
|
||||
expectReads(test, true, true, 8, {2, 2});
|
||||
expectReads(test, true, true, 8, {2, 3}, 64, 1);
|
||||
expectReads(test, true, true, 60, {Unreachable, Unreachable}, 64, 1);
|
||||
});
|
||||
run("private scratch selection", [] {
|
||||
const LoadCase test{ResourceKind::Scratch, IrOpcode::LoadAddressU32, IrType::U32};
|
||||
for (const bool robust : {false, true}) {
|
||||
expectReads(test, robust, false, 8, {0});
|
||||
expectReads(test, robust, true, 64, {0});
|
||||
expectReads(test, robust, true, 8, {2});
|
||||
}
|
||||
});
|
||||
if (failures != 0) {
|
||||
std::cerr << "guarded memory loads: a guest load reached a location the guarded program could not\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "Guarded memory load tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
#include "Recompiler.hpp"
|
||||
#include <spirv/unified1/spirv.hpp>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace ShaderRecompiler;
|
||||
|
||||
void require(bool condition, const std::string& message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
std::uint64_t workgroupBytes(std::span<const std::uint32_t> words) {
|
||||
std::map<std::uint32_t, std::span<const std::uint32_t>> types;
|
||||
std::map<std::uint32_t, std::uint32_t> constants;
|
||||
std::vector<std::uint32_t> pointers;
|
||||
for (std::size_t offset = 5; offset < words.size();) {
|
||||
const auto count = words[offset] >> 16u;
|
||||
require(count != 0u && count <= words.size() - offset, "malformed SPIR-V instruction");
|
||||
const auto instruction = words.subspan(offset, count);
|
||||
const auto opcode = static_cast<spv::Op>(instruction[0] & 0xffffu);
|
||||
if (opcode == spv::OpTypeInt || opcode == spv::OpTypeFloat || opcode == spv::OpTypeVector || opcode == spv::OpTypeArray || opcode == spv::OpTypeStruct || opcode == spv::OpTypePointer) types.emplace(instruction[1], instruction);
|
||||
if (opcode == spv::OpConstant) constants.emplace(instruction[2], instruction[3]);
|
||||
if (opcode == spv::OpVariable && instruction[3] == spv::StorageClassWorkgroup) pointers.push_back(instruction[1]);
|
||||
offset += count;
|
||||
}
|
||||
const std::function<std::uint64_t(std::uint32_t)> size = [&](std::uint32_t id) -> std::uint64_t {
|
||||
const auto type = types.at(id);
|
||||
switch (static_cast<spv::Op>(type[0] & 0xffffu)) {
|
||||
case spv::OpTypeInt: case spv::OpTypeFloat: return type[2] / 8u;
|
||||
case spv::OpTypeVector: return size(type[2]) * type[3];
|
||||
case spv::OpTypeArray: return size(type[2]) * constants.at(type[3]);
|
||||
case spv::OpTypeStruct: {
|
||||
std::uint64_t bytes = 0;
|
||||
for (const auto member : type.subspan(2)) bytes += size(member);
|
||||
return bytes;
|
||||
}
|
||||
default: throw std::runtime_error("unexpected Workgroup variable type");
|
||||
}
|
||||
};
|
||||
std::uint64_t bytes = 0;
|
||||
for (const auto pointer : pointers) {
|
||||
const auto type = types.at(pointer);
|
||||
require((type[0] & 0xffffu) == spv::OpTypePointer, "Workgroup variable is not a pointer");
|
||||
bytes += size(type[3]);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit) {
|
||||
static constexpr std::array<std::uint32_t, 8> code{
|
||||
0x7e020280u, 0x7e0402c1u, 0x7e060280u, 0x7e080281u,
|
||||
0xd9000000u, 0x00000201u, 0xbf8cc07fu, 0xbf810000u
|
||||
};
|
||||
static constexpr std::array<std::uint32_t, 7> capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess};
|
||||
static constexpr std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
|
||||
static constexpr std::array<std::uint32_t, 4> userData{};
|
||||
RecompileRequest request{};
|
||||
request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}};
|
||||
request.context.waveSize = waveSize;
|
||||
request.context.userData = userData;
|
||||
request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 16u, {false, false, false}, false, 1u};
|
||||
request.target.vulkanVersion = 0x00403000u;
|
||||
request.target.spirvVersion = 0x00010600u;
|
||||
request.target.subgroupSize = 32;
|
||||
request.target.bdaAbiVersion = 1;
|
||||
request.target.supportedCapabilities = capabilities;
|
||||
request.target.supportedExtensions = extensions;
|
||||
request.target.maxWorkgroupSize = {1024u, 1024u, 64u};
|
||||
request.target.maxWorkgroupInvocations = 1024;
|
||||
request.target.maxWorkgroupSharedMemoryBytes = limit;
|
||||
request.layout = {0, 0, 0, 128};
|
||||
request.useCache = false;
|
||||
return Recompile(request);
|
||||
}
|
||||
|
||||
void verify(std::uint32_t waveSize, std::uint32_t limit) {
|
||||
const auto result = compile(waveSize, limit);
|
||||
require(!result.spirv.empty(), "LDS atomic program did not compile");
|
||||
const auto declared = workgroupBytes(result.spirv.Words());
|
||||
require(declared <= limit, "declared " + std::to_string(declared) + " Workgroup bytes exceeds limit " + std::to_string(limit));
|
||||
require(result.workgroupMemoryDwords == (limit < 68u ? 17u : 0u), "incorrect device-memory LDS stride at limit " + std::to_string(limit));
|
||||
if (limit >= 68u) require(declared != 0u, "in-budget LDS unexpectedly lost its Workgroup allocation");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
unsigned failures = 0;
|
||||
for (const auto waveSize : {32u, 64u}) {
|
||||
for (const auto limit : {64u, 68u, 72u}) {
|
||||
const auto name = "wave" + std::to_string(waveSize) + " limit" + std::to_string(limit);
|
||||
try {
|
||||
verify(waveSize, limit);
|
||||
std::cout << "LDS lock budget: " << name << " passed\n";
|
||||
} catch (const std::exception& error) {
|
||||
++failures;
|
||||
const std::string message(error.what());
|
||||
std::cerr << "LDS lock budget: " << name << ": " << message.substr(0, message.find("RecompileRequest:")) << '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failures != 0) return 1;
|
||||
std::cout << "LDS lock budget tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include "ResidentPresent.hpp"
|
||||
#include "ImageTableAbi.hpp"
|
||||
#include "SampleLod_spv.h"
|
||||
#include "BufferUpdate_spv.h"
|
||||
#include "SampleArray_spv.h"
|
||||
@@ -311,6 +312,28 @@ public:
|
||||
extensionsEnabled.insert(extensionsEnabled.end(), libraryExtensions.begin(), libraryExtensions.end());
|
||||
address.pNext = &library;
|
||||
}
|
||||
VkPhysicalDeviceDescriptorIndexingFeaturesEXT indexing{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT};
|
||||
if (hasExtension(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME)) {
|
||||
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &indexing};
|
||||
function<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(context.physical, &features);
|
||||
context.descriptorIndexing = indexing.shaderSampledImageArrayNonUniformIndexing == VK_TRUE && indexing.shaderStorageImageArrayNonUniformIndexing == VK_TRUE;
|
||||
context.partiallyBound = indexing.descriptorBindingPartiallyBound == VK_TRUE;
|
||||
context.variableDescriptorCount = indexing.descriptorBindingVariableDescriptorCount == VK_TRUE;
|
||||
context.runtimeDescriptorArray = indexing.runtimeDescriptorArray == VK_TRUE;
|
||||
}
|
||||
indexing = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT};
|
||||
if (context.descriptorIndexing && context.partiallyBound && context.variableDescriptorCount && context.runtimeDescriptorArray) {
|
||||
extensionsEnabled.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME);
|
||||
indexing.shaderSampledImageArrayNonUniformIndexing = VK_TRUE;
|
||||
indexing.shaderStorageImageArrayNonUniformIndexing = VK_TRUE;
|
||||
indexing.descriptorBindingPartiallyBound = VK_TRUE;
|
||||
indexing.descriptorBindingVariableDescriptorCount = VK_TRUE;
|
||||
indexing.runtimeDescriptorArray = VK_TRUE;
|
||||
indexing.pNext = address.pNext;
|
||||
address.pNext = &indexing;
|
||||
} else {
|
||||
context.descriptorIndexing = context.partiallyBound = context.variableDescriptorCount = context.runtimeDescriptorArray = false;
|
||||
}
|
||||
VkPhysicalDeviceRobustness2FeaturesEXT robustness{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT};
|
||||
if (hasExtension(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME)) {
|
||||
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &robustness};
|
||||
@@ -339,6 +362,7 @@ public:
|
||||
std::cout << "Vulkan device: " << properties.deviceName << '\n';
|
||||
context.limits = properties.limits;
|
||||
context.bufferDeviceAddress = true;
|
||||
context.imageTableCapacity = ImageTableCapacity(context);
|
||||
context.formatProperties = function<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties");
|
||||
context.imageFormatProperties = function<PFN_vkGetPhysicalDeviceImageFormatProperties>("vkGetPhysicalDeviceImageFormatProperties");
|
||||
context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue);
|
||||
@@ -2484,6 +2508,7 @@ public:
|
||||
struct Contents {
|
||||
std::map<std::pair<std::uint32_t, std::uint32_t>, VkDescriptorBufferInfo> buffers;
|
||||
std::map<std::pair<std::uint32_t, std::uint32_t>, VkDescriptorImageInfo> images;
|
||||
std::map<std::pair<VkDescriptorSet, std::uint32_t>, std::uint32_t> written;
|
||||
};
|
||||
explicit DescriptorUpdates(Context& context) : context(context), previous(context.functions),
|
||||
update(context.Resolved(&DeviceFunctions::updateDescriptorSets, "vkUpdateDescriptorSets")) {
|
||||
@@ -2502,6 +2527,7 @@ private:
|
||||
Contents contents;
|
||||
for (std::uint32_t i = 0; i < count; ++i) {
|
||||
const auto& write = writes[i];
|
||||
contents.written[{write.dstSet, write.dstBinding}] += write.descriptorCount;
|
||||
for (std::uint32_t element = 0; element < write.descriptorCount; ++element) {
|
||||
const auto key = std::pair{write.dstBinding, write.dstArrayElement + element};
|
||||
if (write.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) contents.buffers[key] = write.pBufferInfo[element];
|
||||
@@ -2572,6 +2598,85 @@ void drawDescriptorWriteTests(const Device& device, Recorder& recorder) {
|
||||
recorder.Activate();
|
||||
}
|
||||
|
||||
void drawSnapshotTableSetTests(const Device& device, Recorder& recorder) {
|
||||
namespace Abi = ShaderRecompiler::ImageTableAbi;
|
||||
using ShaderRecompiler::DescriptorKind;
|
||||
using ShaderRecompiler::DescriptorRole;
|
||||
const auto& base = device.GetContext();
|
||||
TextureDetiler detiler(base);
|
||||
auto context = base;
|
||||
context.detiler = &detiler;
|
||||
if (context.imageTableCapacity < 2u) {
|
||||
std::cout << "image tables unavailable: draw snapshot table sets not tested\n";
|
||||
return;
|
||||
}
|
||||
constexpr std::uint32_t width = 4;
|
||||
constexpr std::uint32_t height = 4;
|
||||
alignas(256) static std::array<float, width * height> texels{};
|
||||
texels.fill(1.0f);
|
||||
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(texels.data()));
|
||||
const std::array<std::uint32_t, 8> texture{static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (22u << 20u) | (((width - 1u) & 3u) << 30u), ((width - 1u) >> 2u) | ((height - 1u) << 14u), 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u};
|
||||
constexpr auto bindingCount = static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::Count);
|
||||
const auto addTable = [&](ShaderRecompiler::RecompileResult& program, ShaderRecompiler::RuntimeAbi::Stage stage, std::uint32_t set, std::uint32_t elements) {
|
||||
ShaderRecompiler::DescriptorBinding map{};
|
||||
map.kind = DescriptorKind::StorageBuffer;
|
||||
map.role = DescriptorRole::ImageTableMap;
|
||||
map.descriptorSet = set;
|
||||
map.binding = static_cast<std::uint32_t>(stage) * bindingCount + static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap);
|
||||
map.count = 1;
|
||||
map.guestDescriptor.assign(Abi::HeaderWords + elements, 0u);
|
||||
map.guestDescriptor[Abi::HeaderVersion] = Abi::Version;
|
||||
map.guestDescriptor[Abi::HeaderElementCount] = elements;
|
||||
map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords;
|
||||
map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords + elements;
|
||||
program.bindings.push_back(map);
|
||||
ShaderRecompiler::DescriptorBinding images{};
|
||||
images.kind = DescriptorKind::SampledImage;
|
||||
images.role = DescriptorRole::ImageTable;
|
||||
images.descriptorSet = set;
|
||||
images.binding = ShaderRecompiler::RuntimeAbi::ImageTableBinding;
|
||||
images.count = elements;
|
||||
for (std::uint32_t element = 0; element < elements; ++element) images.guestDescriptor.insert(images.guestDescriptor.end(), texture.begin(), texture.end());
|
||||
images.imageShapes.assign(elements, ShaderRecompiler::DescriptorImageShape::Image2D);
|
||||
images.imageDepthCompare.assign(elements, false);
|
||||
images.imageSamplers.assign(elements, 0u);
|
||||
program.bindings.push_back(std::move(images));
|
||||
};
|
||||
DescriptorUpdates updates(context);
|
||||
{
|
||||
DescriptorCache cache(context);
|
||||
context.descriptorCache = &cache;
|
||||
Recorder snapshots(context);
|
||||
snapshots.Activate();
|
||||
ShaderRecompiler::RecompileResult vertex, fragment;
|
||||
ShaderRecompiler::DescriptorBinding data{};
|
||||
data.kind = DescriptorKind::StorageBuffer;
|
||||
data.role = DescriptorRole::FlattenedSrt;
|
||||
data.binding = static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::FlattenedSrt);
|
||||
data.count = 1;
|
||||
data.guestDescriptor = {1, 2, 3, 4};
|
||||
vertex.bindings.push_back(data);
|
||||
addTable(vertex, ShaderRecompiler::RuntimeAbi::Stage::Main, 0u, 1u);
|
||||
addTable(fragment, ShaderRecompiler::RuntimeAbi::Stage::Fragment, ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet, 2u);
|
||||
const ColorTarget target{};
|
||||
ShaderResources resources(context, vertex, fragment, target, 0, 0);
|
||||
Require(resources.SplitSets(), "a fragment image table did not get its own descriptor set");
|
||||
const ShaderResources::MovedBuffer moved{0, 0, 16, {5, 6, 7, 8}};
|
||||
const auto draw = resources.PrepareDrawBindings(snapshots, std::span(&moved, 1));
|
||||
Require(draw != nullptr && draw->snapshots.size() == 1, "moved shader data did not create a draw snapshot");
|
||||
const auto& written = updates.calls.back().written;
|
||||
const auto tableWrites = [&](VkDescriptorSet set) {
|
||||
const auto it = written.find({set, ShaderRecompiler::RuntimeAbi::ImageTableBinding});
|
||||
return it == written.end() ? 0u : it->second;
|
||||
};
|
||||
Require(tableWrites(draw->allocation.set) == 1u, "the draw snapshot wrote the wrong number of image table descriptors into the vertex set");
|
||||
Require(tableWrites(draw->fragmentAllocation.set) == 2u, "the draw snapshot wrote the wrong number of image table descriptors into the fragment set");
|
||||
snapshots.Sync();
|
||||
}
|
||||
ClearCachedTextures(context.device);
|
||||
recorder.Activate();
|
||||
}
|
||||
|
||||
void drawSnapshotReuseTests(const Device& device, Recorder& recorder) {
|
||||
const auto& context = device.GetContext();
|
||||
constexpr std::size_t bytes = 65536;
|
||||
@@ -8419,6 +8524,7 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--draw-descriptors-only") {
|
||||
drawDescriptorWriteTests(device, recorder);
|
||||
drawSnapshotTableSetTests(device, recorder);
|
||||
std::cout << "Draw descriptor writes and snapshot overrides passed\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -8476,6 +8582,7 @@ int main(int argc, char** argv) {
|
||||
misalignedRegionTests(device, recorder);
|
||||
drawSnapshotReuseTests(device, recorder);
|
||||
drawDescriptorWriteTests(device, recorder);
|
||||
drawSnapshotTableSetTests(device, recorder);
|
||||
drawSnapshotCapTests(device, recorder);
|
||||
drawSnapshotEvictionTests(device);
|
||||
unwrittenSnapshotTests(device);
|
||||
|
||||
@@ -649,6 +649,7 @@ void verifyKeySensitivity() {
|
||||
changes("f64 denormal preservation", [](SampleRequest& sample) { sample.request.target.float64DenormPreserve = true; });
|
||||
changes("f16 denormal preservation", [](SampleRequest& sample) { sample.request.target.float16DenormPreserve = true; });
|
||||
changes("spanned waves", [](SampleRequest& sample) { sample.request.target.spannedWaves = true; });
|
||||
changes("robust buffer access 2", [](SampleRequest& sample) { sample.request.target.robustBufferAccess2 = true; });
|
||||
changes("the workgroup size limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSize[2] = 128; });
|
||||
changes("the invocation limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupInvocations = 512; });
|
||||
changes("the shared memory limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSharedMemoryBytes = 32768; });
|
||||
@@ -1505,6 +1506,10 @@ std::vector<PlanFixture> planFixtures() {
|
||||
add("bindless material table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x93109010u, 0xf4200406u, 0x20000004u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}});
|
||||
add("bindless whole table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}});
|
||||
add("bindless sample in a loop latch", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xbea80380u, 0xf09c0f08u, 0x00450000u, 0x80288128u, 0xbf0a8428u, 0xbf85fffbu, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}});
|
||||
auto keyed = tables;
|
||||
keyed.resize(20u, 0u);
|
||||
keyed[16] = 3u;
|
||||
add("bindless selected record", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0xf4000400u, 0xfa000040u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, keyed}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}});
|
||||
|
||||
std::vector<std::uint32_t> phis(32, 0u);
|
||||
place(phis, 0, imageWords(PlanTextureAddress));
|
||||
@@ -1650,6 +1655,8 @@ IrResourcePlan handBuiltPlan() {
|
||||
table.dwordCount = 8u;
|
||||
table.dwords[0] = &readConst(3);
|
||||
table.tableColumn = TableColumn{2u, 32u, 16u, 4u, 8u, 0xffu, false, true, {KeyDomain{3u, 4u, 16u, false}, KeyDomain{2u, 8u, 4u, true}}, false};
|
||||
table.tableColumn->selected = true;
|
||||
table.tableColumn->selection = 2u;
|
||||
DescriptorSource heap;
|
||||
heap.dwordCount = 4u;
|
||||
heap.dwords[1] = &readConst(6);
|
||||
@@ -1898,6 +1905,7 @@ void verifyMalformedPlans() {
|
||||
rejects("a uniform fill of five words", [](IrResourcePlan& plan) { plan.uniformFill.fill.words = 5u; });
|
||||
rejects("an image table heap past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].tableColumn->heapSource = 4u; });
|
||||
rejects("an image table key past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].tableColumn->keyDomains[1].source = 4u; });
|
||||
rejects("an image table selection past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].tableColumn->selection = 4u; });
|
||||
auto foreign = handBuiltPlan();
|
||||
IrValue stray(IrOpcode::Void, IrType::U32, 99u);
|
||||
foreign.srtReads[0].value = &stray;
|
||||
@@ -1957,6 +1965,7 @@ void verifyPlanRoundTrip() {
|
||||
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && source.tableColumn->keyDomains.empty(); }); }), "no fixture plans a whole image table");
|
||||
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && !source.tableColumn->keyDomains.empty(); }); }), "no fixture plans an image table with a key domain");
|
||||
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && source.tableColumn->address; }); }), "no fixture plans an image table through a pointer");
|
||||
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && source.tableColumn->selected; }); }), "no fixture plans a selected image table record");
|
||||
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.controlFlow.begin(), plan.controlFlow.end(), [](const ResourceBlock& block) { return block.condition != nullptr; }); }), "no fixture plans a control-flow condition");
|
||||
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.valueStorage.begin(), plan.valueStorage.end(), [](const auto& value) { return value.IsPhi(); }); }), "no fixture plans a phi");
|
||||
require(covers([](const IrResourcePlan& plan) { return !plan.guardedSrtSlots.empty(); }), "no fixture plans a guarded SRT read");
|
||||
|
||||
@@ -1056,6 +1056,7 @@ struct TableGuest {
|
||||
std::array<std::uint32_t, 20> srt{};
|
||||
};
|
||||
|
||||
constexpr std::uint32_t TableFormat16UNorm = 7;
|
||||
constexpr std::uint32_t TableFormat8888UNorm = 56;
|
||||
constexpr std::uint32_t TableFormat32UInt = 20;
|
||||
constexpr std::uint32_t TableFormat16SInt = 12;
|
||||
@@ -1135,6 +1136,13 @@ TableCompiled CompileTable(ShaderRecompiler::RecompileRequest request) {
|
||||
return out;
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 8> TableSelected(const ShaderRecompiler::ImageTableSnapshot& tables, std::uint32_t table, const char* message) {
|
||||
namespace Abi = ShaderRecompiler::ImageTableAbi;
|
||||
const auto& column = tables.tables.at(table);
|
||||
require(column.records == 1u && column.keys == 1u && column.codes.size() == 1u && (column.codes[0] & Abi::PoisonFlag) == 0u, message);
|
||||
return tables.words.at(column.codes[0]).dwords;
|
||||
}
|
||||
|
||||
void verifyImageTableKeys() {
|
||||
using namespace ShaderRecompiler;
|
||||
namespace Abi = ImageTableAbi;
|
||||
@@ -1484,7 +1492,6 @@ void verifyImageTables() {
|
||||
|
||||
void verifyImageTableKeyUnion() {
|
||||
using namespace ShaderRecompiler;
|
||||
namespace Abi = ImageTableAbi;
|
||||
static TableTexture textures[3];
|
||||
alignas(4096) static TableGuest guest;
|
||||
auto& heap = guest.heap;
|
||||
@@ -1499,24 +1506,25 @@ void verifyImageTableKeyUnion() {
|
||||
const auto userData = TableUserData(guest);
|
||||
const auto request = TableRequest(TableTwoKeyCode, userData);
|
||||
const auto plan = GetResourcePlan(request);
|
||||
require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "image table key union: two keyed reads of one table were not planned as one image of one column");
|
||||
const auto& column = plan->descriptorSources.at(plan->info.images[0].source).tableColumn;
|
||||
require(column.has_value() && column->keyDomains.size() == 2u && column->keyDomains[0].value && column->keyDomains[1].value && column->keyDomains[0].source != column->keyDomains[1].source, "image table key union: the column does not carry both value key domains");
|
||||
require(plan->info.images.size() == 2u && plan->info.images[0].table == 0u && plan->info.images[1].table == 1u, "image table key union: two keyed reads of one heap were not planned as two tables");
|
||||
const auto& first = plan->descriptorSources.at(plan->info.images[0].source).tableColumn;
|
||||
const auto& second = plan->descriptorSources.at(plan->info.images[1].source).tableColumn;
|
||||
require(first.has_value() && second.has_value() && first->selected && second->selected && first->heapSource == second->heapSource && first->selection != second->selection, "image table key union: the two keys do not each select a record of the one heap");
|
||||
const auto held = [](const ImageTableSnapshot& tables, std::uint32_t table) { return TableSelected(tables, table, "image table key union: a table does not hold one readable selected record"); };
|
||||
const auto compiled = CompileTable(request);
|
||||
const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0);
|
||||
const auto outside = Abi::PoisonCode(static_cast<std::uint32_t>(Abi::PoisonReason::OutsideDomain));
|
||||
require(compiled.capture->snapshot.tables.tables.size() == 1u && snapshot.keys == 4u && (snapshot.codes[0] & Abi::PoisonFlag) == 0u && (snapshot.codes[1] & Abi::PoisonFlag) == 0u && snapshot.codes[2] == outside && (snapshot.codes[3] & Abi::PoisonFlag) == 0u, "image table key union: the snapshot does not hold the union of the two keys' records");
|
||||
const auto& tables = compiled.capture->snapshot.tables;
|
||||
require(tables.tables.size() == 2u && held(tables, 0u) == heap[1] && held(tables, 1u) == heap[3], "image table key union: the tables do not hold the two keys' records");
|
||||
require(TableCovered(compiled.regions, &heap[1], sizeof(heap[1])) && TableCovered(compiled.regions, &heap[3], sizeof(heap[3])) && !TableCovered(compiled.regions, &heap[0], sizeof(std::uint32_t)) && !TableCovered(compiled.regions, &heap[2], sizeof(std::uint32_t)), "image table key union: records no key selects were captured");
|
||||
const auto* images = TableBinding(*compiled.result, DescriptorRole::ImageTable);
|
||||
require(images != nullptr && images->count == 3u, "image table key union: the binding does not hold the three distinct images of the selected records");
|
||||
require(images != nullptr && images->count == 2u, "image table key union: the binding does not hold the two distinct images of the selected records");
|
||||
guest.srt[17] = 2u;
|
||||
const auto moved = CompileTable(request);
|
||||
const auto& movedSnapshot = moved.capture->snapshot.tables.tables.at(0);
|
||||
require((movedSnapshot.codes[2] & Abi::PoisonFlag) == 0u && movedSnapshot.codes[3] == outside && (movedSnapshot.codes[1] & Abi::PoisonFlag) == 0u && moved.result->spirv == compiled.result->spirv, "image table key union: a changed second key did not move the union or changed the module");
|
||||
const auto& movedTables = moved.capture->snapshot.tables;
|
||||
require(held(movedTables, 0u) == heap[1] && held(movedTables, 1u) == heap[2] && TableCovered(moved.regions, &heap[2], sizeof(heap[2])) && !TableCovered(moved.regions, &heap[3], sizeof(std::uint32_t)) && moved.result->spirv == compiled.result->spirv, "image table key union: a changed second key did not move its record alone or changed the module");
|
||||
}
|
||||
|
||||
void verifyImageTableLateKey() {
|
||||
using namespace ShaderRecompiler;
|
||||
namespace Abi = ImageTableAbi;
|
||||
static TableTexture textures[3];
|
||||
alignas(4096) static TableGuest guest;
|
||||
auto& heap = guest.heap;
|
||||
@@ -1531,15 +1539,16 @@ void verifyImageTableLateKey() {
|
||||
const auto userData = TableUserData(guest);
|
||||
const auto request = TableRequest(TableLateKeyCode, userData);
|
||||
const auto plan = GetResourcePlan(request);
|
||||
require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "image table late key: two keyed reads of one table were not planned as one image of one column");
|
||||
const auto& column = plan->descriptorSources.at(plan->info.images[0].source).tableColumn;
|
||||
require(column.has_value() && column->keyDomains.size() == 2u && column->keyDomains[0].value && column->keyDomains[1].value && column->keyDomains[0].source != column->keyDomains[1].source, "image table late key: the column does not carry both value key domains");
|
||||
require(plan->info.images.size() == 2u && plan->info.images[0].table == 0u && plan->info.images[1].table == 1u, "image table late key: two keyed reads of one heap were not planned as two tables");
|
||||
const auto& first = plan->descriptorSources.at(plan->info.images[0].source).tableColumn;
|
||||
const auto& second = plan->descriptorSources.at(plan->info.images[1].source).tableColumn;
|
||||
require(first.has_value() && second.has_value() && first->selected && second->selected && first->selection != second->selection, "image table late key: a key read after the first image operation does not select its own record");
|
||||
const auto held = [](const ImageTableSnapshot& tables, std::uint32_t table) { return TableSelected(tables, table, "image table late key: a table does not hold one readable selected record"); };
|
||||
const auto compiled = CompileTable(request);
|
||||
const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0);
|
||||
const auto outside = Abi::PoisonCode(static_cast<std::uint32_t>(Abi::PoisonReason::OutsideDomain));
|
||||
require(compiled.capture->snapshot.tables.tables.size() == 1u && snapshot.keys == 4u && (snapshot.codes[0] & Abi::PoisonFlag) == 0u && (snapshot.codes[1] & Abi::PoisonFlag) == 0u && snapshot.codes[2] == outside && (snapshot.codes[3] & Abi::PoisonFlag) == 0u, "image table late key: the snapshot does not hold the union of the two keys' records");
|
||||
const auto& tables = compiled.capture->snapshot.tables;
|
||||
require(tables.tables.size() == 2u && held(tables, 0u) == heap[1] && held(tables, 1u) == heap[3], "image table late key: the tables do not hold the two keys' records");
|
||||
const auto* images = TableBinding(*compiled.result, DescriptorRole::ImageTable);
|
||||
require(images != nullptr && images->count == 3u, "image table late key: the binding does not hold the three distinct images of the selected records");
|
||||
require(images != nullptr && images->count == 2u, "image table late key: the binding does not hold the two distinct images of the selected records");
|
||||
}
|
||||
|
||||
void verifyImageTableUnkeyedAccess() {
|
||||
@@ -1562,13 +1571,19 @@ void verifyImageTableUnkeyedAccess() {
|
||||
const auto userData = TableUserData(guest);
|
||||
const auto request = TableRequest(*code, userData);
|
||||
const auto plan = GetResourcePlan(request);
|
||||
require(!plan->info.images.empty() && std::all_of(plan->info.images.begin(), plan->info.images.end(), [](const ImageResource& image) { return image.table == 0u; }), "image table unkeyed access: the two reads were not planned as one table");
|
||||
const auto& column = plan->descriptorSources.at(plan->info.images[0].source).tableColumn;
|
||||
require(column.has_value() && column->unkeyed && column->keyDomains.empty(), "image table unkeyed access: a column shared with a read that has no key domain kept a key domain");
|
||||
const auto& planned = plan->info.images;
|
||||
require(planned.size() == 2u && std::all_of(planned.begin(), planned.end(), [](const ImageResource& image) { return image.table != NoTable; }), "image table unkeyed access: the two reads were not planned as two tables");
|
||||
const auto columnOf = [&](const ImageResource& image) { return *plan->descriptorSources.at(image.source).tableColumn; };
|
||||
const auto selected = std::find_if(planned.begin(), planned.end(), [&](const ImageResource& image) { return columnOf(image).selected; });
|
||||
const auto whole = std::find_if(planned.begin(), planned.end(), [&](const ImageResource& image) { return !columnOf(image).selected; });
|
||||
require(selected != planned.end() && whole != planned.end(), "image table unkeyed access: the read keyed through the SRT does not select its record beside the read keyed per lane");
|
||||
require(columnOf(*whole).unkeyed && columnOf(*whole).keyDomains.empty(), "image table unkeyed access: the read keyed per lane kept a key domain");
|
||||
const auto compiled = CompileTable(request);
|
||||
const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0);
|
||||
const auto& tables = compiled.capture->snapshot.tables;
|
||||
require(TableSelected(tables, selected->table, "image table unkeyed access: a table does not hold one readable selected record") == heap[2], "image table unkeyed access: the read keyed through the SRT does not hold its record");
|
||||
const auto& snapshot = tables.tables.at(whole->table);
|
||||
const auto outside = Abi::PoisonCode(static_cast<std::uint32_t>(Abi::PoisonReason::OutsideDomain));
|
||||
require(snapshot.keys == 4u && std::none_of(snapshot.codes.begin(), snapshot.codes.end(), [&](std::uint32_t code) { return code == outside; }), "image table unkeyed access: the shared column was narrowed by the keyed read");
|
||||
require(snapshot.keys == 4u && std::none_of(snapshot.codes.begin(), snapshot.codes.end(), [&](std::uint32_t entry) { return entry == outside; }), "image table unkeyed access: the table keyed per lane was narrowed");
|
||||
const auto* images = TableBinding(*compiled.result, DescriptorRole::ImageTable);
|
||||
require(images != nullptr && images->count == 4u, "image table unkeyed access: the binding does not hold the whole domain");
|
||||
}
|
||||
@@ -1592,18 +1607,19 @@ void verifyImageTableSrtKey() {
|
||||
const auto plan = GetResourcePlan(request);
|
||||
require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "image table SRT key: the T# was not planned as a table");
|
||||
const auto& column = plan->descriptorSources.at(plan->info.images[0].source).tableColumn;
|
||||
require(column.has_value() && !column->keyDomains.empty(), "image table SRT key: a key read through the SRT has no key domain");
|
||||
require(column.has_value() && column->selected && column->selection < plan->descriptorSources.size(), "image table SRT key: a T# whose key is read through the SRT does not select its record");
|
||||
const auto compiled = CompileTable(request);
|
||||
const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0);
|
||||
const auto outside = Abi::PoisonCode(static_cast<std::uint32_t>(Abi::PoisonReason::OutsideDomain));
|
||||
require(snapshot.records == 4u && snapshot.keys == 4u && snapshot.codes.size() == 4u && (snapshot.codes[0] & Abi::PoisonFlag) == 0u && snapshot.codes[1] == outside && (snapshot.codes[2] & Abi::PoisonFlag) == 0u && snapshot.codes[3] == outside, "image table SRT key: the snapshot was not narrowed to the key read through the SRT");
|
||||
const auto& tables = compiled.capture->snapshot.tables;
|
||||
const auto& snapshot = tables.tables.at(0);
|
||||
require(snapshot.records == 1u && snapshot.keys == 1u && snapshot.codes.size() == 1u && (snapshot.codes[0] & Abi::PoisonFlag) == 0u && tables.words.at(snapshot.codes[0]).dwords == heap[2], "image table SRT key: the snapshot does not hold the selected record alone");
|
||||
const auto* images = TableBinding(*compiled.result, DescriptorRole::ImageTable);
|
||||
require(images != nullptr && images->count == 2u, "image table SRT key: the binding holds other entries than the selected one and the first");
|
||||
require(TableCovered(compiled.regions, &guest.srt[16], sizeof(std::uint32_t)), "image table SRT key: the key was not captured");
|
||||
require(images != nullptr && images->count == 1u, "image table SRT key: the binding holds other entries than the selected one");
|
||||
require(TableCovered(compiled.regions, &guest.srt[16], sizeof(std::uint32_t)) && TableCovered(compiled.regions, &heap[2], sizeof(heap[2])), "image table SRT key: the key or the selected record was not captured");
|
||||
require(!TableCovered(compiled.regions, &heap[0], sizeof(std::uint32_t)) && !TableCovered(compiled.regions, &heap[1], sizeof(std::uint32_t)) && !TableCovered(compiled.regions, &heap[3], sizeof(std::uint32_t)), "image table SRT key: records the key does not select were captured");
|
||||
guest.srt[16] = 3u;
|
||||
const auto moved = CompileTable(request);
|
||||
const auto& movedSnapshot = moved.capture->snapshot.tables.tables.at(0);
|
||||
require((movedSnapshot.codes[3] & Abi::PoisonFlag) == 0u && movedSnapshot.codes[2] == outside && movedSnapshot.codes[1] == outside && moved.result->spirv == compiled.result->spirv && moved.key == compiled.key, "image table SRT key: a changed key did not move the snapshot alone");
|
||||
const auto& movedTables = moved.capture->snapshot.tables;
|
||||
require(movedTables.words.at(movedTables.tables.at(0).codes.at(0)).dwords == heap[3] && TableCovered(moved.regions, &heap[3], sizeof(heap[3])) && !TableCovered(moved.regions, &heap[2], sizeof(std::uint32_t)) && moved.result->spirv == compiled.result->spirv && moved.key == compiled.key, "image table SRT key: a changed key did not move the selected record alone");
|
||||
}
|
||||
|
||||
void verifyImageTableMaskedOffset() {
|
||||
@@ -1904,6 +1920,281 @@ void verifyPointerTableOffsetCarry() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void verifySelectedRecordFallbacks() {
|
||||
using namespace ShaderRecompiler;
|
||||
namespace Abi = ImageTableAbi;
|
||||
static TableTexture textures[2];
|
||||
alignas(4096) static TableGuest guest;
|
||||
auto& heap = guest.heap;
|
||||
std::fill(heap.begin(), heap.end(), std::array<std::uint32_t, 8>{});
|
||||
heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D);
|
||||
heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D);
|
||||
heap[2] = heap[1];
|
||||
heap[3] = heap[0];
|
||||
FillTableSrt(guest, heap.data(), 32u, 4u);
|
||||
guest.materials = {};
|
||||
guest.srt[16] = 2u;
|
||||
const auto userData = TableUserData(guest);
|
||||
const auto request = TableRequest(TableSrtKeyCode, userData);
|
||||
const auto valid = CompileTable(request);
|
||||
|
||||
heap[2][3] &= 0x0fffffffu;
|
||||
const auto invalid = CompileTable(request);
|
||||
const auto& invalidMap = TableMap(*invalid.result);
|
||||
const auto invalidCodes = TableCodes(invalidMap, 0);
|
||||
require(invalidCodes.size() == 1u && invalidCodes[0] != Abi::NullCode && (invalidCodes[0] & Abi::PoisonFlag) == 0u, "selected record fallbacks: an invalid selected T# has no entry");
|
||||
const auto invalidFlags = invalidMap.at(invalidCodes[0] + Abi::ImageRecordFlags);
|
||||
require((invalidFlags & Abi::PoisonFlag) != 0u, "selected record fallbacks: an invalid selected T# was bound");
|
||||
const auto& poison = invalid.result->imageTablePoison.at(invalidFlags & ~Abi::PoisonFlag);
|
||||
require(poison.reason == static_cast<std::uint32_t>(Abi::PoisonReason::NotImage) && poison.dwordCount == 8u && poison.words[3] == heap[2][3] && invalid.result->imageTableFaults != 0u, "selected record fallbacks: an invalid selected T# was not poisoned with its words");
|
||||
require(invalid.result->variantId == valid.result->variantId, "selected record fallbacks: an invalid selected T# changed the artifact");
|
||||
heap[2] = heap[1];
|
||||
|
||||
guest.srt[16] = 200u;
|
||||
const auto beyond = CompileTable(request);
|
||||
const auto beyondCodes = TableCodes(TableMap(*beyond.result), 0);
|
||||
require(beyondCodes.size() == 1u && beyondCodes[0] == Abi::NullCode && beyond.result->imageTableFaults == 0u && !TableTouched(beyond.regions, heap.data(), sizeof(heap)), "selected record fallbacks: a key past the heap did not select a null image without reading the heap");
|
||||
guest.srt[16] = 2u;
|
||||
|
||||
void* reserved = nullptr;
|
||||
#ifdef _WIN32
|
||||
reserved = VirtualAlloc(nullptr, 4096, MEM_RESERVE, PAGE_NOACCESS);
|
||||
#else
|
||||
reserved = mmap(nullptr, 4096, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (reserved == MAP_FAILED) reserved = nullptr;
|
||||
#endif
|
||||
require(reserved != nullptr, "selected record fallbacks: cannot reserve an inaccessible page");
|
||||
FillTableSrt(guest, reserved, 32u, 4u);
|
||||
const auto unmapped = CompileTable(request);
|
||||
const auto& unmappedTable = unmapped.capture->snapshot.tables.tables.at(0);
|
||||
require(unmappedTable.keys == 1u && unmappedTable.codes.at(0) == Abi::PoisonCode(static_cast<std::uint32_t>(Abi::PoisonReason::Unmapped)) && !TableTouched(unmapped.regions, reserved, 4096u), "selected record fallbacks: an unmapped selected record is not Unmapped poison or was read");
|
||||
FillTableSrt(guest, heap.data(), 32u, 4u);
|
||||
|
||||
std::vector<std::uint32_t> loadedKey(TableSrtKeyCode);
|
||||
const auto shift = std::find(loadedKey.begin(), loadedKey.end(), 0x8f108510u);
|
||||
require(shift != loadedKey.end(), "selected record fallbacks: the program has no s_lshl_b32");
|
||||
loadedKey.insert(shift, {0xf4200406u, 0x20000000u});
|
||||
guest.materials[0][1] = 3u;
|
||||
guest.srt[16] = 4u;
|
||||
const auto loadedRequest = TableRequest(loadedKey, userData);
|
||||
const auto loadedPlan = GetResourcePlan(loadedRequest);
|
||||
require(loadedPlan->info.images.size() == 1u && loadedPlan->descriptorSources.at(loadedPlan->info.images[0].source).tableColumn->selected, "selected record fallbacks: a key loaded from a constant buffer at an SRT offset does not select its record");
|
||||
const auto loaded = CompileTable(loadedRequest);
|
||||
require(TableSelected(loaded.capture->snapshot.tables, 0u, "selected record fallbacks: a loaded key does not hold one readable selected record") == heap[3] && TableCovered(loaded.regions, &guest.materials[0][1], sizeof(std::uint32_t)), "selected record fallbacks: the record of a loaded key was not selected or its key was not captured");
|
||||
const auto materials = TableV(guest.materials.data(), 16u, 3u);
|
||||
const auto unreadable = TableV(reserved, 16u, 3u);
|
||||
std::copy(unreadable.begin(), unreadable.end(), guest.srt.begin() + 8);
|
||||
const auto lost = CompileTable(loadedRequest);
|
||||
const auto& lostTable = lost.capture->snapshot.tables.tables.at(0);
|
||||
require(lostTable.keys == 0u && lostTable.fault == Abi::PoisonCode(static_cast<std::uint32_t>(Abi::PoisonReason::Unmapped)) && lost.result->imageTableFaults != 0u && lost.result->variantId == loaded.result->variantId, "selected record fallbacks: a key that cannot be read did not fault its table");
|
||||
std::copy(materials.begin(), materials.end(), guest.srt.begin() + 8);
|
||||
#ifdef _WIN32
|
||||
VirtualFree(reserved, 0, MEM_RELEASE);
|
||||
#else
|
||||
munmap(reserved, 4096);
|
||||
#endif
|
||||
|
||||
auto writtenKey = loadedKey;
|
||||
const auto store = std::find(writtenKey.begin(), writtenKey.end(), 0x80070000u);
|
||||
require(store != writtenKey.end(), "selected record fallbacks: the program has no buffer_store_dword");
|
||||
*store = 0x80030000u;
|
||||
const auto writtenRequest = TableRequest(writtenKey, userData);
|
||||
const auto writtenPlan = GetResourcePlan(writtenRequest);
|
||||
const auto& writtenColumn = writtenPlan->descriptorSources.at(writtenPlan->info.images.at(0).source).tableColumn;
|
||||
require(writtenColumn.has_value() && !writtenColumn->selected && writtenColumn->keyDomains.empty(), "selected record fallbacks: a key loaded from a buffer the shader writes selected its record");
|
||||
const auto written = CompileTable(writtenRequest);
|
||||
require(written.capture->snapshot.tables.tables.at(0).keys == 4u, "selected record fallbacks: a key loaded from a buffer the shader writes did not keep the whole table");
|
||||
require(!loadedPlan->descriptorSources.at(loadedPlan->info.images[0].source).tableColumn->keyDomains.empty(), "selected record fallbacks: a key loaded from a constant buffer has no key domain");
|
||||
auto pointerWritten = loadedKey;
|
||||
const auto end = std::find(pointerWritten.begin(), pointerWritten.end(), 0xbf810000u);
|
||||
require(end != pointerWritten.end(), "selected record fallbacks: the program has no s_endpgm");
|
||||
pointerWritten.insert(end, {0xdc708000u, 0x001c0100u});
|
||||
const auto pointerPlan = GetResourcePlan(TableRequest(pointerWritten, userData));
|
||||
const auto& pointerColumn = pointerPlan->descriptorSources.at(pointerPlan->info.images.at(0).source).tableColumn;
|
||||
require(pointerPlan->info.bdaWrites && pointerColumn.has_value() && !pointerColumn->selected && pointerColumn->keyDomains.empty(), "selected record fallbacks: a key loaded from a constant buffer in a shader that stores through a pointer selected its record or kept its key domain");
|
||||
|
||||
const auto lanePlan = GetResourcePlan(TableRequest(TableWholeCode, userData));
|
||||
require(!lanePlan->info.images.empty() && !lanePlan->descriptorSources.at(lanePlan->info.images[0].source).tableColumn->selected, "selected record fallbacks: a T# selected per lane left the table");
|
||||
|
||||
alignas(4096) static std::array<std::array<std::uint32_t, 8>, 70000> large{};
|
||||
large.back() = heap[1];
|
||||
FillTableSrt(guest, large.data(), 32u, static_cast<std::uint32_t>(large.size()));
|
||||
guest.srt[16] = static_cast<std::uint32_t>(large.size() - 1u);
|
||||
const auto pastCap = CompileTable(request);
|
||||
require(TableSelected(pastCap.capture->snapshot.tables, 0u, "selected record fallbacks: a record past the key cap does not hold one readable selected record") == heap[1] && !pastCap.capture->snapshot.tables.tables[0].outside && pastCap.result->imageTableFaults == 0u, "selected record fallbacks: a record past the key cap was not selected");
|
||||
FillTableSrt(guest, heap.data(), 32u, 4u);
|
||||
}
|
||||
|
||||
void verifySelectedRecordOffsetCarry() {
|
||||
using namespace ShaderRecompiler;
|
||||
namespace Abi = ImageTableAbi;
|
||||
static TableTexture texture;
|
||||
alignas(4096) static TableGuest guest;
|
||||
std::fill(guest.heap.begin(), guest.heap.end(), std::array<std::uint32_t, 8>{});
|
||||
const auto image = TableT(texture.bytes.data(), TableFormat8888UNorm, TableType2D);
|
||||
auto* words = guest.heap.front().data();
|
||||
std::copy(image.begin(), image.end(), words + 0x50u / 4u);
|
||||
FillTableSrt(guest, guest.heap.data(), 0u, static_cast<std::uint32_t>(sizeof(guest.heap)));
|
||||
const auto userData = TableUserData(guest);
|
||||
const std::vector<std::uint32_t> code{0xf4080100u, 0xfa000000u, 0xf4080700u, 0xfa000030u, 0xf4080800u, 0xfa000010u, 0xf4000440u, 0xfa000040u, 0x8711ff11u,
|
||||
0x000000ffu, 0x8f128511u, 0x8113ff12u, 0xffffff10u, 0x8114ff12u, 0xffffff20u, 0xf4280902u, 0x26000040u, 0xf4280a02u, 0x28000040u, 0xf09c0f08u,
|
||||
0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u};
|
||||
const auto request = TableRequest(code, userData);
|
||||
const auto plan = GetResourcePlan(request);
|
||||
require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "selected record offset carry: a T# read by two loads of its record was not planned as a table");
|
||||
const auto& column = *plan->descriptorSources.at(plan->info.images[0].source).tableColumn;
|
||||
require(column.selected && !column.address && column.stride == 32u && column.addend == 0xffffff10u && column.offset == 0x40u && column.foldedAddend == 0xffffff10u && column.foldedCarry == 16u, "selected record offset carry: the column is wrong");
|
||||
guest.srt[16] = 7u;
|
||||
const auto carried = CompileTable(request);
|
||||
const auto& carriedTable = carried.capture->snapshot.tables.tables.at(0);
|
||||
require(carriedTable.keys == 1u && carriedTable.codes.at(0) == Abi::PoisonCode(static_cast<std::uint32_t>(Abi::PoisonReason::OffsetCarry)) && !TableTouched(carried.regions, guest.heap.data(), sizeof(guest.heap)), "selected record offset carry: the record whose second load wraps past 32 bits is not OffsetCarry poison or was read");
|
||||
guest.srt[16] = 8u;
|
||||
const auto wrapped = CompileTable(request);
|
||||
require(TableSelected(wrapped.capture->snapshot.tables, 0u, "selected record offset carry: the record past the wrap does not hold one readable selected record") == image && TableCovered(wrapped.regions, words + 0x50u / 4u, sizeof(image)), "selected record offset carry: the record past the wrap was not selected");
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> WithImageOperation(std::vector<std::uint32_t> code, std::uint32_t word0, bool keepSample) {
|
||||
const auto sample = std::find(code.begin(), code.end(), 0xf09c0f08u);
|
||||
require(sample != code.end() && sample + 1 != code.end(), "image operation variant: the program has no image_sample_lz");
|
||||
if (keepSample) {
|
||||
code.insert(sample + 2, {word0, 0x00050000u});
|
||||
} else {
|
||||
sample[0] = word0;
|
||||
sample[1] = 0x00050000u;
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
void verifySelectedRecordDirectOperations() {
|
||||
using namespace ShaderRecompiler;
|
||||
static TableTexture textures[2];
|
||||
alignas(4096) static TableGuest guest;
|
||||
auto& heap = guest.heap;
|
||||
std::fill(heap.begin(), heap.end(), std::array<std::uint32_t, 8>{});
|
||||
heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D);
|
||||
heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D);
|
||||
heap[2] = heap[1];
|
||||
heap[3] = heap[0];
|
||||
FillTableSrt(guest, heap.data(), 32u, 4u);
|
||||
guest.srt[16] = 2u;
|
||||
const auto userData = TableUserData(guest);
|
||||
const std::array<std::tuple<const char*, std::uint32_t, std::uint32_t>, 4> operations{{{"image_store", 0xf0200f08u, TableFormat32UInt}, {"image_atomic_add", 0xf0440108u, TableFormat32UInt}, {"image_load_pck", 0xf0080f08u, TableFormat32UInt}, {"image_load_by2", 0xf1080308u, TableFormat16UNorm}}};
|
||||
for (const auto& [name, word0, format] : operations) {
|
||||
const auto what = std::string("selected record direct operations: ") + name;
|
||||
heap[2] = TableT(textures[1].bytes.data(), format, TableType2D);
|
||||
const auto code = WithImageOperation(TableSrtKeyCode, word0, false);
|
||||
const auto request = TableRequest(code, userData);
|
||||
const auto plan = GetResourcePlan(request);
|
||||
require(plan->info.images.size() == 1u && plan->info.images[0].table == NoTable && !plan->descriptorSources.at(plan->info.images[0].source).tableColumn.has_value(), (what + " through a T# whose record the CPU evaluates was planned through an image table").c_str());
|
||||
const auto compiled = CompileTable(request);
|
||||
require(TableBinding(*compiled.result, DescriptorRole::ImageTable) == nullptr && compiled.result->imageTableFaults == 0u && TableCovered(compiled.regions, &heap[2], sizeof(heap[2])), (what + " through a T# whose record the CPU evaluates was not bound from its record").c_str());
|
||||
const auto wholeCode = WithImageOperation(TableWholeCode, word0, false);
|
||||
const auto whole = GetResourcePlan(TableRequest(wholeCode, userData));
|
||||
require(whole->info.images.size() == 1u && whole->info.images[0].table == 0u && whole->info.images[0].tableOperation == (word0 == 0xf0200f08u ? TableOperation::Supported : TableOperation::Unsupported), (what + " through a T# keyed per lane left the image table or has the wrong table operation").c_str());
|
||||
}
|
||||
const auto mixedCode = WithImageOperation(TableSrtKeyCode, 0xf0200f08u, true);
|
||||
const auto mixed = GetResourcePlan(TableRequest(mixedCode, userData));
|
||||
require(mixed->info.images.size() == 2u && std::ranges::all_of(mixed->info.images, [&](const ImageResource& image) { return image.table == NoTable && !mixed->descriptorSources.at(image.source).tableColumn.has_value(); }), "selected record direct operations: a T# sampled and stored was planned through an image table for one of its operations");
|
||||
const auto sampled = GetResourcePlan(TableRequest(TableSrtKeyCode, userData));
|
||||
require(sampled->info.images.size() == 1u && sampled->info.images[0].table == 0u && sampled->descriptorSources.at(sampled->info.images[0].source).tableColumn->selected, "selected record direct operations: a T# that is only sampled no longer selects its record");
|
||||
}
|
||||
|
||||
void verifyEvaluableHeapSamplerDirect() {
|
||||
using namespace ShaderRecompiler;
|
||||
static TableTexture textures[2];
|
||||
alignas(4096) static TableGuest guest;
|
||||
auto& heap = guest.heap;
|
||||
std::fill(heap.begin(), heap.end(), std::array<std::uint32_t, 8>{});
|
||||
heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D);
|
||||
heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D);
|
||||
heap[2] = heap[1];
|
||||
heap[3] = heap[0];
|
||||
FillTableSrt(guest, heap.data(), 32u, 4u);
|
||||
guest.srt[16] = 2u;
|
||||
const auto userData = TableUserData(guest);
|
||||
const auto heapSampler = [](std::vector<std::uint32_t> code) {
|
||||
const auto direct = std::find(code.begin(), code.end(), 0xf4080200u);
|
||||
require(direct != code.end() && direct[1] == 0xfa000010u, "evaluable heap sampler: the program has no S# load from the SRT");
|
||||
code.erase(direct, direct + 2);
|
||||
const auto shift = std::find(code.begin(), code.end(), 0x8f108510u);
|
||||
require(shift != code.end(), "evaluable heap sampler: the program has no key shift");
|
||||
code.insert(shift + 1, {0xf4280202u, 0x20000000u});
|
||||
return code;
|
||||
};
|
||||
const auto evaluatedCode = heapSampler(TableSrtKeyCode);
|
||||
const auto evaluated = GetResourcePlan(TableRequest(evaluatedCode, userData));
|
||||
require(evaluated->info.samplers.size() == 1u && evaluated->info.samplers[0].table == NoTable && !evaluated->descriptorSources.at(evaluated->info.samplers[0].source).tableColumn.has_value(), "evaluable heap sampler: an S# read from a heap at an offset the SRT walker evaluates was planned through a sampler table");
|
||||
const auto perLaneCode = heapSampler(TableWholeCode);
|
||||
const auto perLane = GetResourcePlan(TableRequest(perLaneCode, userData));
|
||||
require(perLane->info.samplers.size() == 1u && perLane->info.samplers[0].table != NoTable && perLane->descriptorSources.at(perLane->info.samplers[0].source).tableColumn.has_value(), "evaluable heap sampler: an S# read from a heap at an offset keyed per lane was not planned through a sampler table");
|
||||
}
|
||||
|
||||
void verifySelectedRecordUniformIndex() {
|
||||
using namespace ShaderRecompiler;
|
||||
static TableTexture textures[2];
|
||||
alignas(4096) static TableGuest guest;
|
||||
auto& heap = guest.heap;
|
||||
std::fill(heap.begin(), heap.end(), std::array<std::uint32_t, 8>{});
|
||||
heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D);
|
||||
heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D);
|
||||
heap[2] = heap[1];
|
||||
heap[3] = heap[0];
|
||||
FillTableSrt(guest, heap.data(), 32u, 4u);
|
||||
guest.srt[16] = 2u;
|
||||
const auto userData = TableUserData(guest);
|
||||
const auto twoWaves = [&](const std::vector<std::uint32_t>& code) {
|
||||
auto request = TableRequest(code, userData);
|
||||
request.context.compute->numThreads = {128u, 1u, 1u};
|
||||
return CompileTable(request);
|
||||
};
|
||||
const auto nonUniform = [](const TableCompiled& compiled) {
|
||||
const auto spirv = compiled.result->spirv.Words();
|
||||
std::uint32_t count = 0;
|
||||
for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) {
|
||||
if ((spirv[cursor] & 0xffffu) == spv::OpDecorate && (spirv[cursor] >> 16u) == 3u && spirv[cursor + 2u] == spv::DecorationNonUniform) ++count;
|
||||
}
|
||||
return count;
|
||||
};
|
||||
const auto selected = twoWaves(TableSrtKeyCode);
|
||||
require(TableBinding(*selected.result, DescriptorRole::ImageTable) != nullptr && nonUniform(selected) == 0u, "selected record uniform index: the image table index of a selected record in a two-wave workgroup is decorated NonUniform");
|
||||
const auto whole = twoWaves(TableWholeCode);
|
||||
require(nonUniform(whole) != 0u, "selected record uniform index: the image table index of a table keyed per lane in a two-wave workgroup is not decorated NonUniform");
|
||||
}
|
||||
|
||||
void verifyChainedTableOffset() {
|
||||
using namespace ShaderRecompiler;
|
||||
namespace Abi = ImageTableAbi;
|
||||
static TableTexture textures[2];
|
||||
alignas(256) static std::array<std::uint32_t, 0x3000> root{};
|
||||
static std::array<std::uint32_t, 8> keys{};
|
||||
static std::array<std::uint32_t, 4> output{};
|
||||
const auto keysV = TableV(keys.data(), 0u, static_cast<std::uint32_t>(sizeof(keys)));
|
||||
const auto outputV = TableV(output.data(), 0u, static_cast<std::uint32_t>(sizeof(output)));
|
||||
std::copy(keysV.begin(), keysV.end(), root.begin());
|
||||
std::copy(outputV.begin(), outputV.end(), root.begin() + 4);
|
||||
const auto at = [&](std::uint32_t offset, std::array<std::uint32_t, 8> words) {
|
||||
std::copy(words.begin(), words.end(), root.begin() + offset / 4u);
|
||||
};
|
||||
at(0x60u + 3u * 32u, TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D));
|
||||
at(0x60u + 5u * 32u, TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D));
|
||||
const auto base = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(root.data()));
|
||||
const std::array<std::uint32_t, 2> userData{static_cast<std::uint32_t>(base), static_cast<std::uint32_t>(base >> 32u)};
|
||||
const std::vector<std::uint32_t> code{0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x7e200500u, 0x8f108210u, 0xf4200444u,
|
||||
0x20000000u, 0x8711ff11u, 0x000000ffu, 0x8f128511u, 0x8113a012u, 0x81149013u, 0xf4080900u, 0x26000040u, 0xf4080a00u, 0x28000040u, 0xf09c0f08u,
|
||||
0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u};
|
||||
auto request = TableRequest(code, userData);
|
||||
request.shader.codeAddress = 0x37000u;
|
||||
const auto plan = GetResourcePlan(request);
|
||||
require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "chained table offset: a T# whose second load offset adds to the first one's was not planned as a table");
|
||||
const auto& column = *plan->descriptorSources.at(plan->info.images[0].source).tableColumn;
|
||||
require(column.address && column.stride == 32u && column.addend == 0x20u && column.offset == 0x40u && column.maxKey == 0xffu && column.foldedAddend == 0x20u && column.foldedCarry == 16u, "chained table offset: the column is wrong");
|
||||
const auto compiled = CompileTable(request);
|
||||
const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0);
|
||||
const auto& words = compiled.capture->snapshot.tables.words;
|
||||
require(snapshot.keys == 256u && (snapshot.codes[3] & Abi::PoisonFlag) == 0u && (snapshot.codes[5] & Abi::PoisonFlag) == 0u, "chained table offset: the records were not resolved");
|
||||
require(words.at(snapshot.codes[3]).dwords[0] == root[(0x60u + 3u * 32u) / 4u] && words.at(snapshot.codes[3]).dwords[7] == root[(0x60u + 3u * 32u) / 4u + 7u] && words.at(snapshot.codes[5]).dwords[0] == root[(0x60u + 5u * 32u) / 4u] && snapshot.codes[3] != snapshot.codes[5], "chained table offset: a record was read from the wrong offset");
|
||||
}
|
||||
|
||||
void verifyDescriptorPhis() {
|
||||
using namespace ShaderRecompiler;
|
||||
constexpr std::uint32_t Format8888UNorm = 56;
|
||||
@@ -2125,15 +2416,15 @@ void verifyUniformTableImage() {
|
||||
request.layout.pushConstantSizeBytes = 128;
|
||||
const auto plan = GetResourcePlan(request);
|
||||
const auto& column = plan->descriptorSources.at(plan->info.images.at(0).source).tableColumn;
|
||||
require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u && column.has_value() && column->stride == 48u && column->dwordCount == 8u, "uniform table image: the T# read from a constant buffer at a user data offset was not planned as one table column");
|
||||
require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u && column.has_value() && column->stride == 48u && column->dwordCount == 8u && column->selected, "uniform table image: the T# read from a constant buffer at a user data offset does not select its record");
|
||||
for (const auto& [key, expected] : {std::pair{2u, second}, std::pair{1u, first}}) {
|
||||
userData[2] = key;
|
||||
AgcDriver::ShaderMemory memory({});
|
||||
const auto capture = memory.Capture(request);
|
||||
const auto& tables = capture->snapshot.tables;
|
||||
require(tables.tables.size() == 1u && key < tables.tables[0].codes.size(), "uniform table image: the capture has no table for the user data key");
|
||||
const auto code = tables.tables[0].codes[key];
|
||||
require((code & ImageTableAbi::PoisonFlag) == 0u && std::equal(expected.begin(), expected.end(), tables.words.at(code).dwords.begin()), "uniform table image: the T# of the record the user data selects was not read");
|
||||
require(tables.tables.size() == 1u && TableSelected(tables, 0u, "uniform table image: a table does not hold one readable selected record") == expected, "uniform table image: the T# of the record the user data selects was not read");
|
||||
const auto regions = memory.Regions();
|
||||
require(TableCovered(regions, guest.table[key].data() + 4, sizeof(expected)) && !TableCovered(regions, guest.table[3u - key].data() + 4, sizeof(std::uint32_t)), "uniform table image: the record the user data does not select was read");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5322,6 +5613,12 @@ int main(int argc, char** argv) {
|
||||
verifyScalarAddressRule();
|
||||
verifyPointerImageTable();
|
||||
verifyPointerTableOffsetCarry();
|
||||
verifySelectedRecordFallbacks();
|
||||
verifySelectedRecordOffsetCarry();
|
||||
verifySelectedRecordDirectOperations();
|
||||
verifyEvaluableHeapSamplerDirect();
|
||||
verifySelectedRecordUniformIndex();
|
||||
verifyChainedTableOffset();
|
||||
std::cout << "Image table keys, maps, records and registration passed\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -5353,6 +5650,7 @@ int main(int argc, char** argv) {
|
||||
verifyScalarAddressRule();
|
||||
verifyPointerImageTable();
|
||||
verifyPointerTableOffsetCarry();
|
||||
verifyChainedTableOffset();
|
||||
verifyDescriptorPhis();
|
||||
verifyUniformTableImage();
|
||||
verifyLaneSelectedDescriptors();
|
||||
|
||||
@@ -109,6 +109,7 @@ public:
|
||||
if (hasExtension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) context.memoryProperties2 = function<PFN_vkGetPhysicalDeviceMemoryProperties2>("vkGetPhysicalDeviceMemoryProperties2");
|
||||
context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue);
|
||||
VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||
pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||
pool.queueFamilyIndex = family;
|
||||
Check(context.Function<PFN_vkCreateCommandPool>("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool");
|
||||
} catch (...) {
|
||||
|
||||
@@ -31,7 +31,9 @@ constexpr std::uint32_t MaskedThreads = 16;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::uint32_t CheckedBytes = 0x2000;
|
||||
constexpr std::uint8_t Fill = 0xcd;
|
||||
#ifdef __linux__
|
||||
#if defined(__linux__) && defined(ANYPS5_ENABLE_ASAN)
|
||||
constexpr std::array<std::uintptr_t, 2> GuestWindowAddresses{0x60000000, 0x200000000000};
|
||||
#elif defined(__linux__)
|
||||
constexpr std::array<std::uintptr_t, 2> GuestWindowAddresses{0x80000000000, 0x200000000000};
|
||||
#endif
|
||||
|
||||
|
||||
@@ -34,7 +34,11 @@ using AgcDriver::Graphics::Require;
|
||||
constexpr std::size_t HostStackContextBytes = 16 * 1024;
|
||||
constexpr std::size_t ContextBytes = 64 * 1024;
|
||||
constexpr std::size_t MappingBytes = 4 * 65536;
|
||||
#ifdef ANYPS5_ENABLE_ASAN
|
||||
constexpr std::size_t FiberStackLimit = 16 * 1024;
|
||||
#else
|
||||
constexpr std::size_t FiberStackLimit = 8 * 1024;
|
||||
#endif
|
||||
constexpr std::uint64_t Fill = 0x5aa5c33c5aa5c33cull;
|
||||
|
||||
alignas(16) std::array<std::uint64_t, ContextBytes / sizeof(std::uint64_t)> FiberContext;
|
||||
|
||||
@@ -110,6 +110,20 @@ alignas(256) constexpr std::array<std::uint32_t, 32> TableWave32Code{
|
||||
0x01090401u, 0x7e120202u, 0x34101287u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 29> SelectedTableCode{
|
||||
0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4000400u, 0xfa000060u,
|
||||
0xf4200444u, 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u,
|
||||
0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c8f08u, 0x01090401u, 0x7e120202u, 0x34101286u,
|
||||
0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 31> SelectedTableCutCode{
|
||||
0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4000400u, 0xfa000060u,
|
||||
0xf4200444u, 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u,
|
||||
0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0x7da400ffu, 0x00001000u, 0xf09c8f08u, 0x01090401u,
|
||||
0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 22> DirectCode{
|
||||
0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, 0xf4080900u, 0xfa000040u, 0x360200bfu, 0x7e020d01u,
|
||||
0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c8f08u, 0x01090401u, 0x7e120202u,
|
||||
@@ -195,6 +209,14 @@ alignas(256) constexpr std::array<std::uint32_t, 17> StoreTableCutCode{
|
||||
0xbf810000u,
|
||||
};
|
||||
|
||||
constexpr std::uint32_t SelectedStoreValue = 0xc0de5eedu;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 18> SelectedStoreCode{
|
||||
0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4000400u, 0xfa000060u, 0xf4200444u, 0x20000000u,
|
||||
0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e0802ffu, SelectedStoreValue, 0xf0208f08u,
|
||||
0x00090401u, 0xbf810000u,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 30> PointerCode{
|
||||
0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x8f108202u, 0xf4200444u,
|
||||
0x20000000u, 0x8711ff11u, 0x000000ffu, 0x8f128511u, 0xf40c0900u, 0x24000040u, 0x360200bfu, 0x7e020d01u,
|
||||
@@ -939,6 +961,43 @@ void RunGuardedTableTests(AgcDriver::VulkanDevice& device) {
|
||||
SetClassRecords();
|
||||
}
|
||||
|
||||
void RunSelectedRecordTests(AgcDriver::VulkanDevice& device) {
|
||||
Fill();
|
||||
SetClassRecords();
|
||||
Keys = {};
|
||||
Srt[24] = 12u;
|
||||
Outcome first;
|
||||
for (const auto record : {2u, 0u, 1u}) {
|
||||
Keys[3] = record;
|
||||
const auto selected = Run(device, SelectedTableCode, 1, Lanes, 64);
|
||||
Require(!Faulted(selected) && selected.imageTableCount == 1u, "image table selected record: record " + std::to_string(record) + " faulted or bound other entries:\n" + selected.log);
|
||||
const auto* words = Table.data() + record * (Stride / 4u);
|
||||
const auto control = RunDirect(device, {words[0], words[1], words[2], words[3]}, {words[4], words[5], words[6], words[7]});
|
||||
Require(Group(selected, 0) == Group(control, 0), "image table selected record: record " + std::to_string(record) + " differs from the direct binding of the same words");
|
||||
if (record == 2u) first = selected;
|
||||
Require(selected.artifactId == first.artifactId && selected.variantId == first.variantId, "image table selected record: another key changed the module");
|
||||
}
|
||||
|
||||
Keys[3] = 1000u;
|
||||
const auto beyond = Run(device, SelectedTableCode, 1, Lanes, 64);
|
||||
Require(!Faulted(beyond) && Zeros(beyond), "image table selected record: a key past the table faulted or did not sample zeros:\n" + beyond.log);
|
||||
|
||||
Keys[3] = 7u;
|
||||
const auto cut = Run(device, SelectedTableCutCode, 1, Lanes, 64);
|
||||
Require(!Faulted(cut), "image table selected record: an invalid record selected with EXEC clear faulted:\n" + cut.log);
|
||||
const auto invalid = Run(device, SelectedTableCode, 1, Lanes, 64);
|
||||
const auto tableAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Table.data())) + 7u * Stride + 16u;
|
||||
char address[32];
|
||||
std::snprintf(address, sizeof(address), "0x%llx", static_cast<unsigned long long>(tableAddress));
|
||||
Require(Faulted(invalid) && invalid.log.find("not an image") != std::string::npos && invalid.log.find(address) != std::string::npos && invalid.log.find("00001234") != std::string::npos && Zeros(invalid), "image table selected record: an invalid record selected with EXEC set did not report its address, words and reason:\n" + invalid.log);
|
||||
|
||||
Keys[3] = 0u;
|
||||
const auto aliased = Run(device, SelectedTableCode, 1, Lanes, 64, true);
|
||||
Require(Faulted(aliased) && aliased.log.find("is written by its own dispatch") != std::string::npos, "image table selected record: a dispatch storing over its selected record did not fault on use:\n" + aliased.log);
|
||||
SetClassRecords();
|
||||
Srt[24] = 0u;
|
||||
}
|
||||
|
||||
template <std::size_t TableWords, std::size_t DirectWords>
|
||||
void RequireModeEntry(AgcDriver::VulkanDevice& device, const std::array<std::uint32_t, TableWords>& tableCode, const std::array<std::uint32_t, DirectWords>& directCode, std::uint32_t record, std::span<const std::uint32_t> image, const std::string& what) {
|
||||
Keys = {};
|
||||
@@ -1026,6 +1085,30 @@ void RunStoreTests(AgcDriver::VulkanDevice& device) {
|
||||
Require(untouched(), "image table store: a store through a null entry reached an image:\n" + null.log);
|
||||
}
|
||||
|
||||
void RunSelectedStoreTests(AgcDriver::VulkanDevice& device) {
|
||||
alignas(256) static std::array<std::uint32_t, Width * Height> storeTexels{};
|
||||
struct Registration {
|
||||
void* data;
|
||||
Registration(void* data, std::size_t bytes) : data(data) { GuestAllocations::Mutation().Add(data, bytes, true, true, true); }
|
||||
~Registration() { GuestAllocations::Mutation().Remove(data); }
|
||||
} registered(storeTexels.data(), sizeof(storeTexels));
|
||||
for (std::uint32_t texel = 0; texel < storeTexels.size(); ++texel) storeTexels[texel] = 0x5000u + texel;
|
||||
const auto saved = storeTexels;
|
||||
SetClassRecords();
|
||||
SetRecord(9, PointClamp, Image(storeTexels.data(), Format32UInt));
|
||||
Keys = {};
|
||||
Keys[3] = 9u;
|
||||
Srt[24] = 12u;
|
||||
const auto stored = Run(device, SelectedStoreCode, 1, Lanes, 64);
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast<std::uintptr_t>(storeTexels.data()), sizeof(storeTexels), nullptr, "test");
|
||||
device.WaitIdle();
|
||||
Require(!Faulted(stored) && stored.imageTableCount == 0u, "image table selected store: an image_store through a T# whose record the CPU evaluates faulted or was bound through an image table:\n" + stored.log);
|
||||
for (std::uint32_t texel = 0; texel < storeTexels.size(); ++texel) {
|
||||
const auto expected = texel < Lanes ? SelectedStoreValue : saved[texel];
|
||||
Require(storeTexels[texel] == expected, "image table selected store: texel " + std::to_string(texel) + " holds " + std::to_string(storeTexels[texel]) + " after an image_store through a T# whose record the CPU evaluates, expected " + std::to_string(expected));
|
||||
}
|
||||
}
|
||||
|
||||
void RunPointerTests(AgcDriver::VulkanDevice& device) {
|
||||
const auto floatImage = Image(FloatTexels.data(), Format32Float);
|
||||
const auto uintImage = Image(UintTexels.data(), Format32UInt);
|
||||
@@ -1352,8 +1435,10 @@ int main() {
|
||||
RunReadTests(*device);
|
||||
RunLoopKeyTests(*device);
|
||||
RunGuardedTableTests(*device);
|
||||
RunSelectedRecordTests(*device);
|
||||
RunModeTests(*device);
|
||||
RunStoreTests(*device);
|
||||
RunSelectedStoreTests(*device);
|
||||
RunPointerTests(*device);
|
||||
RunPointerRecordTests(*device);
|
||||
RunSplitRecordTests(*device);
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include "SceTypes.hpp"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <new>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
extern "C" void AgcDriverShutdown_nid_postfix();
|
||||
|
||||
struct MemorySizeQueryParams {
|
||||
std::uint32_t version;
|
||||
std::uint32_t workers;
|
||||
std::uint32_t sequences;
|
||||
std::uint32_t invocations;
|
||||
std::uint32_t dispatches;
|
||||
std::uint32_t syncPoints;
|
||||
std::uint32_t waits;
|
||||
std::uint32_t computeSequences;
|
||||
std::uint64_t computeQueueMask;
|
||||
};
|
||||
|
||||
struct ConfigParams {
|
||||
std::uint32_t version;
|
||||
std::uint32_t threadFlags;
|
||||
std::uint32_t stackSize;
|
||||
std::uint32_t priority;
|
||||
const char* threadName;
|
||||
std::uint32_t spinMicroseconds;
|
||||
std::uint32_t computeAffinity;
|
||||
std::uint32_t computePriority;
|
||||
std::uint32_t reserved;
|
||||
};
|
||||
|
||||
using JobEntry = void (APS5_VABI*)(void*, std::uint32_t);
|
||||
|
||||
struct Sequence {
|
||||
APS5_VABI virtual int Dispatch(JobEntry, void*, std::uint32_t, const char*) = 0;
|
||||
APS5_VABI virtual int SyncPoint() = 0;
|
||||
APS5_VABI virtual int Wait() = 0;
|
||||
APS5_VABI virtual void CompleteDestructor() = 0;
|
||||
APS5_VABI virtual void DeletingDestructor() = 0;
|
||||
APS5_VABI virtual int DispatchCompute(const void*, void*, std::int32_t, std::int32_t, std::int32_t, const char*) = 0;
|
||||
APS5_VABI virtual int DispatchComputeIndirect(const void*, void*, const void*, const char*) = 0;
|
||||
};
|
||||
|
||||
struct SequenceFactory {
|
||||
APS5_VABI virtual int Create(Sequence**, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, const char*) = 0;
|
||||
APS5_VABI virtual int Destroy(Sequence*) = 0;
|
||||
APS5_VABI virtual void CompleteDestructor() = 0;
|
||||
APS5_VABI virtual void DeletingDestructor() = 0;
|
||||
APS5_VABI virtual int CreateCompute(Sequence**, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, const char*) = 0;
|
||||
APS5_VABI virtual int DestroyCompute(Sequence*) = 0;
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
void APS5_VABI _ZN3sce3Job10JobManagerC1Ev(void*);
|
||||
std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE(const MemorySizeQueryParams*);
|
||||
int APS5_VABI _ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE(void*, void*, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint64_t, const ConfigParams*);
|
||||
int APS5_VABI _ZN3sce3Job10JobManager8shutdownEv(void*);
|
||||
SequenceFactory* APS5_VABI _ZN3sce3Job10JobManager34getUnifiedSequenceFactoryInterfaceENS1_8PriorityE(void*, std::uint32_t);
|
||||
}
|
||||
|
||||
#define Construct _ZN3sce3Job10JobManagerC1Ev
|
||||
#define RequiredSize _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE
|
||||
#define Initialize _ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE
|
||||
#define Shutdown _ZN3sce3Job10JobManager8shutdownEv
|
||||
#define Factories _ZN3sce3Job10JobManager34getUnifiedSequenceFactoryInterfaceENS1_8PriorityE
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int InvalidArgument = static_cast<int>(0x81581000u);
|
||||
constexpr int OutOfRange = static_cast<int>(0x81580000u);
|
||||
constexpr int NullArgument = static_cast<int>(0x81580001u);
|
||||
constexpr int NoNodes = static_cast<int>(0x81580002u);
|
||||
constexpr int LimitExceeded = static_cast<int>(0x81580003u);
|
||||
constexpr int NoneFree = static_cast<int>(0x81580006u);
|
||||
constexpr int Invalid = static_cast<int>(0x81580007u);
|
||||
|
||||
void Require(bool condition, const std::string& message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
alignas(256) std::array<std::uint32_t, 12> ReaderCode{0xf4040100u, 0xfa000000u, 0xf4080201u, 0xfa000000u, 0xbf8cc07fu, 0xf4000182u, 0xfa000000u, 0xbf8cc07fu, 0x7e000206u, 0xe0700000u, 0x80020000u, 0xbf810000u};
|
||||
alignas(256) std::array<std::uint32_t, 4> Value{};
|
||||
alignas(256) std::array<std::uint32_t, 2> Table{};
|
||||
alignas(256) volatile std::uint32_t Output[4]{};
|
||||
alignas(256) std::array<std::uint32_t, 4> OutputDescriptor{};
|
||||
alignas(32) std::array<std::uint32_t, 8> IndirectArguments{1, 1, 1, 0, 0, 0, 0, 0};
|
||||
alignas(16) std::array<std::uint32_t, 8> Specials{};
|
||||
alignas(16) std::array<std::uint32_t, 8> SpecialsWithoutDispatch{};
|
||||
std::array<ShaderRegister, 9> Registers{};
|
||||
alignas(16) std::array<unsigned char, 0x80> Shader{};
|
||||
alignas(16) std::array<unsigned char, 0x80> ShaderWithoutDispatch{};
|
||||
|
||||
std::uint32_t Low(const volatile void* pointer) {
|
||||
return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(pointer));
|
||||
}
|
||||
|
||||
std::uint32_t High(const volatile void* pointer) {
|
||||
return static_cast<std::uint32_t>(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(pointer)) >> 32u);
|
||||
}
|
||||
|
||||
void BuildShader() {
|
||||
Table = {Low(Value.data()), High(Value.data())};
|
||||
OutputDescriptor = {Low(Output), High(Output) & 0xffffu, sizeof(Output), 0x31016facu};
|
||||
const auto code = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(ReaderCode.data()));
|
||||
Registers = {{{0x20c, static_cast<std::uint32_t>(code >> 8u)}, {0x20d, static_cast<std::uint32_t>(code >> 40u) & 0xffu}, {0x207, 1}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 4u << 1u},
|
||||
{0x242, Low(OutputDescriptor.data())}, {0x243, High(OutputDescriptor.data())}}};
|
||||
Specials[4] = 1;
|
||||
const ShaderRegister* registers = Registers.data();
|
||||
for (auto* shader : {&Shader, &ShaderWithoutDispatch}) {
|
||||
const std::uint32_t* specials = shader == &Shader ? Specials.data() : SpecialsWithoutDispatch.data();
|
||||
std::memcpy(shader->data() + 0x20, ®isters, sizeof(registers));
|
||||
std::memcpy(shader->data() + 0x28, &specials, sizeof(specials));
|
||||
(*shader)[0x5c] = static_cast<unsigned char>(Registers.size());
|
||||
}
|
||||
}
|
||||
|
||||
std::uint64_t SizeFor(std::uint32_t computeSequences, std::uint64_t mask, bool limits) {
|
||||
const MemorySizeQueryParams query{1, 2, 1, limits ? 100u : 0u, limits ? 8u : 0u, limits ? 4u : 0u, limits ? 2u : 0u, computeSequences, mask};
|
||||
return RequiredSize(&query);
|
||||
}
|
||||
|
||||
std::atomic<bool> Released{false};
|
||||
std::atomic<std::uint32_t> SeenByJob{0};
|
||||
|
||||
void APS5_VABI BlockingJob(void*, std::uint32_t) {
|
||||
while (!Released) std::this_thread::yield();
|
||||
}
|
||||
|
||||
void APS5_VABI ReadOutputJob(void*, std::uint32_t) {
|
||||
SeenByJob = Output[0];
|
||||
}
|
||||
|
||||
void APS5_VABI Nothing(void*, std::uint32_t) {
|
||||
}
|
||||
|
||||
void SizeCase() {
|
||||
Require(SizeFor(0, 0, false) == 0x10d0 && SizeFor(0, 0, true) == 0x14c0, "size without compute sequences");
|
||||
Require(SizeFor(1, 1, false) == 0x24fd8 && SizeFor(1, 1, true) == 0x253c8, "size with one compute sequence");
|
||||
Require(SizeFor(2, 3, false) == 0x48e20 && SizeFor(2, 3, true) == 0x49210, "size with two compute sequences");
|
||||
Require(SizeFor(3, 7, false) == 0x6cb68, "size with three compute sequences");
|
||||
Require(SizeFor(1, 3, true) == 0x491c8, "size with two queues for one compute sequence");
|
||||
Require(SizeFor(2, 1, true) == 0, "size with fewer queues than compute sequences");
|
||||
}
|
||||
|
||||
void RunCase() {
|
||||
const std::uint64_t size = SizeFor(1, 2, true);
|
||||
void* memory = ::operator new((size + 255) & ~std::uint64_t{255}, std::align_val_t{256});
|
||||
Require(memory != nullptr, "allocation failed");
|
||||
alignas(64) static unsigned char manager[0x100];
|
||||
Construct(manager);
|
||||
const ConfigParams badHandler{1, 0x00021fff, 0x00010000, 0x2bc, nullptr, 0, 0x1fff, 0x50, 0};
|
||||
const ConfigParams noAffinity{1, 0x00021fff, 0x00010000, 0x2bc, nullptr, 0, 0, 0x100, 0};
|
||||
const ConfigParams wideAffinity{1, 0x00021fff, 0x00010000, 0x2bc, nullptr, 0, 0x2000, 0x100, 0};
|
||||
Require(Initialize(manager, memory, size, 2, 1, 1, 2, &badHandler) == InvalidArgument, "a handler priority below 0x100 was accepted");
|
||||
Require(Initialize(manager, memory, size, 2, 1, 1, 2, &noAffinity) == InvalidArgument, "an empty handler affinity was accepted");
|
||||
Require(Initialize(manager, memory, size, 2, 1, 1, 2, &wideAffinity) == InvalidArgument, "a handler affinity beyond 13 cores was accepted");
|
||||
Require(Initialize(manager, memory, size, 2, 1, 0xffffffffu, 2, nullptr) == InvalidArgument, "a sequence count that wraps was accepted");
|
||||
Require(Initialize(manager, memory, size, 2, 1, 1, 2, nullptr) == 0, "initialize with one compute sequence failed");
|
||||
SequenceFactory* factory = Factories(manager, 0);
|
||||
Sequence* sequence = nullptr;
|
||||
Sequence* other = nullptr;
|
||||
Require(factory->CreateCompute(&sequence, 100, 4, 4, 2, "compute") == 0, "the compute sequence was not created");
|
||||
Require(factory->CreateCompute(&other, 1, 1, 1, 1, "second") == NoneFree, "a second compute sequence was created");
|
||||
Require(factory->Create(&other, 1, 1, 1, 1, "cpu") == 0, "the CPU sequence was not created");
|
||||
Require(factory->DestroyCompute(other) == Invalid, "a CPU sequence was destroyed as a compute sequence");
|
||||
Require(factory->Destroy(sequence) == Invalid, "a compute sequence was destroyed as a CPU sequence");
|
||||
Require(factory->Destroy(other) == 0, "the CPU sequence was not destroyed");
|
||||
|
||||
Require(sequence->DispatchCompute(nullptr, Table.data(), 1, 1, 1, nullptr) == NullArgument, "a null shader was accepted");
|
||||
Require(sequence->DispatchCompute(Shader.data(), Table.data(), 0, 1, 1, nullptr) == OutOfRange, "a zero group count was accepted");
|
||||
Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, -1, nullptr) == OutOfRange, "a negative group count was accepted");
|
||||
Require(sequence->DispatchComputeIndirect(Shader.data(), Table.data(), nullptr, nullptr) == NullArgument, "null indirect arguments were accepted");
|
||||
Require(sequence->DispatchComputeIndirect(Shader.data(), Table.data(), reinterpret_cast<const char*>(IndirectArguments.data()) + 4, nullptr) == NullArgument, "misaligned indirect arguments were accepted");
|
||||
|
||||
Value[0] = 0x11110001u;
|
||||
Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, "direct") == 0, "the compute dispatch failed");
|
||||
Require(sequence->Wait() == 0, "the wait after a compute dispatch failed");
|
||||
Require(Output[0] == 0x11110001u, "the compute dispatch had not run when the wait returned: " + std::to_string(Output[0]));
|
||||
|
||||
Value[0] = 0x22220002u;
|
||||
Require(sequence->DispatchComputeIndirect(Shader.data(), Table.data(), IndirectArguments.data(), nullptr) == 0, "the indirect compute dispatch failed");
|
||||
Require(sequence->Wait() == 0, "the wait after an indirect compute dispatch failed");
|
||||
Require(Output[0] == 0x22220002u, "the indirect compute dispatch had not run when the wait returned: " + std::to_string(Output[0]));
|
||||
|
||||
Value[0] = 0x33330003u;
|
||||
Require(sequence->DispatchCompute(ShaderWithoutDispatch.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the dispatch of a shader without the dispatch bit failed");
|
||||
Require(sequence->Wait() == 0, "the wait after a shader without the dispatch bit failed");
|
||||
Require(Output[0] == 0x22220002u, "a shader without the dispatch bit ran");
|
||||
|
||||
Require(sequence->Dispatch(BlockingJob, nullptr, 1, "blocking") == 0, "the blocking job was not dispatched");
|
||||
Require(sequence->SyncPoint() == 0, "the sync point failed");
|
||||
Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the compute dispatch behind a sync point failed");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
Require(Output[0] == 0x22220002u, "a compute dispatch ran before the CPU job ahead of its sync point finished");
|
||||
Released = true;
|
||||
Require(sequence->SyncPoint() == 0, "the second sync point failed");
|
||||
Require(sequence->Dispatch(ReadOutputJob, nullptr, 1, "reader") == 0, "the reading job was not dispatched");
|
||||
Require(sequence->Wait() == 0, "the wait after the ordered dispatches failed");
|
||||
Require(Output[0] == 0x33330003u, "the compute dispatch behind a sync point did not run");
|
||||
Require(SeenByJob == 0x33330003u, "a CPU job behind a sync point ran before the compute dispatch ahead of it finished");
|
||||
|
||||
Value[0] = 0x44440004u;
|
||||
Require(sequence->DispatchCompute(ShaderWithoutDispatch.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the leading dispatch failed");
|
||||
Require(sequence->SyncPoint() == 0, "the sync point between compute dispatches failed");
|
||||
Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, "behind") == 0, "the compute dispatch behind a compute dispatch failed");
|
||||
Require(sequence->Wait() == 0, "the wait after chained compute dispatches failed");
|
||||
Require(Output[0] == 0x44440004u, "a compute dispatch behind a sync point and a compute dispatch did not run");
|
||||
|
||||
for (int index = 0; index < 4; ++index) Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "a compute dispatch within the limit failed");
|
||||
Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == LimitExceeded, "a compute dispatch past the dispatch limit was accepted");
|
||||
Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == Invalid, "a latched compute sequence accepted a dispatch");
|
||||
Require(factory->DestroyCompute(sequence) == 0, "the compute sequence was not destroyed");
|
||||
|
||||
Require(factory->Create(&other, 0x7fff, 0x3ff, 0xff, 1, "nodes") == 0, "the node counting sequence was not created");
|
||||
int accepted = 0;
|
||||
int result = 0;
|
||||
while (accepted < 0x3ff && (result = other->Dispatch(Nothing, nullptr, 1, nullptr)) == 0) ++accepted;
|
||||
Require(result == NoNodes && accepted == 43, "the node pool of a compute configuration holds " + std::to_string(accepted) + " dispatches");
|
||||
Require(Shutdown(manager) == 0, "shutdown failed");
|
||||
::operator delete(memory, std::align_val_t{256});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
device.reset();
|
||||
BuildShader();
|
||||
SizeCase();
|
||||
RunCase();
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
std::cout << "job manager compute test passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
try {
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
} catch (const std::exception& shutdownError) {
|
||||
std::cerr << shutdownError.what() << '\n';
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,13 @@
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -182,14 +186,41 @@ void Check(const char* name, std::uint64_t operations, std::uint32_t run) {
|
||||
}
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const char* name, std::uint64_t operations, std::uint32_t repeats) {
|
||||
std::uint64_t WorkgroupBytes(std::span<const std::uint32_t> words) {
|
||||
std::unordered_map<std::uint32_t, std::span<const std::uint32_t>> types;
|
||||
std::unordered_map<std::uint32_t, std::uint32_t> constants;
|
||||
std::vector<std::uint32_t> pointers;
|
||||
for (std::size_t at = 5; at < words.size();) {
|
||||
const auto count = words[at] >> 16u;
|
||||
Require(count != 0u && count <= words.size() - at, "lds lock budget: malformed SPIR-V");
|
||||
const auto instruction = words.subspan(at, count);
|
||||
const auto opcode = instruction[0] & 0xffffu;
|
||||
if (opcode == spv::OpTypeInt || opcode == spv::OpTypeArray || opcode == spv::OpTypePointer) types[instruction[1]] = instruction;
|
||||
if (opcode == spv::OpConstant) constants[instruction[2]] = instruction[3];
|
||||
if (opcode == spv::OpVariable && instruction[3] == spv::StorageClassWorkgroup) pointers.push_back(instruction[1]);
|
||||
at += count;
|
||||
}
|
||||
std::uint64_t total = 0;
|
||||
for (const auto pointer : pointers) {
|
||||
const auto definition = types.at(pointer);
|
||||
Require((definition[0] & 0xffffu) == spv::OpTypePointer, "lds lock budget: Workgroup storage has no pointer type");
|
||||
const auto array = types.at(definition[3]);
|
||||
Require((array[0] & 0xffffu) == spv::OpTypeArray, "lds lock budget: unexpected Workgroup allocation shape");
|
||||
const auto element = types.at(array[2]);
|
||||
Require((element[0] & 0xffffu) == spv::OpTypeInt && element[2] == 32u, "lds lock budget: Workgroup array is not dwords");
|
||||
total += static_cast<std::uint64_t>(constants.at(array[3])) * sizeof(std::uint32_t);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const char* name, std::uint64_t operations, std::uint32_t repeats, std::uint32_t ldsDwords = LdsDwords, std::optional<std::uint32_t> expectedDeviceDwords = std::nullopt) {
|
||||
std::vector<std::uint32_t> userData(8, 0u);
|
||||
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u));
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
|
||||
std::copy(input.begin(), input.end(), userData.begin());
|
||||
std::copy(output.begin(), output.end(), userData.begin() + 4);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, LdsDwords, {false, false, false}, false, 1};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, ldsDwords, {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, memory},
|
||||
@@ -198,20 +229,46 @@ void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, c
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
if (expectedDeviceDwords) {
|
||||
const auto bytes = WorkgroupBytes(result.spirv.Words());
|
||||
const auto limit = request.target.maxWorkgroupSharedMemoryBytes;
|
||||
Require(bytes <= limit, "lds lock budget: guest LDS and lock exceed the device limit before dispatch");
|
||||
Require(result.workgroupMemoryDwords == *expectedDeviceDwords, "lds lock budget: unexpected device-backed LDS allocation before dispatch");
|
||||
if (*expectedDeviceDwords == 0u) Require(bytes == limit, "lds lock budget: exact-fit case did not retain its lock");
|
||||
std::printf("lds lock budget: %s declarations verified; guest=%u; workgroup=%llu; device=%u\n", name, ldsDwords * 4u, static_cast<unsigned long long>(bytes), result.workgroupMemoryDwords);
|
||||
}
|
||||
for (std::uint32_t run = 0; run < repeats; ++run) {
|
||||
Output.fill(0xdeadbeefu);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
Check(name, operations, run);
|
||||
}
|
||||
if (expectedDeviceDwords) std::printf("lds lock budget: %s GPU readback verified across 256 lanes\n", name);
|
||||
}
|
||||
|
||||
void LockBudget(AgcDriver::VulkanDevice& device) {
|
||||
const auto target = device.Target();
|
||||
const auto limit = target.maxWorkgroupSharedMemoryBytes;
|
||||
std::printf("lds lock budget: robustBufferAccess2=%u; maxWorkgroupSharedMemoryBytes=%u\n", static_cast<unsigned>(target.robustBufferAccess2), limit);
|
||||
if (std::getenv("ANYPS5_REQUIRE_ROBUST2") != nullptr) Require(target.robustBufferAccess2, "lds lock budget: robustBufferAccess2 was not enabled");
|
||||
Require(limit >= 32u && limit % 4u == 0u, "lds lock budget: unexpected device shared-memory limit");
|
||||
const auto operations = static_cast<std::uint64_t>(Threads) * Iterations;
|
||||
Run(device, UniformCode, "limit", operations, 1, limit / 4u, limit / 4u + 1u);
|
||||
Run(device, UniformCode, "limit-4", operations, 1, (limit - 4u) / 4u, 0u);
|
||||
std::puts("lds lock budget tests passed");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--lock-budget") {
|
||||
LockBudget(*device);
|
||||
return 0;
|
||||
}
|
||||
Require(argc == 1, "lds mixed width atomics: unexpected argument");
|
||||
Run(*device, UniformCode, "uniform", static_cast<std::uint64_t>(Threads) * Iterations, 1);
|
||||
Run(*device, DivergentCode, "divergent", static_cast<std::uint64_t>(Threads) / 2u * Iterations, 1);
|
||||
Run(*device, ParityCode, "split lanes", Threads / 2u, Repeats);
|
||||
|
||||
@@ -84,6 +84,7 @@ public:
|
||||
context.formatProperties = function<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties");
|
||||
context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue);
|
||||
VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||
pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||
pool.queueFamilyIndex = family;
|
||||
Check(context.Function<PFN_vkCreateCommandPool>("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool");
|
||||
} catch (...) {
|
||||
|
||||
@@ -110,6 +110,7 @@ public:
|
||||
context.imageFormatProperties = function<PFN_vkGetPhysicalDeviceImageFormatProperties>("vkGetPhysicalDeviceImageFormatProperties");
|
||||
context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue);
|
||||
VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||
pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||
pool.queueFamilyIndex = family;
|
||||
Check(context.Function<PFN_vkCreateCommandPool>("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool");
|
||||
} catch (...) {
|
||||
|
||||
@@ -214,6 +214,7 @@ public:
|
||||
context.formatProperties = function<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties");
|
||||
context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue);
|
||||
VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||
pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||
pool.queueFamilyIndex = family;
|
||||
Check(context.Function<PFN_vkCreateCommandPool>("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool");
|
||||
} catch (...) {
|
||||
|
||||
@@ -130,6 +130,7 @@ public:
|
||||
if (hasExtension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) context.memoryProperties2 = function<PFN_vkGetPhysicalDeviceMemoryProperties2>("vkGetPhysicalDeviceMemoryProperties2");
|
||||
context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue);
|
||||
VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||
pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||
pool.queueFamilyIndex = family;
|
||||
Check(context.Function<PFN_vkCreateCommandPool>("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool");
|
||||
} catch (...) {
|
||||
|
||||
@@ -96,6 +96,7 @@ public:
|
||||
function<PFN_vkGetPhysicalDeviceMemoryProperties>("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory);
|
||||
context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue);
|
||||
VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||
pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||
pool.queueFamilyIndex = family;
|
||||
Check(context.Function<PFN_vkCreateCommandPool>("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool");
|
||||
} catch (...) {
|
||||
|
||||
@@ -47,6 +47,7 @@ bool g_prepare = false;
|
||||
bool g_materialize = false;
|
||||
bool g_graph = false;
|
||||
bool g_maintenance8 = false;
|
||||
bool g_robust2 = false;
|
||||
bool g_code = false;
|
||||
|
||||
bool Replay(const char* path) {
|
||||
@@ -145,6 +146,7 @@ bool Replay(const char* path) {
|
||||
}
|
||||
try {
|
||||
if (g_maintenance8) request.request.target.nonConstantImageOffsets = true;
|
||||
if (g_robust2) request.request.target.robustBufferAccess2 = true;
|
||||
const auto result = ShaderRecompiler::Recompile(request.request);
|
||||
std::printf(" recompiled: %zu SPIR-V words\n", result.spirv.size());
|
||||
if (g_spirv) {
|
||||
@@ -182,7 +184,7 @@ bool Replay(const char* path) {
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 2) {
|
||||
std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--prepare] [--materialize] [--code] [--maintenance8] <shader.req>...\n the driver writes shader_<address>.req files when APS5_DUMP_SHADERS is set\n");
|
||||
std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--prepare] [--materialize] [--code] [--maintenance8] [--robust2] <shader.req>...\n the driver writes shader_<address>.req files when APS5_DUMP_SHADERS is set\n");
|
||||
return 2;
|
||||
}
|
||||
int failures = 0;
|
||||
@@ -203,6 +205,10 @@ int main(int argc, char** argv) {
|
||||
g_maintenance8 = true;
|
||||
continue;
|
||||
}
|
||||
if (std::string(argv[i]) == "--robust2") {
|
||||
g_robust2 = true;
|
||||
continue;
|
||||
}
|
||||
if (std::string(argv[i]) == "--mem") {
|
||||
g_memory = true;
|
||||
continue;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
add_library(libSceHmd2 SHARED EXCLUDE_FROM_ALL Export.cpp)
|
||||
target_include_directories(libSceHmd2 PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(libSceHmd2 PRIVATE libc)
|
||||
configure_windows_unwind(libSceHmd2)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
|
||||
@@ -1,11 +1,42 @@
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include "SceTypes.hpp"
|
||||
#include "Hmd2.hpp"
|
||||
#include "prx/libc/include/ApplicationHeap.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
#include "prx/libSceUserService/UserService.hpp"
|
||||
#include "SceTypes.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::int32_t SCE_HMD2_ERROR_UNSUPPORTED_FEATURE = static_cast<std::int32_t>(0x81110016);
|
||||
constexpr std::int32_t DeviceHandle = 0x0e010000;
|
||||
constexpr Hmd2::FieldOfView DefaultFieldOfView{1.84177089f, 0.947190642f, 1.32898104f, 1.32898104f};
|
||||
|
||||
std::mutex stateMutex;
|
||||
bool initialized = false;
|
||||
std::int32_t openHandle = 0;
|
||||
|
||||
std::uint32_t CompiledSdkVersion() {
|
||||
static const auto* processParameters = static_cast<const std::byte*>(ApplicationProcessParameters_nid_no_patch());
|
||||
std::uint32_t version;
|
||||
std::memcpy(&version, processParameters + 0x10, sizeof(version));
|
||||
return version;
|
||||
}
|
||||
|
||||
std::int32_t GetDeviceInformation(Hmd2::DeviceInformation* info) {
|
||||
if (!info) return Hmd2::ParameterNull;
|
||||
*info = {};
|
||||
if (!initialized) return Hmd2::NotInitialized;
|
||||
info->status = Hmd2::DeviceStatus::NotDetected;
|
||||
if (CompiledSdkVersion() < Hmd2::UserIdSdkVersion) info->userId = -1;
|
||||
info->panelWidth = 4000;
|
||||
info->panelHeight = 2040;
|
||||
info->latency90Hz = 5555;
|
||||
info->latency120Hz = 4160;
|
||||
info->virtualImageDistance = 2.0f;
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,27 +53,40 @@ int APS5_VABI sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign() {
|
||||
}
|
||||
|
||||
std::int32_t APS5_VABI sceHmd2Initialize(const void* param) {
|
||||
(void)param;
|
||||
return SCE_HMD2_ERROR_UNSUPPORTED_FEATURE;
|
||||
}
|
||||
|
||||
int APS5_VABI sceHmd2GetFieldOfView() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
std::lock_guard lock(stateMutex);
|
||||
if (initialized) return Hmd2::AlreadyInitialized;
|
||||
if (!param) return Hmd2::ParameterNull;
|
||||
initialized = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceHmd2GetDeviceInformationByHandle() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
std::int32_t APS5_VABI sceHmd2GetFieldOfView(std::int32_t handle, Hmd2::FieldOfView* fieldOfView) {
|
||||
std::lock_guard lock(stateMutex);
|
||||
if (!fieldOfView) return Hmd2::ParameterNull;
|
||||
if (handle == 0 || handle != openHandle) return Hmd2::InvalidHandle;
|
||||
if (!initialized) return Hmd2::NotInitialized;
|
||||
*fieldOfView = DefaultFieldOfView;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceHmd2Terminate() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
std::int32_t APS5_VABI sceHmd2GetDeviceInformationByHandle(std::int32_t handle, Hmd2::DeviceInformation* info) {
|
||||
std::lock_guard lock(stateMutex);
|
||||
if (handle != openHandle) return Hmd2::InvalidHandle;
|
||||
return GetDeviceInformation(info);
|
||||
}
|
||||
|
||||
std::int32_t APS5_VABI sceHmd2Terminate() {
|
||||
std::lock_guard lock(stateMutex);
|
||||
if (!initialized) return Hmd2::NotInitialized;
|
||||
initialized = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceHmd2Close() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
std::int32_t APS5_VABI sceHmd2Close(std::int32_t handle) {
|
||||
std::lock_guard lock(stateMutex);
|
||||
if (!initialized) return Hmd2::NotInitialized;
|
||||
if (handle != openHandle) return Hmd2::InvalidHandle;
|
||||
openHandle = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -56,19 +100,30 @@ int APS5_VABI sceHmd2GazeGetResultForFoveatedRendering() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceHmd2GetDeviceInformation() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
std::int32_t APS5_VABI sceHmd2GetDeviceInformation(Hmd2::DeviceInformation* info) {
|
||||
std::lock_guard lock(stateMutex);
|
||||
return GetDeviceInformation(info);
|
||||
}
|
||||
|
||||
std::int32_t APS5_VABI sceHmd2GetFieldOfViewWithoutHandle(Hmd2::FieldOfView* fieldOfView) {
|
||||
std::lock_guard lock(stateMutex);
|
||||
if (!fieldOfView) return Hmd2::ParameterNull;
|
||||
if (!initialized) return Hmd2::NotInitialized;
|
||||
*fieldOfView = DefaultFieldOfView;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceHmd2GetFieldOfViewWithoutHandle() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceHmd2Open() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
std::int32_t APS5_VABI sceHmd2Open(std::int32_t userId, std::int32_t type, std::int32_t index, const void* param) {
|
||||
std::lock_guard lock(stateMutex);
|
||||
if (!initialized) return Hmd2::NotInitialized;
|
||||
if (type != 0 || index != 0 || param) return Hmd2::ParameterInvalid;
|
||||
if (userId != USER_SERVICE_INITIAL_USER_ID) {
|
||||
if (CompiledSdkVersion() >= Hmd2::UserIdSdkVersion) return Hmd2::ParameterInvalid;
|
||||
throw std::runtime_error("sceHmd2Open: user is not the initial user");
|
||||
}
|
||||
if (openHandle != 0) return Hmd2::AlreadyOpened;
|
||||
openHandle = DeviceHandle;
|
||||
return DeviceHandle;
|
||||
}
|
||||
|
||||
int APS5_VABI sceHmd2ReprojectionBeginFrame() {
|
||||
@@ -161,8 +216,10 @@ int APS5_VABI sceHmd2ReprojectionTerminate() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceHmd2SetVibration() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
std::int32_t APS5_VABI sceHmd2SetVibration(std::int32_t handle, std::uint32_t level) {
|
||||
(void)level;
|
||||
std::lock_guard lock(stateMutex);
|
||||
if (handle != openHandle) return Hmd2::InvalidHandle;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#ifndef CORE_LIBS_PRX_LIBSCEHMD2_HMD2_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEHMD2_HMD2_HPP
|
||||
|
||||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Hmd2 {
|
||||
|
||||
inline constexpr std::int32_t AlreadyInitialized = static_cast<std::int32_t>(0x8a720001);
|
||||
inline constexpr std::int32_t NotInitialized = static_cast<std::int32_t>(0x8a720002);
|
||||
inline constexpr std::int32_t InvalidHandle = static_cast<std::int32_t>(0x8a720003);
|
||||
inline constexpr std::int32_t AlreadyOpened = static_cast<std::int32_t>(0x8a720006);
|
||||
inline constexpr std::int32_t ParameterNull = static_cast<std::int32_t>(0x8a720008);
|
||||
inline constexpr std::int32_t ParameterInvalid = static_cast<std::int32_t>(0x8a720009);
|
||||
|
||||
inline constexpr std::uint32_t UserIdSdkVersion = 0x07000000;
|
||||
|
||||
enum class DeviceStatus : std::uint32_t {
|
||||
NotDetected = 2,
|
||||
};
|
||||
|
||||
struct DeviceInformation {
|
||||
DeviceStatus status;
|
||||
std::int32_t userId;
|
||||
std::uint32_t panelWidth;
|
||||
std::uint32_t panelHeight;
|
||||
std::uint16_t latency90Hz;
|
||||
std::uint16_t latency120Hz;
|
||||
std::uint8_t hmuMount;
|
||||
std::uint8_t lensSeparationDistance;
|
||||
std::uint8_t reserved[2];
|
||||
float virtualImageDistance;
|
||||
};
|
||||
|
||||
struct FieldOfView {
|
||||
float tanOut;
|
||||
float tanIn;
|
||||
float tanTop;
|
||||
float tanBottom;
|
||||
};
|
||||
|
||||
static_assert(sizeof(DeviceInformation) == 28);
|
||||
static_assert(offsetof(DeviceInformation, userId) == 4);
|
||||
static_assert(offsetof(DeviceInformation, panelWidth) == 8);
|
||||
static_assert(offsetof(DeviceInformation, latency90Hz) == 16);
|
||||
static_assert(offsetof(DeviceInformation, hmuMount) == 20);
|
||||
static_assert(offsetof(DeviceInformation, virtualImageDistance) == 24);
|
||||
static_assert(sizeof(FieldOfView) == 16);
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
std::int32_t APS5_VABI sceHmd2Initialize(const void* param);
|
||||
std::int32_t APS5_VABI sceHmd2Terminate();
|
||||
std::int32_t APS5_VABI sceHmd2Open(std::int32_t userId, std::int32_t type, std::int32_t index, const void* param);
|
||||
std::int32_t APS5_VABI sceHmd2Close(std::int32_t handle);
|
||||
std::int32_t APS5_VABI sceHmd2GetDeviceInformation(Hmd2::DeviceInformation* info);
|
||||
std::int32_t APS5_VABI sceHmd2GetDeviceInformationByHandle(std::int32_t handle, Hmd2::DeviceInformation* info);
|
||||
std::int32_t APS5_VABI sceHmd2GetFieldOfView(std::int32_t handle, Hmd2::FieldOfView* fieldOfView);
|
||||
std::int32_t APS5_VABI sceHmd2GetFieldOfViewWithoutHandle(Hmd2::FieldOfView* fieldOfView);
|
||||
std::int32_t APS5_VABI sceHmd2SetVibration(std::int32_t handle, std::uint32_t level);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,11 +1,12 @@
|
||||
add_library(libSceJobManager SHARED EXCLUDE_FROM_ALL Export.cpp)
|
||||
target_include_directories(libSceJobManager PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(libSceJobManager PRIVATE libSceAgc libSceAgcDriver)
|
||||
configure_windows_unwind(libSceJobManager)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(guest_job_manager_tests ${LIBS_INCLUDE_DIR}/tests/GuestJobManager.cpp)
|
||||
target_include_directories(guest_job_manager_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_job_manager_tests PRIVATE libSceJobManager libc)
|
||||
target_link_libraries(guest_job_manager_tests PRIVATE libSceJobManager libSceAgc libSceAgcDriver libkernel libc)
|
||||
configure_windows_unwind(guest_job_manager_tests)
|
||||
foreach(testCase IN ITEMS memory validation dispatch ordering nested limits priority shutdown)
|
||||
add_test(NAME guest_job_manager_${testCase} COMMAND guest_job_manager_tests ${testCase})
|
||||
|
||||
@@ -22,6 +22,24 @@ int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSiz
|
||||
int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask);
|
||||
int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name);
|
||||
int APS5_VABI scePthreadJoin(Pthread thread, void** result);
|
||||
int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name);
|
||||
int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq);
|
||||
int APS5_VABI sceKernelWaitEqueue(KernelEqueue eq, KernelEvent* ev, int num, int* out, const KernelUseconds* timo);
|
||||
int APS5_VABI sceKernelAddUserEventEdge(KernelEqueue eq, int id);
|
||||
int APS5_VABI sceKernelTriggerUserEvent(KernelEqueue eq, int id, void* udata);
|
||||
int APS5_VABI sceKernelGetEventFilter(const KernelEvent* ev);
|
||||
std::uintptr_t APS5_VABI sceKernelGetEventId(const KernelEvent* ev);
|
||||
int APS5_VABI sceAgcDriverAddEqEvent(KernelEqueue eq, int id, void* udata);
|
||||
int APS5_VABI sceAgcDriverDeleteEqEvent(KernelEqueue eq, int id);
|
||||
int APS5_VABI sceAgcDriverSubmitAcb(std::uint32_t queue, const Packet* packet);
|
||||
std::uint32_t* APS5_VABI sceAgcAcbAcquireMem(CommandBuffer* buf, std::uint32_t gcrControl, const volatile void* base, std::uint64_t sizeBytes, std::uint32_t pollCycles);
|
||||
std::uint32_t* APS5_VABI sceAgcCbSetShRegistersDirect(CommandBuffer* buf, const volatile ShaderRegister* regs, std::uint32_t numRegs);
|
||||
std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer* buf, std::uint32_t offset, const std::uint32_t* values, std::uint32_t numValues);
|
||||
std::uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, std::uint32_t color);
|
||||
std::uint32_t* APS5_VABI sceAgcAcbPopMarker(CommandBuffer* buf);
|
||||
std::uint32_t* APS5_VABI sceAgcCbDispatch(CommandBuffer* buf, std::uint32_t threadGroupX, std::uint32_t threadGroupY, std::uint32_t threadGroupZ, std::uint32_t modifier);
|
||||
std::uint32_t* APS5_VABI sceAgcAcbDispatchIndirect(CommandBuffer* buf, const volatile void* indirectArguments, std::uint32_t modifier);
|
||||
std::uint32_t* APS5_VABI sceAgcCbReleaseMem(CommandBuffer* buf, std::uint8_t action, std::uint16_t gcrControl, std::uint8_t dst, std::uint8_t cachePolicy, const volatile Label* address, std::uint8_t dataSelect, std::uint64_t data, std::uint16_t gdsOffset, std::uint16_t gdsSize, std::uint8_t interrupt, std::uint32_t interruptContextId);
|
||||
}
|
||||
|
||||
namespace {
|
||||
@@ -50,6 +68,11 @@ constexpr std::uint64_t StateEnd = 0x2d8;
|
||||
constexpr std::uint64_t ThreadRecordBytes = 0x48;
|
||||
constexpr std::uint64_t SequenceBytes = 0x240;
|
||||
constexpr std::uint64_t NodeBytes = 72;
|
||||
constexpr std::uint64_t ComputeContextBytes = 0x168;
|
||||
constexpr std::uint64_t ComputeCommandBytes = 0x3ff * 104 + 0xff * 148 + 0x3f * 32;
|
||||
constexpr std::uint32_t FirstComputeQueue = 0x20;
|
||||
constexpr std::uint32_t UserDataRegister = 0x240;
|
||||
constexpr int UserEventFilter = -11;
|
||||
|
||||
struct MemorySizeQueryParams {
|
||||
std::uint32_t version;
|
||||
@@ -94,7 +117,7 @@ std::uint64_t AlignUp(std::uint64_t value, std::uint64_t alignment) {
|
||||
struct Manager;
|
||||
class Sequence;
|
||||
|
||||
enum class ItemType { Dispatch, SyncPoint, Wait };
|
||||
enum class ItemType { Dispatch, SyncPoint, Wait, Compute, ComputeIndirect };
|
||||
|
||||
struct Item {
|
||||
ItemType type;
|
||||
@@ -105,6 +128,11 @@ struct Item {
|
||||
std::uint32_t handedOut;
|
||||
std::uint32_t completed;
|
||||
bool* passed;
|
||||
const void* shader = nullptr;
|
||||
std::array<std::uint32_t, 3> groups{};
|
||||
const void* indirectArguments = nullptr;
|
||||
std::string name;
|
||||
bool named = false;
|
||||
};
|
||||
|
||||
struct Counters {
|
||||
@@ -140,27 +168,27 @@ public:
|
||||
APS5_VABI int Dispatch(JobEntry entry, void* argument, std::uint32_t count, const char* name) override;
|
||||
APS5_VABI int SyncPoint() override;
|
||||
APS5_VABI int Wait() override;
|
||||
APS5_VABI int DispatchCompute(const void*, void*, std::int32_t, std::int32_t, std::int32_t, const char*) override {
|
||||
NotImplemented_nid_no_patch("sce::Job compute dispatch");
|
||||
return 0;
|
||||
}
|
||||
APS5_VABI int DispatchComputeIndirect(const void*, void*, const void*, const char*) override {
|
||||
NotImplemented_nid_no_patch("sce::Job indirect compute dispatch");
|
||||
return 0;
|
||||
}
|
||||
APS5_VABI int DispatchCompute(const void* shader, void* userData, std::int32_t x, std::int32_t y, std::int32_t z, const char* name) override;
|
||||
APS5_VABI int DispatchComputeIndirect(const void* shader, void* userData, const void* indirectArguments, const char* name) override;
|
||||
|
||||
bool Open(Manager* owner, const Counters& limit, std::uint32_t runQueue);
|
||||
int WaitAndClose(std::uint32_t index);
|
||||
void Advance();
|
||||
void DispatchCompleted(std::uint32_t invocations);
|
||||
void CollectCompute();
|
||||
bool Busy() const { return outstanding != 0; }
|
||||
|
||||
Manager* manager = nullptr;
|
||||
bool open = false;
|
||||
bool compute = false;
|
||||
std::uint32_t queue = 0;
|
||||
|
||||
private:
|
||||
int Latch(int code);
|
||||
void RequireOpen() const;
|
||||
int WaitLocked(std::unique_lock<std::mutex>& guard);
|
||||
int AppendCompute(Item item);
|
||||
void SubmitCompute(const Item& item);
|
||||
|
||||
Counters limits;
|
||||
Counters submitted;
|
||||
@@ -172,6 +200,11 @@ private:
|
||||
std::deque<Item> items;
|
||||
std::mutex waiters;
|
||||
std::condition_variable passed;
|
||||
Label label{};
|
||||
std::uint64_t labelSeen = 0;
|
||||
std::uint64_t labelTotal = 0;
|
||||
bool batchStarted = false;
|
||||
std::deque<std::vector<std::uint32_t>> commands;
|
||||
};
|
||||
|
||||
class Factory final : public ISequenceFactory {
|
||||
@@ -196,6 +229,11 @@ struct Manager {
|
||||
std::uint32_t persistentThreads = 0;
|
||||
std::array<std::deque<Item*>, 2> runQueues;
|
||||
std::vector<Sequence> sequences;
|
||||
std::deque<Sequence> computeSequences;
|
||||
std::uint32_t freeComputeSequences = 0;
|
||||
std::vector<std::uint32_t> computeQueues;
|
||||
KernelEqueue computeEvents = 0;
|
||||
Pthread computeHandler = nullptr;
|
||||
std::array<Factory, 2> factories;
|
||||
std::array<Factory, 2> unifiedFactories;
|
||||
std::vector<Pthread> workers;
|
||||
@@ -231,6 +269,8 @@ bool Sequence::Open(Manager* owner, const Counters& limit, std::uint32_t runQueu
|
||||
outstanding = 0;
|
||||
cursor = 0;
|
||||
items.clear();
|
||||
commands.clear();
|
||||
batchStarted = false;
|
||||
if (!TakeNode(*owner)) return false;
|
||||
open = true;
|
||||
return true;
|
||||
@@ -250,6 +290,12 @@ void Sequence::Advance() {
|
||||
manager->work.notify_all();
|
||||
continue;
|
||||
}
|
||||
if (item.type == ItemType::Compute || item.type == ItemType::ComputeIndirect) {
|
||||
++outstanding;
|
||||
++cursor;
|
||||
SubmitCompute(item);
|
||||
continue;
|
||||
}
|
||||
if (outstanding != 0) return;
|
||||
if (item.type == ItemType::SyncPoint) {
|
||||
++completed.syncPoints;
|
||||
@@ -260,6 +306,8 @@ void Sequence::Advance() {
|
||||
const auto retired = static_cast<std::uint32_t>(cursor) + 1;
|
||||
items.erase(items.begin(), items.begin() + static_cast<std::ptrdiff_t>(retired));
|
||||
cursor = 0;
|
||||
commands.clear();
|
||||
batchStarted = false;
|
||||
if (error == 0) {
|
||||
submitted.invocations -= completed.invocations;
|
||||
submitted.dispatches -= completed.dispatches;
|
||||
@@ -296,6 +344,81 @@ int Sequence::Dispatch(JobEntry entry, void* argument, std::uint32_t count, cons
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Sequence::SubmitCompute(const Item& item) {
|
||||
const auto* bytes = static_cast<const std::uint8_t*>(item.shader);
|
||||
const ShaderRegister* registers = nullptr;
|
||||
const std::uint8_t* specials = nullptr;
|
||||
std::memcpy(®isters, bytes + 0x20, sizeof(registers));
|
||||
std::memcpy(&specials, bytes + 0x28, sizeof(specials));
|
||||
const std::uint32_t registerCount = bytes[0x5c];
|
||||
std::uint32_t modifier = 0;
|
||||
std::memcpy(&modifier, specials + 0x10, sizeof(modifier));
|
||||
const auto userData = reinterpret_cast<std::uint64_t>(item.argument);
|
||||
const std::array<std::uint32_t, 2> userDataWords{static_cast<std::uint32_t>(userData), static_cast<std::uint32_t>(userData >> 32)};
|
||||
auto& words = commands.emplace_back(64 + 3 * registerCount + item.name.size() / 4);
|
||||
CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0};
|
||||
bool encoded = sceAgcAcbAcquireMem(&buffer, 0x20380u | (batchStarted ? 0u : 1u), nullptr, 0, 0x190) != nullptr;
|
||||
encoded = encoded && sceAgcCbSetShRegistersDirect(&buffer, registers, registerCount) != nullptr;
|
||||
encoded = encoded && sceAgcCbSetShRegisterRangeDirect(&buffer, UserDataRegister, userDataWords.data(), 2) != nullptr;
|
||||
if (item.named) encoded = encoded && sceAgcAcbPushMarker(&buffer, item.name.c_str(), 0) != nullptr;
|
||||
if ((modifier & 1) != 0) {
|
||||
if (item.type == ItemType::Compute) {
|
||||
encoded = encoded && sceAgcCbDispatch(&buffer, item.groups[0], item.groups[1], item.groups[2], modifier) != nullptr;
|
||||
} else {
|
||||
encoded = encoded && sceAgcAcbDispatchIndirect(&buffer, item.indirectArguments, modifier) != nullptr;
|
||||
}
|
||||
}
|
||||
if (item.named) encoded = encoded && sceAgcAcbPopMarker(&buffer) != nullptr;
|
||||
encoded = encoded && sceAgcCbReleaseMem(&buffer, 0x2f, 0x620, 0, 0, &label, 1, ++labelTotal, 0, 0, 2, 0) != nullptr;
|
||||
if (!encoded) throw std::runtime_error("libSceJobManager: a compute dispatch did not fit its command buffer");
|
||||
batchStarted = true;
|
||||
const Packet packet{words.data(), static_cast<std::uint32_t>(buffer.cursor_up - words.data()), 0, {}};
|
||||
if (sceAgcDriverSubmitAcb(queue, &packet) != 0) throw std::runtime_error("libSceJobManager: sceAgcDriverSubmitAcb failed");
|
||||
}
|
||||
|
||||
void Sequence::CollectCompute() {
|
||||
const std::uint64_t value = label.value;
|
||||
const auto finished = static_cast<std::uint32_t>(value - labelSeen);
|
||||
labelSeen = value;
|
||||
if (finished == 0) return;
|
||||
completed.dispatches += finished;
|
||||
outstanding -= finished;
|
||||
if (outstanding == 0) Advance();
|
||||
}
|
||||
|
||||
int Sequence::AppendCompute(Item item) {
|
||||
const std::lock_guard guard(manager->lock);
|
||||
RequireOpen();
|
||||
if (!compute) throw std::runtime_error("libSceJobManager: a compute dispatch was made on a CPU sequence");
|
||||
if (error != 0) return SequenceInvalid;
|
||||
if (submitted.dispatches++ >= limits.dispatches) return Latch(SequenceLimitExceeded);
|
||||
if (!TakeNode(*manager)) return Latch(SequenceNoNodes);
|
||||
items.push_back(std::move(item));
|
||||
Advance();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Sequence::DispatchCompute(const void* shader, void* userData, std::int32_t x, std::int32_t y, std::int32_t z, const char* name) {
|
||||
if (shader == nullptr) return SequenceNullArgument;
|
||||
if (x <= 0 || y <= 0 || z <= 0) return SequenceOutOfRange;
|
||||
Item item{ItemType::Compute, this, nullptr, userData, 0, 0, 0, nullptr};
|
||||
item.shader = shader;
|
||||
item.groups = {static_cast<std::uint32_t>(x), static_cast<std::uint32_t>(y), static_cast<std::uint32_t>(z)};
|
||||
item.named = name != nullptr;
|
||||
if (item.named) item.name = name;
|
||||
return AppendCompute(std::move(item));
|
||||
}
|
||||
|
||||
int Sequence::DispatchComputeIndirect(const void* shader, void* userData, const void* indirectArguments, const char* name) {
|
||||
if (shader == nullptr || indirectArguments == nullptr || (reinterpret_cast<std::uint64_t>(indirectArguments) & 0x1f) != 0) return SequenceNullArgument;
|
||||
Item item{ItemType::ComputeIndirect, this, nullptr, userData, 0, 0, 0, nullptr};
|
||||
item.shader = shader;
|
||||
item.indirectArguments = indirectArguments;
|
||||
item.named = name != nullptr;
|
||||
if (item.named) item.name = name;
|
||||
return AppendCompute(std::move(item));
|
||||
}
|
||||
|
||||
int Sequence::SyncPoint() {
|
||||
const std::lock_guard guard(manager->lock);
|
||||
RequireOpen();
|
||||
@@ -318,10 +441,11 @@ int Sequence::WaitAndClose(std::uint32_t index) {
|
||||
const int result = WaitLocked(guard);
|
||||
ReturnNodes(*manager, static_cast<std::uint32_t>(items.size()) + 1);
|
||||
items.clear();
|
||||
commands.clear();
|
||||
cursor = 0;
|
||||
outstanding = 0;
|
||||
open = false;
|
||||
manager->freeSequences |= std::uint32_t{1} << index;
|
||||
(compute ? manager->freeComputeSequences : manager->freeSequences) |= std::uint32_t{1} << index;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -353,42 +477,63 @@ bool ValidLimits(std::uint32_t invocations, std::uint32_t dispatches, std::uint3
|
||||
return invocations != 0 || dispatches != 0 || syncPoints != 0 || waits != 0;
|
||||
}
|
||||
|
||||
int Factory::Create(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char*) {
|
||||
template <typename TSequences>
|
||||
int CreateIn(Manager* manager, TSequences& sequences, std::uint32_t& free, std::uint32_t priority, ISequence** out, const Counters& limits) {
|
||||
if (out == nullptr) return SequenceNullArgument;
|
||||
if (!ValidLimits(invocations, dispatches, syncPoints, waits)) return SequenceOutOfRange;
|
||||
if (!ValidLimits(limits.invocations, limits.dispatches, limits.syncPoints, limits.waits)) return SequenceOutOfRange;
|
||||
const std::lock_guard guard(manager->lock);
|
||||
if (manager->freeSequences == 0) return SequenceNoneFree;
|
||||
const int index = std::countr_zero(manager->freeSequences);
|
||||
manager->freeSequences &= ~(std::uint32_t{1} << index);
|
||||
Sequence& sequence = manager->sequences[static_cast<std::size_t>(index)];
|
||||
if (!sequence.Open(manager, {invocations, dispatches, syncPoints, waits}, priority)) return SequenceNoNodes;
|
||||
if (free == 0) return SequenceNoneFree;
|
||||
const int index = std::countr_zero(free);
|
||||
free &= ~(std::uint32_t{1} << index);
|
||||
Sequence& sequence = sequences[static_cast<std::size_t>(index)];
|
||||
if (!sequence.Open(manager, limits, priority)) return SequenceNoNodes;
|
||||
*out = &sequence;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Factory::Destroy(ISequence* sequence) {
|
||||
template <typename TSequences>
|
||||
int DestroyIn(Manager* manager, TSequences& sequences, const std::uint32_t& free, ISequence* sequence) {
|
||||
Sequence* found = nullptr;
|
||||
std::uint32_t index = 0;
|
||||
{
|
||||
const std::lock_guard guard(manager->lock);
|
||||
for (; index < manager->sequences.size(); ++index) {
|
||||
if (&manager->sequences[index] == sequence) break;
|
||||
for (; index < sequences.size(); ++index) {
|
||||
if (&sequences[index] == sequence) break;
|
||||
}
|
||||
if (index == manager->sequences.size() || (manager->freeSequences & (std::uint32_t{1} << index)) != 0) return SequenceInvalid;
|
||||
found = &manager->sequences[index];
|
||||
if (index == sequences.size() || (free & (std::uint32_t{1} << index)) != 0) return SequenceInvalid;
|
||||
found = &sequences[index];
|
||||
}
|
||||
static_cast<void>(found->WaitAndClose(index));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Factory::CreateCompute(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char*) {
|
||||
if (out == nullptr) return SequenceNullArgument;
|
||||
if (!ValidLimits(invocations, dispatches, syncPoints, waits)) return SequenceOutOfRange;
|
||||
return SequenceNoneFree;
|
||||
int Factory::Create(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char*) {
|
||||
return CreateIn(manager, manager->sequences, manager->freeSequences, priority, out, {invocations, dispatches, syncPoints, waits});
|
||||
}
|
||||
|
||||
int Factory::DestroyCompute(ISequence*) {
|
||||
return SequenceInvalid;
|
||||
int Factory::Destroy(ISequence* sequence) {
|
||||
return DestroyIn(manager, manager->sequences, manager->freeSequences, sequence);
|
||||
}
|
||||
|
||||
int Factory::CreateCompute(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char*) {
|
||||
return CreateIn(manager, manager->computeSequences, manager->freeComputeSequences, priority, out, {invocations, dispatches, syncPoints, waits});
|
||||
}
|
||||
|
||||
int Factory::DestroyCompute(ISequence* sequence) {
|
||||
return DestroyIn(manager, manager->computeSequences, manager->freeComputeSequences, sequence);
|
||||
}
|
||||
|
||||
void* APS5_VABI ComputeHandlerEntry(void* argument) {
|
||||
Manager& manager = *static_cast<Manager*>(argument);
|
||||
for (;;) {
|
||||
KernelEvent event{};
|
||||
int count = 0;
|
||||
if (sceKernelWaitEqueue(manager.computeEvents, &event, 1, &count, nullptr) != 0) throw std::runtime_error("libSceJobManager: sceKernelWaitEqueue failed in the compute handler");
|
||||
if (count <= 0) continue;
|
||||
if (sceKernelGetEventFilter(&event) == UserEventFilter && sceKernelGetEventId(&event) == 0) return nullptr;
|
||||
const std::lock_guard guard(manager.lock);
|
||||
for (Sequence& sequence : manager.computeSequences) sequence.CollectCompute();
|
||||
}
|
||||
}
|
||||
|
||||
bool RunOne(Manager& manager, std::unique_lock<std::mutex>& guard) {
|
||||
@@ -421,11 +566,16 @@ void* APS5_VABI WorkerEntry(void* argument) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::uint32_t NodeCount(std::uint64_t memory, std::uint64_t size, std::uint32_t workers, std::uint32_t sequences, bool* fits) {
|
||||
std::uint32_t NodeCount(std::uint64_t memory, std::uint64_t size, std::uint32_t workers, std::uint32_t sequences, std::uint32_t computeQueues, bool* fits) {
|
||||
const std::uint64_t end = memory + size;
|
||||
std::uint64_t address = AlignUp(memory + StateEnd, 8) + workers * ThreadRecordBytes;
|
||||
for (const std::uint64_t bytes : {std::uint64_t{0x100}, std::uint64_t{0x80}, std::uint64_t{0x80}, std::uint64_t{0x80}}) address = AlignUp(address, 64) + bytes;
|
||||
address = AlignUp(AlignUp(address, 64) + sequences * SequenceBytes, 64);
|
||||
address = AlignUp(address, 64) + (sequences + computeQueues) * SequenceBytes;
|
||||
if (computeQueues != 0) {
|
||||
address = AlignUp(address + ComputeContextBytes, 0x100) + AlignUp(ComputeCommandBytes * computeQueues, 8);
|
||||
address += AlignUp(4 * std::uint64_t{computeQueues}, 8) + 8 * std::uint64_t{computeQueues};
|
||||
}
|
||||
address = AlignUp(address, 64);
|
||||
*fits = address <= end;
|
||||
return *fits ? static_cast<std::uint32_t>(((end - address) / NodeBytes) & 0xffff) : 0;
|
||||
}
|
||||
@@ -462,6 +612,30 @@ void StartWorkers(Manager& manager, const ConfigParams& config, std::uint32_t wo
|
||||
}
|
||||
}
|
||||
|
||||
void StartCompute(Manager& manager, std::uint32_t computeSequences, std::uint64_t computeQueueMask, std::uint32_t priority, std::uint32_t affinity) {
|
||||
if (sceKernelCreateEqueue(&manager.computeEvents, "SceJobManagerComputeEventQueue") != 0) throw std::runtime_error("libSceJobManager: sceKernelCreateEqueue failed");
|
||||
for (std::uint64_t mask = computeQueueMask; mask != 0; mask &= mask - 1) {
|
||||
const std::uint32_t queue = FirstComputeQueue + static_cast<std::uint32_t>(std::countr_zero(mask));
|
||||
if (sceAgcDriverAddEqEvent(manager.computeEvents, static_cast<int>(queue), nullptr) != 0) throw std::runtime_error("libSceJobManager: sceAgcDriverAddEqEvent failed");
|
||||
manager.computeQueues.push_back(queue);
|
||||
}
|
||||
manager.computeSequences.resize(computeSequences);
|
||||
for (std::uint32_t index = 0; index < computeSequences; ++index) {
|
||||
manager.computeSequences[index].compute = true;
|
||||
manager.computeSequences[index].queue = manager.computeQueues[index];
|
||||
}
|
||||
manager.freeComputeSequences = computeSequences == MaxSequences ? ~std::uint32_t{0} : (std::uint32_t{1} << computeSequences) - 1;
|
||||
const KernelSchedParam schedule{static_cast<int>(priority)};
|
||||
PthreadAttr attributes = nullptr;
|
||||
if (scePthreadAttrInit(&attributes) != 0 || scePthreadAttrSetinheritsched(&attributes, 0) != 0 || scePthreadAttrSetschedparam(&attributes, &schedule) != 0 ||
|
||||
scePthreadAttrSetstacksize(&attributes, 0x4000) != 0 || scePthreadAttrSetaffinity(&attributes, affinity) != 0) {
|
||||
throw std::runtime_error("libSceJobManager: a compute handler thread attribute was rejected");
|
||||
}
|
||||
const int created = scePthreadCreate(&manager.computeHandler, &attributes, ComputeHandlerEntry, &manager, "SceJobManagerComputeEventHdlr");
|
||||
scePthreadAttrDestroy(&attributes);
|
||||
if (created != 0) throw std::runtime_error("libSceJobManager: scePthreadCreate failed for the compute handler");
|
||||
}
|
||||
|
||||
int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t workers, std::uint32_t sequences, std::uint32_t computeSequences, std::uint64_t computeQueueMask, const ConfigParams* config, bool unified) {
|
||||
if (Implementation(self) != nullptr) return ManagerAlreadyInitialized;
|
||||
if (memory == nullptr) return ManagerNullMemory;
|
||||
@@ -469,7 +643,7 @@ int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t worke
|
||||
if ((base & 0xff) != 0) return ManagerMisalignedMemory;
|
||||
if (size == 0 || workers == 0 || workers > MaxWorkers) return ManagerInvalidArgument;
|
||||
if (unified) {
|
||||
if ((sequences == 0 && computeSequences == 0) || sequences + computeSequences > MaxSequences || (computeQueueMask >> 56) != 0 || static_cast<std::uint32_t>(std::popcount(computeQueueMask)) < computeSequences) return ManagerInvalidArgument;
|
||||
if ((sequences == 0 && computeSequences == 0) || std::uint64_t{sequences} + computeSequences > MaxSequences || (computeQueueMask >> 56) != 0 || static_cast<std::uint32_t>(std::popcount(computeQueueMask)) < computeSequences) return ManagerInvalidArgument;
|
||||
} else if (sequences == 0 || sequences > MaxSequences) {
|
||||
return ManagerInvalidArgument;
|
||||
}
|
||||
@@ -479,12 +653,16 @@ int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t worke
|
||||
const BlockHeader header{0x4d4a, static_cast<std::uint16_t>(StateOffset), 0x05000001, static_cast<std::uint32_t>(size), 0};
|
||||
std::memcpy(memory, &header, sizeof(header));
|
||||
if (config != nullptr && config->version > 1) return ManagerInvalidArgument;
|
||||
if (computeSequences != 0) NotImplemented_nid_no_patch("sce::Job::JobManager::initialize with compute sequences");
|
||||
const ConfigParams& settings = config != nullptr ? *config : DefaultConfig;
|
||||
const auto computeQueues = computeSequences != 0 ? static_cast<std::uint32_t>(std::popcount(computeQueueMask)) : 0;
|
||||
bool fits = false;
|
||||
const std::uint32_t nodes = NodeCount(base, size, workers, sequences, &fits);
|
||||
const std::uint32_t nodes = NodeCount(base, size, workers, sequences, computeQueues, &fits);
|
||||
if (!fits || nodes == 0) return ManagerMemoryTooSmall;
|
||||
if (const int invalid = ValidateWorkers(settings, workers); invalid != 0) return invalid;
|
||||
const bool ownHandlerSettings = (settings.version & 1) != 0;
|
||||
const std::uint32_t handlerPriority = ownHandlerSettings ? settings.computePriority : 0x100;
|
||||
const std::uint32_t handlerAffinity = ownHandlerSettings ? settings.computeAffinity : 0x1fff;
|
||||
if (computeSequences != 0 && (handlerPriority < 0x100 || handlerPriority > 0x2ff || handlerAffinity == 0 || handlerAffinity > 0x1fff)) return ManagerInvalidArgument;
|
||||
auto* manager = new Manager;
|
||||
manager->running = true;
|
||||
manager->freeNodes = nodes - 1;
|
||||
@@ -496,6 +674,7 @@ int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t worke
|
||||
manager->unifiedFactories[priority].manager = manager;
|
||||
manager->unifiedFactories[priority].priority = priority;
|
||||
}
|
||||
if (computeSequences != 0) StartCompute(*manager, computeSequences, computeQueueMask, handlerPriority, handlerAffinity);
|
||||
Implementation(self) = manager;
|
||||
StartWorkers(*manager, settings, workers);
|
||||
return 0;
|
||||
@@ -539,12 +718,19 @@ std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKN
|
||||
}
|
||||
const std::uint64_t total = params->sequences + computeSequences;
|
||||
if (params->invocations > total * MaxInvocations || params->dispatches > total * MaxDispatches || params->syncPoints > total * MaxSyncPoints || params->waits > total * MaxWaits) return 0;
|
||||
if (computeSequences != 0) NotImplemented_nid_no_patch("sce::Job::JobManager::calculateRequiredMemorySize with compute sequences");
|
||||
std::uint64_t bytes = (NodeBytes * params->workers + 0x317) & 0xfc0;
|
||||
bytes = (bytes + 0x13f) & ~std::uint64_t{0x3f};
|
||||
bytes = (bytes + 0x0bf) & ~std::uint64_t{0x3f};
|
||||
bytes = (bytes + 0x13f) & ~std::uint64_t{0x3f};
|
||||
bytes += params->sequences * SequenceBytes;
|
||||
if (computeSequences != 0) {
|
||||
const auto queues = static_cast<std::uint64_t>(std::popcount(params->computeQueueMask));
|
||||
bytes = (bytes + (params->sequences + queues) * SequenceBytes + 0x267) & ~std::uint64_t{0xff};
|
||||
bytes = (bytes + ComputeCommandBytes * queues + 3) & ~std::uint64_t{3};
|
||||
bytes = (bytes + 4 * queues + 7) & ~std::uint64_t{7};
|
||||
bytes += 8 * queues;
|
||||
} else {
|
||||
bytes += params->sequences * SequenceBytes;
|
||||
}
|
||||
const std::uint32_t nodes = params->dispatches + params->syncPoints + params->waits + static_cast<std::uint32_t>(total) + 1;
|
||||
return AlignUp(bytes, 64) + std::uint64_t{nodes} * NodeBytes + 0x800;
|
||||
}
|
||||
@@ -563,12 +749,23 @@ int APS5_VABI _ZN3sce3Job10JobManager8shutdownEv(void* self) {
|
||||
{
|
||||
const std::lock_guard guard(manager->lock);
|
||||
if (manager->blockedWaiters != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: a thread is still blocked in a sequence wait");
|
||||
for (const Sequence& sequence : manager->computeSequences) {
|
||||
if (sequence.open && sequence.Busy()) throw std::runtime_error("sce::Job::JobManager::shutdown: a compute sequence still has work in flight");
|
||||
}
|
||||
manager->running = false;
|
||||
manager->work.notify_all();
|
||||
}
|
||||
for (const Pthread worker : manager->workers) {
|
||||
if (scePthreadJoin(worker, nullptr) != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: scePthreadJoin failed");
|
||||
}
|
||||
if (manager->computeHandler != nullptr) {
|
||||
if (sceKernelAddUserEventEdge(manager->computeEvents, 0) != 0 || sceKernelTriggerUserEvent(manager->computeEvents, 0, nullptr) != 0) {
|
||||
throw std::runtime_error("sce::Job::JobManager::shutdown: the compute handler could not be signalled");
|
||||
}
|
||||
if (scePthreadJoin(manager->computeHandler, nullptr) != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: scePthreadJoin failed");
|
||||
for (const std::uint32_t queue : manager->computeQueues) static_cast<void>(sceAgcDriverDeleteEqEvent(manager->computeEvents, static_cast<int>(queue)));
|
||||
if (sceKernelDeleteEqueue(manager->computeEvents) != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: sceKernelDeleteEqueue failed");
|
||||
}
|
||||
Implementation(self) = nullptr;
|
||||
delete manager;
|
||||
return 0;
|
||||
|
||||
@@ -987,6 +987,8 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void*
|
||||
if (it == g_socks.end()) return fail(NET_EBADF);
|
||||
socket = it->second;
|
||||
}
|
||||
const GuestArena::HostWrite lengthOutput(from == nullptr ? nullptr : fromlen, from == nullptr ? 0 : sizeof(*fromlen));
|
||||
if (!lengthOutput.Open()) return fail(NET_EFAULT);
|
||||
#ifdef _WIN32
|
||||
if (const int wait = wait_for_local_address(s, socket, len == 0)) {
|
||||
if (wait != RECEIVE_NOTHING) return fail(wait);
|
||||
@@ -994,6 +996,10 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void*
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
const auto addressBytes = from != nullptr && socket.type == NET_SOCK_DGRAM ?
|
||||
std::min<std::uint32_t>(*fromlen, socket.family == NET_AF_INET ? 16u : 28u) : 0u;
|
||||
const GuestArena::HostWrite addressOutput(addressBytes == 0 ? nullptr : from, addressBytes);
|
||||
if (!addressOutput.Open()) return fail(NET_EFAULT);
|
||||
const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len);
|
||||
if (!destination.Open()) return fail(NET_EFAULT);
|
||||
sockaddr_storage peer{};
|
||||
|
||||
@@ -4,6 +4,8 @@ add_library(libSceNpTrophy2 SHARED EXCLUDE_FROM_ALL
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/GameInfo.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/TrophyInfo.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/GroupInfo.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/TrophyPackage.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/TitleTrophies.cpp
|
||||
)
|
||||
|
||||
target_include_directories(libSceNpTrophy2 PRIVATE
|
||||
@@ -23,3 +25,16 @@ if(BUILD_TESTING)
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpTrophy2>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(np_trophy2_package_tests EXCLUDE_FROM_ALL tests/TrophyPackage.cpp)
|
||||
target_include_directories(np_trophy2_package_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(np_trophy2_package_tests PRIVATE libSceNpTrophy2 libc)
|
||||
configure_windows_unwind(np_trophy2_package_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(np_trophy2_package_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
|
||||
add_test(NAME np_trophy2_package COMMAND np_trophy2_package_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(np_trophy2_package PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpTrophy2>")
|
||||
endif()
|
||||
endif()
|
||||
@@ -29,7 +29,8 @@ static constexpr std::uint32_t NP_TROPHY2_PROGRESS_PERCENTAGE = 0;
|
||||
static constexpr std::int32_t NP_TROPHY2_TROPHY_GRADE_BRONZE = 4;
|
||||
|
||||
static constexpr std::int32_t NP_TROPHY2_PROGRESS_TYPE_NONE = 0;
|
||||
static constexpr std::uint32_t NP_TROPHY2_PROGRESS_VALUE_NONE = 0;
|
||||
static constexpr std::int32_t NP_TROPHY2_PROGRESS_TYPE_VALUE = 1;
|
||||
static constexpr std::uint64_t NP_TROPHY2_PROGRESS_VALUE_NONE = 0;
|
||||
|
||||
static constexpr std::size_t NP_TROPHY2_ICON_SIZE_NONE = 0;
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
struct NpTrophy2Progress { std::uint32_t value; };
|
||||
struct NpTrophy2Progress {
|
||||
std::int32_t type;
|
||||
std::uint8_t reserved[4];
|
||||
std::uint64_t value;
|
||||
};
|
||||
|
||||
struct NpTrophy2GameDetails {
|
||||
std::uint32_t num_groups;
|
||||
@@ -67,4 +71,12 @@ struct NpTrophy2Data {
|
||||
std::uint64_t timestamp_tick;
|
||||
};
|
||||
|
||||
static_assert(sizeof(NpTrophy2Progress) == 16);
|
||||
static_assert(sizeof(NpTrophy2GameDetails) == 152);
|
||||
static_assert(sizeof(NpTrophy2GameData) == 24);
|
||||
static_assert(sizeof(NpTrophy2GroupDetails) == 152);
|
||||
static_assert(sizeof(NpTrophy2GroupData) == 32);
|
||||
static_assert(sizeof(NpTrophy2Details) == 1312);
|
||||
static_assert(sizeof(NpTrophy2Data) == 32);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,54 @@
|
||||
#ifndef CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TITLETROPHIES_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TITLETROPHIES_HPP
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "prx/libSceNpTrophy2/include/TrophyPackage.hpp"
|
||||
|
||||
namespace NpTrophy2 {
|
||||
|
||||
inline constexpr std::uint64_t MaxUcpSize = std::uint64_t{256} << 20u;
|
||||
|
||||
struct TitleTrophies {
|
||||
std::optional<TrophyPackage> package;
|
||||
};
|
||||
|
||||
std::string PackagePath(std::uint32_t serviceLabel);
|
||||
TitleTrophies LoadTitleTrophies(std::uint32_t serviceLabel, std::string_view language);
|
||||
const TitleTrophies& Title(std::uint32_t serviceLabel);
|
||||
const TrophyPackage* Package(std::uint32_t serviceLabel);
|
||||
void CopyText(char* destination, std::size_t size, std::string_view text);
|
||||
|
||||
struct ContextOwner {
|
||||
std::int32_t userId = 0;
|
||||
std::uint32_t serviceLabel = 0;
|
||||
};
|
||||
|
||||
int CreateContext(std::int32_t userId, std::uint32_t serviceLabel);
|
||||
void DestroyContext(int context);
|
||||
std::optional<ContextOwner> FindContext(int context);
|
||||
|
||||
std::optional<std::uint64_t> ProgressTarget(const TrophyDefinition& trophy);
|
||||
std::uint64_t TrophyProgress(const TrophyDefinition& trophy, std::optional<std::uint64_t> stat, bool unlocked);
|
||||
|
||||
struct TrophyCounts {
|
||||
std::uint32_t trophies = 0;
|
||||
std::uint32_t unlocked = 0;
|
||||
std::array<std::uint32_t, GradeBronze + 1> grades{};
|
||||
std::array<std::uint32_t, GradeBronze + 1> unlockedGrades{};
|
||||
|
||||
std::uint32_t Percentage() const;
|
||||
};
|
||||
|
||||
TrophyCounts CountTrophies(const TrophyPackage& package, std::optional<std::int32_t> group);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,96 @@
|
||||
#ifndef CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TROPHYPACKAGE_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TROPHYPACKAGE_HPP
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace NpTrophy2 {
|
||||
|
||||
inline constexpr std::uint32_t UcpMagic = 0xB228C60Au;
|
||||
inline constexpr std::uint32_t UcpVersion = 1;
|
||||
inline constexpr std::uint64_t UcpHeaderSize = 0x40;
|
||||
inline constexpr std::uint64_t UcpTocHeaderSize = 0x20;
|
||||
inline constexpr std::uint64_t UcpEntrySize = 0x40;
|
||||
inline constexpr std::size_t UcpNameSize = 0x20;
|
||||
inline constexpr std::uint64_t UcpMaxEntries = 4096;
|
||||
|
||||
using UcpFiles = std::map<std::string, std::string_view, std::less<>>;
|
||||
|
||||
UcpFiles ReadUcp(std::string_view data);
|
||||
|
||||
class Json {
|
||||
public:
|
||||
enum class Kind { Null, Bool, Number, String, Array, Object };
|
||||
|
||||
Kind kind = Kind::Null;
|
||||
bool boolean = false;
|
||||
std::string text;
|
||||
std::vector<Json> items;
|
||||
std::vector<std::pair<std::string, Json>> members;
|
||||
|
||||
const Json* Find(std::string_view key) const;
|
||||
bool IsTrue() const;
|
||||
std::optional<std::uint64_t> Unsigned() const;
|
||||
std::optional<std::int32_t> Id() const;
|
||||
};
|
||||
|
||||
Json ParseJson(std::string_view text);
|
||||
|
||||
inline constexpr std::int32_t GradePlatinum = 1;
|
||||
inline constexpr std::int32_t GradeGold = 2;
|
||||
inline constexpr std::int32_t GradeSilver = 3;
|
||||
inline constexpr std::int32_t GradeBronze = 4;
|
||||
inline constexpr std::int32_t BaseGroup = -1;
|
||||
inline constexpr std::size_t MaxTrophies = 1000;
|
||||
inline constexpr std::size_t MaxGroups = 50;
|
||||
|
||||
enum class Comparator { None, GreaterEqual, Greater, LessEqual, Less };
|
||||
|
||||
struct UnlockCondition {
|
||||
std::uint64_t statId = 0;
|
||||
std::uint64_t target = 0;
|
||||
Comparator comparator = Comparator::None;
|
||||
|
||||
bool Reached(std::uint64_t value) const;
|
||||
};
|
||||
|
||||
struct TrophyDefinition {
|
||||
std::int32_t id = 0;
|
||||
std::int32_t grade = GradeBronze;
|
||||
std::int32_t groupId = BaseGroup;
|
||||
bool hidden = false;
|
||||
bool hasReward = false;
|
||||
bool progressive = false;
|
||||
std::optional<std::int32_t> platinumId;
|
||||
std::optional<std::uint64_t> statId;
|
||||
std::optional<std::uint64_t> target;
|
||||
Comparator comparator = Comparator::None;
|
||||
std::string name;
|
||||
std::string detail;
|
||||
std::string reward;
|
||||
|
||||
std::optional<UnlockCondition> Condition() const;
|
||||
};
|
||||
|
||||
struct TrophyPackage {
|
||||
std::string language;
|
||||
std::string title;
|
||||
std::map<std::int32_t, TrophyDefinition> trophies;
|
||||
std::map<std::int32_t, std::string> groups;
|
||||
std::vector<std::string> skipped;
|
||||
};
|
||||
|
||||
TrophyPackage ParseTrophyPackage(std::string_view ucp, std::string_view language);
|
||||
|
||||
std::string_view LanguageTag(int consoleLanguage);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,26 +1,68 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "prx/libSceNpTrophy2/include/NpTrophy2.hpp"
|
||||
#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
namespace NpTrophy2 {
|
||||
|
||||
namespace {
|
||||
|
||||
struct Contexts {
|
||||
std::mutex mutex;
|
||||
std::map<int, ContextOwner> owners;
|
||||
int next = NP_TROPHY2_CONTEXT_DEFAULT;
|
||||
};
|
||||
|
||||
Contexts& contexts() {
|
||||
static Contexts table;
|
||||
return table;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int CreateContext(std::int32_t userId, std::uint32_t serviceLabel) {
|
||||
auto& table = contexts();
|
||||
std::lock_guard lock(table.mutex);
|
||||
const int context = table.next++;
|
||||
table.owners[context] = {userId, serviceLabel};
|
||||
return context;
|
||||
}
|
||||
|
||||
void DestroyContext(int context) {
|
||||
auto& table = contexts();
|
||||
std::lock_guard lock(table.mutex);
|
||||
table.owners.erase(context);
|
||||
}
|
||||
|
||||
std::optional<ContextOwner> FindContext(int context) {
|
||||
auto& table = contexts();
|
||||
std::lock_guard lock(table.mutex);
|
||||
const auto found = table.owners.find(context);
|
||||
if (found == table.owners.end()) return std::nullopt;
|
||||
return found->second;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI sceNpTrophy2CreateContext(int* context, int user_id, uint32_t service_label, uint64_t options) {
|
||||
if (context == nullptr) {
|
||||
APS5_INVALID_ARG_EX;
|
||||
}
|
||||
(void)user_id;
|
||||
(void)service_label;
|
||||
(void)options;
|
||||
*context = NP_TROPHY2_CONTEXT_DEFAULT;
|
||||
*context = NpTrophy2::CreateContext(user_id, service_label);
|
||||
return SCE_NP_TROPHY2_OK;
|
||||
}
|
||||
|
||||
int APS5_VABI sceNpTrophy2DestroyContext(int context) {
|
||||
(void)context;
|
||||
NpTrophy2::DestroyContext(context);
|
||||
return SCE_NP_TROPHY2_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "prx/libSceNpTrophy2/include/NpTrophy2.hpp"
|
||||
#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
extern "C" {
|
||||
@@ -12,26 +14,50 @@ int APS5_VABI sceNpTrophy2GetGameInfo(int context, int handle, NpTrophy2GameDeta
|
||||
if (details == nullptr && data == nullptr) {
|
||||
APS5_INVALID_ARG_EX;
|
||||
}
|
||||
(void)context;
|
||||
(void)handle;
|
||||
const auto owner = NpTrophy2::FindContext(context);
|
||||
const auto* package = owner ? NpTrophy2::Package(owner->serviceLabel) : nullptr;
|
||||
if (package == nullptr) {
|
||||
if (details != nullptr) {
|
||||
std::memset(details, 0, sizeof(*details));
|
||||
details->num_groups = NP_TROPHY2_NUM_GROUPS;
|
||||
details->num_trophies = NP_TROPHY2_NUM_TROPHIES;
|
||||
details->num_platinum = NP_TROPHY2_NUM_PLATINUM;
|
||||
details->num_gold = NP_TROPHY2_NUM_GOLD;
|
||||
details->num_silver = NP_TROPHY2_NUM_SILVER;
|
||||
details->num_bronze = NP_TROPHY2_NUM_BRONZE;
|
||||
std::strncpy(details->title, NP_TROPHY2_GAME_TITLE, sizeof(details->title) - 1);
|
||||
}
|
||||
if (data != nullptr) {
|
||||
std::memset(data, 0, sizeof(*data));
|
||||
data->unlocked_trophies = NP_TROPHY2_UNLOCKED_TROPHIES;
|
||||
data->unlocked_platinum = NP_TROPHY2_UNLOCKED_PLATINUM;
|
||||
data->unlocked_gold = NP_TROPHY2_UNLOCKED_GOLD;
|
||||
data->unlocked_silver = NP_TROPHY2_UNLOCKED_SILVER;
|
||||
data->unlocked_bronze = NP_TROPHY2_UNLOCKED_BRONZE;
|
||||
data->progress_percentage = NP_TROPHY2_PROGRESS_PERCENTAGE;
|
||||
}
|
||||
return SCE_NP_TROPHY2_OK;
|
||||
}
|
||||
const auto counts = NpTrophy2::CountTrophies(*package, std::nullopt);
|
||||
if (details != nullptr) {
|
||||
std::memset(details, 0, sizeof(*details));
|
||||
details->num_groups = NP_TROPHY2_NUM_GROUPS;
|
||||
details->num_trophies = NP_TROPHY2_NUM_TROPHIES;
|
||||
details->num_platinum = NP_TROPHY2_NUM_PLATINUM;
|
||||
details->num_gold = NP_TROPHY2_NUM_GOLD;
|
||||
details->num_silver = NP_TROPHY2_NUM_SILVER;
|
||||
details->num_bronze = NP_TROPHY2_NUM_BRONZE;
|
||||
std::strncpy(details->title, NP_TROPHY2_GAME_TITLE, sizeof(details->title) - 1);
|
||||
details->num_groups = static_cast<std::uint32_t>(package->groups.size());
|
||||
details->num_trophies = counts.trophies;
|
||||
details->num_platinum = counts.grades[NpTrophy2::GradePlatinum];
|
||||
details->num_gold = counts.grades[NpTrophy2::GradeGold];
|
||||
details->num_silver = counts.grades[NpTrophy2::GradeSilver];
|
||||
details->num_bronze = counts.grades[NpTrophy2::GradeBronze];
|
||||
NpTrophy2::CopyText(details->title, sizeof(details->title), package->title);
|
||||
}
|
||||
if (data != nullptr) {
|
||||
std::memset(data, 0, sizeof(*data));
|
||||
data->unlocked_trophies = NP_TROPHY2_UNLOCKED_TROPHIES;
|
||||
data->unlocked_platinum = NP_TROPHY2_UNLOCKED_PLATINUM;
|
||||
data->unlocked_gold = NP_TROPHY2_UNLOCKED_GOLD;
|
||||
data->unlocked_silver = NP_TROPHY2_UNLOCKED_SILVER;
|
||||
data->unlocked_bronze = NP_TROPHY2_UNLOCKED_BRONZE;
|
||||
data->progress_percentage = NP_TROPHY2_PROGRESS_PERCENTAGE;
|
||||
data->unlocked_trophies = counts.unlocked;
|
||||
data->unlocked_platinum = counts.unlockedGrades[NpTrophy2::GradePlatinum];
|
||||
data->unlocked_gold = counts.unlockedGrades[NpTrophy2::GradeGold];
|
||||
data->unlocked_silver = counts.unlockedGrades[NpTrophy2::GradeSilver];
|
||||
data->unlocked_bronze = counts.unlockedGrades[NpTrophy2::GradeBronze];
|
||||
data->progress_percentage = counts.Percentage();
|
||||
}
|
||||
return SCE_NP_TROPHY2_OK;
|
||||
}
|
||||
|
||||
@@ -1,41 +1,85 @@
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "prx/libSceNpTrophy2/include/NpTrophy2.hpp"
|
||||
#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
void fillPlaceholderDetails(std::int32_t groupId, NpTrophy2GroupDetails* details) {
|
||||
std::memset(details, 0, sizeof(*details));
|
||||
details->group_id = groupId;
|
||||
details->num_trophies = NP_TROPHY2_NUM_TROPHIES;
|
||||
details->num_platinum = NP_TROPHY2_NUM_PLATINUM;
|
||||
details->num_gold = NP_TROPHY2_NUM_GOLD;
|
||||
details->num_silver = NP_TROPHY2_NUM_SILVER;
|
||||
details->num_bronze = NP_TROPHY2_NUM_BRONZE;
|
||||
std::strncpy(details->title, NP_TROPHY2_GROUP_TITLE, sizeof(details->title) - 1);
|
||||
}
|
||||
|
||||
void fillPlaceholderData(std::int32_t groupId, NpTrophy2GroupData* data) {
|
||||
std::memset(data, 0, sizeof(*data));
|
||||
data->group_id = groupId;
|
||||
data->unlocked_trophies = NP_TROPHY2_UNLOCKED_TROPHIES;
|
||||
data->unlocked_platinum = NP_TROPHY2_UNLOCKED_PLATINUM;
|
||||
data->unlocked_gold = NP_TROPHY2_UNLOCKED_GOLD;
|
||||
data->unlocked_silver = NP_TROPHY2_UNLOCKED_SILVER;
|
||||
data->unlocked_bronze = NP_TROPHY2_UNLOCKED_BRONZE;
|
||||
data->progress_percentage = NP_TROPHY2_PROGRESS_PERCENTAGE;
|
||||
}
|
||||
|
||||
void fillGroup(const NpTrophy2::TrophyPackage& package, std::int32_t groupId, const std::string& title, NpTrophy2GroupDetails* details, NpTrophy2GroupData* data) {
|
||||
const auto counts = NpTrophy2::CountTrophies(package, groupId);
|
||||
if (details != nullptr) {
|
||||
std::memset(details, 0, sizeof(*details));
|
||||
details->group_id = groupId;
|
||||
details->num_trophies = counts.trophies;
|
||||
details->num_platinum = counts.grades[NpTrophy2::GradePlatinum];
|
||||
details->num_gold = counts.grades[NpTrophy2::GradeGold];
|
||||
details->num_silver = counts.grades[NpTrophy2::GradeSilver];
|
||||
details->num_bronze = counts.grades[NpTrophy2::GradeBronze];
|
||||
NpTrophy2::CopyText(details->title, sizeof(details->title), title);
|
||||
}
|
||||
if (data != nullptr) {
|
||||
std::memset(data, 0, sizeof(*data));
|
||||
data->group_id = groupId;
|
||||
data->unlocked_trophies = counts.unlocked;
|
||||
data->unlocked_platinum = counts.unlockedGrades[NpTrophy2::GradePlatinum];
|
||||
data->unlocked_gold = counts.unlockedGrades[NpTrophy2::GradeGold];
|
||||
data->unlocked_silver = counts.unlockedGrades[NpTrophy2::GradeSilver];
|
||||
data->unlocked_bronze = counts.unlockedGrades[NpTrophy2::GradeBronze];
|
||||
data->progress_percentage = counts.Percentage();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI sceNpTrophy2GetGroupInfo(int context, int handle, int group_id, NpTrophy2GroupDetails* details, NpTrophy2GroupData* data) {
|
||||
if (details == nullptr && data == nullptr) {
|
||||
APS5_INVALID_ARG_EX;
|
||||
}
|
||||
(void)context;
|
||||
(void)handle;
|
||||
const std::int32_t normalized_group_id = (group_id < 0 ? NP_TROPHY2_GROUP_ID_BASE : group_id);
|
||||
if (details != nullptr) {
|
||||
std::memset(details, 0, sizeof(*details));
|
||||
details->group_id = normalized_group_id;
|
||||
details->num_trophies = NP_TROPHY2_NUM_TROPHIES;
|
||||
details->num_platinum = NP_TROPHY2_NUM_PLATINUM;
|
||||
details->num_gold = NP_TROPHY2_NUM_GOLD;
|
||||
details->num_silver = NP_TROPHY2_NUM_SILVER;
|
||||
details->num_bronze = NP_TROPHY2_NUM_BRONZE;
|
||||
std::strncpy(details->title, NP_TROPHY2_GROUP_TITLE, sizeof(details->title) - 1);
|
||||
const auto owner = NpTrophy2::FindContext(context);
|
||||
const auto* package = owner ? NpTrophy2::Package(owner->serviceLabel) : nullptr;
|
||||
if (package == nullptr) {
|
||||
const std::int32_t normalized_group_id = (group_id < 0 ? NP_TROPHY2_GROUP_ID_BASE : group_id);
|
||||
if (details != nullptr) fillPlaceholderDetails(normalized_group_id, details);
|
||||
if (data != nullptr) fillPlaceholderData(normalized_group_id, data);
|
||||
return SCE_NP_TROPHY2_OK;
|
||||
}
|
||||
if (data != nullptr) {
|
||||
std::memset(data, 0, sizeof(*data));
|
||||
data->group_id = normalized_group_id;
|
||||
data->unlocked_trophies = NP_TROPHY2_UNLOCKED_TROPHIES;
|
||||
data->unlocked_platinum = NP_TROPHY2_UNLOCKED_PLATINUM;
|
||||
data->unlocked_gold = NP_TROPHY2_UNLOCKED_GOLD;
|
||||
data->unlocked_silver = NP_TROPHY2_UNLOCKED_SILVER;
|
||||
data->unlocked_bronze = NP_TROPHY2_UNLOCKED_BRONZE;
|
||||
data->progress_percentage = NP_TROPHY2_PROGRESS_PERCENTAGE;
|
||||
const auto group = package->groups.find(group_id);
|
||||
if (group == package->groups.end()) {
|
||||
throw std::invalid_argument(std::string(__func__) + ": group " + std::to_string(group_id) + " is not in " + NpTrophy2::PackagePath(owner->serviceLabel));
|
||||
}
|
||||
fillGroup(*package, group->first, group->second, details, data);
|
||||
return SCE_NP_TROPHY2_OK;
|
||||
}
|
||||
|
||||
@@ -43,29 +87,21 @@ int APS5_VABI sceNpTrophy2GetGroupInfoArray(int context, int handle, uint32_t of
|
||||
if (count == nullptr) {
|
||||
APS5_INVALID_ARG_EX;
|
||||
}
|
||||
(void)context;
|
||||
(void)handle;
|
||||
const uint32_t out_count = (offset == 0 && limit != 0 ? 1u : 0u);
|
||||
*count = out_count;
|
||||
if (out_count != 0 && details_array != nullptr) {
|
||||
std::memset(details_array, 0, sizeof(*details_array));
|
||||
details_array->group_id = NP_TROPHY2_GROUP_ID_BASE;
|
||||
details_array->num_trophies = NP_TROPHY2_NUM_TROPHIES;
|
||||
details_array->num_platinum = NP_TROPHY2_NUM_PLATINUM;
|
||||
details_array->num_gold = NP_TROPHY2_NUM_GOLD;
|
||||
details_array->num_silver = NP_TROPHY2_NUM_SILVER;
|
||||
details_array->num_bronze = NP_TROPHY2_NUM_BRONZE;
|
||||
std::strncpy(details_array->title, NP_TROPHY2_GROUP_TITLE, sizeof(details_array->title) - 1);
|
||||
const auto owner = NpTrophy2::FindContext(context);
|
||||
const auto* package = owner ? NpTrophy2::Package(owner->serviceLabel) : nullptr;
|
||||
if (package == nullptr) {
|
||||
const uint32_t out_count = (offset == 0 && limit != 0 ? 1u : 0u);
|
||||
*count = out_count;
|
||||
if (out_count != 0 && details_array != nullptr) fillPlaceholderDetails(NP_TROPHY2_GROUP_ID_BASE, details_array);
|
||||
if (out_count != 0 && data_array != nullptr) fillPlaceholderData(NP_TROPHY2_GROUP_ID_BASE, data_array);
|
||||
return SCE_NP_TROPHY2_OK;
|
||||
}
|
||||
if (out_count != 0 && data_array != nullptr) {
|
||||
std::memset(data_array, 0, sizeof(*data_array));
|
||||
data_array->group_id = NP_TROPHY2_GROUP_ID_BASE;
|
||||
data_array->unlocked_trophies = NP_TROPHY2_UNLOCKED_TROPHIES;
|
||||
data_array->unlocked_platinum = NP_TROPHY2_UNLOCKED_PLATINUM;
|
||||
data_array->unlocked_gold = NP_TROPHY2_UNLOCKED_GOLD;
|
||||
data_array->unlocked_silver = NP_TROPHY2_UNLOCKED_SILVER;
|
||||
data_array->unlocked_bronze = NP_TROPHY2_UNLOCKED_BRONZE;
|
||||
data_array->progress_percentage = NP_TROPHY2_PROGRESS_PERCENTAGE;
|
||||
*count = 0;
|
||||
auto group = package->groups.begin();
|
||||
std::advance(group, std::min<std::size_t>(offset, package->groups.size()));
|
||||
for (; group != package->groups.end() && *count < limit; ++group, ++*count) {
|
||||
fillGroup(*package, group->first, group->second, details_array != nullptr ? &details_array[*count] : nullptr, data_array != nullptr ? &data_array[*count] : nullptr);
|
||||
}
|
||||
return SCE_NP_TROPHY2_OK;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp"
|
||||
#include "prx/libSceSystemService/SystemService.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
|
||||
namespace NpTrophy2 {
|
||||
|
||||
namespace {
|
||||
|
||||
bool tracing() {
|
||||
static const bool trace = std::getenv("APS5_TRACE_UDS") != nullptr;
|
||||
return trace;
|
||||
}
|
||||
|
||||
std::string labelled(const char* format, std::uint32_t serviceLabel) {
|
||||
char path[64];
|
||||
std::snprintf(path, sizeof(path), format, static_cast<unsigned>(serviceLabel));
|
||||
return path;
|
||||
}
|
||||
|
||||
std::optional<std::string> readTitleFile(const std::string& guestPath) {
|
||||
const auto path = ResolvePath_nid_no_patch(guestPath.c_str());
|
||||
std::error_code error;
|
||||
const bool exists = std::filesystem::exists(path, error);
|
||||
if (error) throw std::filesystem::filesystem_error("cannot check the file", path, error);
|
||||
if (!exists) return std::nullopt;
|
||||
const auto size = std::filesystem::file_size(path);
|
||||
if (size > MaxUcpSize) throw std::runtime_error("it has " + std::to_string(size) + " bytes, more than " + std::to_string(MaxUcpSize));
|
||||
std::string bytes(static_cast<std::size_t>(size), '\0');
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.read(bytes.data(), static_cast<std::streamsize>(bytes.size()))) throw std::runtime_error("cannot read " + path.string());
|
||||
return bytes;
|
||||
}
|
||||
|
||||
template <typename TParsed, typename TParse>
|
||||
std::optional<TParsed> loadTitleFile(const std::string& guestPath, const char* without, TParse parse) {
|
||||
std::optional<std::string> bytes;
|
||||
try {
|
||||
bytes = readTitleFile(guestPath);
|
||||
} catch (const std::runtime_error& error) {
|
||||
std::fprintf(stderr, "[trophy] %s is not used, so %s: %s\n", guestPath.c_str(), without, error.what());
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!bytes) {
|
||||
if (tracing()) std::fprintf(stderr, "[uds] %s does not exist, so %s\n", guestPath.c_str(), without);
|
||||
return std::nullopt;
|
||||
}
|
||||
try {
|
||||
return parse(*bytes);
|
||||
} catch (const std::runtime_error& error) {
|
||||
std::fprintf(stderr, "[trophy] %s is malformed, so %s: %s\n", guestPath.c_str(), without, error.what());
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
std::string counted(std::size_t count, const char* one, const char* many) {
|
||||
return std::to_string(count) + " " + (count == 1 ? one : many);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::string PackagePath(std::uint32_t serviceLabel) {
|
||||
return labelled("/app0/sce_sys/trophy2/trophy%02u.ucp", serviceLabel);
|
||||
}
|
||||
|
||||
TitleTrophies LoadTitleTrophies(std::uint32_t serviceLabel, std::string_view language) {
|
||||
TitleTrophies title;
|
||||
const auto packagePath = PackagePath(serviceLabel);
|
||||
title.package = loadTitleFile<TrophyPackage>(packagePath, "trophy queries report placeholders and stats unlock no trophies",
|
||||
[language](std::string_view bytes) { return ParseTrophyPackage(bytes, language); });
|
||||
if (title.package) {
|
||||
std::fprintf(stderr, "[trophy] %s: %s, %s, texts in %s\n", packagePath.c_str(), counted(title.package->trophies.size(), "trophy", "trophies").c_str(),
|
||||
counted(title.package->groups.size(), "group", "groups").c_str(), title.package->language.c_str());
|
||||
for (const auto& skipped : title.package->skipped) std::fprintf(stderr, "[trophy] %s: %s\n", packagePath.c_str(), skipped.c_str());
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
const TitleTrophies& Title(std::uint32_t serviceLabel) {
|
||||
static std::mutex mutex;
|
||||
static std::map<std::uint32_t, std::unique_ptr<const TitleTrophies>> titles;
|
||||
std::lock_guard lock(mutex);
|
||||
auto& title = titles[serviceLabel];
|
||||
if (!title) title = std::make_unique<const TitleTrophies>(LoadTitleTrophies(serviceLabel, LanguageTag(SystemServiceConsoleLanguage("libSceNpTrophy2"))));
|
||||
return *title;
|
||||
}
|
||||
|
||||
const TrophyPackage* Package(std::uint32_t serviceLabel) {
|
||||
const auto& title = Title(serviceLabel);
|
||||
return title.package ? &*title.package : nullptr;
|
||||
}
|
||||
|
||||
void CopyText(char* destination, std::size_t size, std::string_view text) {
|
||||
if (size == 0) throw std::invalid_argument("text cannot be copied into an empty buffer");
|
||||
auto length = std::min(text.size(), size - 1);
|
||||
while (length > 0 && length < text.size() && (static_cast<unsigned char>(text[length]) & 0xC0u) == 0x80u) --length;
|
||||
std::memcpy(destination, text.data(), length);
|
||||
destination[length] = '\0';
|
||||
}
|
||||
|
||||
std::optional<std::uint64_t> ProgressTarget(const TrophyDefinition& trophy) {
|
||||
if (!trophy.progressive) return std::nullopt;
|
||||
return trophy.target;
|
||||
}
|
||||
|
||||
std::uint64_t TrophyProgress(const TrophyDefinition& trophy, std::optional<std::uint64_t> stat, bool unlocked) {
|
||||
const auto target = ProgressTarget(trophy);
|
||||
if (!target) throw std::invalid_argument("trophy " + std::to_string(trophy.id) + " is not progressive or has no target");
|
||||
if (unlocked) return *target;
|
||||
return std::min(stat.value_or(0), *target);
|
||||
}
|
||||
|
||||
TrophyCounts CountTrophies(const TrophyPackage& package, std::optional<std::int32_t> group) {
|
||||
TrophyCounts counts;
|
||||
for (const auto& [id, trophy] : package.trophies) {
|
||||
if (group && trophy.groupId != *group) continue;
|
||||
++counts.trophies;
|
||||
++counts.grades[static_cast<std::size_t>(trophy.grade)];
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
std::uint32_t TrophyCounts::Percentage() const {
|
||||
const auto points = [](const std::array<std::uint32_t, GradeBronze + 1>& counts) { return counts[GradeGold] * 6u + counts[GradeSilver] * 2u + counts[GradeBronze]; };
|
||||
const auto total = points(grades);
|
||||
return total == 0 ? 0 : points(unlockedGrades) * 100u / total;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,39 +1,88 @@
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "prx/libSceNpTrophy2/include/NpTrophy2.hpp"
|
||||
#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
void fillPlaceholderDetails(int trophyId, NpTrophy2Details* details) {
|
||||
std::memset(details, 0, sizeof(*details));
|
||||
details->trophy_id = trophyId;
|
||||
details->trophy_grade = NP_TROPHY2_TROPHY_GRADE_BRONZE;
|
||||
details->group_id = NP_TROPHY2_GROUP_ID_BASE;
|
||||
details->hidden = false;
|
||||
details->has_reward = false;
|
||||
details->target.type = NP_TROPHY2_PROGRESS_TYPE_NONE;
|
||||
details->target.value = NP_TROPHY2_PROGRESS_VALUE_NONE;
|
||||
std::strncpy(details->name, NP_TROPHY2_TROPHY_NAME, sizeof(details->name) - 1);
|
||||
std::strncpy(details->description, NP_TROPHY2_TROPHY_DESCRIPTION, sizeof(details->description) - 1);
|
||||
std::strncpy(details->reward, NP_TROPHY2_TROPHY_REWARD, sizeof(details->reward) - 1);
|
||||
}
|
||||
|
||||
void fillPlaceholderData(int trophyId, NpTrophy2Data* data) {
|
||||
std::memset(data, 0, sizeof(*data));
|
||||
data->trophy_id = trophyId;
|
||||
data->unlocked = false;
|
||||
data->progress.type = NP_TROPHY2_PROGRESS_TYPE_NONE;
|
||||
data->progress.value = NP_TROPHY2_PROGRESS_VALUE_NONE;
|
||||
data->timestamp_tick = 0;
|
||||
}
|
||||
|
||||
void fillDetails(const NpTrophy2::TrophyDefinition& trophy, NpTrophy2Details* details) {
|
||||
std::memset(details, 0, sizeof(*details));
|
||||
details->trophy_id = trophy.id;
|
||||
details->trophy_grade = trophy.grade;
|
||||
details->group_id = trophy.groupId;
|
||||
details->hidden = trophy.hidden;
|
||||
details->has_reward = trophy.hasReward;
|
||||
if (const auto target = NpTrophy2::ProgressTarget(trophy)) {
|
||||
details->target.type = NP_TROPHY2_PROGRESS_TYPE_VALUE;
|
||||
details->target.value = *target;
|
||||
}
|
||||
NpTrophy2::CopyText(details->name, sizeof(details->name), trophy.name);
|
||||
NpTrophy2::CopyText(details->description, sizeof(details->description), trophy.detail);
|
||||
NpTrophy2::CopyText(details->reward, sizeof(details->reward), trophy.reward);
|
||||
}
|
||||
|
||||
void fillData(const NpTrophy2::TrophyDefinition& trophy, NpTrophy2Data* data) {
|
||||
std::memset(data, 0, sizeof(*data));
|
||||
data->trophy_id = trophy.id;
|
||||
data->unlocked = false;
|
||||
data->timestamp_tick = 0;
|
||||
if (!NpTrophy2::ProgressTarget(trophy)) return;
|
||||
data->progress.type = NP_TROPHY2_PROGRESS_TYPE_VALUE;
|
||||
data->progress.value = NpTrophy2::TrophyProgress(trophy, std::nullopt, data->unlocked);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI sceNpTrophy2GetTrophyInfo(int context, int handle, int trophy_id, NpTrophy2Details* details, NpTrophy2Data* data) {
|
||||
if (details == nullptr && data == nullptr) {
|
||||
APS5_INVALID_ARG_EX;
|
||||
}
|
||||
(void)context;
|
||||
(void)handle;
|
||||
if (details != nullptr) {
|
||||
std::memset(details, 0, sizeof(*details));
|
||||
details->trophy_id = trophy_id;
|
||||
details->trophy_grade = NP_TROPHY2_TROPHY_GRADE_BRONZE;
|
||||
details->group_id = NP_TROPHY2_GROUP_ID_BASE;
|
||||
details->hidden = false;
|
||||
details->has_reward = false;
|
||||
details->target.value = NP_TROPHY2_PROGRESS_VALUE_NONE;
|
||||
std::strncpy(details->name, NP_TROPHY2_TROPHY_NAME, sizeof(details->name) - 1);
|
||||
std::strncpy(details->description, NP_TROPHY2_TROPHY_DESCRIPTION, sizeof(details->description) - 1);
|
||||
std::strncpy(details->reward, NP_TROPHY2_TROPHY_REWARD, sizeof(details->reward) - 1);
|
||||
const auto owner = NpTrophy2::FindContext(context);
|
||||
const auto* package = owner ? NpTrophy2::Package(owner->serviceLabel) : nullptr;
|
||||
if (package == nullptr) {
|
||||
if (details != nullptr) fillPlaceholderDetails(trophy_id, details);
|
||||
if (data != nullptr) fillPlaceholderData(trophy_id, data);
|
||||
return SCE_NP_TROPHY2_OK;
|
||||
}
|
||||
if (data != nullptr) {
|
||||
std::memset(data, 0, sizeof(*data));
|
||||
data->trophy_id = trophy_id;
|
||||
data->unlocked = false;
|
||||
data->progress.value = NP_TROPHY2_PROGRESS_VALUE_NONE;
|
||||
data->timestamp_tick = 0;
|
||||
const auto trophy = package->trophies.find(trophy_id);
|
||||
if (trophy == package->trophies.end()) {
|
||||
throw std::invalid_argument(std::string(__func__) + ": trophy " + std::to_string(trophy_id) + " is not in " + NpTrophy2::PackagePath(owner->serviceLabel));
|
||||
}
|
||||
if (details != nullptr) fillDetails(trophy->second, details);
|
||||
if (data != nullptr) fillData(trophy->second, data);
|
||||
return SCE_NP_TROPHY2_OK;
|
||||
}
|
||||
|
||||
@@ -41,28 +90,22 @@ int APS5_VABI sceNpTrophy2GetTrophyInfoArray(int context, int handle, uint32_t o
|
||||
if (count == nullptr) {
|
||||
APS5_INVALID_ARG_EX;
|
||||
}
|
||||
(void)context;
|
||||
(void)handle;
|
||||
const uint32_t out_count = (offset == 0 && limit != 0 ? 1u : 0u);
|
||||
*count = out_count;
|
||||
if (out_count != 0 && details_array != nullptr) {
|
||||
std::memset(details_array, 0, sizeof(*details_array));
|
||||
details_array->trophy_id = NP_TROPHY2_TROPHY_ID_DEFAULT;
|
||||
details_array->trophy_grade = NP_TROPHY2_TROPHY_GRADE_BRONZE;
|
||||
details_array->group_id = NP_TROPHY2_GROUP_ID_BASE;
|
||||
details_array->hidden = false;
|
||||
details_array->has_reward = false;
|
||||
details_array->target.value = NP_TROPHY2_PROGRESS_VALUE_NONE;
|
||||
std::strncpy(details_array->name, NP_TROPHY2_TROPHY_NAME, sizeof(details_array->name) - 1);
|
||||
std::strncpy(details_array->description, NP_TROPHY2_TROPHY_DESCRIPTION, sizeof(details_array->description) - 1);
|
||||
std::strncpy(details_array->reward, NP_TROPHY2_TROPHY_REWARD, sizeof(details_array->reward) - 1);
|
||||
const auto owner = NpTrophy2::FindContext(context);
|
||||
const auto* package = owner ? NpTrophy2::Package(owner->serviceLabel) : nullptr;
|
||||
if (package == nullptr) {
|
||||
const uint32_t out_count = (offset == 0 && limit != 0 ? 1u : 0u);
|
||||
*count = out_count;
|
||||
if (out_count != 0 && details_array != nullptr) fillPlaceholderDetails(NP_TROPHY2_TROPHY_ID_DEFAULT, details_array);
|
||||
if (out_count != 0 && data_array != nullptr) fillPlaceholderData(NP_TROPHY2_TROPHY_ID_DEFAULT, data_array);
|
||||
return SCE_NP_TROPHY2_OK;
|
||||
}
|
||||
if (out_count != 0 && data_array != nullptr) {
|
||||
std::memset(data_array, 0, sizeof(*data_array));
|
||||
data_array->trophy_id = NP_TROPHY2_TROPHY_ID_DEFAULT;
|
||||
data_array->unlocked = false;
|
||||
data_array->progress.value = NP_TROPHY2_PROGRESS_VALUE_NONE;
|
||||
data_array->timestamp_tick = 0;
|
||||
*count = 0;
|
||||
auto trophy = package->trophies.begin();
|
||||
std::advance(trophy, std::min<std::size_t>(offset, package->trophies.size()));
|
||||
for (; trophy != package->trophies.end() && *count < limit; ++trophy, ++*count) {
|
||||
if (details_array != nullptr) fillDetails(trophy->second, &details_array[*count]);
|
||||
if (data_array != nullptr) fillData(trophy->second, &data_array[*count]);
|
||||
}
|
||||
return SCE_NP_TROPHY2_OK;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
#include "prx/libSceNpTrophy2/include/TrophyPackage.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
#include <system_error>
|
||||
|
||||
namespace NpTrophy2 {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int MaxJsonDepth = 64;
|
||||
constexpr std::size_t MaxJsonValues = std::size_t{1} << 20u;
|
||||
|
||||
std::uint64_t bigEndian(std::string_view data, std::uint64_t offset, std::size_t size) {
|
||||
std::uint64_t value = 0;
|
||||
for (std::size_t i = 0; i < size; ++i) value = (value << 8u) | static_cast<std::uint8_t>(data[offset + i]);
|
||||
return value;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void appendUtf8(std::string& out, std::uint32_t code) {
|
||||
if (code < 0x80) {
|
||||
out += static_cast<char>(code);
|
||||
} else if (code < 0x800) {
|
||||
out += static_cast<char>(0xC0 | (code >> 6));
|
||||
out += static_cast<char>(0x80 | (code & 0x3F));
|
||||
} else if (code < 0x10000) {
|
||||
out += static_cast<char>(0xE0 | (code >> 12));
|
||||
out += static_cast<char>(0x80 | ((code >> 6) & 0x3F));
|
||||
out += static_cast<char>(0x80 | (code & 0x3F));
|
||||
} else {
|
||||
out += static_cast<char>(0xF0 | (code >> 18));
|
||||
out += static_cast<char>(0x80 | ((code >> 12) & 0x3F));
|
||||
out += static_cast<char>(0x80 | ((code >> 6) & 0x3F));
|
||||
out += static_cast<char>(0x80 | (code & 0x3F));
|
||||
}
|
||||
}
|
||||
|
||||
class JsonReader {
|
||||
public:
|
||||
explicit JsonReader(std::string_view text) : text(text) {}
|
||||
|
||||
Json Document() {
|
||||
if (text.starts_with("\xEF\xBB\xBF")) position = 3;
|
||||
auto document = readValue(0);
|
||||
skipSpace();
|
||||
if (position != text.size()) fail("text follows the document");
|
||||
return document;
|
||||
}
|
||||
|
||||
private:
|
||||
[[noreturn]] void fail(const std::string& problem) const {
|
||||
throw std::runtime_error(problem + " at byte " + std::to_string(position));
|
||||
}
|
||||
|
||||
void skipSpace() {
|
||||
while (position < text.size() && (text[position] == ' ' || text[position] == '\t' || text[position] == '\n' || text[position] == '\r')) ++position;
|
||||
}
|
||||
|
||||
bool take(char expected) {
|
||||
if (position >= text.size() || text[position] != expected) return false;
|
||||
++position;
|
||||
return true;
|
||||
}
|
||||
|
||||
void expect(std::string_view word) {
|
||||
if (text.substr(position, word.size()) != word) fail("expected " + std::string(word));
|
||||
position += word.size();
|
||||
}
|
||||
|
||||
Json readValue(int depth) {
|
||||
if (depth >= MaxJsonDepth) fail("values nest deeper than " + std::to_string(MaxJsonDepth) + " levels");
|
||||
if (++values > MaxJsonValues) fail("the document holds more than " + std::to_string(MaxJsonValues) + " values");
|
||||
skipSpace();
|
||||
if (position >= text.size()) fail("the document ends before a value");
|
||||
Json result;
|
||||
switch (text[position]) {
|
||||
case '{':
|
||||
result.kind = Json::Kind::Object;
|
||||
readObject(result, depth);
|
||||
break;
|
||||
case '[':
|
||||
result.kind = Json::Kind::Array;
|
||||
readArray(result, depth);
|
||||
break;
|
||||
case '"':
|
||||
result.kind = Json::Kind::String;
|
||||
result.text = readString();
|
||||
break;
|
||||
case 't':
|
||||
expect("true");
|
||||
result.kind = Json::Kind::Bool;
|
||||
result.boolean = true;
|
||||
break;
|
||||
case 'f':
|
||||
expect("false");
|
||||
result.kind = Json::Kind::Bool;
|
||||
break;
|
||||
case 'n':
|
||||
expect("null");
|
||||
break;
|
||||
default:
|
||||
result.kind = Json::Kind::Number;
|
||||
result.text = readNumber();
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void readObject(Json& result, int depth) {
|
||||
++position;
|
||||
skipSpace();
|
||||
if (take('}')) return;
|
||||
while (true) {
|
||||
skipSpace();
|
||||
if (position >= text.size() || text[position] != '"') fail("expected a member name");
|
||||
auto key = readString();
|
||||
skipSpace();
|
||||
if (!take(':')) fail("expected ':'");
|
||||
result.members.emplace_back(std::move(key), readValue(depth + 1));
|
||||
skipSpace();
|
||||
if (take('}')) return;
|
||||
if (!take(',')) fail("expected ',' or '}'");
|
||||
}
|
||||
}
|
||||
|
||||
void readArray(Json& result, int depth) {
|
||||
++position;
|
||||
skipSpace();
|
||||
if (take(']')) return;
|
||||
while (true) {
|
||||
result.items.push_back(readValue(depth + 1));
|
||||
skipSpace();
|
||||
if (take(']')) return;
|
||||
if (!take(',')) fail("expected ',' or ']'");
|
||||
}
|
||||
}
|
||||
|
||||
std::uint32_t readHexDigits() {
|
||||
if (text.size() - position < 4) fail("a \\u escape is cut short");
|
||||
std::uint32_t code = 0;
|
||||
const auto* end = text.data() + position + 4;
|
||||
const auto [stop, error] = std::from_chars(text.data() + position, end, code, 16);
|
||||
if (error != std::errc{} || stop != end) fail("a \\u escape does not have 4 hex digits");
|
||||
position += 4;
|
||||
return code;
|
||||
}
|
||||
|
||||
std::string readString() {
|
||||
++position;
|
||||
std::string out;
|
||||
while (true) {
|
||||
if (position >= text.size()) fail("a string is not closed");
|
||||
const char character = text[position++];
|
||||
if (character == '"') return out;
|
||||
if (static_cast<unsigned char>(character) < 0x20) fail("a string holds a control character");
|
||||
if (character != '\\') {
|
||||
out += character;
|
||||
continue;
|
||||
}
|
||||
if (position >= text.size()) fail("a string is not closed");
|
||||
switch (text[position++]) {
|
||||
case '"': out += '"'; break;
|
||||
case '\\': out += '\\'; break;
|
||||
case '/': out += '/'; break;
|
||||
case 'b': out += '\b'; break;
|
||||
case 'f': out += '\f'; break;
|
||||
case 'n': out += '\n'; break;
|
||||
case 'r': out += '\r'; break;
|
||||
case 't': out += '\t'; break;
|
||||
case 'u': {
|
||||
auto code = readHexDigits();
|
||||
if (code >= 0xDC00 && code < 0xE000) fail("a low surrogate has no high surrogate");
|
||||
if (code >= 0xD800 && code < 0xDC00) {
|
||||
if (text.substr(position, 2) != "\\u") fail("a high surrogate has no low surrogate");
|
||||
position += 2;
|
||||
const auto low = readHexDigits();
|
||||
if (low < 0xDC00 || low >= 0xE000) fail("a high surrogate has no low surrogate");
|
||||
code = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00);
|
||||
}
|
||||
appendUtf8(out, code);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
fail("a string has an unknown escape");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool skipDigits() {
|
||||
const auto start = position;
|
||||
while (position < text.size() && text[position] >= '0' && text[position] <= '9') ++position;
|
||||
return position != start;
|
||||
}
|
||||
|
||||
std::string readNumber() {
|
||||
const auto start = position;
|
||||
take('-');
|
||||
if (!take('0') && !skipDigits()) fail("expected a value");
|
||||
if (take('.') && !skipDigits()) fail("a number has no digits after '.'");
|
||||
if (take('e') || take('E')) {
|
||||
if (!take('+')) take('-');
|
||||
if (!skipDigits()) fail("a number has no exponent digits");
|
||||
}
|
||||
return std::string(text.substr(start, position - start));
|
||||
}
|
||||
|
||||
std::string_view text;
|
||||
std::size_t position = 0;
|
||||
std::size_t values = 0;
|
||||
};
|
||||
|
||||
std::string describe(const Json& value) {
|
||||
switch (value.kind) {
|
||||
case Json::Kind::Null: return "null";
|
||||
case Json::Kind::Bool: return value.boolean ? "true" : "false";
|
||||
case Json::Kind::Number: return value.text;
|
||||
case Json::Kind::String: return "\"" + value.text + "\"";
|
||||
case Json::Kind::Array: return "an array";
|
||||
case Json::Kind::Object: return "an object";
|
||||
}
|
||||
throw std::logic_error("a JSON value has an unknown kind");
|
||||
}
|
||||
|
||||
std::string kindName(Json::Kind kind) {
|
||||
switch (kind) {
|
||||
case Json::Kind::Null: return "null";
|
||||
case Json::Kind::Bool: return "boolean";
|
||||
case Json::Kind::Number: return "number";
|
||||
case Json::Kind::String: return "string";
|
||||
case Json::Kind::Array: return "array";
|
||||
case Json::Kind::Object: return "object";
|
||||
}
|
||||
throw std::logic_error("a JSON value has an unknown kind");
|
||||
}
|
||||
|
||||
const Json& member(const Json& object, std::string_view key, Json::Kind kind, const std::string& where) {
|
||||
const auto* found = object.Find(key);
|
||||
if (found == nullptr || found->kind != kind) throw std::runtime_error(where + " has no " + std::string(key) + " " + kindName(kind));
|
||||
return *found;
|
||||
}
|
||||
|
||||
Json document(const UcpFiles& files, const std::string& name) {
|
||||
const auto found = files.find(name);
|
||||
if (found == files.end()) throw std::runtime_error("it has no " + name);
|
||||
Json parsed;
|
||||
try {
|
||||
parsed = ParseJson(found->second);
|
||||
} catch (const std::runtime_error& error) {
|
||||
throw std::runtime_error(name + " is not JSON: " + error.what());
|
||||
}
|
||||
if (parsed.kind != Json::Kind::Object) throw std::runtime_error(name + " is not a JSON object");
|
||||
return parsed;
|
||||
}
|
||||
|
||||
Comparator comparatorOf(const Json& value) {
|
||||
if (value.kind != Json::Kind::String) return Comparator::None;
|
||||
if (value.text == "ge") return Comparator::GreaterEqual;
|
||||
if (value.text == "gt") return Comparator::Greater;
|
||||
if (value.text == "le") return Comparator::LessEqual;
|
||||
if (value.text == "lt") return Comparator::Less;
|
||||
return Comparator::None;
|
||||
}
|
||||
|
||||
std::string join(const std::vector<std::string>& parts, std::string_view separator) {
|
||||
std::string out;
|
||||
for (const auto& part : parts) {
|
||||
if (!out.empty()) out += separator;
|
||||
out += part;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string alternatives(const std::vector<std::string>& parts) {
|
||||
std::string out;
|
||||
for (std::size_t index = 0; index < parts.size(); ++index) {
|
||||
if (index != 0) out += index + 1 == parts.size() ? " or " : ", ";
|
||||
out += parts[index];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
TrophyDefinition readTrophy(const Json& definition, std::size_t index, std::vector<std::string>& skipped) {
|
||||
const auto where = "tropconf.json trophies[" + std::to_string(index) + "]";
|
||||
if (definition.kind != Json::Kind::Object) throw std::runtime_error(where + " is not an object");
|
||||
const auto* id = definition.Find("id");
|
||||
const auto parsedId = id != nullptr ? id->Id() : std::nullopt;
|
||||
if (!parsedId) throw std::runtime_error(where + " has no id from 0 to 2147483647");
|
||||
TrophyDefinition trophy;
|
||||
trophy.id = *parsedId;
|
||||
const auto name = "tropconf.json trophy " + std::to_string(trophy.id);
|
||||
const auto* grade = definition.Find("grade");
|
||||
const auto letter = grade != nullptr && grade->kind == Json::Kind::String && grade->text.size() == 1 ? std::string_view("PGSB").find(grade->text[0]) : std::string_view::npos;
|
||||
if (letter == std::string_view::npos) throw std::runtime_error(name + " has no grade P, G, S or B");
|
||||
trophy.grade = GradePlatinum + static_cast<std::int32_t>(letter);
|
||||
if (const auto* group = definition.Find("groupId")) {
|
||||
const auto groupId = group->Id();
|
||||
if (!groupId) throw std::runtime_error(name + " has groupId " + describe(*group) + ", not an id from 0 to 2147483647");
|
||||
trophy.groupId = *groupId;
|
||||
}
|
||||
if (const auto* platinum = definition.Find("platinumTrophyId")) trophy.platinumId = platinum->Id();
|
||||
const auto* hidden = definition.Find("hidden");
|
||||
trophy.hidden = hidden != nullptr && hidden->IsTrue();
|
||||
const auto* reward = definition.Find("hasReward");
|
||||
trophy.hasReward = reward != nullptr && reward->IsTrue();
|
||||
const auto* condition = definition.Find("unlockCondition");
|
||||
if (condition == nullptr || condition->kind != Json::Kind::Object) return trophy;
|
||||
const auto* progressive = condition->Find("progressive");
|
||||
trophy.progressive = progressive != nullptr && progressive->IsTrue();
|
||||
if (const auto* stat = condition->Find("udsStatId")) {
|
||||
trophy.statId = stat->Unsigned();
|
||||
if (!trophy.statId) throw std::runtime_error(name + " has udsStatId " + describe(*stat) + ", not an unsigned integer");
|
||||
}
|
||||
std::vector<std::string> problems;
|
||||
if (const auto* target = condition->Find("targetValue")) {
|
||||
trophy.target = target->Unsigned();
|
||||
if (!trophy.target) problems.push_back("its targetValue " + describe(*target) + " is not an unsigned integer");
|
||||
} else if (trophy.statId) {
|
||||
problems.emplace_back("it has no targetValue");
|
||||
}
|
||||
if (const auto* comparator = condition->Find("comparator")) {
|
||||
trophy.comparator = comparatorOf(*comparator);
|
||||
if (trophy.comparator == Comparator::None) problems.push_back("its comparator " + describe(*comparator) + " is not ge, gt, le or lt");
|
||||
} else if (trophy.statId) {
|
||||
problems.emplace_back("it has no comparator");
|
||||
}
|
||||
if (!problems.empty()) skipped.push_back(name + " never unlocks through stats: " + join(problems, ", "));
|
||||
return trophy;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const Json* Json::Find(std::string_view key) const {
|
||||
for (auto member = members.rbegin(); member != members.rend(); ++member) {
|
||||
if (member->first == key) return &member->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool Json::IsTrue() const {
|
||||
return kind == Kind::Bool && boolean;
|
||||
}
|
||||
|
||||
std::optional<std::uint64_t> Json::Unsigned() const {
|
||||
if (kind != Kind::Number && kind != Kind::String) return std::nullopt;
|
||||
std::uint64_t value = 0;
|
||||
const auto* end = text.data() + text.size();
|
||||
const auto [stop, error] = std::from_chars(text.data(), end, value);
|
||||
if (error != std::errc{} || stop != end) return std::nullopt;
|
||||
return value;
|
||||
}
|
||||
|
||||
std::optional<std::int32_t> Json::Id() const {
|
||||
const auto value = Unsigned();
|
||||
if (!value || *value > static_cast<std::uint64_t>(INT32_MAX)) return std::nullopt;
|
||||
return static_cast<std::int32_t>(*value);
|
||||
}
|
||||
|
||||
Json ParseJson(std::string_view text) {
|
||||
return JsonReader(text).Document();
|
||||
}
|
||||
|
||||
UcpFiles ReadUcp(std::string_view data) {
|
||||
if (data.size() < UcpHeaderSize) throw std::runtime_error("it has " + std::to_string(data.size()) + " bytes, fewer than the " + std::to_string(UcpHeaderSize) + "-byte header");
|
||||
const auto magic = bigEndian(data, 0, 4);
|
||||
if (magic != UcpMagic) throw std::runtime_error("its magic is " + hex(magic) + ", not " + hex(UcpMagic));
|
||||
const auto version = bigEndian(data, 4, 4);
|
||||
if (version != UcpVersion) throw std::runtime_error("its version is " + std::to_string(version) + ", not " + std::to_string(UcpVersion));
|
||||
const auto size = bigEndian(data, 8, 8);
|
||||
if (size < UcpHeaderSize || size > data.size()) throw std::runtime_error("its size " + hex(size) + " is not from the header size " + hex(UcpHeaderSize) + " to the file size " + hex(data.size()));
|
||||
const auto count = bigEndian(data, 0x10, 4);
|
||||
if (count > UcpMaxEntries) throw std::runtime_error("it lists " + std::to_string(count) + " files, more than " + std::to_string(UcpMaxEntries));
|
||||
const auto toc = bigEndian(data, 0x14, 4);
|
||||
if (toc > size || UcpTocHeaderSize + count * UcpEntrySize > size - toc) {
|
||||
throw std::runtime_error("its table of " + std::to_string(count) + " files at " + hex(toc) + " ends past its size " + hex(size));
|
||||
}
|
||||
UcpFiles files;
|
||||
for (std::uint64_t index = 0; index < count; ++index) {
|
||||
const auto entry = toc + UcpTocHeaderSize + index * UcpEntrySize;
|
||||
const auto field = data.substr(entry, UcpNameSize);
|
||||
const std::string name(field.substr(0, field.find('\0')));
|
||||
const auto offset = bigEndian(data, entry + UcpNameSize, 8);
|
||||
const auto length = bigEndian(data, entry + UcpNameSize + 8, 8);
|
||||
if (offset > size || length > size - offset) {
|
||||
throw std::runtime_error("its file \"" + name + "\" at " + hex(offset) + " with " + hex(length) + " bytes ends past its size " + hex(size));
|
||||
}
|
||||
files.try_emplace(name, data.substr(offset, length));
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
bool UnlockCondition::Reached(std::uint64_t value) const {
|
||||
switch (comparator) {
|
||||
case Comparator::GreaterEqual: return value >= target;
|
||||
case Comparator::Greater: return value > target;
|
||||
case Comparator::LessEqual: return value <= target;
|
||||
case Comparator::Less: return value < target;
|
||||
case Comparator::None: break;
|
||||
}
|
||||
throw std::logic_error("an unlock condition on stat " + std::to_string(statId) + " has no comparator");
|
||||
}
|
||||
|
||||
std::optional<UnlockCondition> TrophyDefinition::Condition() const {
|
||||
if (grade == GradePlatinum || !statId || !target || comparator == Comparator::None) return std::nullopt;
|
||||
return UnlockCondition{*statId, *target, comparator};
|
||||
}
|
||||
|
||||
TrophyPackage ParseTrophyPackage(std::string_view ucp, std::string_view language) {
|
||||
const auto files = ReadUcp(ucp);
|
||||
const auto conf = document(files, "tropconf.json");
|
||||
const auto& trophies = member(conf, "trophies", Json::Kind::Array, "tropconf.json");
|
||||
if (trophies.items.size() > MaxTrophies) throw std::runtime_error("tropconf.json lists " + std::to_string(trophies.items.size()) + " trophies, more than " + std::to_string(MaxTrophies));
|
||||
const auto& defaultLanguage = member(conf, "defaultLanguage", Json::Kind::String, "tropconf.json");
|
||||
TrophyPackage package;
|
||||
const std::array<std::string_view, 3> candidates{language, "en-US", defaultLanguage.text};
|
||||
std::vector<std::string> tried;
|
||||
for (std::size_t index = 0; index < candidates.size() && package.language.empty(); ++index) {
|
||||
const auto candidate = candidates[index];
|
||||
const auto earlier = candidates.begin() + static_cast<std::ptrdiff_t>(index);
|
||||
if (candidate.empty() || std::find(candidates.begin(), earlier, candidate) != earlier) continue;
|
||||
if (files.contains("tropmeta_" + std::string(candidate) + ".json")) {
|
||||
package.language = candidate;
|
||||
} else {
|
||||
tried.push_back(index + 1 == candidates.size() ? "its defaultLanguage \"" + std::string(candidate) + "\"" : std::string(candidate));
|
||||
}
|
||||
}
|
||||
if (package.language.empty()) throw std::runtime_error("it has no tropmeta_<language>.json for " + alternatives(tried));
|
||||
const auto metaName = "tropmeta_" + package.language + ".json";
|
||||
const auto meta = document(files, metaName);
|
||||
const auto& metadata = member(meta, "metadata", Json::Kind::Object, metaName);
|
||||
const auto& titleMetadata = member(metadata, "titleMetadata", Json::Kind::Object, metaName + " metadata");
|
||||
package.title = member(titleMetadata, "name", Json::Kind::String, metaName + " titleMetadata").text;
|
||||
const auto& trophyMetadata = member(metadata, "trophyMetadata", Json::Kind::Array, metaName + " metadata");
|
||||
for (std::size_t index = 0; index < trophies.items.size(); ++index) {
|
||||
auto trophy = readTrophy(trophies.items[index], index, package.skipped);
|
||||
package.groups.try_emplace(trophy.groupId, trophy.groupId == BaseGroup ? package.title : std::string());
|
||||
if (package.groups.size() > MaxGroups) throw std::runtime_error("tropconf.json uses more than " + std::to_string(MaxGroups) + " groups");
|
||||
const auto id = trophy.id;
|
||||
if (!package.trophies.emplace(id, std::move(trophy)).second) throw std::runtime_error("tropconf.json lists trophy " + std::to_string(id) + " twice");
|
||||
}
|
||||
if (const auto* groups = metadata.Find("groupMetadata"); groups != nullptr && groups->kind == Json::Kind::Array) {
|
||||
for (const auto& group : groups->items) {
|
||||
const auto* id = group.Find("id");
|
||||
const auto* name = group.Find("name");
|
||||
const auto groupId = id != nullptr ? id->Id() : std::nullopt;
|
||||
if (!groupId || name == nullptr || name->kind != Json::Kind::String) continue;
|
||||
if (const auto found = package.groups.find(*groupId); found != package.groups.end()) found->second = name->text;
|
||||
}
|
||||
}
|
||||
for (const auto& text : trophyMetadata.items) {
|
||||
const auto* id = text.Find("id");
|
||||
const auto trophyId = id != nullptr ? id->Id() : std::nullopt;
|
||||
const auto found = trophyId ? package.trophies.find(*trophyId) : package.trophies.end();
|
||||
if (found == package.trophies.end()) continue;
|
||||
for (const auto& [key, field] : {std::pair{"name", &found->second.name}, std::pair{"detail", &found->second.detail}, std::pair{"reward", &found->second.reward}}) {
|
||||
if (const auto* value = text.Find(key); value != nullptr && value->kind == Json::Kind::String) *field = value->text;
|
||||
}
|
||||
}
|
||||
return package;
|
||||
}
|
||||
|
||||
std::string_view LanguageTag(int consoleLanguage) {
|
||||
static constexpr std::array<std::string_view, 31> tags = {
|
||||
"ja-JP", "en-US", "fr-FR", "es-ES", "de-DE", "it-IT", "nl-NL", "pt-PT", "ru-RU", "ko-KR", "zh-Hant",
|
||||
"zh-Hans", "fi-FI", "sv-SE", "da-DK", "no-NO", "pl-PL", "pt-BR", "en-GB", "tr-TR", "es-419", "ar-AE",
|
||||
"fr-CA", "cs-CZ", "hu-HU", "el-GR", "ro-RO", "th-TH", "vi-VN", "id-ID", "uk-UA"};
|
||||
if (consoleLanguage < 0 || consoleLanguage >= static_cast<int>(tags.size())) {
|
||||
throw std::out_of_range("console language " + std::to_string(consoleLanguage) + " has no locale tag");
|
||||
}
|
||||
return tags[static_cast<std::size_t>(consoleLanguage)];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp"
|
||||
#include "prx/libSceNpTrophy2/include/TrophyPackage.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using Files = std::vector<std::pair<std::string, std::string>>;
|
||||
|
||||
void check(bool condition, const char* message) {
|
||||
if (condition) return;
|
||||
std::fprintf(stderr, "%s\n", message);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
template <typename TAction>
|
||||
std::string failure(TAction action) {
|
||||
try {
|
||||
action();
|
||||
} catch (const std::runtime_error& error) {
|
||||
return error.what();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool contains(const std::string& text, const char* part) {
|
||||
return text.find(part) != std::string::npos;
|
||||
}
|
||||
|
||||
void putBigEndian(std::string& data, std::size_t offset, std::uint64_t value, std::size_t size) {
|
||||
for (std::size_t i = 0; i < size; ++i) data[offset + i] = static_cast<char>((value >> (8 * (size - 1 - i))) & 0xFF);
|
||||
}
|
||||
|
||||
std::string ucp(const Files& files) {
|
||||
const std::size_t toc = 0x40;
|
||||
std::size_t offset = toc + 0x20 + files.size() * 0x40;
|
||||
std::size_t size = offset;
|
||||
for (const auto& [name, contents] : files) size += contents.size();
|
||||
std::string data(size, '\0');
|
||||
putBigEndian(data, 0, 0xB228C60A, 4);
|
||||
putBigEndian(data, 4, 1, 4);
|
||||
putBigEndian(data, 8, size, 8);
|
||||
putBigEndian(data, 0x10, files.size(), 4);
|
||||
putBigEndian(data, 0x14, toc, 4);
|
||||
std::size_t entry = toc + 0x20;
|
||||
for (const auto& [name, contents] : files) {
|
||||
data.replace(entry, name.size(), name);
|
||||
putBigEndian(data, entry + 0x20, offset, 8);
|
||||
putBigEndian(data, entry + 0x28, contents.size(), 8);
|
||||
data.replace(offset, contents.size(), contents);
|
||||
offset += contents.size();
|
||||
entry += 0x40;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
const char TropConf[] = R"({"defaultLanguage":"en-US","trophies":[
|
||||
{"id":"0","grade":"P","hidden":false},
|
||||
{"id":"1","grade":"G","platinumTrophyId":"0","unlockCondition":{"udsStatId":"10","comparator":"ge","targetValue":"10","progressive":false}},
|
||||
{"id":"2","grade":"S","platinumTrophyId":0,"unlockCondition":{"udsStatId":20,"comparator":"gt","targetValue":100}},
|
||||
{"id":3,"grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"le","targetValue":"5"}},
|
||||
{"id":"4","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"lt","targetValue":"3"}},
|
||||
{"id":"5","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"40","comparator":"ge","targetValue":"5","progressive":true}},
|
||||
{"id":"6","grade":"B","groupId":"1","hidden":true,"hasReward":true,"platinumTrophyId":"-1"},
|
||||
{"id":"7","grade":"B","groupId":"1","unlockCondition":{"udsStatId":"10","comparator":"eq","targetValue":"-1"}}
|
||||
]})";
|
||||
|
||||
const char TropMetaEnglish[] = R"({"metadata":{"titleMetadata":{"name":"Synthetic Game"},
|
||||
"trophyMetadata":[{"id":"0","name":"All of them","detail":"Unlock every trophy"},{"id":"1","name":"Ten","detail":"Reach ten"},
|
||||
{"id":"6","name":"Secret","detail":"A hidden one","reward":"A hat"},{"id":"99","name":"Not a trophy"}],
|
||||
"groupMetadata":[{"id":"1","name":"Expansion"},{"id":"9","name":"Unused"}]}})";
|
||||
|
||||
const char TropMetaFrench[] = R"({"metadata":{"titleMetadata":{"name":"Jeu synth\u00e9tique"},"trophyMetadata":[{"id":"1","name":"Dix"}]}})";
|
||||
|
||||
std::string trophyPackage() {
|
||||
return ucp({{"tropconf.json", TropConf}, {"tropmeta_en-US.json", TropMetaEnglish}, {"tropmeta_fr-FR.json", TropMetaFrench}, {"trop0001.png", "not an image"}});
|
||||
}
|
||||
|
||||
void testContainer() {
|
||||
const auto data = ucp({{"a.json", "{}"}, {"b.bin", std::string("\0\1\2", 3)}, {"a.json", "second"}});
|
||||
const auto files = NpTrophy2::ReadUcp(data);
|
||||
check(files.size() == 2 && files.at("a.json") == "{}" && files.at("b.bin") == std::string_view("\0\1\2", 3), "the container files were not read, or a repeated name replaced the first");
|
||||
check(NpTrophy2::ReadUcp(ucp({})).empty(), "an empty container was not read");
|
||||
const std::string longName(0x20, 'n');
|
||||
const auto named = NpTrophy2::ReadUcp(ucp({{longName, "x"}}));
|
||||
check(named.size() == 1 && named.begin()->first == longName, "a name that fills its 32 bytes was not read whole");
|
||||
auto padded = data + "trailing";
|
||||
check(NpTrophy2::ReadUcp(padded).size() == 2, "bytes after the container size were not ignored");
|
||||
|
||||
for (std::size_t length = 0; length < data.size(); ++length) {
|
||||
check(!failure([&] { NpTrophy2::ReadUcp(std::string_view(data).substr(0, length)); }).empty(), "a truncated container was read");
|
||||
}
|
||||
check(contains(failure([&] { NpTrophy2::ReadUcp(std::string_view(data).substr(0, 0x3F)); }), "fewer than the 64-byte header"), "a short header was not named");
|
||||
auto corrupt = data;
|
||||
corrupt[0] = 0;
|
||||
check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "magic is 0x28c60a"), "a wrong magic was not named");
|
||||
corrupt = data;
|
||||
putBigEndian(corrupt, 4, 2, 4);
|
||||
check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "version is 2"), "a wrong version was not named");
|
||||
corrupt = data;
|
||||
putBigEndian(corrupt, 8, 0x3F, 8);
|
||||
check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "its size 0x3f"), "a size inside the header was accepted");
|
||||
corrupt = data;
|
||||
putBigEndian(corrupt, 8, data.size() + 1, 8);
|
||||
check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "is not from the header size"), "a size past the file was accepted");
|
||||
corrupt = data;
|
||||
putBigEndian(corrupt, 0x10, 4097, 4);
|
||||
check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "more than 4096"), "too many files were accepted");
|
||||
corrupt = data;
|
||||
putBigEndian(corrupt, 0x10, 40, 4);
|
||||
check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "table of 40 files"), "a table past the size was accepted");
|
||||
corrupt = data;
|
||||
putBigEndian(corrupt, 0x14, data.size() + 1, 4);
|
||||
check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "ends past its size"), "a table offset past the size was accepted");
|
||||
corrupt = data;
|
||||
putBigEndian(corrupt, 0x60 + 0x20, data.size() + 1, 8);
|
||||
check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "its file \"a.json\""), "a file offset past the size was accepted");
|
||||
corrupt = data;
|
||||
putBigEndian(corrupt, 0x60 + 0x28, ~std::uint64_t{0}, 8);
|
||||
check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "ends past its size"), "a file length past the size was accepted");
|
||||
}
|
||||
|
||||
void testJson() {
|
||||
const auto value = NpTrophy2::ParseJson("\xEF\xBB\xBF {\"a\": [1, -2.5e+3, \"x\\u00e9\\ud83d\\ude00\\n\", true, false, null, {}], \"a\": 7, \"b\": \"12\"} ");
|
||||
check(value.kind == NpTrophy2::Json::Kind::Object && value.members.size() == 3, "an object was not parsed");
|
||||
check(value.Find("a") != nullptr && value.Find("a")->text == "7", "a repeated key did not keep its last value");
|
||||
const auto& list = value.members[0].second.items;
|
||||
check(list.size() == 7 && list[0].text == "1" && list[1].text == "-2.5e+3" && list[2].text == "x\xC3\xA9\xF0\x9F\x98\x80\n", "array values or escapes were not parsed");
|
||||
check(list[3].IsTrue() && !list[4].IsTrue() && list[5].kind == NpTrophy2::Json::Kind::Null && list[6].kind == NpTrophy2::Json::Kind::Object, "literals were not parsed");
|
||||
check(value.Find("b")->Unsigned() == std::optional<std::uint64_t>(12) && list[0].Unsigned() == std::optional<std::uint64_t>(1), "numbers and numeric strings are not unsigned");
|
||||
check(!list[1].Unsigned() && !list[3].Unsigned() && !NpTrophy2::ParseJson("\"\"").Unsigned() && !NpTrophy2::ParseJson("\" 1\"").Unsigned(), "a value that is not an unsigned integer was read as one");
|
||||
check(NpTrophy2::ParseJson("18446744073709551615").Unsigned() == std::optional<std::uint64_t>(UINT64_MAX) && !NpTrophy2::ParseJson("18446744073709551616").Unsigned(), "the unsigned range is wrong");
|
||||
check(NpTrophy2::ParseJson("2147483647").Id() == std::optional<std::int32_t>(INT32_MAX) && !NpTrophy2::ParseJson("2147483648").Id() && !NpTrophy2::ParseJson("-1").Id(), "the id range is wrong");
|
||||
std::string deep(64, '[');
|
||||
deep += std::string(64, ']');
|
||||
check(NpTrophy2::ParseJson(deep).kind == NpTrophy2::Json::Kind::Array, "64 levels of nesting were refused");
|
||||
for (const char* text : {"", "{", "[1,]", "{\"a\":1,}", "{\"a\" 1}", "{a:1}", "01", "1.", "-", "1e", "+1", "\"\\x\"", "\"\\u12g4\"", "\"\\ud800\"", "\"\\udc00\"",
|
||||
"\"\\ud800\\u0041\"", "\"a\nb\"", "\"open", "tru", "nul", "{} {}", "[1] x"}) {
|
||||
check(!failure([text] { NpTrophy2::ParseJson(text); }).empty(), "malformed JSON was parsed");
|
||||
}
|
||||
const auto deeper = "[" + deep + "]";
|
||||
check(contains(failure([&deeper] { NpTrophy2::ParseJson(deeper); }), "deeper than 64"), "deep nesting was not refused");
|
||||
std::string most = "[";
|
||||
for (std::size_t value = 2; value < 1048576; ++value) most += "0,";
|
||||
most += "0]";
|
||||
check(NpTrophy2::ParseJson(most).items.size() == 1048575, "a document of 1048576 values was refused");
|
||||
const auto more = "[0," + most.substr(1);
|
||||
check(contains(failure([&more] { NpTrophy2::ParseJson(more); }), "holds more than 1048576 values"), "a document of more than 1048576 values was parsed");
|
||||
}
|
||||
|
||||
void testPackage() {
|
||||
const auto package = NpTrophy2::ParseTrophyPackage(trophyPackage(), "en-US");
|
||||
check(package.language == "en-US" && package.title == "Synthetic Game" && package.trophies.size() == 8, "the package was not read");
|
||||
const auto& platinum = package.trophies.at(0);
|
||||
check(platinum.grade == NpTrophy2::GradePlatinum && platinum.name == "All of them" && platinum.detail == "Unlock every trophy" && !platinum.Condition(), "the platinum was not read");
|
||||
const auto& gold = package.trophies.at(1);
|
||||
check(gold.grade == NpTrophy2::GradeGold && gold.name == "Ten" && gold.platinumId == std::optional<std::int32_t>(0) && gold.groupId == NpTrophy2::BaseGroup, "trophy 1 was not read");
|
||||
const auto condition = gold.Condition();
|
||||
check(condition && condition->statId == 10 && condition->target == 10 && condition->comparator == NpTrophy2::Comparator::GreaterEqual && !gold.progressive, "trophy 1's condition was not read");
|
||||
check(package.trophies.at(2).grade == NpTrophy2::GradeSilver && package.trophies.at(3).grade == NpTrophy2::GradeBronze, "grades were not read");
|
||||
check(package.trophies.at(2).Condition()->comparator == NpTrophy2::Comparator::Greater && package.trophies.at(3).Condition()->comparator == NpTrophy2::Comparator::LessEqual &&
|
||||
package.trophies.at(4).Condition()->comparator == NpTrophy2::Comparator::Less,
|
||||
"comparators were not read");
|
||||
const auto& progressive = package.trophies.at(5);
|
||||
check(progressive.progressive && NpTrophy2::ProgressTarget(progressive) == std::optional<std::uint64_t>(5) && !NpTrophy2::ProgressTarget(gold), "the progressive target was not read");
|
||||
const auto& hidden = package.trophies.at(6);
|
||||
check(hidden.hidden && hidden.hasReward && hidden.groupId == 1 && hidden.name == "Secret" && hidden.reward == "A hat" && !hidden.platinumId, "trophy 6 was not read");
|
||||
check(!package.trophies.at(7).Condition() && package.trophies.at(7).name.empty(), "an unusable condition was kept");
|
||||
check(package.skipped.size() == 1 && contains(package.skipped[0], "trophy 7 never unlocks through stats") && contains(package.skipped[0], "\"-1\"") && contains(package.skipped[0], "\"eq\""),
|
||||
"the unusable condition was not reported");
|
||||
check(package.groups.size() == 2 && package.groups.at(NpTrophy2::BaseGroup) == "Synthetic Game" && package.groups.at(1) == "Expansion", "groups were not read");
|
||||
|
||||
const auto french = NpTrophy2::ParseTrophyPackage(trophyPackage(), "fr-FR");
|
||||
check(french.language == "fr-FR" && french.title == "Jeu synth\xC3\xA9tique" && french.trophies.at(1).name == "Dix" && french.trophies.at(6).name.empty(), "the system language was not used");
|
||||
check(NpTrophy2::ParseTrophyPackage(trophyPackage(), "de-DE").language == "en-US", "English was not the fallback");
|
||||
std::string conf = TropConf;
|
||||
conf.replace(conf.find("en-US"), 5, "ja-JP");
|
||||
check(NpTrophy2::ParseTrophyPackage(ucp({{"tropconf.json", conf}, {"tropmeta_ja-JP.json", TropMetaEnglish}}), "de-DE").language == "ja-JP", "the default language was not the last fallback");
|
||||
const auto withoutTexts = ucp({{"tropconf.json", conf}});
|
||||
check(contains(failure([&withoutTexts] { NpTrophy2::ParseTrophyPackage(withoutTexts, "de-DE"); }), "no tropmeta_<language>.json for de-DE, en-US or its defaultLanguage \"ja-JP\""),
|
||||
"a package without texts was read");
|
||||
const auto missingTexts = [](const std::string& defaultLanguage, const char* language) {
|
||||
std::string text = TropConf;
|
||||
text.replace(text.find("en-US"), 5, defaultLanguage);
|
||||
const auto data = ucp({{"tropconf.json", text}});
|
||||
return failure([&data, language] { NpTrophy2::ParseTrophyPackage(data, language); });
|
||||
};
|
||||
check(missingTexts("en-US", "en-US") == "it has no tropmeta_<language>.json for en-US", "a missing text named en-US more than once");
|
||||
check(missingTexts("ja-JP", "en-US") == "it has no tropmeta_<language>.json for en-US or its defaultLanguage \"ja-JP\"", "a missing text in English named English twice");
|
||||
check(missingTexts("en-US", "de-DE") == "it has no tropmeta_<language>.json for de-DE or en-US", "a missing text with an English default named English twice");
|
||||
|
||||
const auto broken = [](const std::string& replaced, const std::string& replacement) {
|
||||
std::string text = TropConf;
|
||||
text.replace(text.find(replaced), replaced.size(), replacement);
|
||||
const auto data = ucp({{"tropconf.json", text}, {"tropmeta_en-US.json", TropMetaEnglish}});
|
||||
return failure([&data] { NpTrophy2::ParseTrophyPackage(data, "en-US"); });
|
||||
};
|
||||
check(contains(broken(R"("grade":"G")", R"("grade":"X")"), "trophy 1 has no grade"), "a wrong grade was accepted");
|
||||
check(contains(broken(R"("grade":"G")", R"("grade":"GS")"), "trophy 1 has no grade"), "a two-letter grade was accepted");
|
||||
check(contains(broken(R"({"id":"4",)", R"({"id":"3",)"), "lists trophy 3 twice"), "a repeated trophy was accepted");
|
||||
check(contains(broken(R"({"id":"4",)", R"({"id":"-4",)"), "trophies[4] has no id"), "a negative id was accepted");
|
||||
check(contains(broken(R"("groupId":"1","hidden")", R"("groupId":"x","hidden")"), "trophy 6 has groupId \"x\""), "a wrong group was accepted");
|
||||
check(contains(broken(R"("udsStatId":"10")", R"("udsStatId":"-10")"), "udsStatId \"-10\""), "a negative stat id was accepted");
|
||||
check(contains(broken(R"("trophies":[)", R"("trophies":0,"other":[)"), "has no trophies array"), "trophies that are not an array were accepted");
|
||||
const auto withoutConf = ucp({{"tropmeta_en-US.json", TropMetaEnglish}});
|
||||
check(contains(failure([&withoutConf] { NpTrophy2::ParseTrophyPackage(withoutConf, "en-US"); }), "no tropconf.json"), "a package without tropconf.json was read");
|
||||
const auto brokenConf = ucp({{"tropconf.json", "{\"trophies\":["}, {"tropmeta_en-US.json", TropMetaEnglish}});
|
||||
check(contains(failure([&brokenConf] { NpTrophy2::ParseTrophyPackage(brokenConf, "en-US"); }), "tropconf.json is not JSON"), "a tropconf.json that is not JSON was read");
|
||||
const auto withoutTitle = ucp({{"tropconf.json", TropConf}, {"tropmeta_en-US.json", "{\"metadata\":{}}"}});
|
||||
check(contains(failure([&withoutTitle] { NpTrophy2::ParseTrophyPackage(withoutTitle, "en-US"); }), "has no titleMetadata"), "texts without a title were read");
|
||||
std::string groups = R"({"defaultLanguage":"en-US","trophies":[)";
|
||||
for (int id = 0; id <= 50; ++id) groups += (id == 0 ? "" : ",") + std::string(R"({"id":)") + std::to_string(id) + R"(,"grade":"B","groupId":)" + std::to_string(id) + "}";
|
||||
groups += "]}";
|
||||
const auto manyGroups = ucp({{"tropconf.json", groups}, {"tropmeta_en-US.json", TropMetaEnglish}});
|
||||
check(contains(failure([&manyGroups] { NpTrophy2::ParseTrophyPackage(manyGroups, "en-US"); }), "more than 50 groups"), "more than 50 groups were accepted");
|
||||
std::string padded = TropConf;
|
||||
std::string padding = "\"padding\":[";
|
||||
for (std::size_t value = 0; value < 1048576; ++value) padding += "0,";
|
||||
padded.insert(1, padding + "0],");
|
||||
const auto oversized = ucp({{"tropconf.json", padded}, {"tropmeta_en-US.json", TropMetaEnglish}});
|
||||
check(contains(failure([&oversized] { NpTrophy2::ParseTrophyPackage(oversized, "en-US"); }), "tropconf.json is not JSON: the document holds more than 1048576 values"),
|
||||
"a tropconf.json of more than 1048576 values was read");
|
||||
check(contains(failure([] { NpTrophy2::ParseTrophyPackage("not a package", "en-US"); }), "fewer than the 64-byte header"), "a package that is not a container was read");
|
||||
check(NpTrophy2::LanguageTag(1) == "en-US" && NpTrophy2::LanguageTag(0) == "ja-JP" && NpTrophy2::LanguageTag(2) == "fr-FR" && NpTrophy2::LanguageTag(29) == "id-ID", "language tags are wrong");
|
||||
bool thrown = false;
|
||||
try {
|
||||
NpTrophy2::LanguageTag(31);
|
||||
} catch (const std::out_of_range&) {
|
||||
thrown = true;
|
||||
}
|
||||
check(thrown, "language 31 has a tag");
|
||||
}
|
||||
|
||||
void testProgressAndCounts() {
|
||||
const auto package = NpTrophy2::ParseTrophyPackage(trophyPackage(), "en-US");
|
||||
const auto& progressive = package.trophies.at(5);
|
||||
check(NpTrophy2::TrophyProgress(progressive, std::nullopt, false) == 0 && NpTrophy2::TrophyProgress(progressive, 3, false) == 3 && NpTrophy2::TrophyProgress(progressive, 9, false) == 5 &&
|
||||
NpTrophy2::TrophyProgress(progressive, 1, true) == 5,
|
||||
"progress is not the stat up to the target");
|
||||
bool thrown = false;
|
||||
try {
|
||||
NpTrophy2::TrophyProgress(package.trophies.at(1), 3, false);
|
||||
} catch (const std::invalid_argument&) {
|
||||
thrown = true;
|
||||
}
|
||||
check(thrown, "a trophy that is not progressive reported progress");
|
||||
|
||||
const auto all = NpTrophy2::CountTrophies(package, std::nullopt);
|
||||
check(all.trophies == 8 && all.unlocked == 0 && all.grades[NpTrophy2::GradePlatinum] == 1 && all.grades[NpTrophy2::GradeGold] == 1 && all.grades[NpTrophy2::GradeSilver] == 1 &&
|
||||
all.grades[NpTrophy2::GradeBronze] == 5,
|
||||
"the game counts are wrong");
|
||||
check(all.Percentage() == 0, "a game without unlocks has progress");
|
||||
const auto expansion = NpTrophy2::CountTrophies(package, 1);
|
||||
check(expansion.trophies == 2 && expansion.unlocked == 0 && expansion.Percentage() == 0, "the group counts are wrong");
|
||||
check(NpTrophy2::CountTrophies(package, 5).trophies == 0 && NpTrophy2::CountTrophies(package, 5).Percentage() == 0, "an empty group has trophies");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
const auto previous = std::filesystem::current_path();
|
||||
const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-package-" + std::to_string(std::random_device{}()));
|
||||
std::filesystem::create_directories(root);
|
||||
std::filesystem::current_path(root);
|
||||
testContainer();
|
||||
testJson();
|
||||
testPackage();
|
||||
testProgressAndCounts();
|
||||
std::filesystem::current_path(previous);
|
||||
std::filesystem::remove_all(root);
|
||||
std::puts("Trophy package tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -13,3 +13,15 @@ target_include_directories(libSceNpUniversalDataSystem PRIVATE
|
||||
)
|
||||
target_link_libraries(libSceNpUniversalDataSystem PRIVATE libc)
|
||||
configure_windows_unwind(libSceNpUniversalDataSystem)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(guest_np_trophy_data_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpTrophyData.cpp)
|
||||
target_include_directories(guest_np_trophy_data_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_np_trophy_data_tests PRIVATE libSceNpUniversalDataSystem libSceNpTrophy2 libc)
|
||||
configure_windows_unwind(guest_np_trophy_data_tests)
|
||||
add_test(NAME guest_np_trophy_data COMMAND guest_np_trophy_data_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_np_trophy_data PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpUniversalDataSystem>")
|
||||
endif()
|
||||
endif()
|
||||
@@ -0,0 +1,14 @@
|
||||
add_library(libScePlayerSelectionDialog SHARED EXCLUDE_FROM_ALL Export.cpp)
|
||||
target_include_directories(libScePlayerSelectionDialog PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
configure_windows_unwind(libScePlayerSelectionDialog)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(guest_player_selection_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestPlayerSelectionDialog.cpp)
|
||||
target_include_directories(guest_player_selection_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_player_selection_dialog_tests PRIVATE libScePlayerSelectionDialog)
|
||||
add_test(NAME guest_player_selection_dialog COMMAND guest_player_selection_dialog_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_player_selection_dialog PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libScePlayerSelectionDialog>")
|
||||
endif()
|
||||
endif()
|
||||
@@ -1,11 +1,60 @@
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int COMMON_DIALOG_STATUS_NONE = 0;
|
||||
constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1;
|
||||
constexpr int COMMON_DIALOG_STATUS_RUNNING = 2;
|
||||
constexpr int COMMON_DIALOG_STATUS_FINISHED = 3;
|
||||
constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast<int>(0x80B80003u);
|
||||
constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x80B80004u);
|
||||
constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast<int>(0x80B80005u);
|
||||
constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast<int>(0x80B80007u);
|
||||
constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast<int>(0x80B8000Du);
|
||||
constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1;
|
||||
|
||||
std::atomic<int> g_status{COMMON_DIALOG_STATUS_NONE};
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI scePlayerSelectionDialogInitialize(void) {
|
||||
int expected = COMMON_DIALOG_STATUS_NONE;
|
||||
if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI scePlayerSelectionDialogOpen(const void* param) {
|
||||
const int status = g_status.load();
|
||||
if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED;
|
||||
if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY;
|
||||
if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL;
|
||||
g_status = COMMON_DIALOG_STATUS_FINISHED;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI scePlayerSelectionDialogUpdateStatus(void) {
|
||||
return g_status.load();
|
||||
}
|
||||
|
||||
int APS5_VABI scePlayerSelectionDialogGetResult(void* result) {
|
||||
const int status = g_status.load();
|
||||
if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED;
|
||||
if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL;
|
||||
if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED;
|
||||
const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED;
|
||||
std::memcpy(result, &canceled, sizeof(canceled));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI scePlayerSelectionDialogTerminate(void) {
|
||||
if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <charconv>
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -28,20 +27,6 @@
|
||||
|
||||
namespace {
|
||||
|
||||
int ConsoleLanguage() {
|
||||
const char* configured = std::getenv("ANYPS5_LANGUAGE");
|
||||
if (configured == nullptr || configured[0] == '\0') return SYSTEM_SERVICE_PARAM_LANG_ENGLISH_US;
|
||||
const std::string_view text(configured);
|
||||
int language = -1;
|
||||
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), language);
|
||||
if (error != std::errc{} || end != text.data() + text.size() || language < SYSTEM_SERVICE_PARAM_LANG_JAPANESE || language > SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN) {
|
||||
throw std::runtime_error("sceSystemServiceParamGetInt: ANYPS5_LANGUAGE must be a console language number from " +
|
||||
std::to_string(SYSTEM_SERVICE_PARAM_LANG_JAPANESE) + " to " + std::to_string(SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN) +
|
||||
", not \"" + std::string(text) + "\"");
|
||||
}
|
||||
return language;
|
||||
}
|
||||
|
||||
struct ConsoleTimeZone {
|
||||
int minutes;
|
||||
bool summer;
|
||||
@@ -298,7 +283,7 @@ int APS5_VABI sceSystemServiceParamGetInt(int paramId, int* value) {
|
||||
}
|
||||
switch (paramId) {
|
||||
case SYSTEM_SERVICE_PARAM_ID_LANG:
|
||||
case SYSTEM_SERVICE_PARAM_ID_400: *value = ConsoleLanguage(); break;
|
||||
case SYSTEM_SERVICE_PARAM_ID_400: *value = SystemServiceConsoleLanguage("sceSystemServiceParamGetInt"); break;
|
||||
case SYSTEM_SERVICE_PARAM_ID_DATE_FORMAT: *value = SYSTEM_SERVICE_PARAM_DATE_FORMAT_DDMMYYYY; break;
|
||||
case SYSTEM_SERVICE_PARAM_ID_TIME_FORMAT: *value = SYSTEM_SERVICE_PARAM_TIME_FORMAT_24HOUR; break;
|
||||
case SYSTEM_SERVICE_PARAM_ID_TIME_ZONE: *value = HostTimeZone().minutes; break;
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
#ifndef CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_SYSTEMSERVICE_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_SYSTEMSERVICE_HPP
|
||||
|
||||
#include <charconv>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
|
||||
constexpr int SYSTEM_SERVICE_OK = 0;
|
||||
constexpr int SYSTEM_SERVICE_ERROR_PARAMETER = -2136932349;
|
||||
@@ -31,4 +37,18 @@ constexpr int SYSTEM_SERVICE_PARAM_TIME_FORMAT_24HOUR = 1;
|
||||
constexpr int SYSTEM_SERVICE_PARAM_GAME_PARENTAL_OFF = 0;
|
||||
constexpr int SYSTEM_SERVICE_PARAM_ENTER_BUTTON_CROSS = 1;
|
||||
|
||||
inline int SystemServiceConsoleLanguage(std::string_view caller) {
|
||||
const char* configured = std::getenv("ANYPS5_LANGUAGE");
|
||||
if (configured == nullptr || configured[0] == '\0') return SYSTEM_SERVICE_PARAM_LANG_ENGLISH_US;
|
||||
const std::string_view text(configured);
|
||||
int language = -1;
|
||||
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), language);
|
||||
if (error != std::errc{} || end != text.data() + text.size() || language < SYSTEM_SERVICE_PARAM_LANG_JAPANESE || language > SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN) {
|
||||
throw std::runtime_error(std::string(caller) + ": ANYPS5_LANGUAGE must be a console language number from " +
|
||||
std::to_string(SYSTEM_SERVICE_PARAM_LANG_JAPANESE) + " to " + std::to_string(SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN) +
|
||||
", not \"" + std::string(text) + "\"");
|
||||
}
|
||||
return language;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "general/VabiMacros.hpp"
|
||||
#include "general/ExportMacros.hpp"
|
||||
|
||||
extern "C" void NotImplemented_nid_no_patch(const char* funcName);
|
||||
extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* funcName);
|
||||
extern "C" void CxaFinalize_nid_no_patch(void* dsoHandle);
|
||||
extern "C" bool StopAtPrefixLetter_nid_no_patch(const char* str, char** endptr, int base);
|
||||
#ifndef _WIN32
|
||||
|
||||
@@ -614,6 +614,6 @@ extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle) {
|
||||
}
|
||||
#endif
|
||||
|
||||
extern "C" void NotImplemented_nid_no_patch(const char* funcName) {
|
||||
extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* funcName) {
|
||||
throw std::runtime_error(std::string(funcName) + " not implemented");
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
@@ -265,19 +266,15 @@ void* APS5_VABI sceLibcMspaceAlignedAlloc_nid_postfix(void* handle, std::size_t
|
||||
return sceLibcMspaceMemalign_nid_postfix(handle, alignment, size);
|
||||
}
|
||||
|
||||
void* APS5_VABI sceLibcMspaceReallocalign_nid_postfix(void* handle, void* pointer, std::size_t size, std::size_t alignment) {
|
||||
if (alignment == 0 || (alignment & (alignment - 1))) {
|
||||
Error(22);
|
||||
return nullptr;
|
||||
}
|
||||
const auto effective = std::max<std::size_t>(alignment, 16);
|
||||
void* APS5_VABI sceLibcMspaceReallocalign_nid_postfix(void* handle, void* pointer, std::size_t alignment, std::size_t size) {
|
||||
if (alignment >> 63) return nullptr;
|
||||
const auto effective = std::bit_ceil(std::max<std::size_t>(alignment, MinimumAlignment));
|
||||
std::lock_guard lock(arenaMutex);
|
||||
auto* arena = Find(handle);
|
||||
if (!pointer) return Allocate(arena, size, effective);
|
||||
std::map<std::uintptr_t, Chunk>::iterator chunk;
|
||||
if (!FindUsed(arena, pointer, chunk)) return nullptr;
|
||||
if (!size) { Release(*arena, chunk); return nullptr; }
|
||||
const auto needed = AlignUp(size, Granule);
|
||||
const auto needed = AlignUp(std::max<std::size_t>(size, 1), MinimumAlignment);
|
||||
if (needed < size) { Error(12); return nullptr; }
|
||||
const auto start = chunk->first;
|
||||
const auto capacity = chunk->second.end - start;
|
||||
@@ -311,4 +308,5 @@ std::size_t APS5_VABI sceLibcMspaceMallocUsableSize_nid_postfix(const void* poin
|
||||
Error(22);
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "prx/libc/include/FileStream.hpp"
|
||||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include <cerrno>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
#else
|
||||
@@ -134,6 +137,16 @@ int APS5_VABI fileno_nid_postfix(FileStream* stream) {
|
||||
stream->GuestState().descriptor = static_cast<std::int16_t>(descriptor);
|
||||
return descriptor;
|
||||
}
|
||||
void APS5_VABI clearerr_unlocked_nid_postfix(FileStream* stream) {
|
||||
stream->ClearError();
|
||||
}
|
||||
int APS5_VABI fileno_unlocked_nid_postfix(FileStream* stream) {
|
||||
if (stream == nullptr || !stream->IsOpen()) return -1;
|
||||
return fileno_nid_postfix(stream);
|
||||
}
|
||||
[[noreturn]] char* APS5_VABI fgetln_nid_postfix(FileStream*, std::size_t*) {
|
||||
throw std::runtime_error("fgetln: the console's export is a hlt instruction, which ends the process");
|
||||
}
|
||||
int APS5_VABI setvbuf_nid_postfix(FileStream* stream, char* buffer, int mode, std::size_t size) {
|
||||
if (stream && !stream->IsOpen()) { errno = 9; return -1; }
|
||||
if (mode < 0 || mode > 2) return -1;
|
||||
|
||||
@@ -993,6 +993,10 @@ int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* bas
|
||||
File::RefuseArchivedFile(fd, "getdirentries");
|
||||
if (buf == nullptr) return SceErrorFromErrno(GUEST_EFAULT);
|
||||
if (nbytes <= 0) return SceErrorFromErrno(GUEST_EINVAL);
|
||||
if (!File::DirectoryDescriptorPath(fd)) {
|
||||
if (WithoutParameterHandler([fd] { return ::_get_osfhandle(fd); }) == -1) return SceErrorFromErrno(GUEST_EBADF);
|
||||
return SceErrorFromErrno(GUEST_EINVAL);
|
||||
}
|
||||
if (basep != nullptr) *basep = 0;
|
||||
return File::ReadDirectoryDescriptor(fd, buf, nbytes);
|
||||
}
|
||||
@@ -1010,6 +1014,9 @@ int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* bas
|
||||
constexpr std::size_t GuestMaxName = 255;
|
||||
if (buf == nullptr) return SceErrorFromErrno(GUEST_EFAULT);
|
||||
if (nbytes <= 0) return SceErrorFromErrno(GUEST_EINVAL);
|
||||
struct stat status{};
|
||||
if (::fstat(fd, &status) != 0) return SceErrorFromErrno(errno);
|
||||
if (!S_ISDIR(status.st_mode)) return SceErrorFromErrno(GUEST_EINVAL);
|
||||
const off_t base = ::lseek(fd, 0, SEEK_CUR);
|
||||
if (base < 0) return SceErrorFromErrno(errno);
|
||||
if (basep != nullptr) *basep = static_cast<int64_t>(base);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
static std::atomic<uint64_t> g_coredumpHandler{0};
|
||||
static std::atomic<uint64_t> g_coredumpContext{0};
|
||||
|
||||
static constexpr int SCE_COREDUMP_ERROR_INVALID_ARGUMENT = static_cast<int>(0x81180000u);
|
||||
static constexpr int SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER = static_cast<int>(0x81180003u);
|
||||
|
||||
static void requireRegionOutsideHandler(const char* function, const void* mem, size_t size) {
|
||||
@@ -55,9 +56,10 @@ int APS5_VABI sceCoredumpAttachUserFile(uint32_t userValue, const char* path) {
|
||||
return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER;
|
||||
}
|
||||
|
||||
int APS5_VABI sceCoredumpGetStopInfoGpu_Agc(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
int APS5_VABI sceCoredumpGetStopInfoGpu_Agc(void* info, size_t size) {
|
||||
(void)size;
|
||||
if (info == nullptr) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT;
|
||||
return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER;
|
||||
}
|
||||
|
||||
int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const void* mem, size_t size, const char* name) {
|
||||
@@ -99,14 +101,13 @@ int APS5_VABI sceCoredumpGetThreadContextInfo(void* threadContextInfo, size_t th
|
||||
return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER;
|
||||
}
|
||||
|
||||
int APS5_VABI sceCoredumpWriteUserString() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
int APS5_VABI sceCoredumpWriteUserString(uint32_t first, const char* text, int third) {
|
||||
if (text == nullptr || first > 15 || third < 0 || third > 256) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceCoredumpConfigDumpMode() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
int APS5_VABI sceCoredumpConfigDumpMode(int mode) {
|
||||
return mode == 1 ? 0 : SCE_COREDUMP_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
int APS5_VABI sceCoredumpAttachUserMemoryFile(uint32_t userValue, const void* mem, size_t size) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
@@ -28,6 +29,10 @@
|
||||
#include <sys/time.h>
|
||||
#include <sys/times.h>
|
||||
#include <unistd.h>
|
||||
#ifdef __linux__
|
||||
#include <dlfcn.h>
|
||||
#include <link.h>
|
||||
#endif // __linux__
|
||||
#endif
|
||||
|
||||
extern "C" int* APS5_VABI __error_nid_postfix();
|
||||
@@ -179,6 +184,39 @@ std::uint64_t APS5_VABI sceKernelGetProcessTimeCounterFrequency() {
|
||||
return 1000000000ULL;
|
||||
}
|
||||
|
||||
static std::string DescribeCallSite(const void* caller) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(caller);
|
||||
char text[192];
|
||||
#ifdef _WIN32
|
||||
const auto image = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
|
||||
std::snprintf(text, sizeof(text), "exe+0x%llx", static_cast<unsigned long long>(address - image));
|
||||
#elif defined(__linux__)
|
||||
Dl_info info{};
|
||||
link_map* map = nullptr;
|
||||
if (dladdr1(caller, &info, reinterpret_cast<void**>(&map), RTLD_DL_LINKMAP) == 0 || map == nullptr) {
|
||||
std::snprintf(text, sizeof(text), "0x%llx", static_cast<unsigned long long>(address));
|
||||
return text;
|
||||
}
|
||||
const char* path = map->l_name;
|
||||
const char* slash = path == nullptr ? nullptr : std::strrchr(path, '/');
|
||||
const char* module = path == nullptr || *path == '\0' ? "exe" : slash == nullptr ? path : slash + 1;
|
||||
std::snprintf(text, sizeof(text), "%s+0x%llx", module, static_cast<unsigned long long>(address - map->l_addr));
|
||||
#else
|
||||
std::snprintf(text, sizeof(text), "0x%llx", static_cast<unsigned long long>(address));
|
||||
#endif
|
||||
return text;
|
||||
}
|
||||
|
||||
static unsigned long CurrentThreadIdForTrace() {
|
||||
#ifdef _WIN32
|
||||
return GetCurrentThreadId();
|
||||
#elif defined(__linux__)
|
||||
return static_cast<unsigned long>(gettid());
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Debug aid: APS5_TRACE_USLEEP reports every 2000 calls which guest call sites sleep, so a CPU
|
||||
// thread polling for GPU or I/O completion can be found (offsets are relative to the executable).
|
||||
static void TraceSleep(const void* caller, std::uint64_t microseconds) {
|
||||
@@ -192,15 +230,10 @@ static void TraceSleep(const void* caller, std::uint64_t microseconds) {
|
||||
++site.first;
|
||||
site.second += microseconds;
|
||||
if (++calls % 2000 != 0) return;
|
||||
#ifdef _WIN32
|
||||
const auto image = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
|
||||
#else
|
||||
const std::uintptr_t image = 0;
|
||||
#endif
|
||||
std::vector<std::pair<std::uintptr_t, std::pair<std::uint64_t, std::uint64_t>>> hot(sites.begin(), sites.end());
|
||||
std::sort(hot.begin(), hot.end(), [](const auto& a, const auto& b) { return a.second.first > b.second.first; });
|
||||
std::fprintf(stderr, "[usleep] %llu calls:", static_cast<unsigned long long>(calls));
|
||||
for (std::size_t i = 0; i < hot.size() && i < 6; ++i) std::fprintf(stderr, " exe+0x%llx x%llu (%llu us)", static_cast<unsigned long long>(hot[i].first - image), static_cast<unsigned long long>(hot[i].second.first), static_cast<unsigned long long>(hot[i].second.second));
|
||||
for (std::size_t i = 0; i < hot.size() && i < 6; ++i) std::fprintf(stderr, " %s x%llu (%llu us)", DescribeCallSite(reinterpret_cast<const void*>(hot[i].first)).c_str(), static_cast<unsigned long long>(hot[i].second.first), static_cast<unsigned long long>(hot[i].second.second));
|
||||
std::fprintf(stderr, "\n");
|
||||
}
|
||||
|
||||
@@ -211,16 +244,13 @@ void KernelTraceWait_nid_postfix(const char* kind, const void* caller, std::uint
|
||||
static std::mutex mutex;
|
||||
static std::unordered_map<std::string, Site> sites;
|
||||
static std::uint64_t lastReport = 0;
|
||||
char key[96];
|
||||
#ifdef _WIN32
|
||||
const auto image = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
|
||||
const unsigned long thread = GetCurrentThreadId();
|
||||
#else
|
||||
const std::uintptr_t image = 0;
|
||||
const unsigned long thread = 0;
|
||||
#endif
|
||||
std::snprintf(key, sizeof(key), "%s exe+0x%llx", kind, static_cast<unsigned long long>(reinterpret_cast<std::uintptr_t>(caller) - image));
|
||||
static std::unordered_map<const void*, std::string> callSites;
|
||||
char key[224];
|
||||
const unsigned long thread = CurrentThreadIdForTrace();
|
||||
std::lock_guard lock(mutex);
|
||||
auto callSite = callSites.find(caller);
|
||||
if (callSite == callSites.end()) callSite = callSites.emplace(caller, DescribeCallSite(caller)).first;
|
||||
std::snprintf(key, sizeof(key), "%s %s", kind, callSite->second.c_str());
|
||||
auto& site = sites[key];
|
||||
++site.calls;
|
||||
site.timeouts += timedOut ? 1u : 0u;
|
||||
|
||||
@@ -14,9 +14,13 @@ int APS5_VABI sceCoredumpAttachMemoryRegion(uint32_t userValue, const void* mem,
|
||||
int APS5_VABI sceCoredumpAttachUserMemoryFile(uint32_t userValue, const void* mem, size_t size);
|
||||
int APS5_VABI sceCoredumpGetStopInfoCpu(void* info, size_t size);
|
||||
int APS5_VABI sceCoredumpGetThreadContextInfo(void* threadContextInfo, size_t threadContextInfoSize);
|
||||
int APS5_VABI sceCoredumpGetStopInfoGpu_Agc(void* info, size_t size);
|
||||
int APS5_VABI sceCoredumpWriteUserString(uint32_t first, const char* text, int third);
|
||||
int APS5_VABI sceCoredumpConfigDumpMode(int mode);
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr int InvalidArgument = static_cast<int>(0x81180000u);
|
||||
constexpr int NotInCoredumpHandler = static_cast<int>(0x81180003u);
|
||||
constexpr uint32_t UserFileTruncateIfExceedLimit = 0x80000000u;
|
||||
bool handlerRan = false;
|
||||
@@ -90,5 +94,18 @@ int main() {
|
||||
Require(sceCoredumpGetThreadContextInfo(context.data() + 8, 144) == NotInCoredumpHandler);
|
||||
Require(region == original && context == originalContext);
|
||||
Require(!handlerRan);
|
||||
|
||||
Require(sceCoredumpGetStopInfoGpu_Agc(region.data() + 8, 8) == NotInCoredumpHandler);
|
||||
Require(sceCoredumpGetStopInfoGpu_Agc(region.data() + 8, 3) == NotInCoredumpHandler);
|
||||
Require(sceCoredumpGetStopInfoGpu_Agc(nullptr, 8) == InvalidArgument);
|
||||
Require(sceCoredumpWriteUserString(1, "text", 4) == 0);
|
||||
Require(sceCoredumpWriteUserString(0, "", 0) == 0);
|
||||
Require(sceCoredumpWriteUserString(1, nullptr, 4) == InvalidArgument);
|
||||
Require(sceCoredumpWriteUserString(15, "x", 256) == 0 && sceCoredumpWriteUserString(0, "x", 0) == 0);
|
||||
Require(sceCoredumpWriteUserString(16, "x", 1) == InvalidArgument && sceCoredumpWriteUserString(0x80000000u, "x", 1) == InvalidArgument);
|
||||
Require(sceCoredumpWriteUserString(1, "x", 257) == InvalidArgument && sceCoredumpWriteUserString(1, "x", -1) == InvalidArgument);
|
||||
Require(region == original);
|
||||
Require(sceCoredumpConfigDumpMode(1) == 0);
|
||||
for (const int mode : {0, 2, -1}) Require(sceCoredumpConfigDumpMode(mode) == InvalidArgument);
|
||||
return 0;
|
||||
}
|
||||
@@ -66,7 +66,7 @@ static void CheckName(const std::u8string& utf8Name) {
|
||||
Require(sceKernelGetdents(directory, buffer.data(), 8) == 0);
|
||||
Require(sceKernelGetdents(directory, buffer.data(), 256) == 0);
|
||||
Require(sceKernelClose(directory) == 0);
|
||||
Require(sceKernelGetdents(-1, buffer.data(), 256) == SCE_KERNEL_ERROR_ENOTDIR);
|
||||
Require(sceKernelGetdents(-1, buffer.data(), 256) == SCE_KERNEL_ERROR_EBADF);
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ static void Check(bool value, int line) {
|
||||
}
|
||||
#define Require(value) Check((value), __LINE__)
|
||||
|
||||
static constexpr int ErrorEnotdir = static_cast<int>(0x80020014u);
|
||||
static constexpr int ErrorEinval = static_cast<int>(0x80020016u);
|
||||
|
||||
static void VerifyCloseReuse(const std::filesystem::path& root, const char* scenario,
|
||||
bool useUnderscoreClose) {
|
||||
@@ -49,7 +49,7 @@ static void VerifyCloseReuse(const std::filesystem::path& root, const char* scen
|
||||
Require(file == directory);
|
||||
|
||||
std::array<char, 256> entries{};
|
||||
Require(sceKernelGetdents(file, entries.data(), static_cast<int>(entries.size())) == ErrorEnotdir);
|
||||
Require(sceKernelGetdents(file, entries.data(), static_cast<int>(entries.size())) == ErrorEinval);
|
||||
Require((useUnderscoreClose ? _close_nid_postfix(file) : close_nid_postfix(file)) == 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
@@ -55,6 +56,8 @@ int APS5_VABI mkdir_nid_postfix(const char*, unsigned short);
|
||||
int APS5_VABI sceKernelOpen(const char*, int, unsigned short);
|
||||
int APS5_VABI sceKernelClose(int);
|
||||
int APS5_VABI sceKernelGetdents(int, char*, int);
|
||||
int APS5_VABI getdents_nid_postfix(int, char*, int);
|
||||
int APS5_VABI getdirentries_nid_postfix(int, char*, int, int64_t*);
|
||||
std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t);
|
||||
std::int64_t APS5_VABI sceKernelReadv(int, const void*, int);
|
||||
std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t);
|
||||
@@ -596,7 +599,13 @@ int main() {
|
||||
Require(rename_nid_postfix(name(replacement).c_str(), name(dangling).c_str()) == 0);
|
||||
Require(!std::filesystem::is_symlink(dangling) && std::filesystem::is_regular_file(dangling));
|
||||
#endif
|
||||
std::filesystem::remove_all(area);
|
||||
std::error_code cleanupError;
|
||||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||
std::filesystem::remove_all(area, cleanupError);
|
||||
if (!cleanupError) break;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
}
|
||||
Require(!cleanupError);
|
||||
}
|
||||
Require(remove_nid_postfix(file.string().c_str()) == 0);
|
||||
Require(!std::filesystem::exists(file));
|
||||
@@ -886,6 +895,13 @@ int main() {
|
||||
Require(sceKernelPwrite(directoryDescriptor, "x", 1, 0) == static_cast<int>(0x80020009u));
|
||||
std::vector<char> listing(4096);
|
||||
Require(sceKernelGetdents(directoryDescriptor, listing.data(), static_cast<int>(listing.size())) > 0);
|
||||
const int plainFile = open_nid_postfix(presentName.c_str(), 0, 0);
|
||||
Require(plainFile >= 0);
|
||||
Require(sceKernelGetdents(plainFile, listing.data(), static_cast<int>(listing.size())) == static_cast<int>(0x80020016u));
|
||||
Require(getdents_nid_postfix(plainFile, listing.data(), static_cast<int>(listing.size())) == -1 && *__error_nid_postfix() == 22);
|
||||
int64_t plainBase = 0;
|
||||
Require(getdirentries_nid_postfix(plainFile, listing.data(), static_cast<int>(listing.size()), &plainBase) == -1 && *__error_nid_postfix() == 22);
|
||||
Require(close_nid_postfix(plainFile) == 0);
|
||||
Require(close_nid_postfix(directoryDescriptor) == 0);
|
||||
const int flaggedDirectory = sceKernelOpen(rootName.c_str(), 0x20000, 0);
|
||||
Require(flaggedDirectory >= 0 && sceKernelClose(flaggedDirectory) == 0);
|
||||
|
||||
+113
-11
@@ -1,17 +1,18 @@
|
||||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include "ProcessParametersFixture.hpp"
|
||||
#include "prx/libSceHmd2/Hmd2.hpp"
|
||||
#include "prx/libSceUserService/UserService.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
|
||||
extern "C" {
|
||||
std::int32_t APS5_VABI sceHmd2Initialize(const void* param);
|
||||
int APS5_VABI sceHmd2Open();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
alignas(16) std::array<std::byte, 0x40> process{};
|
||||
|
||||
void Require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::fprintf(stderr, "HMD2: %s\n", message);
|
||||
@@ -19,19 +20,120 @@ void Require(bool condition, const char* message) {
|
||||
}
|
||||
}
|
||||
|
||||
void SetSdkVersion(std::uint32_t version) {
|
||||
std::memcpy(process.data() + 0x10, &version, sizeof(version));
|
||||
}
|
||||
|
||||
int main() {
|
||||
void InstallProcessParameters() {
|
||||
const std::uint64_t size = process.size();
|
||||
const std::uint32_t magic = 0x4942524f;
|
||||
std::memcpy(process.data(), &size, sizeof(size));
|
||||
std::memcpy(process.data() + 8, &magic, sizeof(magic));
|
||||
Require(ProcessParametersFixture::Install(process.data(), process.size()), "process parameters were not installed");
|
||||
}
|
||||
|
||||
bool IsDefaultFieldOfView(const Hmd2::FieldOfView& fieldOfView) {
|
||||
return fieldOfView.tanOut == 1.84177089f && fieldOfView.tanIn == 0.947190642f &&
|
||||
fieldOfView.tanTop == 1.32898104f && fieldOfView.tanBottom == 1.32898104f;
|
||||
}
|
||||
|
||||
void RequireNotDetected(const Hmd2::DeviceInformation& info, std::int32_t userId) {
|
||||
Require(info.status == Hmd2::DeviceStatus::NotDetected, "device status is not NOT_DETECTED");
|
||||
Require(info.userId == userId, "device user does not match the SDK version");
|
||||
Require(info.panelWidth == 4000 && info.panelHeight == 2040, "panel resolution differs from the firmware");
|
||||
Require(info.latency90Hz == 5555 && info.latency120Hz == 4160, "flip latencies differ from the firmware");
|
||||
Require(info.hmuMount == 0 && info.lensSeparationDistance == 0, "a missing headset reported a mount or lens separation");
|
||||
Require(info.reserved[0] == 0 && info.reserved[1] == 0, "reserved bytes were not cleared");
|
||||
Require(info.virtualImageDistance == 2.0f, "virtual image distance differs from the firmware");
|
||||
}
|
||||
|
||||
void TestBeforeInitialize() {
|
||||
Hmd2::DeviceInformation info;
|
||||
std::memset(&info, 0xcc, sizeof(info));
|
||||
Require(sceHmd2GetDeviceInformation(&info) == Hmd2::NotInitialized, "device information before initialization");
|
||||
const Hmd2::DeviceInformation cleared{};
|
||||
Require(std::memcmp(&info, &cleared, sizeof(info)) == 0, "device information was not cleared before the initialization check");
|
||||
Require(sceHmd2GetDeviceInformation(nullptr) == Hmd2::ParameterNull, "null device information before initialization");
|
||||
Hmd2::FieldOfView fieldOfView{};
|
||||
Require(sceHmd2GetFieldOfViewWithoutHandle(&fieldOfView) == Hmd2::NotInitialized, "field of view before initialization");
|
||||
Require(sceHmd2GetFieldOfView(0, &fieldOfView) == Hmd2::InvalidHandle, "field of view of handle 0 before initialization");
|
||||
Require(sceHmd2GetDeviceInformationByHandle(0, &info) == Hmd2::NotInitialized, "device information of handle 0 before initialization");
|
||||
Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr) == Hmd2::NotInitialized, "open before initialization");
|
||||
Require(sceHmd2Close(0) == Hmd2::NotInitialized, "close before initialization");
|
||||
Require(sceHmd2Terminate() == Hmd2::NotInitialized, "terminate before initialization");
|
||||
Require(sceHmd2SetVibration(0, 0) == 0, "vibration of the unopened handle 0 failed");
|
||||
}
|
||||
|
||||
void TestInitialized() {
|
||||
const std::uint8_t param[16]{};
|
||||
Require(sceHmd2Initialize(param) == static_cast<std::int32_t>(0x81110016), "initialization did not report the unsupported feature");
|
||||
Require(sceHmd2Initialize(nullptr) == static_cast<std::int32_t>(0x81110016), "initialization without a param did not report the unsupported feature");
|
||||
Require(sceHmd2Initialize(nullptr) == Hmd2::ParameterNull, "initialization without a param");
|
||||
Require(sceHmd2Initialize(param) == 0, "initialization failed");
|
||||
Require(sceHmd2Initialize(param) == Hmd2::AlreadyInitialized, "second initialization");
|
||||
|
||||
Hmd2::DeviceInformation info;
|
||||
std::memset(&info, 0xcc, sizeof(info));
|
||||
SetSdkVersion(0x06000000);
|
||||
Require(sceHmd2GetDeviceInformation(&info) == 0, "device information failed");
|
||||
RequireNotDetected(info, -1);
|
||||
SetSdkVersion(0x07590001);
|
||||
std::memset(&info, 0xcc, sizeof(info));
|
||||
Require(sceHmd2GetDeviceInformation(&info) == 0, "device information for SDK 7.59 failed");
|
||||
RequireNotDetected(info, 0);
|
||||
|
||||
Hmd2::FieldOfView fieldOfView{};
|
||||
Require(sceHmd2GetFieldOfViewWithoutHandle(&fieldOfView) == 0 && IsDefaultFieldOfView(fieldOfView), "default field of view");
|
||||
Require(sceHmd2GetFieldOfViewWithoutHandle(nullptr) == Hmd2::ParameterNull, "null field of view");
|
||||
Require(sceHmd2GetFieldOfView(12345, &fieldOfView) == Hmd2::InvalidHandle, "field of view of an unknown handle");
|
||||
Require(sceHmd2GetFieldOfView(12345, nullptr) == Hmd2::ParameterNull, "null field of view of an unknown handle");
|
||||
Require(sceHmd2GetDeviceInformationByHandle(12345, &info) == Hmd2::InvalidHandle, "device information of an unknown handle");
|
||||
Require(sceHmd2SetVibration(12345, 0) == Hmd2::InvalidHandle, "vibration of an unknown handle");
|
||||
Require(sceHmd2Close(12345) == Hmd2::InvalidHandle, "close of an unknown handle");
|
||||
|
||||
Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 1, 0, nullptr) == Hmd2::ParameterInvalid, "open with a device type");
|
||||
Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 1, nullptr) == Hmd2::ParameterInvalid, "open with a device index");
|
||||
Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, param) == Hmd2::ParameterInvalid, "open with a param");
|
||||
Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID + 1, 0, 0, nullptr) == Hmd2::ParameterInvalid, "open for another user on SDK 7.59");
|
||||
|
||||
const auto handle = sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr);
|
||||
Require(handle > 0, "open without a headset failed");
|
||||
Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr) == Hmd2::AlreadyOpened, "second open");
|
||||
std::memset(&info, 0xcc, sizeof(info));
|
||||
Require(sceHmd2GetDeviceInformationByHandle(handle, &info) == 0, "device information by handle failed");
|
||||
RequireNotDetected(info, 0);
|
||||
fieldOfView = {};
|
||||
Require(sceHmd2GetFieldOfView(handle, &fieldOfView) == 0 && IsDefaultFieldOfView(fieldOfView), "field of view by handle");
|
||||
Require(sceHmd2SetVibration(handle, 26) == 0, "vibration of a disconnected headset failed");
|
||||
Require(sceHmd2Close(handle) == 0, "close failed");
|
||||
Require(sceHmd2Close(handle) == Hmd2::InvalidHandle, "second close");
|
||||
Require(sceHmd2Close(0) == 0, "close of the unopened handle 0 failed");
|
||||
|
||||
const auto kept = sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr);
|
||||
Require(kept > 0, "reopen failed");
|
||||
Require(sceHmd2Terminate() == 0, "terminate failed");
|
||||
Require(sceHmd2Terminate() == Hmd2::NotInitialized, "second terminate");
|
||||
Require(sceHmd2GetFieldOfView(kept, &fieldOfView) == Hmd2::NotInitialized, "field of view after terminate");
|
||||
Require(sceHmd2Close(kept) == Hmd2::NotInitialized, "close after terminate");
|
||||
Require(sceHmd2Initialize(param) == 0, "reinitialization failed");
|
||||
Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr) == Hmd2::AlreadyOpened, "terminate closed the open handle");
|
||||
Require(sceHmd2Close(kept) == 0, "close after reinitialization failed");
|
||||
|
||||
SetSdkVersion(0x06000000);
|
||||
bool threw = false;
|
||||
try {
|
||||
sceHmd2Open();
|
||||
sceHmd2Open(USER_SERVICE_INITIAL_USER_ID + 1, 0, 0, nullptr);
|
||||
} catch (const std::runtime_error&) {
|
||||
threw = true;
|
||||
}
|
||||
Require(threw, "open after failed initialization did not throw");
|
||||
Require(threw, "open for another user before SDK 7.00 did not throw");
|
||||
Require(sceHmd2Terminate() == 0, "final terminate failed");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
InstallProcessParameters();
|
||||
TestBeforeInitialize();
|
||||
TestInitialized();
|
||||
std::puts("HMD2 tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
extern "C" {
|
||||
int APS5_VABI sceNpTrophy2CreateContext(int*, int, uint32_t, uint64_t);
|
||||
int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*);
|
||||
int APS5_VABI sceNpTrophy2GetGroupInfo(int, int, int, NpTrophy2GroupDetails*, NpTrophy2GroupData*);
|
||||
int APS5_VABI sceNpTrophy2GetGroupInfoArray(int, int, uint32_t, uint32_t, NpTrophy2GroupDetails*, NpTrophy2GroupData*, uint32_t*);
|
||||
int APS5_VABI sceNpTrophy2GetTrophyInfo(int, int, int, NpTrophy2Details*, NpTrophy2Data*);
|
||||
int APS5_VABI sceNpTrophy2GetTrophyInfoArray(int, int, uint32_t, uint32_t, NpTrophy2Details*, NpTrophy2Data*, uint32_t*);
|
||||
}
|
||||
|
||||
static constexpr int InitialUser = 0x10000000;
|
||||
|
||||
static const char TropConf[] = R"({"defaultLanguage":"en-US","trophies":[
|
||||
{"id":"0","grade":"P"},
|
||||
{"id":"1","grade":"G","platinumTrophyId":"0","unlockCondition":{"udsStatId":"10","comparator":"ge","targetValue":"10","progressive":false}},
|
||||
{"id":"2","grade":"S","platinumTrophyId":"0","unlockCondition":{"udsStatId":"20","comparator":"gt","targetValue":"100"}},
|
||||
{"id":"3","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"le","targetValue":"5"}},
|
||||
{"id":"4","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"lt","targetValue":"3"}},
|
||||
{"id":"5","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"40","comparator":"ge","targetValue":"5","progressive":true}},
|
||||
{"id":"6","grade":"B","groupId":"1","hidden":true,"hasReward":true},
|
||||
{"id":"7","grade":"B","groupId":"1"}
|
||||
]})";
|
||||
|
||||
static const char TropMeta[] = R"({"metadata":{"titleMetadata":{"name":"Synthetic Game"},
|
||||
"trophyMetadata":[{"id":"0","name":"All of them"},{"id":"1","name":"Ten","detail":"Reach ten"},{"id":"5","name":"Five steps"},
|
||||
{"id":"6","name":"Secret","detail":"A hidden one","reward":"A hat"}],
|
||||
"groupMetadata":[{"id":"1","name":"Expansion"}]}})";
|
||||
|
||||
static void Require(bool value, const char* message) {
|
||||
if (value) return;
|
||||
std::fprintf(stderr, "%s\n", message);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
template <typename TCall>
|
||||
static bool ThrowsInvalidArgument(TCall call) {
|
||||
try {
|
||||
call();
|
||||
} catch (const std::invalid_argument&) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void PutBigEndian(std::string& data, std::size_t offset, std::uint64_t value, std::size_t size) {
|
||||
for (std::size_t i = 0; i < size; ++i) data[offset + i] = static_cast<char>((value >> (8 * (size - 1 - i))) & 0xFF);
|
||||
}
|
||||
|
||||
static std::string Ucp(const std::vector<std::pair<std::string, std::string>>& files) {
|
||||
std::size_t offset = 0x60 + files.size() * 0x40;
|
||||
std::size_t size = offset;
|
||||
for (const auto& file : files) size += file.second.size();
|
||||
std::string data(size, '\0');
|
||||
PutBigEndian(data, 0, 0xB228C60A, 4);
|
||||
PutBigEndian(data, 4, 1, 4);
|
||||
PutBigEndian(data, 8, size, 8);
|
||||
PutBigEndian(data, 0x10, files.size(), 4);
|
||||
PutBigEndian(data, 0x14, 0x40, 4);
|
||||
std::size_t entry = 0x60;
|
||||
for (const auto& [name, contents] : files) {
|
||||
data.replace(entry, name.size(), name);
|
||||
PutBigEndian(data, entry + 0x20, offset, 8);
|
||||
PutBigEndian(data, entry + 0x28, contents.size(), 8);
|
||||
data.replace(offset, contents.size(), contents);
|
||||
offset += contents.size();
|
||||
entry += 0x40;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
static void Write(const std::filesystem::path& path, const std::string& text) {
|
||||
std::filesystem::create_directories(path.parent_path());
|
||||
std::FILE* file = std::fopen(path.string().c_str(), "wb");
|
||||
Require(file != nullptr, "a test file was not created");
|
||||
const bool written = std::fwrite(text.data(), 1, text.size(), file) == text.size();
|
||||
Require(std::fclose(file) == 0 && written, "a test file was not written");
|
||||
}
|
||||
|
||||
static int TrophyContext(std::uint32_t serviceLabel) {
|
||||
int context = 0;
|
||||
Require(sceNpTrophy2CreateContext(&context, InitialUser, serviceLabel, 0) == 0, "trophy context was not created");
|
||||
return context;
|
||||
}
|
||||
|
||||
static NpTrophy2Data Trophy(int context, int trophyId, NpTrophy2Details* details = nullptr) {
|
||||
NpTrophy2Data data{};
|
||||
Require(sceNpTrophy2GetTrophyInfo(context, 1, trophyId, details, &data) == 0, "trophy info was not returned");
|
||||
Require(data.trophy_id == trophyId, "trophy info is for another trophy");
|
||||
return data;
|
||||
}
|
||||
|
||||
static NpTrophy2GameData Game(int context, NpTrophy2GameDetails* details = nullptr) {
|
||||
NpTrophy2GameData data{};
|
||||
Require(sceNpTrophy2GetGameInfo(context, 1, details, &data) == 0, "game info was not returned");
|
||||
return data;
|
||||
}
|
||||
|
||||
static void ReportTrophyData(int context) {
|
||||
NpTrophy2GameDetails game{};
|
||||
const auto progress = Game(context, &game);
|
||||
Require(std::strcmp(game.title, "Synthetic Game") == 0 && game.num_groups == 2 && game.num_trophies == 8 && game.num_platinum == 1 && game.num_gold == 1 && game.num_silver == 1 && game.num_bronze == 5,
|
||||
"the game details are not the title's");
|
||||
Require(progress.unlocked_trophies == 0 && progress.progress_percentage == 0, "the game has unlocks before any event");
|
||||
|
||||
NpTrophy2Details details{};
|
||||
Trophy(context, 1, &details);
|
||||
Require(std::strcmp(details.name, "Ten") == 0 && std::strcmp(details.description, "Reach ten") == 0 && details.trophy_grade == 2 && details.group_id == -1 && !details.hidden &&
|
||||
details.target.type == 0 && details.target.value == 0,
|
||||
"trophy 1 does not report its name, grade and group");
|
||||
Trophy(context, 6, &details);
|
||||
Require(std::strcmp(details.name, "Secret") == 0 && std::strcmp(details.reward, "A hat") == 0 && details.trophy_grade == 4 && details.group_id == 1 && details.hidden && details.has_reward,
|
||||
"trophy 6 does not report its hidden flag, reward and group");
|
||||
Trophy(context, 0, &details);
|
||||
Require(details.trophy_grade == 1 && std::strcmp(details.name, "All of them") == 0, "the platinum is not reported");
|
||||
const auto steps = Trophy(context, 5, &details);
|
||||
Require(details.target.type == 1 && details.target.value == 5 && !steps.unlocked && steps.progress.type == 1 && steps.progress.value == 0, "the progressive trophy does not report its target");
|
||||
NpTrophy2Data unknown{};
|
||||
Require(ThrowsInvalidArgument([context, &unknown] { sceNpTrophy2GetTrophyInfo(context, 1, 99, nullptr, &unknown); }), "a trophy that is not in the package was reported");
|
||||
|
||||
std::vector<NpTrophy2Details> list(10);
|
||||
std::vector<NpTrophy2Data> listed(10);
|
||||
std::uint32_t count = 0;
|
||||
Require(sceNpTrophy2GetTrophyInfoArray(context, 1, 0, 10, list.data(), listed.data(), &count) == 0 && count == 8, "the trophy list does not hold every trophy");
|
||||
for (std::uint32_t i = 0; i < count; ++i) Require(list[i].trophy_id == static_cast<int>(i) && listed[i].trophy_id == static_cast<int>(i), "the trophy list is not in id order");
|
||||
Require(std::strcmp(list[5].name, "Five steps") == 0 && listed[5].progress.type == 1, "the trophy list does not carry names and progress");
|
||||
Require(sceNpTrophy2GetTrophyInfoArray(context, 1, 6, 5, list.data(), nullptr, &count) == 0 && count == 2 && list[0].trophy_id == 6 && list[1].trophy_id == 7,
|
||||
"the trophy list does not start at its offset");
|
||||
Require(sceNpTrophy2GetTrophyInfoArray(context, 1, 8, 5, nullptr, listed.data(), &count) == 0 && count == 0, "the trophy list goes past its end");
|
||||
|
||||
NpTrophy2GroupDetails group{};
|
||||
NpTrophy2GroupData groupData{};
|
||||
Require(sceNpTrophy2GetGroupInfo(context, 1, 1, &group, &groupData) == 0 && std::strcmp(group.title, "Expansion") == 0 && group.num_trophies == 2 && group.num_bronze == 2 && groupData.group_id == 1,
|
||||
"group 1 is not reported");
|
||||
NpTrophy2GroupData unknownGroup{};
|
||||
Require(ThrowsInvalidArgument([context, &unknownGroup] { sceNpTrophy2GetGroupInfo(context, 1, 7, nullptr, &unknownGroup); }), "a group that is not in the package was reported");
|
||||
std::vector<NpTrophy2GroupDetails> groups(4);
|
||||
Require(sceNpTrophy2GetGroupInfoArray(context, 1, 0, 4, groups.data(), nullptr, &count) == 0 && count == 2 && groups[0].group_id == -1 && std::strcmp(groups[0].title, "Synthetic Game") == 0 &&
|
||||
groups[0].num_trophies == 6 && groups[1].group_id == 1,
|
||||
"the group list does not hold the base game and group 1");
|
||||
}
|
||||
|
||||
static void ReportPlaceholders() {
|
||||
for (const std::uint32_t serviceLabel : {1u, 2u}) {
|
||||
const auto context = TrophyContext(serviceLabel);
|
||||
NpTrophy2GameDetails game{};
|
||||
Game(context, &game);
|
||||
NpTrophy2Details details{};
|
||||
Trophy(context, 12, &details);
|
||||
std::uint32_t count = 0;
|
||||
NpTrophy2Data listed{};
|
||||
Require(std::strcmp(game.title, "Game") == 0 && std::strcmp(details.name, "Trophy") == 0 &&
|
||||
sceNpTrophy2GetTrophyInfoArray(context, 1, 0, 10, nullptr, &listed, &count) == 0 && count == 1,
|
||||
"a missing or malformed trophy package does not leave the placeholders");
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
const auto previous = std::filesystem::current_path();
|
||||
const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-data-" + std::to_string(std::random_device{}()));
|
||||
std::filesystem::create_directories(root);
|
||||
std::filesystem::current_path(root);
|
||||
Write("app0/sce_sys/param.json", R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}}})");
|
||||
Write("app0/sce_sys/trophy2/trophy00.ucp", Ucp({{"tropconf.json", TropConf}, {"tropmeta_en-US.json", TropMeta}}));
|
||||
Write("app0/sce_sys/trophy2/trophy02.ucp", "not a package");
|
||||
|
||||
const auto context = TrophyContext(0);
|
||||
ReportTrophyData(context);
|
||||
ReportPlaceholders();
|
||||
|
||||
std::filesystem::current_path(previous);
|
||||
std::filesystem::remove_all(root);
|
||||
std::puts("Trophy data tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
|
||||
extern "C" {
|
||||
int APS5_VABI scePlayerSelectionDialogInitialize(void);
|
||||
int APS5_VABI scePlayerSelectionDialogOpen(const void* param);
|
||||
int APS5_VABI scePlayerSelectionDialogUpdateStatus(void);
|
||||
int APS5_VABI scePlayerSelectionDialogGetResult(void* result);
|
||||
int APS5_VABI scePlayerSelectionDialogTerminate(void);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
void Require(bool value) { if (!value) std::abort(); }
|
||||
|
||||
constexpr int kStatusFinished = 3;
|
||||
constexpr int kErrNotInitialized = static_cast<int>(0x80B80003);
|
||||
constexpr int kErrAlreadyInitialized = static_cast<int>(0x80B80004);
|
||||
constexpr int kErrNotFinished = static_cast<int>(0x80B80005);
|
||||
constexpr int kErrArgNull = static_cast<int>(0x80B8000D);
|
||||
constexpr int kResultUserCanceled = 1;
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::int64_t param = 0;
|
||||
std::int32_t result[2] = {-1, -1};
|
||||
|
||||
Require(scePlayerSelectionDialogTerminate() == kErrNotInitialized);
|
||||
Require(scePlayerSelectionDialogOpen(¶m) == kErrNotInitialized);
|
||||
Require(scePlayerSelectionDialogGetResult(result) == kErrNotInitialized);
|
||||
|
||||
Require(scePlayerSelectionDialogInitialize() == 0);
|
||||
Require(scePlayerSelectionDialogInitialize() == kErrAlreadyInitialized);
|
||||
|
||||
Require(scePlayerSelectionDialogOpen(nullptr) == kErrArgNull);
|
||||
Require(scePlayerSelectionDialogGetResult(result) == kErrNotFinished);
|
||||
|
||||
Require(scePlayerSelectionDialogOpen(¶m) == 0);
|
||||
Require(scePlayerSelectionDialogUpdateStatus() == kStatusFinished);
|
||||
|
||||
Require(scePlayerSelectionDialogGetResult(nullptr) == kErrArgNull);
|
||||
Require(scePlayerSelectionDialogGetResult(result) == 0);
|
||||
Require(result[0] == kResultUserCanceled);
|
||||
Require(result[1] == -1);
|
||||
|
||||
Require(scePlayerSelectionDialogTerminate() == 0);
|
||||
Require(scePlayerSelectionDialogTerminate() == kErrNotInitialized);
|
||||
}
|
||||
@@ -243,6 +243,51 @@ int main(int argc, char** argv) {
|
||||
if (mask & 1) Require(Protection(address) == PAGE_READONLY, "sender address rearmed");
|
||||
if (mask & 2) Require(Protection(length) == PAGE_READONLY, "sender length rearmed");
|
||||
}
|
||||
if (api == 5) {
|
||||
for (const bool readOnlyAddress : {true, false}) {
|
||||
auto* address = static_cast<unsigned char*>(outputMapping) + 16;
|
||||
auto* length = reinterpret_cast<std::uint32_t*>(static_cast<unsigned char*>(outputMapping) + page + 16);
|
||||
{
|
||||
const GuestArena::HostWrite addressDestination(address, 32);
|
||||
const GuestArena::HostWrite lengthDestination(length, sizeof(*length));
|
||||
Require(addressDestination.Open() && lengthDestination.Open(), "initialize refused sender outputs");
|
||||
std::memset(address, 0xa5, 32);
|
||||
*length = 32;
|
||||
}
|
||||
void* refused = static_cast<char*>(outputMapping) + (readOnlyAddress ? 0 : page);
|
||||
Require(sceKernelMprotect(refused, page, 1) == 0, "make one sender output read-only");
|
||||
send();
|
||||
waitForPacket();
|
||||
std::array<char, capacity> recovery{};
|
||||
Require(receive(recovery.data(), recovery.size(), 0, address, length) == static_cast<int>(0x8041010eu) && *error == 14,
|
||||
"read-only sender output fails with EFAULT");
|
||||
Require(*length == 32 && std::all_of(address, address + 32, [](unsigned char value) { return value == 0xa5; }),
|
||||
"refused sender outputs remain unchanged");
|
||||
Require(receive(recovery.data(), recovery.size(), 0, nullptr, nullptr) == sizeof(payload) &&
|
||||
std::memcmp(recovery.data(), payload, sizeof(payload)) == 0, "refused sender output preserves queued data");
|
||||
Require(sceKernelMprotect(refused, page, 3) == 0, "restore sender output permission");
|
||||
}
|
||||
auto* address = static_cast<unsigned char*>(outputMapping) + 16;
|
||||
Require(sceKernelMprotect(outputMapping, page, 1) == 0, "make an unused sender address read-only");
|
||||
std::uint32_t zero = 0;
|
||||
std::array<char, capacity> recovery{};
|
||||
send();
|
||||
waitForPacket();
|
||||
Require(receive(recovery.data(), recovery.size(), 0, address, &zero) == sizeof(payload) && zero == 0 &&
|
||||
std::memcmp(recovery.data(), payload, sizeof(payload)) == 0, "zero-capacity sender address needs no write access");
|
||||
Require(sceKernelMprotect(outputMapping, page, 3) == 0, "restore unused sender address permission");
|
||||
address = static_cast<unsigned char*>(outputMapping) + page - senderAddress.size();
|
||||
Require(sceKernelMprotect(static_cast<char*>(outputMapping) + page, page, 1) == 0,
|
||||
"make bytes beyond the sender address read-only");
|
||||
std::uint32_t oversized = 32;
|
||||
send();
|
||||
waitForPacket();
|
||||
Require(receive(recovery.data(), recovery.size(), 0, address, &oversized) == sizeof(payload) &&
|
||||
oversized == senderAddress.size() && std::memcmp(address, senderAddress.data(), oversized) == 0,
|
||||
"guard only the sender address bytes the receive writes");
|
||||
Require(sceKernelMprotect(static_cast<char*>(outputMapping) + page, page, 3) == 0,
|
||||
"restore bytes beyond the sender address");
|
||||
}
|
||||
Require(sceKernelMunmap(outputTwin, page * 2) == 0, "unmap the second sender output mapping");
|
||||
Require(sceKernelMunmap(outputMapping, page * 2) == 0, "unmap sender outputs");
|
||||
Require(sceKernelReleaseDirectMemory(outputPhysical, page * 2) == 0, "release sender output memory");
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
#endif
|
||||
extern "C" {
|
||||
FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*);
|
||||
void APS5_VABI clearerr_unlocked_nid_postfix(FileStream*);
|
||||
int APS5_VABI fileno_unlocked_nid_postfix(FileStream*);
|
||||
char* APS5_VABI fgetln_nid_postfix(FileStream*, std::size_t*);
|
||||
std::size_t APS5_VABI fread_nid_postfix(void*, std::size_t, std::size_t, FileStream*);
|
||||
std::size_t APS5_VABI fwrite_nid_postfix(const void*, std::size_t, std::size_t, FileStream*);
|
||||
FileStream* APS5_VABI freopen_nid_postfix(const char*, const char*, FileStream*);
|
||||
@@ -169,6 +172,22 @@ static void CheckUnlockedStreams() {
|
||||
Require(std::filesystem::remove(filename));
|
||||
}
|
||||
|
||||
static void CheckUnlockedQueries() {
|
||||
FileStream stream(std::tmpfile());
|
||||
Require(fputc_nid_postfix(0x7f, &stream) == 0x7f);
|
||||
std::rewind(stream.GetHandle());
|
||||
Require(fgetc_nid_postfix(&stream) == 0x7f && feof_nid_postfix(&stream) == 0);
|
||||
Require(fgetc_nid_postfix(&stream) == -1 && feof_nid_postfix(&stream) != 0);
|
||||
clearerr_unlocked_nid_postfix(&stream);
|
||||
Require(feof_nid_postfix(&stream) == 0 && ferror_nid_postfix(&stream) == 0);
|
||||
Require(fileno_unlocked_nid_postfix(&stream) == fileno_nid_postfix(&stream) && fileno_unlocked_nid_postfix(&stream) >= 0);
|
||||
Require(fileno_unlocked_nid_postfix(nullptr) == -1);
|
||||
bool halted = false;
|
||||
std::size_t length = 0;
|
||||
try { fgetln_nid_postfix(&stream, &length); } catch (const std::runtime_error&) { halted = true; }
|
||||
Require(halted);
|
||||
}
|
||||
|
||||
static void CheckTryLock() {
|
||||
FileStream stream(std::tmpfile());
|
||||
const auto tryFromOtherThread = [&] {
|
||||
@@ -843,5 +862,6 @@ int main(int argc, char** argv) {
|
||||
CheckUnlockedStreamLock(false);
|
||||
CheckUnlockedStreamLock(true);
|
||||
CheckTryLock();
|
||||
CheckUnlockedQueries();
|
||||
return CheckBinaryModes() ? 0 : 1;
|
||||
}
|
||||
@@ -52,7 +52,7 @@ static void CheckAlignment() {
|
||||
Require(aligned(blocks[0]));
|
||||
void* small = sceLibcMspaceMemalign_nid_postfix(arena, 16, 40);
|
||||
Require(aligned(small));
|
||||
void* moved = sceLibcMspaceReallocalign_nid_postfix(arena, nullptr, 40, 8);
|
||||
void* moved = sceLibcMspaceReallocalign_nid_postfix(arena, nullptr, 8, 40);
|
||||
Require(aligned(moved));
|
||||
void* posix = nullptr;
|
||||
Require(sceLibcMspacePosixMemalign_nid_postfix(arena, &posix, 8, 40) == 0 && aligned(posix));
|
||||
@@ -102,7 +102,7 @@ static void CheckMovedBlockKeepsUsableBytes() {
|
||||
void* other = sceLibcMspaceMalloc_nid_postfix(arena, 1);
|
||||
const auto otherUsable = fill(other);
|
||||
Require(sceLibcMspaceMalloc_nid_postfix(arena, 1) != nullptr);
|
||||
void* realigned = sceLibcMspaceReallocalign_nid_postfix(arena, other, 4096, 256);
|
||||
void* realigned = sceLibcMspaceReallocalign_nid_postfix(arena, other, 256, 4096);
|
||||
Require(realigned != nullptr && realigned != other && kept(realigned, otherUsable));
|
||||
Require(sceLibcMspaceDestroy_nid_postfix(arena) == 0);
|
||||
}
|
||||
@@ -126,7 +126,7 @@ int main() {
|
||||
Require(sceLibcMspaceRealloc_nid_postfix(arena, first, size) == nullptr);
|
||||
Require(*__error_nid_postfix() == 12);
|
||||
*__error_nid_postfix() = 0;
|
||||
Require(sceLibcMspaceReallocalign_nid_postfix(arena, first, size, 16) == nullptr);
|
||||
Require(sceLibcMspaceReallocalign_nid_postfix(arena, first, 16, size) == nullptr);
|
||||
Require(*__error_nid_postfix() == 12);
|
||||
Require(sceLibcMspaceMallocUsableSize_nid_postfix(first) == 128);
|
||||
for (int i = 0; i < 128; ++i) Require(first[i] == i);
|
||||
@@ -194,12 +194,17 @@ int main() {
|
||||
unsigned char* realigned = static_cast<unsigned char*>(sceLibcMspaceReallocalign_nid_postfix(arena, nullptr, 64, 64));
|
||||
Require(realigned && (reinterpret_cast<std::uintptr_t>(realigned) & 63) == 0);
|
||||
for (int i = 0; i < 64; ++i) realigned[i] = static_cast<unsigned char>(i + 1);
|
||||
unsigned char* regrown = static_cast<unsigned char*>(sceLibcMspaceReallocalign_nid_postfix(arena, realigned, 256, 64));
|
||||
unsigned char* regrown = static_cast<unsigned char*>(sceLibcMspaceReallocalign_nid_postfix(arena, realigned, 64, 256));
|
||||
Require(regrown && (reinterpret_cast<std::uintptr_t>(regrown) & 63) == 0);
|
||||
for (int i = 0; i < 64; ++i) Require(regrown[i] == static_cast<unsigned char>(i + 1));
|
||||
Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 16, 0) == nullptr);
|
||||
Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 16, 3) == nullptr);
|
||||
Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 0, 16) == nullptr);
|
||||
*__error_nid_postfix() = 0;
|
||||
Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 0, 16) == regrown);
|
||||
Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 3, 16) == regrown);
|
||||
Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, std::size_t{1} << 63, 16) == nullptr);
|
||||
Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 64, 0) == regrown && regrown[63] == 64);
|
||||
Require(*__error_nid_postfix() == 0);
|
||||
unsigned char* rounded = static_cast<unsigned char*>(sceLibcMspaceReallocalign_nid_postfix(arena, nullptr, 100, 0));
|
||||
Require(rounded && (reinterpret_cast<std::uintptr_t>(rounded) & 127) == 0);
|
||||
Require(sceLibcMspaceMallocStatsFast_nid_postfix(arena, &stats) == 0 && stats.currentInuseSize >= 100);
|
||||
sceLibcMspaceFree_nid_postfix(arena, memaligned);
|
||||
void* region = sceLibcMspaceMalloc_nid_postfix(arena, 8192);
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#ifndef CORE_LIBS_TESTS_PROCESSPARAMETERSFIXTURE_HPP
|
||||
#define CORE_LIBS_TESTS_PROCESSPARAMETERSFIXTURE_HPP
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#elif defined(__linux__)
|
||||
#include "prx/libc/include/specifics/linux/ElfTypes.hpp"
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace ProcessParametersFixture {
|
||||
namespace {
|
||||
|
||||
#ifdef _WIN32
|
||||
[[gnu::section(".proctst"), gnu::used]] alignas(4) std::uint32_t metadata[2]{};
|
||||
#endif
|
||||
|
||||
bool Install(std::byte* process, std::size_t size) {
|
||||
#ifdef _WIN32
|
||||
auto* image = reinterpret_cast<std::byte*>(GetModuleHandleW(nullptr));
|
||||
if (image == nullptr) return false;
|
||||
auto* dos = reinterpret_cast<IMAGE_DOS_HEADER*>(image);
|
||||
auto* nt = reinterpret_cast<IMAGE_NT_HEADERS64*>(image + dos->e_lfanew);
|
||||
auto* sections = IMAGE_FIRST_SECTION(nt);
|
||||
IMAGE_SECTION_HEADER* target = nullptr;
|
||||
for (unsigned index = 0; index < nt->FileHeader.NumberOfSections; ++index) {
|
||||
if (std::memcmp(sections[index].Name, ".proctst", 8) == 0) {
|
||||
target = §ions[index];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (target == nullptr) return false;
|
||||
metadata[0] = static_cast<std::uint32_t>(process - image);
|
||||
metadata[1] = static_cast<std::uint32_t>(size);
|
||||
DWORD oldProtect = 0;
|
||||
if (VirtualProtect(target, sizeof(*target), PAGE_READWRITE, &oldProtect) == 0) return false;
|
||||
std::memcpy(target->Name, ".procpar", 8);
|
||||
target->Misc.VirtualSize = sizeof(metadata);
|
||||
DWORD restored = 0;
|
||||
VirtualProtect(target, sizeof(*target), oldProtect, &restored);
|
||||
return true;
|
||||
#elif defined(__linux__)
|
||||
struct Request {
|
||||
std::byte* process;
|
||||
std::size_t size;
|
||||
bool installed;
|
||||
} request{process, size, false};
|
||||
dl_iterate_phdr([](dl_phdr_info* image, std::size_t, void* data) {
|
||||
auto& request = *static_cast<Request*>(data);
|
||||
if (image->dlpi_name != nullptr && image->dlpi_name[0] != '\0') return 0;
|
||||
for (std::uint16_t index = 0; index < image->dlpi_phnum; ++index) {
|
||||
const auto& header = image->dlpi_phdr[index];
|
||||
if (header.p_type == PT_LOAD || header.p_type == PT_GNU_EH_FRAME || header.p_type < 0x60000000u) continue;
|
||||
auto* mutableHeader = const_cast<Elf64_Phdr*>(&header);
|
||||
const auto pageSize = static_cast<std::uintptr_t>(sysconf(_SC_PAGESIZE));
|
||||
const auto pageStart = reinterpret_cast<std::uintptr_t>(mutableHeader) & ~(pageSize - 1);
|
||||
const auto pageEnd = (reinterpret_cast<std::uintptr_t>(mutableHeader + 1) + pageSize - 1) & ~(pageSize - 1);
|
||||
int protection = PROT_READ;
|
||||
for (std::uint16_t loadIndex = 0; loadIndex < image->dlpi_phnum; ++loadIndex) {
|
||||
const auto& load = image->dlpi_phdr[loadIndex];
|
||||
const auto start = image->dlpi_addr + load.p_vaddr;
|
||||
if (load.p_type == PT_LOAD && pageStart >= (start & ~(pageSize - 1)) && pageStart < start + load.p_memsz && (load.p_flags & PF_X)) protection |= PROT_EXEC;
|
||||
}
|
||||
if (mprotect(reinterpret_cast<void*>(pageStart), pageEnd - pageStart, protection | PROT_WRITE) != 0) return 1;
|
||||
mutableHeader->p_type = 0x61000001;
|
||||
mutableHeader->p_vaddr = reinterpret_cast<std::uintptr_t>(request.process) - image->dlpi_addr;
|
||||
mutableHeader->p_paddr = mutableHeader->p_vaddr;
|
||||
mutableHeader->p_filesz = request.size;
|
||||
mutableHeader->p_memsz = request.size;
|
||||
request.installed = mprotect(reinterpret_cast<void*>(pageStart), pageEnd - pageStart, protection) == 0;
|
||||
return 1;
|
||||
}
|
||||
return 1;
|
||||
}, &request);
|
||||
return request.installed;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -57,7 +57,7 @@ static void TestTypeInfoVtables() {
|
||||
if (catchType(&other, &single, &object, 0)) throw std::runtime_error("unrelated RTTI catch succeeded");
|
||||
}
|
||||
|
||||
extern "C" void NotImplemented_nid_no_patch(const char*);
|
||||
extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char*);
|
||||
|
||||
static void RequireHandle(bool value) {
|
||||
if (!value) throw std::runtime_error("child process setup failed");
|
||||
|
||||
@@ -6,6 +6,6 @@ extern "C" int* APS5_VABI __error_nid_postfix() {
|
||||
return &error;
|
||||
}
|
||||
|
||||
extern "C" void NotImplemented_nid_no_patch(const char* name) {
|
||||
extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* name) {
|
||||
throw std::runtime_error(name);
|
||||
}
|
||||
@@ -735,6 +735,12 @@ if(BUILD_TESTING)
|
||||
)
|
||||
add_test(NAME reloc_table_bounds COMMAND reloc_table_bounds_tests $<TARGET_FILE:relinker>)
|
||||
endif()
|
||||
|
||||
set(relinkerTestTemp ${CMAKE_CURRENT_BINARY_DIR}/test-temp)
|
||||
file(MAKE_DIRECTORY ${relinkerTestTemp})
|
||||
get_property(relinkerTests DIRECTORY PROPERTY TESTS)
|
||||
set_property(TEST ${relinkerTests} APPEND PROPERTY ENVIRONMENT_MODIFICATION
|
||||
"TMPDIR=set:${relinkerTestTemp};TEMP=set:${relinkerTestTemp};TMP=set:${relinkerTestTemp}")
|
||||
endif()
|
||||
|
||||
if(BUILD_TESTING AND Python3_Interpreter_FOUND)
|
||||
|
||||
@@ -265,6 +265,7 @@ private:
|
||||
append(key, value.vertexSubgroups);
|
||||
append(key, value.float16DenormPreserve);
|
||||
append(key, value.spannedWaves);
|
||||
append(key, value.robustBufferAccess2);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+3
-1
@@ -44,9 +44,11 @@ struct TableColumn {
|
||||
std::uint32_t indirectImmediate = 0;
|
||||
std::uint32_t foldedAddend = 0;
|
||||
std::uint32_t foldedCarry = 0;
|
||||
bool selected = false;
|
||||
std::uint32_t selection = 0;
|
||||
|
||||
bool operator==(const TableColumn& other) const {
|
||||
return heapSource == other.heapSource && stride == other.stride && addend == other.addend && offset == other.offset && dwordCount == other.dwordCount && sampler == other.sampler && address == other.address && indirect == other.indirect && indirectOffset == other.indirectOffset && indirectImmediate == other.indirectImmediate && foldedAddend == other.foldedAddend && foldedCarry == other.foldedCarry;
|
||||
return heapSource == other.heapSource && stride == other.stride && addend == other.addend && offset == other.offset && dwordCount == other.dwordCount && sampler == other.sampler && address == other.address && indirect == other.indirect && indirectOffset == other.indirectOffset && indirectImmediate == other.indirectImmediate && foldedAddend == other.foldedAddend && foldedCarry == other.foldedCarry && selected == other.selected && selection == other.selection;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -65,10 +65,30 @@ void IrValue::ReplaceAllUsesWith(IrValue* replacement) {
|
||||
if (replacement == this) {
|
||||
return;
|
||||
}
|
||||
while (!operandUses.empty()) {
|
||||
const IrUse use = operandUses.back();
|
||||
use.user->ReplaceArgument(use.operand, replacement);
|
||||
if (replacement != nullptr) {
|
||||
const auto reserveAdditional = [count = operandUses.size()](auto& list) {
|
||||
const auto maximum = list.max_size();
|
||||
if (count > maximum - list.size()) {
|
||||
throw std::length_error("IrValue::ReplaceAllUsesWith use list is too large");
|
||||
}
|
||||
const auto required = list.size() + count;
|
||||
if (required > list.capacity()) {
|
||||
const auto grown = list.capacity() > maximum / 2 ? maximum : list.capacity() * 2;
|
||||
list.reserve(std::max(required, grown));
|
||||
}
|
||||
};
|
||||
reserveAdditional(replacement->uses);
|
||||
reserveAdditional(replacement->operandUses);
|
||||
}
|
||||
for (auto use = operandUses.rbegin(); use != operandUses.rend(); ++use) {
|
||||
use->user->arguments[use->operand] = replacement;
|
||||
if (replacement != nullptr) {
|
||||
replacement->uses.push_back(use->user);
|
||||
replacement->operandUses.push_back(*use);
|
||||
}
|
||||
}
|
||||
uses.clear();
|
||||
operandUses.clear();
|
||||
}
|
||||
|
||||
void IrValue::SetImmediateU32(std::uint32_t value) {
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
#include "IntermediateRepresentation/IrBlock.hpp"
|
||||
#include <bit>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
using namespace ShaderRecompiler;
|
||||
|
||||
namespace IrRewriteAllocation {
|
||||
extern std::ptrdiff_t failureAfter;
|
||||
extern std::size_t allocations;
|
||||
extern bool countAllocations;
|
||||
}
|
||||
using namespace IrRewriteAllocation;
|
||||
|
||||
void Require(bool condition) {
|
||||
if (!condition) throw std::runtime_error("IR use rewrite assertion failed");
|
||||
}
|
||||
|
||||
struct Graph {
|
||||
IrBlock block{1};
|
||||
std::vector<std::unique_ptr<IrValue>> values;
|
||||
|
||||
explicit Graph(std::size_t count) {
|
||||
for (std::size_t index = 0; index < count; ++index) {
|
||||
auto value = std::make_unique<IrValue>(IrOpcode::IAdd32, IrType::U32, static_cast<std::uint32_t>(index));
|
||||
value->SetFlags(static_cast<std::uint64_t>(index * 17));
|
||||
block.AppendInstruction(value.get());
|
||||
values.push_back(std::move(value));
|
||||
}
|
||||
}
|
||||
|
||||
IrValue* Value(std::size_t index) const { return values.at(index).get(); }
|
||||
};
|
||||
|
||||
void ReferenceReplace(IrValue* source, IrValue* replacement) {
|
||||
if (source == replacement) return;
|
||||
while (!source->OperandUses().empty()) {
|
||||
const IrUse use = source->OperandUses().back();
|
||||
use.user->ReplaceArgument(use.operand, replacement);
|
||||
}
|
||||
}
|
||||
|
||||
void EqualGraphs(const Graph& first, const Graph& second) {
|
||||
Require(first.values.size() == second.values.size());
|
||||
for (std::size_t index = 0; index < first.values.size(); ++index) {
|
||||
const auto* left = first.Value(index);
|
||||
const auto* right = second.Value(index);
|
||||
Require(left->Opcode() == right->Opcode() && left->Type() == right->Type());
|
||||
Require(left->Id() == right->Id() && left->Flags<std::uint64_t>() == right->Flags<std::uint64_t>());
|
||||
Require((left->Parent() == nullptr) == (right->Parent() == nullptr));
|
||||
if (left->Parent()) Require(left->Parent()->Id() == right->Parent()->Id());
|
||||
Require(left->ArgumentCount() == right->ArgumentCount());
|
||||
for (std::size_t operand = 0; operand < left->ArgumentCount(); ++operand) {
|
||||
const auto* a = left->Argument(operand);
|
||||
const auto* b = right->Argument(operand);
|
||||
Require((a == nullptr) == (b == nullptr));
|
||||
if (a) Require(a->Id() == b->Id());
|
||||
}
|
||||
Require(left->Uses().size() == right->Uses().size());
|
||||
for (std::size_t use = 0; use < left->Uses().size(); ++use) {
|
||||
Require(left->Uses()[use]->Id() == right->Uses()[use]->Id());
|
||||
}
|
||||
Require(left->OperandUses().size() == right->OperandUses().size());
|
||||
for (std::size_t use = 0; use < left->OperandUses().size(); ++use) {
|
||||
const auto& a = left->OperandUses()[use];
|
||||
const auto& b = right->OperandUses()[use];
|
||||
Require(a.user->Id() == b.user->Id() && a.operand == b.operand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CheckSimpleReplacement() {
|
||||
Graph graph(5);
|
||||
auto* source = graph.Value(0);
|
||||
auto* target = graph.Value(1);
|
||||
auto* first = graph.Value(2);
|
||||
auto* second = graph.Value(3);
|
||||
auto* existing = graph.Value(4);
|
||||
existing->AddArgument(target);
|
||||
first->AddArgument(source);
|
||||
second->AddArgument(source);
|
||||
first->AddArgument(source);
|
||||
first->AddArgument(target);
|
||||
source->ReplaceAllUsesWith(target);
|
||||
Require(!source->HasUses() && source->OperandUses().empty());
|
||||
Require(first->Argument(0) == target && first->Argument(1) == target && first->Argument(2) == target);
|
||||
Require(second->Argument(0) == target);
|
||||
const std::vector<IrUse> expected{{existing, 0}, {first, 2}, {first, 1}, {second, 0}, {first, 0}};
|
||||
Require(target->OperandUses() == expected);
|
||||
Require(target->Uses() == std::vector<IrValue*>({existing, first, first, second, first}));
|
||||
first->ReplaceArgument(1, source);
|
||||
Require(source->UseCount() == 1);
|
||||
source->ReplaceAllUsesWith(nullptr);
|
||||
Require(first->Argument(1) == nullptr && target->UseCount() == 4);
|
||||
Require(source->Opcode() == IrOpcode::IAdd32 && source->Parent() == &graph.block);
|
||||
}
|
||||
|
||||
void CheckSelfAndEmpty() {
|
||||
Graph graph(3);
|
||||
graph.Value(2)->AddArgument(graph.Value(0));
|
||||
graph.Value(0)->ReplaceAllUsesWith(graph.Value(0));
|
||||
Require(graph.Value(0)->UseCount() == 1 && graph.Value(2)->Argument(0) == graph.Value(0));
|
||||
graph.Value(1)->ReplaceAllUsesWith(graph.Value(0));
|
||||
Require(graph.Value(0)->UseCount() == 1);
|
||||
graph.Value(1)->ReplaceAllUsesWith(nullptr);
|
||||
Require(!graph.Value(1)->HasUses());
|
||||
}
|
||||
|
||||
void CheckPhiAndIdentity() {
|
||||
Graph graph(4);
|
||||
auto* source = graph.Value(0);
|
||||
auto* target = graph.Value(1);
|
||||
auto* phi = graph.Value(2);
|
||||
phi->ReplaceOpcode(IrOpcode::Phi);
|
||||
phi->AddPhiOperand(&graph.block, source);
|
||||
phi->AddPhiOperand(&graph.block, source);
|
||||
graph.Value(3)->ReplaceOpcode(IrOpcode::Identity);
|
||||
graph.Value(3)->AddArgument(source);
|
||||
source->ReplaceUsesWith(target, false);
|
||||
Require(source->Opcode() == IrOpcode::Unreachable);
|
||||
Require(phi->Argument(0) == target && phi->Argument(1) == target);
|
||||
Require(phi->PhiBlockCount() == 2 && phi->PhiBlock(0) == &graph.block && phi->PhiBlock(1) == &graph.block);
|
||||
Require(graph.Value(3)->Resolve() == target);
|
||||
Require(target->UseCount() == 3);
|
||||
}
|
||||
|
||||
void CheckAllocationFailure() {
|
||||
for (std::ptrdiff_t failure = 0; failure < 2; ++failure) {
|
||||
Graph graph(12);
|
||||
for (std::size_t index = 2; index < 12; ++index) {
|
||||
graph.Value(index)->AddArgument(graph.Value(0));
|
||||
}
|
||||
bool caught = false;
|
||||
failureAfter = failure;
|
||||
try {
|
||||
graph.Value(0)->ReplaceAllUsesWith(graph.Value(1));
|
||||
} catch (const std::bad_alloc&) {
|
||||
caught = true;
|
||||
} catch (...) {
|
||||
failureAfter = -1;
|
||||
throw;
|
||||
}
|
||||
failureAfter = -1;
|
||||
Require(caught && graph.Value(0)->UseCount() == 10);
|
||||
Require(graph.Value(0)->OperandUses().size() == 10);
|
||||
Require(!graph.Value(1)->HasUses() && graph.Value(1)->OperandUses().empty());
|
||||
for (std::size_t index = 2; index < 12; ++index) {
|
||||
Require(graph.Value(index)->Argument(0) == graph.Value(0));
|
||||
}
|
||||
graph.Value(0)->ReplaceAllUsesWith(graph.Value(1));
|
||||
Require(graph.Value(1)->UseCount() == 10);
|
||||
}
|
||||
}
|
||||
|
||||
void CheckGeometricGrowth() {
|
||||
constexpr std::size_t count = 4096;
|
||||
Graph graph(count + 2);
|
||||
std::vector<std::unique_ptr<IrValue>> sources;
|
||||
for (std::size_t index = 2; index < count + 2; ++index) {
|
||||
sources.push_back(std::make_unique<IrValue>(IrOpcode::UndefU32, IrType::U32, index + count + 2));
|
||||
graph.Value(index)->AddArgument(sources.back().get());
|
||||
}
|
||||
allocations = 0;
|
||||
countAllocations = true;
|
||||
for (auto& source : sources) source->ReplaceAllUsesWith(graph.Value(1));
|
||||
countAllocations = false;
|
||||
Require(allocations <= 2 * (std::bit_width(count) + 1));
|
||||
Require(graph.Value(1)->UseCount() == count);
|
||||
for (std::size_t index = 2; index < count + 2; ++index) Require(graph.Value(index)->Argument(0) == graph.Value(1));
|
||||
}
|
||||
|
||||
void CheckLargeFanOut() {
|
||||
constexpr std::size_t count = 100000;
|
||||
Graph graph(count + 2);
|
||||
for (std::size_t index = 2; index < count + 2; ++index) {
|
||||
graph.Value(index)->AddArgument(graph.Value(0));
|
||||
graph.Value(index)->AddArgument(graph.Value(0));
|
||||
}
|
||||
graph.Value(0)->ReplaceAllUsesWith(graph.Value(1));
|
||||
Require(graph.Value(1)->UseCount() == count * 2 && !graph.Value(0)->HasUses());
|
||||
for (std::size_t index = 2; index < count + 2; ++index) {
|
||||
Require(graph.Value(index)->Argument(0) == graph.Value(1) && graph.Value(index)->Argument(1) == graph.Value(1));
|
||||
}
|
||||
}
|
||||
|
||||
void CheckDifferentialPrograms() {
|
||||
std::mt19937 random(0x4f23a771);
|
||||
constexpr std::size_t count = 32;
|
||||
for (std::size_t trial = 0; trial < 2000; ++trial) {
|
||||
Graph actual(count), reference(count);
|
||||
for (std::size_t index = 0; index < count; ++index) {
|
||||
const auto operands = random() % 5;
|
||||
for (std::size_t operand = 0; operand < operands; ++operand) {
|
||||
const auto argument = random() % (count + 1);
|
||||
actual.Value(index)->AddArgument(argument == count ? nullptr : actual.Value(argument));
|
||||
reference.Value(index)->AddArgument(argument == count ? nullptr : reference.Value(argument));
|
||||
}
|
||||
}
|
||||
for (std::size_t step = 0; step < 24; ++step) {
|
||||
const auto source = random() % count;
|
||||
const auto replacement = random() % (count + 1);
|
||||
if (random() % 4 == 0 && actual.Value(source)->ArgumentCount() > 0) {
|
||||
const auto operand = random() % actual.Value(source)->ArgumentCount();
|
||||
actual.Value(source)->ReplaceArgument(operand, replacement == count ? nullptr : actual.Value(replacement));
|
||||
reference.Value(source)->ReplaceArgument(operand, replacement == count ? nullptr : reference.Value(replacement));
|
||||
} else {
|
||||
actual.Value(source)->ReplaceAllUsesWith(replacement == count ? nullptr : actual.Value(replacement));
|
||||
ReferenceReplace(reference.Value(source), replacement == count ? nullptr : reference.Value(replacement));
|
||||
}
|
||||
EqualGraphs(actual, reference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
const char* current = "setup";
|
||||
try {
|
||||
const std::pair<const char*, void (*)()> cases[] = {
|
||||
{"ordered duplicate operands", CheckSimpleReplacement},
|
||||
{"self and empty replacement", CheckSelfAndEmpty},
|
||||
{"phi and identity rewriting", CheckPhiAndIdentity},
|
||||
{"allocation failure integrity", CheckAllocationFailure},
|
||||
{"geometric use-list growth", CheckGeometricGrowth},
|
||||
{"large fan-out", CheckLargeFanOut},
|
||||
{"differential use graphs", CheckDifferentialPrograms}
|
||||
};
|
||||
for (const auto& [name, test] : cases) {
|
||||
current = name;
|
||||
test();
|
||||
}
|
||||
std::puts("7 IR rewrite suites passed; 2000 differential use graphs and 48000 mutation comparisons");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
failureAfter = -1;
|
||||
countAllocations = false;
|
||||
std::fprintf(stderr, "%s: %s\n", current, error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <new>
|
||||
|
||||
namespace IrRewriteAllocation {
|
||||
std::ptrdiff_t failureAfter = -1;
|
||||
std::size_t allocations = 0;
|
||||
bool countAllocations = false;
|
||||
}
|
||||
|
||||
void* operator new(std::size_t size) {
|
||||
if (IrRewriteAllocation::failureAfter == 0) throw std::bad_alloc();
|
||||
if (IrRewriteAllocation::failureAfter > 0) --IrRewriteAllocation::failureAfter;
|
||||
if (IrRewriteAllocation::countAllocations) ++IrRewriteAllocation::allocations;
|
||||
if (void* memory = std::malloc(size == 0 ? 1 : size)) return memory;
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
|
||||
void operator delete(void* memory) noexcept { std::free(memory); }
|
||||
void operator delete(void* memory, std::size_t) noexcept { std::free(memory); }
|
||||
@@ -201,17 +201,25 @@ bool replaceBinaryIdentity(IrValue& inst, IrType type, std::uint64_t identity) {
|
||||
|
||||
bool foldSelect(IrValue& inst) {
|
||||
auto& condition = resolveArg(inst, 0);
|
||||
auto& trueValue = resolveArg(inst, 1);
|
||||
auto& falseValue = resolveArg(inst, 2);
|
||||
if (isImmediate(condition, IrType::U1)) {
|
||||
replaceWith(inst, condition.ImmediateBool() ? trueValue : falseValue);
|
||||
replaceWith(inst, resolveArg(inst, condition.ImmediateBool() ? 1 : 2));
|
||||
return true;
|
||||
}
|
||||
bool folded = false;
|
||||
for (std::size_t arm = 1; arm <= 2; arm++) {
|
||||
auto& nested = resolveArg(inst, arm);
|
||||
if (nested.Opcode() == inst.Opcode() && nested.ArgumentCount() == 3u && &resolveArg(nested, 0) == &condition) {
|
||||
inst.ReplaceArgument(arm, &resolveArg(nested, arm));
|
||||
folded = true;
|
||||
}
|
||||
}
|
||||
auto& trueValue = resolveArg(inst, 1);
|
||||
auto& falseValue = resolveArg(inst, 2);
|
||||
if (trueValue == falseValue) {
|
||||
replaceWith(inst, trueValue);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return folded;
|
||||
}
|
||||
|
||||
bool foldPhi(IrValue& inst) {
|
||||
|
||||
@@ -19,6 +19,58 @@ const IrValue* maskedOperand(const IrValue* value, IrOpcode opcode, std::uint32_
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const IrValue* rightOperand(const IrValue* value, IrOpcode opcode, std::uint32_t bits) {
|
||||
if (value->Opcode() != opcode || value->ArgumentCount() != 2u) return nullptr;
|
||||
return isImmediate(value->Argument(1)->Resolve(), bits) ? value->Argument(0)->Resolve() : nullptr;
|
||||
}
|
||||
|
||||
const IrValue* bitsOf(const IrValue* value) {
|
||||
value = value->Resolve();
|
||||
while ((value->Opcode() == IrOpcode::BitCastU32F32 || value->Opcode() == IrOpcode::BitCastF32U32) && value->ArgumentCount() == 1u) value = value->Argument(0)->Resolve();
|
||||
return value;
|
||||
}
|
||||
|
||||
bool testsForNan(const IrValue* condition, const IrValue* bits) {
|
||||
condition = condition->Resolve();
|
||||
if (condition->Opcode() == IrOpcode::FPIsNan32 && condition->ArgumentCount() == 1u) return bitsOf(condition->Argument(0)) == bits;
|
||||
const IrValue* magnitude = rightOperand(condition, IrOpcode::UGreaterThan32, 0x7f800000u);
|
||||
if (magnitude != nullptr) magnitude = maskedOperand(magnitude, IrOpcode::BitwiseAnd32, 0x7fffffffu);
|
||||
return magnitude != nullptr && bitsOf(magnitude) == bits;
|
||||
}
|
||||
|
||||
bool takenOnlyAsNan(const IrValue* condition, const IrValue* arm) {
|
||||
const IrValue* bits = bitsOf(arm);
|
||||
if (const IrValue* quieted = maskedOperand(bits, IrOpcode::BitwiseOr32, 0x00400000u)) bits = bitsOf(quieted);
|
||||
return testsForNan(condition, bits);
|
||||
}
|
||||
|
||||
const IrValue* tinyGuardedProduct(const IrValue* condition, const IrValue* arm) {
|
||||
const IrValue* product = arm->Resolve();
|
||||
if (product->Opcode() != IrOpcode::FPMul32 || product->ArgumentCount() != 2u) return nullptr;
|
||||
const IrValue* offset = rightOperand(condition->Resolve(), IrOpcode::ULessThan32, 0x207fffffu);
|
||||
const IrValue* magnitude = offset != nullptr ? rightOperand(offset, IrOpcode::ISub32, 1u) : nullptr;
|
||||
const IrValue* scaled = magnitude != nullptr ? maskedOperand(magnitude, IrOpcode::BitwiseAnd32, 0x7fffffffu) : nullptr;
|
||||
if (scaled == nullptr) return nullptr;
|
||||
scaled = bitsOf(scaled);
|
||||
if (scaled->Opcode() != IrOpcode::FPMul32 || scaled->ArgumentCount() != 2u) return nullptr;
|
||||
const IrValue* lhs = product->Argument(0)->Resolve();
|
||||
const IrValue* rhs = product->Argument(1)->Resolve();
|
||||
for (std::size_t index = 0; index < 2u; ++index) {
|
||||
const IrValue* factor = maskedOperand(scaled->Argument(index)->Resolve(), IrOpcode::FPMul32, 0x5f800000u);
|
||||
const IrValue* other = scaled->Argument(1u - index)->Resolve();
|
||||
if (factor != nullptr && ((factor == lhs && other == rhs) || (factor == rhs && other == lhs))) return product;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool clearsSignificand(const IrValue& value) {
|
||||
for (std::size_t index = 0; index < value.ArgumentCount(); ++index) {
|
||||
const IrValue* mask = value.Argument(index)->Resolve();
|
||||
if (mask->HasImmediate() && (mask->ImmediateU32() & 0x007fffffu) == 0u) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
IrValue* flushedSource(const IrValue& value) {
|
||||
if (value.Opcode() != IrOpcode::SelectU32 || value.ArgumentCount() != 3u) return nullptr;
|
||||
IrValue* source = value.Argument(2)->Resolve();
|
||||
@@ -65,10 +117,14 @@ private:
|
||||
return false;
|
||||
case IrOpcode::BitwiseAnd32:
|
||||
if (const IrValue* operand = maskedOperand(&value, IrOpcode::BitwiseAnd32, 0x7fffffffu)) return Contains(operand);
|
||||
return false;
|
||||
return value.ArgumentCount() == 2u && clearsSignificand(value);
|
||||
case IrOpcode::SelectU32:
|
||||
case IrOpcode::SelectF32:
|
||||
return Contains(value.Argument(1)) && Contains(value.Argument(2));
|
||||
case IrOpcode::SelectF32: {
|
||||
const IrValue* condition = value.Argument(0);
|
||||
const IrValue* product = tinyGuardedProduct(condition, value.Argument(2));
|
||||
const bool taken = takenOnlyAsNan(condition, value.Argument(1)) || Contains(value.Argument(1));
|
||||
return taken && (product != nullptr ? Contains(product->Argument(0)) && Contains(product->Argument(1)) : Contains(value.Argument(2)));
|
||||
}
|
||||
case IrOpcode::FPMin32:
|
||||
case IrOpcode::FPMax32:
|
||||
case IrOpcode::FPMinTri32:
|
||||
|
||||
@@ -313,6 +313,9 @@ bool TableTraced() {
|
||||
|
||||
constexpr std::size_t PoisonReasonCount = static_cast<std::size_t>(PoisonReason::Count);
|
||||
|
||||
enum class NarrowSkip : std::size_t { Value, NotBuffer, KeyScan, Pending, Unmapped, Count };
|
||||
constexpr std::array<const char*, static_cast<std::size_t>(NarrowSkip::Count)> NarrowSkipNames{"value", "not-buffer", "key-scan", "pending-write", "unmapped"};
|
||||
|
||||
struct TableCounters {
|
||||
std::atomic<std::uint64_t> snapshots{0};
|
||||
std::atomic<std::uint64_t> tables{0};
|
||||
@@ -320,6 +323,9 @@ struct TableCounters {
|
||||
std::array<std::atomic<std::uint64_t>, PoisonReasonCount> poison{};
|
||||
std::atomic<std::uint64_t> narrowed{0};
|
||||
std::atomic<std::uint64_t> narrowSkipped{0};
|
||||
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(NarrowSkip::Count)> narrowSkips{};
|
||||
std::atomic<std::uint64_t> narrowPositions{0};
|
||||
std::atomic<std::uint64_t> selected{0};
|
||||
std::atomic<std::uint64_t> nanoseconds{0};
|
||||
std::atomic<long long> lastReport{0};
|
||||
};
|
||||
@@ -347,9 +353,16 @@ void reportTables() {
|
||||
if (count == 0) continue;
|
||||
poison += " " + std::string(ImageTableAbi::PoisonReasonName(static_cast<PoisonReason>(reason))) + "=" + std::to_string(count);
|
||||
}
|
||||
std::fprintf(stderr, "[image-table] (10 s): %llu snapshots, %llu tables, %llu keys, structural poison:%s, narrowing %llu applied / %llu skipped, %.1f ms\n",
|
||||
static_cast<unsigned long long>(snapshots), static_cast<unsigned long long>(counters.tables.exchange(0, std::memory_order_relaxed)), static_cast<unsigned long long>(counters.keys.exchange(0, std::memory_order_relaxed)), poison.empty() ? " none" : poison.c_str(),
|
||||
static_cast<unsigned long long>(counters.narrowed.exchange(0, std::memory_order_relaxed)), static_cast<unsigned long long>(counters.narrowSkipped.exchange(0, std::memory_order_relaxed)), static_cast<double>(counters.nanoseconds.exchange(0, std::memory_order_relaxed)) / 1e6);
|
||||
std::string skips;
|
||||
for (std::size_t reason = 0; reason < counters.narrowSkips.size(); reason++) {
|
||||
const auto count = counters.narrowSkips[reason].exchange(0, std::memory_order_relaxed);
|
||||
if (count == 0) continue;
|
||||
skips += " " + std::string(NarrowSkipNames[reason]) + "=" + std::to_string(count);
|
||||
if (reason == static_cast<std::size_t>(NarrowSkip::KeyScan)) skips += " (up to " + std::to_string(counters.narrowPositions.exchange(0, std::memory_order_relaxed)) + " positions, APS5_IMAGE_TABLE_KEY_SCAN " + std::to_string(TableKeyScanLimit()) + ")";
|
||||
}
|
||||
std::fprintf(stderr, "[image-table] (10 s): %llu snapshots, %llu tables, %llu keys, %llu selected records, structural poison:%s, narrowing %llu applied / %llu skipped%s%s, %.1f ms\n",
|
||||
static_cast<unsigned long long>(snapshots), static_cast<unsigned long long>(counters.tables.exchange(0, std::memory_order_relaxed)), static_cast<unsigned long long>(counters.keys.exchange(0, std::memory_order_relaxed)), static_cast<unsigned long long>(counters.selected.exchange(0, std::memory_order_relaxed)), poison.empty() ? " none" : poison.c_str(),
|
||||
static_cast<unsigned long long>(counters.narrowed.exchange(0, std::memory_order_relaxed)), static_cast<unsigned long long>(counters.narrowSkipped.exchange(0, std::memory_order_relaxed)), skips.empty() ? "" : ":", skips.c_str(), static_cast<double>(counters.nanoseconds.exchange(0, std::memory_order_relaxed)) / 1e6);
|
||||
}
|
||||
|
||||
enum class RecordState : std::uint8_t { OutsideDomain, Read, Unmapped, Carried };
|
||||
@@ -471,8 +484,10 @@ private:
|
||||
}
|
||||
|
||||
void Narrow(const TableColumn& column, ColumnRecords& records) {
|
||||
const auto skip = [&] {
|
||||
tableCounters().narrowSkipped.fetch_add(1, std::memory_order_relaxed);
|
||||
const auto skip = [&](NarrowSkip reason) {
|
||||
auto& counters = tableCounters();
|
||||
counters.narrowSkipped.fetch_add(1, std::memory_order_relaxed);
|
||||
counters.narrowSkips[static_cast<std::size_t>(reason)].fetch_add(1, std::memory_order_relaxed);
|
||||
};
|
||||
std::vector<std::uint32_t> values{0u};
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> scanned;
|
||||
@@ -482,7 +497,7 @@ private:
|
||||
try {
|
||||
walker.EvaluateDescriptorSource(plan, domain.source, runtime, key);
|
||||
} catch (const std::exception&) {
|
||||
skip();
|
||||
skip(NarrowSkip::Value);
|
||||
return;
|
||||
}
|
||||
values.push_back(key.dwords[0]);
|
||||
@@ -490,7 +505,7 @@ private:
|
||||
}
|
||||
const auto keyBuffer = EvaluateBuffer(domain.source);
|
||||
if (keyBuffer.Type() != 0u) {
|
||||
skip();
|
||||
skip(NarrowSkip::NotBuffer);
|
||||
return;
|
||||
}
|
||||
const auto base = keyBuffer.Base48();
|
||||
@@ -498,18 +513,21 @@ private:
|
||||
const std::uint64_t first = domain.offset & ~3u;
|
||||
const std::uint64_t positions = size >= first + 4u ? (size - first - 4u) / domain.stride + 1u : 0u;
|
||||
if (positions > TableKeyScanLimit() || first > 0xffffffffull) {
|
||||
skip();
|
||||
auto& widest = tableCounters().narrowPositions;
|
||||
auto seen = widest.load(std::memory_order_relaxed);
|
||||
while (seen < positions && !widest.compare_exchange_weak(seen, positions, std::memory_order_relaxed)) {}
|
||||
skip(NarrowSkip::KeyScan);
|
||||
return;
|
||||
}
|
||||
if (positions != 0u && runtime.pendingWrite != nullptr && runtime.pendingWrite(runtime.userContext, base + first, size - first)) {
|
||||
skip();
|
||||
skip(NarrowSkip::Pending);
|
||||
return;
|
||||
}
|
||||
if (runtime.readWords != nullptr && positions != 0u && domain.stride % 4u == 0u && domain.stride <= 64u) {
|
||||
const auto words = static_cast<std::size_t>(((positions - 1u) * domain.stride) / 4u + 1u);
|
||||
for (std::uint64_t page = (base + first) & ~(PageBytes - 1u); page < base + first + words * 4u; page += PageBytes) {
|
||||
if (!PageAccessible(std::max(page, base + first), base, base + size)) {
|
||||
skip();
|
||||
skip(NarrowSkip::Unmapped);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -522,7 +540,7 @@ private:
|
||||
for (std::uint64_t index = 0; index < positions; index++) {
|
||||
const auto address = base + first + index * domain.stride;
|
||||
if (!PageAccessible(address, base, base + size)) {
|
||||
skip();
|
||||
skip(NarrowSkip::Unmapped);
|
||||
return;
|
||||
}
|
||||
values.push_back(Read(address));
|
||||
@@ -547,6 +565,53 @@ private:
|
||||
tableCounters().narrowed.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void Select(const TableColumn& column, ColumnRecords& records) {
|
||||
const auto base = records.base;
|
||||
const auto size = records.size;
|
||||
DescriptorValue selection;
|
||||
try {
|
||||
walker.EvaluateDescriptorSource(plan, column.selection, runtime, selection);
|
||||
} catch (const std::exception&) {
|
||||
records.fault = ImageTableAbi::PoisonCode(static_cast<std::uint32_t>(PoisonReason::Unmapped));
|
||||
return;
|
||||
}
|
||||
tableCounters().selected.fetch_add(1, std::memory_order_relaxed);
|
||||
const auto offset = selection.dwords[0];
|
||||
const auto entry = (column.addend + column.offset) & ~3u;
|
||||
records.base = base + ((offset + column.offset) & ~3u) - entry;
|
||||
records.size = entry + static_cast<std::uint64_t>(column.dwordCount) * sizeof(std::uint32_t);
|
||||
records.records = 1u;
|
||||
records.keys = 1u;
|
||||
records.states.assign(1u, RecordState::Read);
|
||||
records.words.assign(1u, 0u);
|
||||
if (offset >= column.foldedAddend && offset > 0xffffffffu - column.foldedCarry) {
|
||||
records.states.front() = RecordState::Carried;
|
||||
return;
|
||||
}
|
||||
DescriptorValue value;
|
||||
value.dwordCount = 8u;
|
||||
std::uint64_t low = std::numeric_limits<std::uint64_t>::max();
|
||||
std::uint64_t high = 0;
|
||||
for (std::uint32_t dword = 0; dword < column.dwordCount; dword++) {
|
||||
const auto position = ImageTableAbi::ScalarBufferDword(offset, column.offset + dword * 4u, size);
|
||||
if (!position.has_value()) {
|
||||
value.availableDwordMask |= static_cast<std::uint8_t>(1u << dword);
|
||||
continue;
|
||||
}
|
||||
const auto address = base + *position;
|
||||
if (!PageAccessible(address, base, base + size)) {
|
||||
records.states.front() = RecordState::Unmapped;
|
||||
return;
|
||||
}
|
||||
value.dwords[dword] = Read(address);
|
||||
value.availableDwordMask |= static_cast<std::uint8_t>(1u << dword);
|
||||
low = std::min(low, address);
|
||||
high = std::max(high, address + sizeof(std::uint32_t));
|
||||
}
|
||||
records.words.front() = WordsIndex(value);
|
||||
if (low < high) tables.ranges.emplace_back(low, high - low);
|
||||
}
|
||||
|
||||
std::uint32_t WordsIndex(const DescriptorValue& value) {
|
||||
const auto [found, inserted] = wordsIndex.try_emplace({value.dwords, value.dwordCount, value.availableDwordMask}, static_cast<std::uint32_t>(tables.words.size()));
|
||||
if (inserted) tables.words.push_back(value);
|
||||
@@ -577,6 +642,10 @@ private:
|
||||
}
|
||||
records.base = heap.Base48();
|
||||
records.size = heap.GetSize();
|
||||
if (column.selected) {
|
||||
Select(column, records);
|
||||
return records;
|
||||
}
|
||||
records.records = ImageTableAbi::ScalarBufferRecords(column.addend, column.stride, column.offset + (column.indirect ? 4u : 0u), records.size);
|
||||
if (records.size != 0u) tables.ranges.emplace_back(records.base, records.size);
|
||||
}
|
||||
|
||||
@@ -146,11 +146,15 @@ public:
|
||||
}
|
||||
std::vector<const IrValue*> referenced;
|
||||
for (const auto& plan : m_tableColumns) {
|
||||
for (std::uint32_t argument = 0; argument < plan.handle->ArgumentCount(); argument++) {
|
||||
plan.handle->ReplaceArgument(argument, plan.offset);
|
||||
}
|
||||
const auto& column = *m_sources[plan.source].tableColumn;
|
||||
IrValue* offset = column.selected ? &m_builder.Constant(column.addend) : plan.offset;
|
||||
for (std::uint32_t argument = 0; argument < plan.handle->ArgumentCount(); argument++) {
|
||||
plan.handle->ReplaceArgument(argument, offset);
|
||||
}
|
||||
KeepSourceAlive(*plan.handle, m_sources[column.heapSource], referenced);
|
||||
if (column.selected) {
|
||||
KeepSourceAlive(*plan.handle, m_sources[column.selection], referenced);
|
||||
}
|
||||
if (plan.keyDomain.has_value()) {
|
||||
KeepSourceAlive(*plan.handle, m_sources[plan.keyDomain->source], referenced);
|
||||
}
|
||||
@@ -542,11 +546,15 @@ private:
|
||||
static IrValue* SplitImmediateAddend(IrValue* value, std::uint32_t& addend) {
|
||||
value = value->Resolve();
|
||||
addend = 0;
|
||||
if (value->Opcode() == IrOpcode::IAdd32 && value->ArgumentCount() == 2u) {
|
||||
if (immediateU32(value->Argument(0), addend)) return value->Argument(1)->Resolve();
|
||||
if (immediateU32(value->Argument(1), addend)) return value->Argument(0)->Resolve();
|
||||
while (value->Opcode() == IrOpcode::IAdd32 && value->ArgumentCount() == 2u) {
|
||||
std::uint32_t immediate = 0;
|
||||
IrValue* rest = nullptr;
|
||||
if (immediateU32(value->Argument(0), immediate)) rest = value->Argument(1)->Resolve();
|
||||
else if (immediateU32(value->Argument(1), immediate)) rest = value->Argument(0)->Resolve();
|
||||
else break;
|
||||
addend += immediate;
|
||||
value = rest;
|
||||
}
|
||||
addend = 0;
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -650,6 +658,18 @@ private:
|
||||
column.address = address;
|
||||
column.foldedAddend = folded ? baseAddend : 0u;
|
||||
column.foldedCarry = folded ? carry : 0u;
|
||||
if (!address && !sampler) {
|
||||
DescriptorSource direct;
|
||||
MakeSource(handle, width, false, false, direct);
|
||||
DescriptorSource selection;
|
||||
selection.dwordCount = 1u;
|
||||
selection.dwords[0] = offset;
|
||||
std::uint32_t badDword = 0;
|
||||
if (ValidateSource(direct, badDword) && ValidateSource(selection, badDword)) {
|
||||
column.selected = true;
|
||||
column.selection = InternSource(selection);
|
||||
}
|
||||
}
|
||||
const auto domain = masked ? std::optional<KeyDomain>{} : MatchKeyDomain(key);
|
||||
DescriptorSource columnSource = heapSource;
|
||||
columnSource.tableColumn = column;
|
||||
@@ -1273,6 +1293,7 @@ private:
|
||||
return;
|
||||
}
|
||||
IndexMemoryUsers();
|
||||
const auto directImages = DirectImageHandles();
|
||||
for (auto& block : m_program.Blocks()) {
|
||||
for (IrValue* inst : block->Instructions()) {
|
||||
const auto imageInfo = ImageOpcodeInfoOf(inst->Opcode());
|
||||
@@ -1284,7 +1305,10 @@ private:
|
||||
continue;
|
||||
}
|
||||
const auto memory = m_program.Resources().memoryInfo[flags.index];
|
||||
static_cast<void>(PlanTableColumn(*inst->Argument(0)->Resolve(), false, memory.imageR128));
|
||||
IrValue& image = *inst->Argument(0)->Resolve();
|
||||
if (std::ranges::none_of(directImages, [&](const IrValue* handle) { return SameHandle(*handle, image); })) {
|
||||
static_cast<void>(PlanTableColumn(image, false, memory.imageR128));
|
||||
}
|
||||
if (imageInfo.needsSampler && inst->ArgumentCount() > 1u && !DirectSampler(*inst->Argument(1)->Resolve())) {
|
||||
static_cast<void>(PlanTableColumn(*inst->Argument(1)->Resolve(), true, false));
|
||||
}
|
||||
@@ -1328,6 +1352,54 @@ private:
|
||||
return ValidateSource(descriptor, badDword);
|
||||
}
|
||||
|
||||
bool DirectImage(const IrValue& handle) {
|
||||
if (handle.Opcode() != IrOpcode::GetImageResource || handle.ArgumentCount() != 8u) {
|
||||
return false;
|
||||
}
|
||||
DescriptorSource descriptor;
|
||||
MakeSource(handle, 8u, false, false, descriptor);
|
||||
std::uint32_t badDword = 0;
|
||||
return ValidateSource(descriptor, badDword);
|
||||
}
|
||||
|
||||
std::vector<const IrValue*> DirectImageHandles() {
|
||||
std::vector<const IrValue*> handles;
|
||||
for (auto& block : m_program.Blocks()) {
|
||||
for (IrValue* inst : block->Instructions()) {
|
||||
const auto imageInfo = ImageOpcodeInfoOf(inst->Opcode());
|
||||
if (imageInfo.access == ImageAccess::None || inst->ArgumentCount() == 0u) {
|
||||
continue;
|
||||
}
|
||||
const auto flags = inst->Flags<MemoryFlags>();
|
||||
if (flags.index >= m_program.Resources().memoryInfo.size()) {
|
||||
continue;
|
||||
}
|
||||
const auto& memory = m_program.Resources().memoryInfo[flags.index];
|
||||
const bool tableUnsupported = imageInfo.resourceClass == ImageResourceClass::Storage || memory.imagePacked || memory.imageByElements != 0u;
|
||||
const IrValue* handle = inst->Argument(0)->Resolve();
|
||||
if (tableUnsupported && DirectImage(*handle)) {
|
||||
handles.push_back(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
return handles;
|
||||
}
|
||||
|
||||
bool SameHandle(const IrValue& left, const IrValue& right) const {
|
||||
if (&left == &right) {
|
||||
return true;
|
||||
}
|
||||
if (left.Opcode() != right.Opcode() || left.ArgumentCount() != right.ArgumentCount()) {
|
||||
return false;
|
||||
}
|
||||
for (std::size_t index = 0; index < left.ArgumentCount(); index++) {
|
||||
if (!EquivalentValue(m_program.Resources(), left.Argument(index), right.Argument(index))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsTablePlanningMemory(std::uint32_t index) const {
|
||||
return std::ranges::find(m_tablePlanningMemory, index) != m_tablePlanningMemory.end();
|
||||
}
|
||||
@@ -1336,8 +1408,59 @@ private:
|
||||
return source < m_sources.size() && m_sources[source].tableColumn.has_value();
|
||||
}
|
||||
|
||||
bool SameBufferSource(const IrValue& handle, std::uint32_t source) const {
|
||||
const auto& current = m_sources[source];
|
||||
if (current.tableColumn.has_value() || current.dwordCount != 4u || handle.ArgumentCount() != 4u) {
|
||||
return false;
|
||||
}
|
||||
for (std::uint32_t dword = 0; dword < 4u; dword++) {
|
||||
if (!EquivalentValue(m_program.Resources(), current.dwords[dword], handle.Argument(dword))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SelectionWritten(const TableColumn& column) const {
|
||||
const auto written = [&](const auto& matches) {
|
||||
return std::any_of(m_info.buffers.begin(), m_info.buffers.end(), [&](const auto& buffer) { return (buffer.written || buffer.atomic) && matches(buffer.source); });
|
||||
};
|
||||
std::vector<const IrValue*> pending{m_sources[column.selection].dwords[0]};
|
||||
std::vector<const IrValue*> seen;
|
||||
while (!pending.empty()) {
|
||||
const IrValue* current = pending.back()->Resolve();
|
||||
pending.pop_back();
|
||||
if (std::ranges::find(seen, current) != seen.end()) {
|
||||
continue;
|
||||
}
|
||||
seen.push_back(current);
|
||||
const bool pointer = current->Opcode() == IrOpcode::LoadAddressU32;
|
||||
const bool buffer = current->Opcode() == IrOpcode::ReadConstBuffer && current->ArgumentCount() != 0u;
|
||||
if ((pointer || buffer) && m_info.bdaWrites) {
|
||||
return true;
|
||||
}
|
||||
if (pointer && written([](std::uint32_t) { return true; })) {
|
||||
return true;
|
||||
}
|
||||
if (buffer) {
|
||||
const IrValue* handle = current->Argument(0)->Resolve();
|
||||
if (written([&](std::uint32_t source) { return SameBufferSource(*handle, source); })) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (std::size_t index = 0; index < current->ArgumentCount(); index++) {
|
||||
pending.push_back(current->Argument(index));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void CheckTableStores() {
|
||||
for (auto& source : m_sources) {
|
||||
if (source.tableColumn.has_value() && source.tableColumn->selected && SelectionWritten(*source.tableColumn)) {
|
||||
source.tableColumn->selected = false;
|
||||
source.tableColumn->keyDomains.clear();
|
||||
}
|
||||
if (!source.tableColumn.has_value() || source.tableColumn->keyDomains.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -532,6 +532,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program)
|
||||
targetOptions.fragmentDescriptorSet = request.target.registrationModules;
|
||||
targetOptions.float16DenormPreserve = request.target.float16DenormPreserve;
|
||||
targetOptions.spannedWaves = request.target.spannedWaves;
|
||||
targetOptions.robustBufferAccess2 = request.target.robustBufferAccess2;
|
||||
|
||||
constexpr SpirvEmitter spirvEmitter;
|
||||
CompiledShaderArtifact result;
|
||||
|
||||
@@ -284,6 +284,7 @@ struct SpirvTarget {
|
||||
bool vertexSubgroups = true;
|
||||
bool float16DenormPreserve = false;
|
||||
bool spannedWaves = false;
|
||||
bool robustBufferAccess2 = false;
|
||||
};
|
||||
|
||||
struct BindingLayout {
|
||||
|
||||
@@ -108,8 +108,8 @@ static_assert(sizeof(IrResourcePlan) == 712, "IrResourcePlan changed: update Enc
|
||||
static_assert(sizeof(IrValue) == 152, "IrValue changed: update the plan value encoder");
|
||||
static_assert(sizeof(GuestRegister) == 8, "GuestRegister changed: update the plan value encoder");
|
||||
static_assert(sizeof(MemoryInfo) == 80, "MemoryInfo changed: update the plan encoder");
|
||||
static_assert(sizeof(DescriptorSource) == 160, "DescriptorSource changed: update the plan encoder");
|
||||
static_assert(sizeof(TableColumn) == 80, "TableColumn changed: update the plan encoder");
|
||||
static_assert(sizeof(DescriptorSource) == 168, "DescriptorSource changed: update the plan encoder");
|
||||
static_assert(sizeof(TableColumn) == 88, "TableColumn changed: update the plan encoder");
|
||||
static_assert(sizeof(KeyDomain) == 16, "KeyDomain changed: update the plan encoder");
|
||||
static_assert(sizeof(ResourceBlock) == 80, "ResourceBlock changed: update the plan encoder");
|
||||
static_assert(sizeof(SrtRead) == 16, "SrtRead changed: update the plan encoder");
|
||||
@@ -149,7 +149,7 @@ static_assert(fieldCount<IrResourcePlan>() == 23, "IrResourcePlan changed: updat
|
||||
static_assert(fieldCount<GuestRegister>() == 2, "GuestRegister changed: update the plan value encoder");
|
||||
static_assert(fieldCount<MemoryInfo>() == 31, "MemoryInfo changed: update the plan encoder");
|
||||
static_assert(fieldCount<DescriptorSource>() == 3, "DescriptorSource changed: update the plan encoder");
|
||||
static_assert(fieldCount<TableColumn>() == 15, "TableColumn changed: update the plan encoder");
|
||||
static_assert(fieldCount<TableColumn>() == 17, "TableColumn changed: update the plan encoder");
|
||||
static_assert(fieldCount<KeyDomain>() == 4, "KeyDomain changed: update the plan encoder");
|
||||
static_assert(fieldCount<ResourceBlock>() == 4, "ResourceBlock changed: update the plan encoder");
|
||||
static_assert(fieldCount<SrtRead>() == 2, "SrtRead changed: update the plan encoder");
|
||||
@@ -1047,6 +1047,8 @@ bool encodePlan(Writer& writer, const IrResourcePlan& plan) {
|
||||
out.Value(column.indirectImmediate);
|
||||
out.Value(column.foldedAddend);
|
||||
out.Value(column.foldedCarry);
|
||||
out.Value(column.selected);
|
||||
out.Value(column.selection);
|
||||
});
|
||||
writer.List(plan.controlFlow, [&](Writer& out, const ResourceBlock& block) {
|
||||
out.Value(valueIndex(block.condition));
|
||||
@@ -1202,6 +1204,8 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) {
|
||||
in.Value(column.indirectImmediate);
|
||||
in.Value(column.foldedAddend);
|
||||
in.Value(column.foldedCarry);
|
||||
in.Value(column.selected);
|
||||
in.Value(column.selection);
|
||||
source.tableColumn = table ? std::optional(std::move(column)) : std::nullopt;
|
||||
});
|
||||
reader.List(plan.controlFlow, 28, [&](Reader& in, ResourceBlock& block) {
|
||||
@@ -1234,7 +1238,7 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) {
|
||||
if (!valid || !reader.Done() || fill.words > plan.uniformFill.values.size()) return false;
|
||||
const auto knownSource = [&](std::uint32_t source) { return source < plan.descriptorSources.size(); };
|
||||
if (!std::ranges::all_of(plan.materializationSources, knownSource)) return false;
|
||||
if (!std::ranges::all_of(plan.descriptorSources, [&](const DescriptorSource& source) { return source.dwordCount <= source.dwords.size() && (!source.tableColumn.has_value() || (knownSource(source.tableColumn->heapSource) && std::ranges::all_of(source.tableColumn->keyDomains, [&](const KeyDomain& domain) { return knownSource(domain.source); }))); })) return false;
|
||||
if (!std::ranges::all_of(plan.descriptorSources, [&](const DescriptorSource& source) { return source.dwordCount <= source.dwords.size() && (!source.tableColumn.has_value() || (knownSource(source.tableColumn->heapSource) && (!source.tableColumn->selected || knownSource(source.tableColumn->selection)) && std::ranges::all_of(source.tableColumn->keyDomains, [&](const KeyDomain& domain) { return knownSource(domain.source); }))); })) return false;
|
||||
if (!std::ranges::all_of(plan.info.buffers, [&](const BufferResource& buffer) { return knownSource(buffer.source); })) return false;
|
||||
if (!std::ranges::all_of(plan.info.images, [&](const ImageResource& image) { return knownSource(image.source); })) return false;
|
||||
if (!std::ranges::all_of(plan.info.samplers, [&](const SamplerResource& sampler) { return knownSource(sampler.source); })) return false;
|
||||
|
||||
@@ -26,6 +26,7 @@ struct SpirvTargetOptions {
|
||||
bool fragmentDescriptorSet = false;
|
||||
bool float16DenormPreserve = false;
|
||||
bool spannedWaves = false;
|
||||
bool robustBufferAccess2 = false;
|
||||
};
|
||||
|
||||
struct SpirvEmitInfo {
|
||||
|
||||
@@ -117,6 +117,10 @@ std::uint32_t EmitMemoryElementIndex(SpirvEmitterState& state, const MemoryResou
|
||||
std::uint32_t EmitMemoryElementInBounds(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index);
|
||||
bool InFunctionStorage(const SpirvEmitterState& state, ResourceKind kind);
|
||||
std::uint32_t EmitMemoryElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index);
|
||||
std::uint32_t EmitValueOrZero(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t value);
|
||||
std::uint32_t LdsStorageDwords(const SpirvEmitterState& state);
|
||||
bool LoadsSelectable(const SpirvEmitterState& state, const MemoryResourceAccess& access);
|
||||
std::uint32_t GuardedElementIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t condition, std::uint32_t index);
|
||||
std::uint32_t EmitStorageBufferElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index, std::uint32_t pointerType);
|
||||
std::uint32_t EmitTBufferBitcastU32ToI32(SpirvEmitterState& state, std::uint32_t value);
|
||||
bool IsSignedFormatComponent(SpirvFormatComponentType type);
|
||||
|
||||
@@ -83,6 +83,7 @@ struct SpirvTableAccess {
|
||||
std::uint32_t dword3 = 0;
|
||||
std::uint32_t samplerElement = 0;
|
||||
bool active = false;
|
||||
bool uniform = false;
|
||||
};
|
||||
|
||||
struct SpirvEmitterState {
|
||||
@@ -107,6 +108,7 @@ struct SpirvEmitterState {
|
||||
bool registrationModules = false;
|
||||
bool fragmentDescriptorSet = false;
|
||||
bool float16DenormPreserve = false;
|
||||
bool robustBufferAccess2 = false;
|
||||
std::unordered_set<const IrValue*> sharedLaneValues;
|
||||
std::uint32_t laneHalf = 0;
|
||||
std::uint32_t spirvVersion = 0x00010300u;
|
||||
|
||||
@@ -42,13 +42,11 @@ public:
|
||||
vec4FloatType = type(spv::OpTypeVector, floatType, 4u);
|
||||
functionType = type(spv::OpTypeFunction, voidType);
|
||||
|
||||
perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpMemberDecorate, {0u, spv::DecorationInvariant}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType);
|
||||
if (clipDistances != 0u) {
|
||||
clipArrayType = array(floatType, clipDistances);
|
||||
perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpMemberDecorate, {0u, spv::DecorationInvariant}}, {spv::OpMemberDecorate, {1u, spv::DecorationBuiltIn, spv::BuiltInClipDistance}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType, clipArrayType);
|
||||
ptrInputClip = pointer(spv::StorageClassInput, clipArrayType);
|
||||
ptrOutputClip = pointer(spv::StorageClassOutput, clipArrayType);
|
||||
} else {
|
||||
perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpMemberDecorate, {0u, spv::DecorationInvariant}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType);
|
||||
}
|
||||
|
||||
ptrInputVec4Float = pointer(spv::StorageClassInput, vec4FloatType);
|
||||
@@ -126,8 +124,8 @@ public:
|
||||
store(access(ptrOutputVec4Float, glOut, invocation, intConstant(0)), position);
|
||||
if (clipDistances != 0u) {
|
||||
std::array<std::uint32_t, 3> clips {};
|
||||
for (std::uint32_t i = 0; i < clips.size(); i++) clips[i] = load(clipArrayType, access(ptrInputClip, glIn, intConstant(i), intConstant(1)));
|
||||
const auto own = load(clipArrayType, access(ptrInputClip, glIn, index, intConstant(1)));
|
||||
for (std::uint32_t i = 0; i < clips.size(); i++) clips[i] = load(clipArrayType, access(ptrInputClip, clipIn, intConstant(i)));
|
||||
const auto own = load(clipArrayType, access(ptrInputClip, clipIn, index));
|
||||
std::vector<std::uint32_t> values;
|
||||
for (std::uint32_t k = 0; k < clipDistances; k++) {
|
||||
auto sum = constant(floatType, 0);
|
||||
@@ -136,7 +134,7 @@ public:
|
||||
}
|
||||
const auto clip = builder.AllocateId();
|
||||
builder.AddFunction(spv::OpCompositeConstruct, clipArrayType, clip, std::span<const std::uint32_t>(values));
|
||||
store(access(ptrOutputClip, glOut, invocation, intConstant(1)), clip);
|
||||
store(access(ptrOutputClip, clipOut, invocation), clip);
|
||||
}
|
||||
|
||||
for (std::uint32_t i = 0; i < parameters.size(); i++) {
|
||||
@@ -171,7 +169,7 @@ public:
|
||||
|
||||
const auto position = load(vec4FloatType, access(ptrInputVec4Float, glIn, index, intConstant(0)));
|
||||
store(access(ptrOutputVec4Float, glOut, intConstant(0)), position);
|
||||
if (clipDistances != 0u) store(access(ptrOutputClip, glOut, intConstant(1)), load(clipArrayType, access(ptrInputClip, glIn, index, intConstant(1))));
|
||||
if (clipDistances != 0u) store(clipOut, load(clipArrayType, access(ptrInputClip, clipIn, index)));
|
||||
for (std::uint32_t i = 0; i < parameters.size(); i++) {
|
||||
store(outputs[i], load(vec4FloatType, access(ptrInputVec4Float, inputs[i], index)));
|
||||
}
|
||||
@@ -287,6 +285,12 @@ private:
|
||||
return variable;
|
||||
}
|
||||
|
||||
std::uint32_t clipVariable(spv::StorageClass storage, std::uint32_t type) {
|
||||
const auto variable = addInterface(storage, type);
|
||||
decorate(variable, spv::DecorationBuiltIn, spv::BuiltInClipDistance);
|
||||
return variable;
|
||||
}
|
||||
|
||||
void decorate(std::uint32_t target, spv::Decoration decoration, std::uint32_t value) {
|
||||
builder.AddAnnotation(spv::OpDecorate, target, decoration, value);
|
||||
}
|
||||
@@ -320,6 +324,7 @@ private:
|
||||
decorate(tessCoord, spv::DecorationBuiltIn, spv::BuiltInTessCoord);
|
||||
}
|
||||
glIn = addInterface(spv::StorageClassInput, array(perVertexType, tessControl ? 3u : 4u));
|
||||
if (clipDistances != 0u) clipIn = clipVariable(spv::StorageClassInput, array(clipArrayType, tessControl ? 3u : 4u));
|
||||
|
||||
inputs.resize(parameters.size());
|
||||
std::array<std::uint32_t, 32> locations {};
|
||||
@@ -340,6 +345,7 @@ private:
|
||||
const auto tessControl = model == spv::ExecutionModelTessellationControl;
|
||||
if (tessControl) {
|
||||
glOut = addInterface(spv::StorageClassOutput, array(perVertexType, 4u));
|
||||
if (clipDistances != 0u) clipOut = clipVariable(spv::StorageClassOutput, array(clipArrayType, 4u));
|
||||
tessInner = addInterface(spv::StorageClassOutput, array(floatType, 2u));
|
||||
decorate(tessInner, spv::DecorationBuiltIn, spv::BuiltInTessLevelInner);
|
||||
builder.AddAnnotation(spv::OpDecorate, tessInner, spv::DecorationPatch);
|
||||
@@ -348,6 +354,7 @@ private:
|
||||
builder.AddAnnotation(spv::OpDecorate, tessOuter, spv::DecorationPatch);
|
||||
} else {
|
||||
glOut = addInterface(spv::StorageClassOutput, perVertexType);
|
||||
if (clipDistances != 0u) clipOut = clipVariable(spv::StorageClassOutput, clipArrayType);
|
||||
}
|
||||
|
||||
outputs.resize(parameters.size());
|
||||
@@ -394,6 +401,8 @@ private:
|
||||
std::uint32_t ptrOutputVec4Float = 0;
|
||||
std::uint32_t glIn = 0;
|
||||
std::uint32_t glOut = 0;
|
||||
std::uint32_t clipIn = 0;
|
||||
std::uint32_t clipOut = 0;
|
||||
std::uint32_t tessInner = 0;
|
||||
std::uint32_t tessOuter = 0;
|
||||
std::uint32_t tessCoord = 0;
|
||||
|
||||
@@ -260,6 +260,7 @@ std::vector<std::uint32_t> SpirvEmitter::Emit(const IrProgram& program, const Sh
|
||||
state.registrationModules = target.registrationModules;
|
||||
state.fragmentDescriptorSet = target.fragmentDescriptorSet;
|
||||
state.float16DenormPreserve = target.float16DenormPreserve;
|
||||
state.robustBufferAccess2 = target.robustBufferAccess2;
|
||||
state.hostSubgroupSize = target.subgroupSize;
|
||||
if (HostSubgroupSingleLane(state)) {
|
||||
state.requirements.subgroupBallot = false;
|
||||
|
||||
@@ -1701,7 +1701,9 @@ TableAccessResult EmitTableAccess(SpirvValueEmitContext& ctx, const IrValue& ins
|
||||
result.flags = flags;
|
||||
live = selected;
|
||||
state.tableAccess.element = Binary(state, spv::OpIAdd, u32, LoadTableMap(state, ConstantU32(state, ImageTableAbi::HeaderElementBase)), element);
|
||||
DecorateTableNonUniform(state, state.tableAccess.element);
|
||||
const auto& sources = state.program.Resources().descriptorSources;
|
||||
state.tableAccess.uniform = base.source < sources.size() && sources[base.source].tableColumn.has_value() && sources[base.source].tableColumn->selected;
|
||||
if (!state.tableAccess.uniform) DecorateTableNonUniform(state, state.tableAccess.element);
|
||||
state.tableAccess.dword3 = dword3;
|
||||
}
|
||||
if (tableSampler) {
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace {
|
||||
if (ShaderWorkgroupInput(state) == nullptr) {
|
||||
FailEmit("function LDS was not prepared before function emission");
|
||||
}
|
||||
state.ldsVariable = state.module.DefineGlobalVariable(TypeU32ArrayPointer(state, spv::StorageClassWorkgroup, LdsDwordCount(state) + (state.requirements.ldsLock ? 1u : 0u)), spv::StorageClassWorkgroup);
|
||||
state.ldsVariable = state.module.DefineGlobalVariable(TypeU32ArrayPointer(state, spv::StorageClassWorkgroup, LdsStorageDwords(state)), spv::StorageClassWorkgroup);
|
||||
state.module.AddName(state.ldsVariable, "lds_dwords");
|
||||
}
|
||||
|
||||
@@ -173,6 +173,26 @@ bool InFunctionStorage(const SpirvEmitterState& state, ResourceKind kind) {
|
||||
return kind == ResourceKind::Lds && !LdsInDeviceMemory(state) && ShaderWorkgroupInput(state) == nullptr;
|
||||
}
|
||||
|
||||
std::uint32_t EmitValueOrZero(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t value) {
|
||||
return Select(state, TypeU32(state), condition, value, ConstantU32(state, 0u));
|
||||
}
|
||||
|
||||
std::uint32_t LdsStorageDwords(const SpirvEmitterState& state) {
|
||||
return LdsDwordCount(state) + (state.requirements.ldsLock ? 1u : 0u);
|
||||
}
|
||||
|
||||
bool LoadsSelectable(const SpirvEmitterState& state, const MemoryResourceAccess& access) {
|
||||
if (access.kind == ResourceKind::Scratch) return true;
|
||||
if (access.kind == ResourceKind::Lds) return false;
|
||||
return state.robustBufferAccess2;
|
||||
}
|
||||
|
||||
std::uint32_t GuardedElementIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t condition, std::uint32_t index) {
|
||||
if (access.kind == ResourceKind::Lds) FailEmit("LDS loads keep their branch and have no guarded element");
|
||||
const auto fallback = access.kind == ResourceKind::Scratch ? 0u : 0xfffffffeu;
|
||||
return Select(state, TypeU32(state), condition, index, ConstantU32(state, fallback));
|
||||
}
|
||||
|
||||
std::uint32_t EmitMemoryElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index) {
|
||||
if (access.kind == ResourceKind::Lds && LdsInDeviceMemory(state)) {
|
||||
return EmitStorageBufferElementPointer(state, access, EmitAddU32(state, state.ldsBufferBase, index), TypeStorageBufferElementPointer(state));
|
||||
|
||||
@@ -166,10 +166,10 @@ std::uint32_t LoadSampledImageDescriptor(SpirvEmitterState& state, std::uint32_t
|
||||
if (found == state.tableImageVariables.end() || !state.tableAccess.active) ExitDescriptorBindingFailure(state, kind, resource, "image table array was not emitted for the mode");
|
||||
const auto pointer = state.module.AllocateId();
|
||||
state.module.AddFunction(spv::OpAccessChain, pointerType, pointer, found->variable, state.tableAccess.element);
|
||||
DecorateTableNonUniform(state, pointer);
|
||||
if (!state.tableAccess.uniform) DecorateTableNonUniform(state, pointer);
|
||||
const auto image = state.module.AllocateId();
|
||||
state.module.AddFunction(spv::OpLoad, imageType, image, pointer);
|
||||
DecorateTableNonUniform(state, image);
|
||||
if (!state.tableAccess.uniform) DecorateTableNonUniform(state, image);
|
||||
return image;
|
||||
}
|
||||
const auto arrayIndex = ResourceForDescriptor(state, kind, resource);
|
||||
@@ -216,7 +216,7 @@ std::uint32_t MakeSampledImage(SpirvEmitterState& state, std::uint32_t resource,
|
||||
const auto sampledImage = state.module.AllocateId();
|
||||
const auto sampledType = state.module.Type(spv::OpTypeSampledImage, ImageType(state, imageResource));
|
||||
state.module.AddFunction(spv::OpSampledImage, sampledType, sampledImage, image, samplerId);
|
||||
if (imageResource.table != NoTable || state.program.Info().samplers.at(sampler).table != NoTable) DecorateTableNonUniform(state, sampledImage);
|
||||
if ((imageResource.table != NoTable && !state.tableAccess.uniform) || state.program.Info().samplers.at(sampler).table != NoTable) DecorateTableNonUniform(state, sampledImage);
|
||||
return sampledImage;
|
||||
}
|
||||
|
||||
|
||||
@@ -134,36 +134,35 @@ std::uint32_t LoadSubwordInBounds(SpirvValueEmitContext& ctx, const MemoryResour
|
||||
return Binary(state, spv::OpShiftRightArithmetic, TypeU32(state), left, ConstantU32(state, 32u - bits));
|
||||
}
|
||||
|
||||
std::uint32_t LoadWordPrepared(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, const MemoryResourceAccess& resource) {
|
||||
std::uint32_t LoadEnable(SpirvEmitterState& state, const MemoryResourceAccess& resource, std::uint32_t index, std::uint32_t active) {
|
||||
const auto inBounds = EmitMemoryElementInBounds(state, resource, index);
|
||||
return active == 0u ? inBounds : Binary(state, spv::OpLogicalAnd, TypeBool(state), active, inBounds);
|
||||
}
|
||||
|
||||
std::uint32_t LoadWordPrepared(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, const MemoryResourceAccess& resource, std::uint32_t active) {
|
||||
auto& state = ctx.state;
|
||||
const auto index = EmitMemoryElementIndex(state, resource, DwordIndex(ctx, inst, mem));
|
||||
return EmitValueOrZeroIfCondition(state, EmitMemoryElementInBounds(state, resource, index), [&]() {
|
||||
return LoadWordInBounds(ctx, resource, index);
|
||||
});
|
||||
const auto enable = LoadEnable(state, resource, index, active);
|
||||
if (!LoadsSelectable(state, resource)) return EmitValueOrZeroIfCondition(state, enable, [&] { return LoadWordInBounds(ctx, resource, index); });
|
||||
return EmitValueOrZero(state, enable, LoadWordInBounds(ctx, resource, GuardedElementIndex(state, resource, enable, index)));
|
||||
}
|
||||
|
||||
std::uint32_t LoadWord(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem) {
|
||||
return EmitValueOrZeroIfCondition(ctx.state, ActiveArgument(ctx, inst), [&]() {
|
||||
const auto resource = PrepareMemoryResourceAccess(ctx.state, mem);
|
||||
return LoadWordPrepared(ctx, inst, mem, resource);
|
||||
});
|
||||
return LoadWordPrepared(ctx, inst, mem, PrepareMemoryResourceAccess(ctx.state, mem), ActiveArgument(ctx, inst));
|
||||
}
|
||||
|
||||
std::uint32_t LoadSubwordPrepared(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, const MemoryResourceAccess& resource, std::uint32_t bits, bool signExtend) {
|
||||
std::uint32_t LoadSubwordPrepared(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, const MemoryResourceAccess& resource, std::uint32_t bits, bool signExtend, std::uint32_t active) {
|
||||
auto& state = ctx.state;
|
||||
const auto address = ByteAddress(ctx, inst, mem);
|
||||
const auto rawIndex = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u));
|
||||
const auto index = EmitMemoryElementIndex(state, resource, rawIndex);
|
||||
return EmitValueOrZeroIfCondition(state, EmitMemoryElementInBounds(state, resource, index), [&]() {
|
||||
return LoadSubwordInBounds(ctx, resource, address, index, bits, signExtend);
|
||||
});
|
||||
const auto enable = LoadEnable(state, resource, index, active);
|
||||
if (!LoadsSelectable(state, resource)) return EmitValueOrZeroIfCondition(state, enable, [&] { return LoadSubwordInBounds(ctx, resource, address, index, bits, signExtend); });
|
||||
return EmitValueOrZero(state, enable, LoadSubwordInBounds(ctx, resource, address, GuardedElementIndex(state, resource, enable, index), bits, signExtend));
|
||||
}
|
||||
|
||||
std::uint32_t LoadSubword(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t bits) {
|
||||
return EmitValueOrZeroIfCondition(ctx.state, ActiveArgument(ctx, inst), [&]() {
|
||||
const auto resource = PrepareMemoryResourceAccess(ctx.state, mem);
|
||||
return LoadSubwordPrepared(ctx, inst, mem, resource, bits, false);
|
||||
});
|
||||
return LoadSubwordPrepared(ctx, inst, mem, PrepareMemoryResourceAccess(ctx.state, mem), bits, false, ActiveArgument(ctx, inst));
|
||||
}
|
||||
|
||||
std::uint32_t ConstantDeviceAddress(SpirvEmitterState& state, std::uint64_t value) {
|
||||
@@ -368,20 +367,19 @@ std::uint32_t ConstructU32Composite(SpirvEmitterState& state, std::uint32_t comp
|
||||
|
||||
std::uint32_t LoadWideShared(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t components) {
|
||||
auto& state = ctx.state;
|
||||
return EmitValueOrDefaultIfCondition(state, ActiveArgument(ctx, inst), TypeU32Composite(state, components), ConstantU32CompositeZero(state, components), [&]() {
|
||||
const auto resource = PrepareMemoryResourceAccess(state, mem);
|
||||
const auto base = ByteAddress(ctx, inst, mem);
|
||||
std::array<std::uint32_t, 4> values{};
|
||||
for (std::uint32_t component = 0; component < components; component++) {
|
||||
const auto address = component == 0u ? base : Binary(state, spv::OpIAdd, TypeU32(state), base, ConstantU32(state, component * 4u));
|
||||
const auto rawIndex = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u));
|
||||
const auto index = EmitMemoryElementIndex(state, resource, rawIndex);
|
||||
values.at(component) = EmitValueOrZeroIfCondition(state, EmitMemoryElementInBounds(state, resource, index), [&]() {
|
||||
return LoadWordInBounds(ctx, resource, index);
|
||||
});
|
||||
}
|
||||
return ConstructU32Composite(state, components, values);
|
||||
});
|
||||
const auto active = ActiveArgument(ctx, inst);
|
||||
const auto resource = PrepareMemoryResourceAccess(state, mem);
|
||||
const auto base = ByteAddress(ctx, inst, mem);
|
||||
std::array<std::uint32_t, 4> values{};
|
||||
for (std::uint32_t component = 0; component < components; component++) {
|
||||
const auto address = component == 0u ? base : Binary(state, spv::OpIAdd, TypeU32(state), base, ConstantU32(state, component * 4u));
|
||||
const auto rawIndex = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u));
|
||||
const auto index = EmitMemoryElementIndex(state, resource, rawIndex);
|
||||
const auto enable = LoadEnable(state, resource, index, active);
|
||||
values.at(component) = LoadsSelectable(state, resource) ? EmitValueOrZero(state, enable, LoadWordInBounds(ctx, resource, GuardedElementIndex(state, resource, enable, index)))
|
||||
: EmitValueOrZeroIfCondition(state, enable, [&] { return LoadWordInBounds(ctx, resource, index); });
|
||||
}
|
||||
return ConstructU32Composite(state, components, values);
|
||||
}
|
||||
|
||||
void StoreWideShared(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t components) {
|
||||
|
||||
@@ -1203,7 +1203,7 @@ void EmitProgram(SpirvEmitterState& state) {
|
||||
state.module.AddFunction(spv::OpFunction, TypeVoid(state), state.meshGuestFunc != 0u ? state.meshGuestFunc : state.mainFunc, spv::FunctionControlMaskNone, TypeFunction(state));
|
||||
EmitLabel(state, state.entryLabel);
|
||||
if (state.requirements.functionLds) {
|
||||
state.module.AddFunction(spv::OpVariable, TypeU32ArrayPointer(state, spv::StorageClassFunction, LdsDwordCount(state)), state.ldsVariable, spv::StorageClassFunction);
|
||||
state.module.AddFunction(spv::OpVariable, TypeU32ArrayPointer(state, spv::StorageClassFunction, LdsStorageDwords(state)), state.ldsVariable, spv::StorageClassFunction);
|
||||
}
|
||||
if (state.requirements.functionScratch) {
|
||||
for (std::uint32_t half = 0; half < state.laneCount; half++) {
|
||||
|
||||
@@ -160,7 +160,7 @@ std::uint32_t directWordIndex(SpirvEmitterState& state, const MemoryResourceAcce
|
||||
return EmitAddU32(state, relative, base);
|
||||
}
|
||||
|
||||
std::uint32_t readBuffer(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t address, std::uint32_t bits) {
|
||||
std::uint32_t readBuffer(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t address, std::uint32_t bits, std::uint32_t enable = 0u) {
|
||||
if (context.Memory(instruction).gpuDescriptor) return EmitBdaRead(context, instruction, address, bits);
|
||||
auto& state = context.state;
|
||||
const auto access = directAccess(context, instruction, false, !context.Memory(instruction).coherent);
|
||||
@@ -169,26 +169,37 @@ std::uint32_t readBuffer(SpirvValueEmitContext& context, const IrValue& instruct
|
||||
const auto unaligned = nonzero(state, misalignment);
|
||||
const auto next = EmitAddU32(state, index, ConstantU32(state, 1u));
|
||||
const auto last = Select(state, TypeU32(state), unaligned, next, index);
|
||||
return EmitValueOrZeroIfCondition(state, EmitMemoryElementInBounds(state, access, last), [&] {
|
||||
const auto load = [&](std::uint32_t word) {
|
||||
const auto value = state.module.AllocateId();
|
||||
const auto pointer = EmitMemoryElementPointer(state, access, word);
|
||||
if (access.memoryAccess != 0u) state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer, access.memoryAccess);
|
||||
else state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer);
|
||||
return value;
|
||||
};
|
||||
const auto shift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), misalignment, ConstantU32(state, 3u));
|
||||
const auto low = Binary(state, spv::OpShiftRightLogical, TypeU32(state), load(index), shift);
|
||||
const auto high = EmitValueOrZeroIfCondition(state, unaligned, [&] {
|
||||
return Binary(state, spv::OpShiftLeftLogical, TypeU32(state), load(next), Binary(state, spv::OpISub, TypeU32(state), ConstantU32(state, 32u), shift));
|
||||
});
|
||||
const auto bounds = EmitMemoryElementInBounds(state, access, last);
|
||||
const auto inBounds = enable != 0u ? both(state, enable, bounds) : bounds;
|
||||
const auto load = [&](std::uint32_t word) {
|
||||
const auto value = state.module.AllocateId();
|
||||
const auto pointer = EmitMemoryElementPointer(state, access, word);
|
||||
if (access.memoryAccess != 0u) state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer, access.memoryAccess);
|
||||
else state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer);
|
||||
return value;
|
||||
};
|
||||
const auto shift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), misalignment, ConstantU32(state, 3u));
|
||||
const auto combine = [&](std::uint32_t low, std::uint32_t high) {
|
||||
auto value = EmitOrU32(state, low, high);
|
||||
if (bits != 32u) {
|
||||
const auto subwordShift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), EmitAndConstant(state, address, 3u), ConstantU32(state, 3u));
|
||||
value = Binary(state, spv::OpShiftRightLogical, TypeU32(state), value, subwordShift);
|
||||
}
|
||||
return EmitAndConstant(state, value, bits == 32u ? 0xffffffffu : (1u << bits) - 1u);
|
||||
});
|
||||
};
|
||||
const auto highShift = Binary(state, spv::OpISub, TypeU32(state), ConstantU32(state, 32u), shift);
|
||||
if (!LoadsSelectable(state, access)) {
|
||||
return EmitValueOrZeroIfCondition(state, inBounds, [&] {
|
||||
const auto low = Binary(state, spv::OpShiftRightLogical, TypeU32(state), load(index), shift);
|
||||
const auto high = EmitValueOrZeroIfCondition(state, unaligned, [&] { return Binary(state, spv::OpShiftLeftLogical, TypeU32(state), load(next), highShift); });
|
||||
return combine(low, high);
|
||||
});
|
||||
}
|
||||
const auto lowIndex = GuardedElementIndex(state, access, inBounds, index);
|
||||
const auto highIndex = Select(state, TypeU32(state), both(state, inBounds, unaligned), next, lowIndex);
|
||||
const auto low = Binary(state, spv::OpShiftRightLogical, TypeU32(state), load(lowIndex), shift);
|
||||
const auto high = EmitValueOrZero(state, unaligned, Binary(state, spv::OpShiftLeftLogical, TypeU32(state), load(highIndex), highShift));
|
||||
return EmitValueOrZero(state, inBounds, combine(low, high));
|
||||
}
|
||||
|
||||
std::uint32_t byteBuffers(SpirvEmitterState& state) {
|
||||
@@ -726,7 +737,8 @@ std::uint32_t EmitRuntimeBufferLoad(SpirvValueEmitContext& context, const IrValu
|
||||
std::array<std::uint32_t, 4> values{};
|
||||
for (std::uint32_t component = 0; component < components; ++component) {
|
||||
const auto access = address(context, instruction, resource, component * 4u, memory.dataBits / 8u);
|
||||
values[component] = EmitValueOrZeroIfCondition(state, access.inBounds, [&] { return readBuffer(context, instruction, access.guest, memory.dataBits); });
|
||||
values[component] = memory.gpuDescriptor ? EmitValueOrZeroIfCondition(state, access.inBounds, [&] { return readBuffer(context, instruction, access.guest, memory.dataBits); })
|
||||
: readBuffer(context, instruction, access.guest, memory.dataBits, access.inBounds);
|
||||
}
|
||||
return composite(state, components, values);
|
||||
};
|
||||
@@ -776,7 +788,7 @@ std::uint32_t EmitRuntimeScalarBufferLoad(SpirvValueEmitContext& context, const
|
||||
const auto byte = EmitAndConstant(state, address, ~3u);
|
||||
const auto offset = Unary(state, spv::OpUConvert, u64, byte);
|
||||
const auto inBounds = both(state, noCarry, both(state, resource.valid, Binary(state, spv::OpULessThanEqual, TypeBool(state), Binary(state, spv::OpIAdd, u64, offset, BdaConstant(state, 4u)), size)));
|
||||
if (!context.Memory(instruction).gpuDescriptor) return EmitValueOrZeroIfCondition(state, inBounds, [&] { return readBuffer(context, instruction, byte, 32u); });
|
||||
if (!context.Memory(instruction).gpuDescriptor) return readBuffer(context, instruction, byte, 32u, inBounds);
|
||||
return EmitValueOrZeroIfCondition(state, inBounds, [&] { return readBuffer(context, instruction, AddBdaAddress(context, instruction, resource.base, offset, false), 32u); });
|
||||
}
|
||||
|
||||
|
||||
@@ -136,11 +136,13 @@ flowchart LR
|
||||
|
||||
A T# or S# that a shader loads from a buffer at a runtime offset (`key * stride + addend`, or `(key << n) & mask`) is not a direct descriptor. [`ResourceTracker.cpp`](../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) plans it as a table column (`TableColumn`): the heap V#'s descriptor source, the record stride and addend, the descriptor's offset in the record, and the key domains that may narrow it. Accesses with the same heap and record shape share one column. The shader keeps computing the offset and indexes a table binding: images use one variable-count `SAMPLED_IMAGE` array (`RuntimeAbi::ImageTableBinding`, in the fragment set when pipeline libraries split the sets), samplers a `SAMPLER` array (`Binding::SamplerTable`). Devices without `runtimeDescriptorArray`, `descriptorBindingPartiallyBound` and `descriptorBindingVariableDescriptorCount` refuse such shaders.
|
||||
|
||||
At every invocation, `TableSnapshotter` in [`ResourceMaterializer.cpp`](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) reads each column's records from guest memory into `ResourceSnapshot::tables`: heap base and size, the record count (at most `ImageTableAbi::MaxKeys`), one code per record (an index into the distinct descriptor words, or a poison code) and the ranges it read. A column narrows to the records its keys select, plus record 0, when every access has a key domain: a value the SRT walker evaluates from user data, SRT reads and constants, or a key buffer of at most 256 positions (`APS5_IMAGE_TABLE_KEY_SCAN`). A loop-carried, lane-reduced or masked key keeps the whole column, so the cost of an invocation grows with the column's records and its distinct T#s. The snapshot is part of the invocation, not of the compiled variant: a changed table changes neither the SPIR-V nor the variant's disk key.
|
||||
A T# whose eight dwords and record offset the SRT walker can evaluate is planned as a selected column (`TableColumn::selected`): the offset is kept as a descriptor source (`TableColumn::selection`) and the shader indexes entry 0 of a one-record table. The column stays a whole table when the shader may write the memory its offset is read from at run time: the buffer it is read from, or any memory when the read or a write goes through a pointer or a GPU-only descriptor. Samplers are not selected: an S# the walker can evaluate is a direct sampler.
|
||||
|
||||
At every invocation, `TableSnapshotter` in [`ResourceMaterializer.cpp`](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) reads each column's records from guest memory into `ResourceSnapshot::tables`: heap base and size, the record count (at most `ImageTableAbi::MaxKeys`), one code per record (an index into the distinct descriptor words, or a poison code) and the ranges it read. A selected column evaluates its offset and reads only that record, as the scalar loads would: dwords past the heap read zeros, an unmapped record or an offset that carries past 32 bits is poison, and an offset the walker cannot read faults the table instead of throwing. Its heap base moves to the record, so the shader's fault address and the written-heap check cover that record alone. Any other column narrows to the records its keys select, plus record 0, when every access has a key domain: a key buffer of at most 256 positions (`APS5_IMAGE_TABLE_KEY_SCAN`), or a value the SRT walker evaluates from user data, SRT reads and constants (which, for an image, selects its record before it could narrow). A loop-carried, lane-reduced or masked key keeps the whole column, so the cost of an invocation grows with the column's records and its distinct T#s. The snapshot is part of the invocation, not of the compiled variant: a changed table or key changes neither the SPIR-V nor the variant's disk key.
|
||||
|
||||
`ResolveImageTables` turns the snapshot into the map buffer (`DescriptorRole::ImageTableMap`, layout in [`ImageTableAbi.hpp`](../../core/shader/recompiler/ImageTableAbi.hpp)): a header, eight words per table (map start, key count, heap base and size, the code for keys past the snapshot, a table fault) and one record per key (flags with the decode mode, the swizzle dword and the element for images; flags and element for samplers). The shader reads `map[key]` and switches over the image's prepared runtime modes. Each distinct T# becomes one element, up to `ImageElementBudget` and the device's capacity. An entry that does not decode, is multisampled or over budget, pairs a reducing sampler with a point-only mode, is an unnormalized or reducing S#, or whose surface has no mapped byte is poisoned: it reads zeros and reports a fault through the BDA fault buffer only when a lane selects it (`APS5_IMAGE_TABLE_STRICT=1` throws at the snapshot instead). The driver's `resolveImageTables` in [`ShaderResources.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) looks up or creates a texture per element and keeps the last four resolved tables per thread for reuse within one collect epoch while their memory is unchanged. Cached objects grow with their tables, so the resource cache keeps at most `ResourceCache::TableElementBudget()` table elements (65,536, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) and evicts its least recently used table objects past that, and the dispatch cache evicts its least recently used entries while their captured words exceed `Driver::DispatchCacheBytes()` (256 MiB, `APS5_DISPATCH_CACHE_BYTES`).
|
||||
|
||||
The disk cache (`FormatVersion` 33) stores the table and table operation of each image and sampler, the image shapes of the table binding, the invocation's table poison list, read ranges and fault count, and the table column of each descriptor source in the front-end plan, with its key domains.
|
||||
The disk cache stores the table and table operation of each image and sampler, the image shapes of the table binding, the invocation's table poison list, read ranges and fault count, and the table column of each descriptor source in the front-end plan, with its key domains and its selection.
|
||||
|
||||
### Untracked storage image comparisons
|
||||
|
||||
|
||||
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user