Compare commits
72 Commits
KytyPS5-20
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
9e41d8320a | |
|
|
e998f21e71 | |
|
|
3197ef093c | |
|
|
fa0e81bfba | |
|
|
01ce973882 | |
|
|
f57d2c0965 | |
|
|
735a229236 | |
|
|
9ade7e3582 | |
|
|
259a40abfc | |
|
|
f1577a5a94 | |
|
|
d08a6cef64 | |
|
|
907c65f54c | |
|
|
fe6499b86e | |
|
|
6309ac838d | |
|
|
19799b3079 | |
|
|
4e16234d32 | |
|
|
3c9da9c3f4 | |
|
|
ff6dffe298 | |
|
|
5f6dc0a8d0 | |
|
|
34893dd8c8 | |
|
|
50595d1c13 | |
|
|
717c54e085 | |
|
|
31b7be9ff2 | |
|
|
40692dfb64 | |
|
|
9a3aca963a | |
|
|
1fa79d5cac | |
|
|
c9ec82e32e | |
|
|
2f88a0a3b2 | |
|
|
7e884f55e0 | |
|
|
d70cbd4184 | |
|
|
9bcf80bf67 | |
|
|
ed1195d1e7 | |
|
|
25e1cc7ec9 | |
|
|
a287a54f26 | |
|
|
56738aa4e1 | |
|
|
c11254a996 | |
|
|
aec8c88755 | |
|
|
68d7bc94e2 | |
|
|
8ee31abd9d | |
|
|
9a7c54da58 | |
|
|
5dc6914688 | |
|
|
a7a734b5d3 | |
|
|
e2c8b61de6 | |
|
|
145578ac03 | |
|
|
f01a3750d4 | |
|
|
0e9c77b0a2 | |
|
|
d692d8ee08 | |
|
|
460a196fca | |
|
|
652154aead | |
|
|
49ebbfd627 | |
|
|
7688093ca3 | |
|
|
df08b83287 | |
|
|
fa68e104ea | |
|
|
fd66f580b4 | |
|
|
8f7d745383 | |
|
|
f4692a450e | |
|
|
df5b9b92be | |
|
|
5b0ef07742 | |
|
|
5ce9a52936 | |
|
|
dad900c61d | |
|
|
5b730d4df0 | |
|
|
e699a35f20 | |
|
|
41beda4367 | |
|
|
048fe9f413 | |
|
|
e51de54ee4 | |
|
|
984cfcebd1 | |
|
|
df0412afb5 | |
|
|
f28c0fccb2 | |
|
|
4c87dbebb2 | |
|
|
16a7fe689a | |
|
|
6f0cddd440 | |
|
|
4084a008ea |
|
|
@ -40,6 +40,11 @@ if (NOT (WIN32 OR LINUX OR APPLE))
|
|||
message(FATAL_ERROR "only Windows, Linux, and macOS builds are supported")
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
# Prevent Windows headers from defining min/max macros in all targets.
|
||||
add_compile_definitions(NOMINMAX)
|
||||
endif()
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
|
@ -298,28 +303,27 @@ list(APPEND kyty_emulator_src "${fault_buffer_shader_header}")
|
|||
list(APPEND kyty_emulator_src ${gpu_tiler_shader_headers})
|
||||
|
||||
set(gpu_blit_generated_dir "${PROJECT_BINARY_DIR}/gpu_blit_shaders")
|
||||
set(gpu_blit_shader_sources
|
||||
"${gpu_tiler_shader_dir}/gpu_blit_fs_triangle.vert"
|
||||
"${gpu_tiler_shader_dir}/gpu_blit_color_to_ms_depth.frag"
|
||||
"${gpu_tiler_shader_dir}/gpu_video_out_overlay.frag"
|
||||
)
|
||||
foreach(shader_source IN LISTS gpu_blit_shader_sources)
|
||||
get_filename_component(shader_name "${shader_source}" NAME_WE)
|
||||
function(add_gpu_blit_shader shader_name shader_file)
|
||||
set(shader_source "${gpu_tiler_shader_dir}/${shader_file}")
|
||||
set(shader_spv "${gpu_blit_generated_dir}/${shader_name}.spv")
|
||||
set(shader_header "${gpu_blit_generated_dir}/${shader_name}_spv.h")
|
||||
string(TOUPPER "${shader_name}_SPV" shader_symbol)
|
||||
add_custom_command(
|
||||
OUTPUT "${shader_header}"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${gpu_blit_generated_dir}"
|
||||
COMMAND "${KYTY_GLSLANG_VALIDATOR}" -V --target-env vulkan1.3 -Os
|
||||
COMMAND "${KYTY_GLSLANG_VALIDATOR}" -V --target-env vulkan1.3 -Os ${ARGN}
|
||||
"-I${gpu_tiler_shader_dir}" -o "${shader_spv}" "${shader_source}"
|
||||
COMMAND ${CMAKE_COMMAND} -DINPUT=${shader_spv} -DOUTPUT=${shader_header}
|
||||
-DSYMBOL=${shader_symbol} -P "${KYTY_SOURCE_DIR}/embed_spirv.cmake"
|
||||
DEPENDS "${shader_source}"
|
||||
VERBATIM
|
||||
)
|
||||
list(APPEND gpu_blit_shader_headers "${shader_header}")
|
||||
endforeach()
|
||||
set(gpu_blit_shader_headers ${gpu_blit_shader_headers} "${shader_header}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
add_gpu_blit_shader(gpu_blit_fs_triangle gpu_blit_fs_triangle.vert)
|
||||
add_gpu_blit_shader(gpu_blit_color_to_ms_depth gpu_blit_color_to_ms_depth.frag)
|
||||
add_gpu_blit_shader(gpu_video_out_overlay gpu_video_out_overlay.frag)
|
||||
add_gpu_blit_shader(gpu_imgui_hdr gpu_video_out_overlay.frag -DIMGUI_HDR=1)
|
||||
list(APPEND kyty_emulator_src ${gpu_blit_shader_headers})
|
||||
|
||||
list(APPEND kyty_emulator_src
|
||||
|
|
@ -428,6 +432,14 @@ add_executable(avplayer_file_tests EXCLUDE_FROM_ALL
|
|||
target_link_libraries(avplayer_file_tests common FFmpeg::ffmpeg ZArchive::zarchive)
|
||||
target_include_directories(avplayer_file_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(vdecsw_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/VdecswTests.cpp"
|
||||
"${KYTY_SOURCE_DIR}/libs/videoDec2Decoder.cpp"
|
||||
"${KYTY_SOURCE_DIR}/loader/timer.cpp"
|
||||
)
|
||||
target_link_libraries(vdecsw_tests common FFmpeg::ffmpeg)
|
||||
target_include_directories(vdecsw_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(bit_array_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/BitArrayTests.cpp"
|
||||
)
|
||||
|
|
@ -618,6 +630,7 @@ if(BUILD_TESTING)
|
|||
add_test(NAME archive_file COMMAND $<TARGET_FILE:archive_file_tests>)
|
||||
add_test(NAME trophy_system COMMAND $<TARGET_FILE:trophy_system_tests>)
|
||||
add_test(NAME avplayer_file COMMAND $<TARGET_FILE:avplayer_file_tests>)
|
||||
add_test(NAME vdecsw COMMAND $<TARGET_FILE:vdecsw_tests>)
|
||||
add_test(NAME bit_array COMMAND $<TARGET_FILE:bit_array_tests>)
|
||||
add_test(NAME lru_cache COMMAND $<TARGET_FILE:lru_cache_tests>)
|
||||
add_test(NAME shader_vertex_metadata COMMAND $<TARGET_FILE:shader_vertex_metadata_tests>)
|
||||
|
|
@ -694,6 +707,7 @@ if(BUILD_TESTING)
|
|||
archive_file_tests
|
||||
trophy_system_tests
|
||||
avplayer_file_tests
|
||||
vdecsw_tests
|
||||
bit_array_tests
|
||||
lru_cache_tests
|
||||
shader_vertex_metadata_tests
|
||||
|
|
|
|||
|
|
@ -72,6 +72,10 @@ bool FullscreenEnabled() {
|
|||
return g_config->fullscreen_enabled;
|
||||
}
|
||||
|
||||
bool HdrEnabled() {
|
||||
return g_config->hdr_enabled;
|
||||
}
|
||||
|
||||
bool HideCursorEnabled() {
|
||||
return g_config->hide_cursor_enabled;
|
||||
}
|
||||
|
|
@ -96,6 +100,10 @@ bool VulkanValidationEnabled() {
|
|||
return g_config->vulkan_validation_enabled;
|
||||
}
|
||||
|
||||
bool IsNetworkConnected() {
|
||||
return g_config->network_connected;
|
||||
}
|
||||
|
||||
bool ShaderValidationEnabled() {
|
||||
return g_config->shader_validation_enabled;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,10 +53,12 @@ struct ConfigOptions {
|
|||
PresentMode present_mode = PresentMode::Mailbox;
|
||||
int32_t gpu_index = -1;
|
||||
bool fullscreen_enabled = false;
|
||||
bool hdr_enabled = false;
|
||||
bool hide_cursor_enabled = false;
|
||||
bool splash_enabled = true;
|
||||
bool vr_enabled = false;
|
||||
bool amd_cpu_enabled = false;
|
||||
bool network_connected = false;
|
||||
uint32_t vblank_frequency = 60;
|
||||
uint32_t console_language = DEFAULT_CONSOLE_LANGUAGE;
|
||||
bool vulkan_validation_enabled = false;
|
||||
|
|
@ -83,7 +85,8 @@ struct ConfigOptions {
|
|||
bool red_zone_protection_enabled = false;
|
||||
#endif
|
||||
Keymap keymap;
|
||||
std::vector<std::string> guest_arguments;
|
||||
// Ordinary boots need a non-null argv[0]; --guest-exec supplies the caller's arguments.
|
||||
std::vector<std::string> guest_arguments {""};
|
||||
};
|
||||
|
||||
void Load(const ConfigOptions& cfg);
|
||||
|
|
@ -99,9 +102,11 @@ uint32_t GetControllerVibrationIntensity();
|
|||
PresentMode GetPresentMode();
|
||||
int32_t GetGpuIndex();
|
||||
bool FullscreenEnabled();
|
||||
bool HdrEnabled();
|
||||
bool HideCursorEnabled();
|
||||
bool VrEnabled();
|
||||
bool AmdCpuEnabled();
|
||||
bool IsNetworkConnected();
|
||||
uint32_t GetVblankFrequency();
|
||||
uint32_t GetConsoleLanguage();
|
||||
bool VulkanValidationEnabled();
|
||||
|
|
|
|||
|
|
@ -8,10 +8,6 @@
|
|||
#include <condition_variable> // IWYU pragma: keep
|
||||
#include <mutex>
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS && KYTY_COMPILER == KYTY_COMPILER_CLANG
|
||||
#define KYTY_WIN_CS
|
||||
#endif
|
||||
|
||||
// macOS has no clock_nanosleep.
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS && !defined(__APPLE__)
|
||||
#define KYTY_POSIX_HIGH_RES_SLEEP
|
||||
|
|
@ -22,7 +18,7 @@
|
|||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
#include <windows.h> // IWYU pragma: keep
|
||||
// IWYU pragma: no_include <winbase.h>
|
||||
constexpr DWORD KYTY_CS_SPIN_COUNT = 4000;
|
||||
|
|
@ -133,7 +129,7 @@ namespace Common {
|
|||
using thread_id_t = std::thread::id;
|
||||
|
||||
struct MutexPrivate {
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
MutexPrivate() { InitializeCriticalSectionAndSpinCount(&m_cs, KYTY_CS_SPIN_COUNT); }
|
||||
~MutexPrivate() { DeleteCriticalSection(&m_cs); }
|
||||
KYTY_CLASS_NO_COPY(MutexPrivate);
|
||||
|
|
@ -144,7 +140,7 @@ struct MutexPrivate {
|
|||
};
|
||||
|
||||
struct CondVarPrivate {
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
CondVarPrivate() {
|
||||
static auto func = ResolveInitializeConditionVariable();
|
||||
EXIT_NOT_IMPLEMENTED(func == nullptr);
|
||||
|
|
@ -216,7 +212,7 @@ void Thread::Detach() {
|
|||
}
|
||||
|
||||
void Thread::SleepMicro(uint32_t micros) {
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
SleepHighResolution100ns(static_cast<uint64_t>(micros) * 10);
|
||||
#elif defined(KYTY_POSIX_HIGH_RES_SLEEP)
|
||||
SleepHighResolutionNanos(static_cast<uint64_t>(micros) * 1000);
|
||||
|
|
@ -226,7 +222,7 @@ void Thread::SleepMicro(uint32_t micros) {
|
|||
}
|
||||
|
||||
void Thread::SleepNano(uint64_t nanos) {
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
SleepHighResolution100ns((nanos + 99) / 100);
|
||||
#elif defined(KYTY_POSIX_HIGH_RES_SLEEP)
|
||||
SleepHighResolutionNanos(nanos);
|
||||
|
|
@ -262,7 +258,7 @@ Mutex::~Mutex() {
|
|||
}
|
||||
|
||||
void Mutex::Lock() {
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
EnterCriticalSection(&m_mutex->m_cs);
|
||||
#else
|
||||
m_mutex->m_mutex.lock();
|
||||
|
|
@ -270,7 +266,7 @@ void Mutex::Lock() {
|
|||
}
|
||||
|
||||
void Mutex::Unlock() {
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
LeaveCriticalSection(&m_mutex->m_cs);
|
||||
#else
|
||||
m_mutex->m_mutex.unlock();
|
||||
|
|
@ -278,7 +274,7 @@ void Mutex::Unlock() {
|
|||
}
|
||||
|
||||
bool Mutex::TryLock() {
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
return (TryEnterCriticalSection(&m_mutex->m_cs) != 0);
|
||||
#else
|
||||
return m_mutex->m_mutex.try_lock();
|
||||
|
|
@ -292,7 +288,7 @@ CondVar::~CondVar() {
|
|||
}
|
||||
|
||||
void CondVar::Wait(Mutex* mutex) {
|
||||
#ifndef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
std::unique_lock<std::recursive_mutex> cpp_lock(mutex->m_mutex->m_mutex, std::adopt_lock_t());
|
||||
#endif
|
||||
auto poll_callback = [&] {
|
||||
|
|
@ -300,7 +296,7 @@ void CondVar::Wait(Mutex* mutex) {
|
|||
if (callback == nullptr) {
|
||||
return;
|
||||
}
|
||||
#if defined(KYTY_WIN_CS)
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
LeaveCriticalSection(&mutex->m_mutex->m_cs);
|
||||
callback();
|
||||
EnterCriticalSection(&mutex->m_mutex->m_cs);
|
||||
|
|
@ -310,7 +306,7 @@ void CondVar::Wait(Mutex* mutex) {
|
|||
cpp_lock.lock();
|
||||
#endif
|
||||
};
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
static auto func = ResolveSleepConditionVariableCS();
|
||||
EXIT_NOT_IMPLEMENTED(func == nullptr);
|
||||
if (g_cond_wait_poll_callback == nullptr) {
|
||||
|
|
@ -340,10 +336,10 @@ void CondVar::SetWaitPollCallback(wait_poll_func_t callback) {
|
|||
|
||||
bool CondVar::WaitFor(Mutex* mutex, uint32_t micros) {
|
||||
bool ok = false;
|
||||
#ifndef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
std::unique_lock<std::recursive_mutex> cpp_lock(mutex->m_mutex->m_mutex, std::adopt_lock_t());
|
||||
#endif
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
static auto func = ResolveSleepConditionVariableCS();
|
||||
EXIT_NOT_IMPLEMENTED(func == nullptr);
|
||||
ok = !(func(&m_cond_var->m_cv, &mutex->m_mutex->m_cs, (micros < 1000 ? 1 : micros / 1000)) ==
|
||||
|
|
@ -358,7 +354,7 @@ bool CondVar::WaitFor(Mutex* mutex, uint32_t micros) {
|
|||
}
|
||||
|
||||
void CondVar::Signal() {
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
static auto func = ResolveWakeConditionVariable();
|
||||
EXIT_NOT_IMPLEMENTED(func == nullptr);
|
||||
func(&m_cond_var->m_cv);
|
||||
|
|
@ -368,7 +364,7 @@ void CondVar::Signal() {
|
|||
}
|
||||
|
||||
void CondVar::SignalAll() {
|
||||
#ifdef KYTY_WIN_CS
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
static auto func = ResolveWakeAllConditionVariable();
|
||||
EXIT_NOT_IMPLEMENTED(func == nullptr);
|
||||
func(&m_cond_var->m_cv);
|
||||
|
|
|
|||
|
|
@ -1934,13 +1934,7 @@ KYTY_CP_OP_PARSER(CpOpIndirectBuffer) {
|
|||
|
||||
auto* indirect_buffer =
|
||||
reinterpret_cast<const uint32_t*>(buffer[0] | (static_cast<uint64_t>(buffer[1]) << 32u));
|
||||
uint32_t indirect_num_dw = control & 0xfffffu;
|
||||
static std::atomic<uint32_t> indirect_log_count {0};
|
||||
if (indirect_log_count.fetch_add(1) < 128) {
|
||||
LOGF("\t indirect buffer: addr=0x%016" PRIx64 ", num_dw=%" PRIu32 ", control=0x%08" PRIx32
|
||||
"\n",
|
||||
reinterpret_cast<uint64_t>(indirect_buffer), indirect_num_dw, control);
|
||||
}
|
||||
const uint32_t indirect_num_dw = control & 0xfffffu;
|
||||
|
||||
if (indirect_num_dw == 0) {
|
||||
return 3;
|
||||
|
|
@ -2148,27 +2142,23 @@ KYTY_CP_OP_PARSER(CpOpIndirectUcRegs) {
|
|||
KYTY_CP_OP_PARSER(CpOpMarker) {
|
||||
KYTY_PROFILER_FUNCTION();
|
||||
|
||||
// EXIT_NOT_IMPLEMENTED(cmd_id != 0xC0001000);
|
||||
|
||||
uint32_t id = buffer[0] & 0xfffu;
|
||||
uint32_t len_dw = ((cmd_id >> 16u) & 0x3fffu);
|
||||
|
||||
switch (id) {
|
||||
case 0x0: break;
|
||||
case 0x4: cp.SetUserDataMarker(HW::UserSgprType::Vsharp); break;
|
||||
case 0xd: cp.SetUserDataMarker(HW::UserSgprType::Region); break;
|
||||
case 0x777: {
|
||||
switch (buffer[0]) {
|
||||
case 0x68750004: cp.SetUserDataMarker(HW::UserSgprType::Vsharp); break;
|
||||
case 0x6875000d: cp.SetUserDataMarker(HW::UserSgprType::Region); break;
|
||||
case 0x68750777: {
|
||||
cp.Flip();
|
||||
break;
|
||||
}
|
||||
case 0x778: {
|
||||
case 0x68750778: {
|
||||
auto* addr =
|
||||
reinterpret_cast<void*>(buffer[1] | (static_cast<uint64_t>(buffer[2]) << 32u));
|
||||
uint32_t value = buffer[3];
|
||||
cp.Flip(addr, value);
|
||||
break;
|
||||
}
|
||||
case 0x781: {
|
||||
case 0x68750781: {
|
||||
auto* addr =
|
||||
reinterpret_cast<void*>(buffer[1] | (static_cast<uint64_t>(buffer[2]) << 32u));
|
||||
uint32_t value = buffer[3];
|
||||
|
|
@ -2177,7 +2167,8 @@ KYTY_CP_OP_PARSER(CpOpMarker) {
|
|||
cp.FlipWithInterrupt(eop_event_type, cache_action, addr, value);
|
||||
break;
|
||||
}
|
||||
default: EXIT("unknown marker at %05" PRIx32 ": 0x%" PRIx32 "\n", num_dw - dw, id);
|
||||
// NOP payloads are caller-owned data; only complete known marker tags execute.
|
||||
default: break;
|
||||
}
|
||||
|
||||
return len_dw + 1;
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ constexpr FormatInfo kFormatInfo[] = {
|
|||
{BufferFormat::k16_16UInt, 4, 0, 4, true, true},
|
||||
{BufferFormat::k16_16SInt, 4, 0, 4, true, false, true},
|
||||
{BufferFormat::k16_16Float, 4, 0, 4, true, false},
|
||||
{BufferFormat::k11_11_10UNorm, 4, 0, 0, true, false},
|
||||
{BufferFormat::k11_11_10UInt, 4, 0, 4, true, true},
|
||||
{BufferFormat::k11_11_10Float, 4, 0, 4, true, false},
|
||||
{BufferFormat::k10_11_11Float, 4, 0, 4, true, false},
|
||||
|
|
@ -237,6 +238,7 @@ BufferFormat RemapTextureFormat(BufferFormat format) {
|
|||
switch (format) {
|
||||
case BufferFormat::k8UScaled: return BufferFormat::k8UNorm;
|
||||
case BufferFormat::k8_8UScaled: return BufferFormat::k8_8UNorm;
|
||||
case BufferFormat::k11_11_10UNorm:
|
||||
case BufferFormat::k11_11_10UInt: return BufferFormat::k32UInt;
|
||||
default: return format;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -576,12 +576,7 @@ bool GuestGpu::Process(Submission& submission) {
|
|||
}
|
||||
}
|
||||
if (progressed) {
|
||||
if (complete) {
|
||||
m_renderer.RunGarbageCollector();
|
||||
}
|
||||
cp.BufferFlush();
|
||||
} else if (complete) {
|
||||
m_renderer.RunGarbageCollector();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -602,20 +597,16 @@ bool GuestGpu::Process(Submission& submission) {
|
|||
complete = cp.Process(submission.command_execution, submission.commands) ==
|
||||
Pm4ProcessResult::Complete;
|
||||
if (submission.command_execution.MadeProgress()) {
|
||||
if (complete) {
|
||||
m_renderer.RunGarbageCollector();
|
||||
}
|
||||
cp.BufferFlush();
|
||||
} else if (complete) {
|
||||
m_renderer.RunGarbageCollector();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SubmissionType::FlipPreparation:
|
||||
m_renderer.RunGarbageCollector();
|
||||
cp.PrepareCpuFlip(submission.flip_request_id);
|
||||
break;
|
||||
case SubmissionType::SuspendPoint:
|
||||
// Age caches at the frame boundary, not once per command list.
|
||||
m_renderer.RunGarbageCollector();
|
||||
cp.EmitGlobalBarrier();
|
||||
m_renderer.GetCommandScheduler().DeferPriorityOperation(
|
||||
[ready = m_suspend_point_ready] { ready->release(); });
|
||||
|
|
@ -1072,8 +1063,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
case 0x03: with_interrupt = false; break;
|
||||
case 0x01:
|
||||
if (!IsAsyncComputeQueue()) {
|
||||
Sync::TriggerEopEventAtEndOfPipe(command, m_interrupt_event_id,
|
||||
interrupt_context_id);
|
||||
TriggerEopEventAtEndOfPipe(interrupt_context_id);
|
||||
return;
|
||||
}
|
||||
with_interrupt = true;
|
||||
|
|
@ -1082,24 +1072,15 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
default: EXIT("unknown interrupt_selector\n");
|
||||
}
|
||||
|
||||
auto write32 = [&](bool with_writeback) {
|
||||
auto write32 = [&] {
|
||||
auto* dst = static_cast<uint32_t*>(dst_gpu_addr);
|
||||
auto data = static_cast<uint32_t>(value);
|
||||
EXIT_IF(dst == nullptr);
|
||||
(void)command.Handle();
|
||||
std::memcpy(dst, &data, sizeof(data));
|
||||
|
||||
if (with_interrupt) {
|
||||
if (with_writeback) {
|
||||
Sync::WriteAtEndOfPipeWithInterruptWriteBack32(m_submit_id, command, dst,
|
||||
data, m_interrupt_event_id,
|
||||
interrupt_context_id);
|
||||
} else {
|
||||
Sync::WriteAtEndOfPipeWithInterrupt32(m_submit_id, command, dst, data,
|
||||
m_interrupt_event_id, interrupt_context_id);
|
||||
}
|
||||
} else if (with_writeback) {
|
||||
Sync::WriteAtEndOfPipeWithWriteBack32(m_submit_id, command, dst, data);
|
||||
} else {
|
||||
Sync::WriteAtEndOfPipe32(m_submit_id, command, dst, data);
|
||||
TriggerEopEventAtEndOfPipe(interrupt_context_id);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1111,15 +1092,13 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
SynchronizeGpu();
|
||||
Sync::ReadGds(*m_renderer.GetBufferCache().GetGdsBuffer(), dst, value & 0xffffu,
|
||||
value >> 16u);
|
||||
Sync::WriteAtEndOfPipeGds32(m_submit_id, command, dst, value & 0xffffu,
|
||||
value >> 16u);
|
||||
if (with_interrupt) {
|
||||
m_renderer.TriggerInterrupt(m_interrupt_event_id, interrupt_context_id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if (eop_event_type == 0x04 && cache_action == 0x00 && event_index == 0x05) {
|
||||
write32(false);
|
||||
write32();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
|
@ -1128,8 +1107,8 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
if constexpr (sizeof(T) == sizeof(uint32_t)) {
|
||||
if (event_write_source == 0x02 && eop_event_type == 0x2f && event_index == 0x06) {
|
||||
switch (cache_action) {
|
||||
case 0x00: write32(false); return;
|
||||
case 0x38: write32(true); return;
|
||||
case 0x00:
|
||||
case 0x38: write32(); return;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
|
@ -1137,25 +1116,14 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
if (event_write_source == 0x04) {
|
||||
value = Sync::ReadReferenceClock();
|
||||
}
|
||||
auto write64 = [&](bool with_writeback) {
|
||||
auto write64 = [&] {
|
||||
auto* dst = static_cast<uint64_t*>(dst_gpu_addr);
|
||||
EXIT_IF(dst == nullptr);
|
||||
(void)command.Handle();
|
||||
std::memcpy(dst, &value, sizeof(value));
|
||||
|
||||
if (with_interrupt) {
|
||||
if (with_writeback) {
|
||||
Sync::WriteAtEndOfPipeWithInterruptWriteBack64(
|
||||
m_submit_id, command, dst, value, m_interrupt_event_id,
|
||||
interrupt_context_id);
|
||||
} else {
|
||||
Sync::WriteAtEndOfPipeWithInterrupt64(m_submit_id, command, dst,
|
||||
value, m_interrupt_event_id,
|
||||
interrupt_context_id);
|
||||
}
|
||||
} else if (with_writeback) {
|
||||
Sync::WriteAtEndOfPipeWithWriteBack64(m_submit_id, command, dst,
|
||||
value);
|
||||
} else {
|
||||
Sync::WriteAtEndOfPipe64(m_submit_id, command, dst, value);
|
||||
TriggerEopEventAtEndOfPipe(interrupt_context_id);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1164,7 +1132,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
switch (eop_event_type) {
|
||||
case 0x04:
|
||||
if (event_index == 0x05) {
|
||||
write64(false);
|
||||
write64();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
|
@ -1172,7 +1140,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
case 0x28:
|
||||
case 0x2f:
|
||||
if (event_index == 0x00) {
|
||||
write64(false);
|
||||
write64();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
|
@ -1180,7 +1148,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
case 0x2d:
|
||||
case 0x30:
|
||||
if (event_index == 0x00 && !with_interrupt) {
|
||||
write64(false);
|
||||
write64();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
|
@ -1195,20 +1163,20 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
if (((eop_event_type == 0x04 || eop_event_type == 0x28) &&
|
||||
event_index == 0x05) ||
|
||||
(event_index == 0x00)) {
|
||||
write64(true);
|
||||
write64();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 0x2b:
|
||||
case 0x2d:
|
||||
if (event_index == 0x00 && !with_interrupt) {
|
||||
write64(true);
|
||||
write64();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 0x2f:
|
||||
if (event_index == 0x06 && !with_interrupt) {
|
||||
write64(true);
|
||||
write64();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
|
@ -1217,7 +1185,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
|||
break;
|
||||
case 0x3b:
|
||||
if (eop_event_type == 0x04 && event_index == 0x05 && with_interrupt) {
|
||||
write64(true);
|
||||
write64();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
|
@ -1270,7 +1238,11 @@ void CommandProcessor::EmitGlobalBarrier() {
|
|||
}
|
||||
|
||||
void CommandProcessor::TriggerEopEventAtEndOfPipe(uint32_t interrupt_context_id) {
|
||||
Sync::TriggerEopEventAtEndOfPipe(CurrentBuffer(), m_interrupt_event_id, interrupt_context_id);
|
||||
(void)CurrentBuffer().Handle();
|
||||
GetScheduler().DeferPriorityOperation(
|
||||
[&renderer = m_renderer, event_id = m_interrupt_event_id, interrupt_context_id] {
|
||||
renderer.TriggerInterrupt(event_id, interrupt_context_id);
|
||||
});
|
||||
}
|
||||
|
||||
void CommandProcessor::TriggerEvent(uint32_t event_type, uint32_t event_index,
|
||||
|
|
@ -1360,8 +1332,9 @@ void CommandProcessor::Flip() {
|
|||
auto& command = CurrentBuffer();
|
||||
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
|
||||
m_flip.flip_arg);
|
||||
Sync::WriteAtEndOfPipeOnlyFlip(m_submit_id, command, m_flip.handle, m_flip.index,
|
||||
m_flip.flip_mode, m_flip.flip_arg, request);
|
||||
(void)command.Handle();
|
||||
GetScheduler().DeferPriorityOperation(
|
||||
[&renderer = m_renderer, request] { renderer.GetVideoOut().CompleteFlip(request); });
|
||||
GetScheduler().Flush();
|
||||
}
|
||||
|
||||
|
|
@ -1375,12 +1348,13 @@ void CommandProcessor::Flip(void* dst_gpu_addr, uint32_t value) {
|
|||
reinterpret_cast<uint64_t>(dst_gpu_addr), value);
|
||||
}
|
||||
|
||||
EXIT_IF(dst_gpu_addr == nullptr);
|
||||
std::memcpy(dst_gpu_addr, &value, sizeof(value));
|
||||
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
|
||||
m_flip.flip_arg);
|
||||
Sync::WriteAtEndOfPipeWithFlip32(m_submit_id, command, static_cast<uint32_t*>(dst_gpu_addr),
|
||||
value, m_flip.handle, m_flip.index, m_flip.flip_mode,
|
||||
m_flip.flip_arg, request);
|
||||
(void)command.Handle();
|
||||
GetScheduler().DeferPriorityOperation(
|
||||
[&renderer = m_renderer, request] { renderer.GetVideoOut().CompleteFlip(request); });
|
||||
GetScheduler().Flush();
|
||||
}
|
||||
|
||||
|
|
@ -1400,12 +1374,16 @@ void CommandProcessor::FlipWithInterrupt(uint32_t eop_event_type, uint32_t cache
|
|||
if (eop_event_type != 0x00000004 || cache_action != 0x00000038) {
|
||||
EXIT("unknown event type\n");
|
||||
}
|
||||
EXIT_IF(dst_gpu_addr == nullptr);
|
||||
std::memcpy(dst_gpu_addr, &value, sizeof(value));
|
||||
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
|
||||
m_flip.flip_arg);
|
||||
Sync::WriteAtEndOfPipeWithInterruptWriteBackFlip32(
|
||||
m_submit_id, command, static_cast<uint32_t*>(dst_gpu_addr), value, m_flip.handle,
|
||||
m_flip.index, m_flip.flip_mode, m_flip.flip_arg, request, m_interrupt_event_id);
|
||||
(void)command.Handle();
|
||||
GetScheduler().DeferPriorityOperation(
|
||||
[&renderer = m_renderer, event_id = m_interrupt_event_id, request] {
|
||||
renderer.GetVideoOut().CompleteFlip(request);
|
||||
renderer.TriggerInterrupt(event_id, 0);
|
||||
});
|
||||
GetScheduler().Flush();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ struct GraphicContext {
|
|||
// bool fp64_denorm_preserve = false; // Temporarily disabled.
|
||||
bool attachment_feedback_loop_enabled = false;
|
||||
bool provoking_vertex_last_enabled = false;
|
||||
bool depth_color_copy_enabled = false;
|
||||
bool maintenance8_enabled = false;
|
||||
bool supports_block_texel_view = false;
|
||||
bool mesh_shader_enabled = false;
|
||||
vk::PhysicalDeviceMeshShaderPropertiesEXT mesh_shader_properties = {};
|
||||
|
|
|
|||
|
|
@ -27,6 +27,26 @@ namespace {
|
|||
constexpr uint64_t MiB = 1024 * 1024;
|
||||
constexpr uint64_t GdsBufferSize = 64 * 1024;
|
||||
|
||||
std::pair<Buffer*, uint64_t> UploadMemory(GraphicContext& graphics, CommandScheduler& scheduler,
|
||||
StreamBuffer& staging, uint64_t size, uint64_t alignment,
|
||||
vk::BufferUsageFlags usage, const auto& write) {
|
||||
const auto [mapped, offset] = staging.Map(size, alignment);
|
||||
if (mapped != nullptr) {
|
||||
write(mapped, offset);
|
||||
staging.Commit();
|
||||
return {&staging, offset};
|
||||
}
|
||||
|
||||
// Large transfers use the same one-time host allocation as ordinary buffer uploads.
|
||||
auto temporary = std::make_unique<Buffer>(graphics, scheduler, MemoryUsage::Upload, 0,
|
||||
usage, size);
|
||||
write(temporary->Mapped().data(), 0);
|
||||
temporary->Flush(0, size);
|
||||
auto* buffer = temporary.get();
|
||||
scheduler.DeferOperation([owner = std::move(temporary)]() mutable { owner.reset(); });
|
||||
return {buffer, 0};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void BufferCache::WriteDataBuffer(Buffer& buffer, uint64_t address, const void* source,
|
||||
|
|
@ -447,28 +467,17 @@ vk::Buffer BufferCache::UploadCopies(Buffer& buffer, std::span<vk::BufferCopy> c
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
auto [mapped, base_offset] = m_staging_buffer.Map(total_size, 4);
|
||||
if (mapped != nullptr) {
|
||||
const auto source = UploadMemory(m_graphics, m_scheduler, m_staging_buffer, total_size, 4,
|
||||
vk::BufferUsageFlagBits::eTransferSrc, [&](uint8_t* mapped, uint64_t base_offset) {
|
||||
for (auto& copy: copies) {
|
||||
const auto address = buffer.CpuAddress() + copy.dstOffset;
|
||||
std::memcpy(mapped + copy.srcOffset, reinterpret_cast<const void*>(address), copy.size);
|
||||
if (!LibKernel::Memory::CopySparseMemory(address, mapped + copy.srcOffset, copy.size)) {
|
||||
EXIT("BufferCache: failed to read sparse guest buffer memory\n");
|
||||
}
|
||||
copy.srcOffset += base_offset;
|
||||
}
|
||||
m_staging_buffer.Commit();
|
||||
return m_staging_buffer.Handle();
|
||||
}
|
||||
|
||||
auto temporary = std::make_unique<Buffer>(m_graphics, m_scheduler, MemoryUsage::Upload, 0,
|
||||
vk::BufferUsageFlagBits::eTransferSrc, total_size);
|
||||
for (const auto& copy: copies) {
|
||||
const auto address = buffer.CpuAddress() + copy.dstOffset;
|
||||
std::memcpy(temporary->Mapped().data() + copy.srcOffset,
|
||||
reinterpret_cast<const void*>(address), copy.size);
|
||||
}
|
||||
temporary->Flush(0, total_size);
|
||||
const auto handle = temporary->Handle();
|
||||
m_scheduler.DeferOperation([owner = std::move(temporary)]() mutable { owner.reset(); });
|
||||
return handle;
|
||||
});
|
||||
return source.first->Handle();
|
||||
}
|
||||
|
||||
std::pair<Buffer*, uint64_t> BufferCache::ObtainBuffer(uint64_t vaddr, uint64_t size,
|
||||
|
|
@ -486,7 +495,9 @@ std::pair<Buffer*, uint64_t> BufferCache::ObtainBuffer(uint64_t vaddr, uint64_t
|
|||
m_graphics.physical_device_properties.limits.minUniformBufferOffsetAlignment, 1);
|
||||
auto [mapped, offset] = m_stream_buffer.Map(size, alignment, false);
|
||||
if (mapped != nullptr) {
|
||||
std::memcpy(mapped, reinterpret_cast<const void*>(vaddr), size);
|
||||
if (!LibKernel::Memory::CopySparseMemory(vaddr, mapped, size)) {
|
||||
EXIT("BufferCache: failed to read sparse guest buffer memory\n");
|
||||
}
|
||||
m_stream_buffer.Commit();
|
||||
return {&m_stream_buffer, offset};
|
||||
}
|
||||
|
|
@ -521,12 +532,13 @@ std::pair<Buffer*, uint64_t> BufferCache::ObtainBufferForImage(uint64_t vaddr, u
|
|||
return ObtainBuffer(vaddr, size, false, false);
|
||||
}
|
||||
|
||||
auto [staging, stage_offset] = m_staging_buffer.Map(size, 16);
|
||||
if (staging == nullptr || !Libs::LibKernel::Memory::TryReadSparseBacking(vaddr, staging, size)) {
|
||||
EXIT("BufferCache: failed to read mapped guest image backing\n");
|
||||
}
|
||||
m_staging_buffer.Commit();
|
||||
return {&m_staging_buffer, stage_offset};
|
||||
return UploadMemory(m_graphics, m_scheduler, m_staging_buffer, size, 16,
|
||||
vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eStorageBuffer,
|
||||
[&](uint8_t* mapped, uint64_t) {
|
||||
if (!Libs::LibKernel::Memory::TryReadSparseBacking(vaddr, mapped, size)) {
|
||||
EXIT("BufferCache: failed to read mapped guest image backing\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void BufferCache::FillBuffer(uint64_t vaddr, uint64_t size, uint32_t value, bool is_gds) {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
#include <mutex>
|
||||
#include <span>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vulkan/vulkan_format_traits.hpp>
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
|
@ -36,7 +37,9 @@ constexpr uint64_t NumFramesBeforeRemoval = 32;
|
|||
[[nodiscard]] bool DecodeColorClear(const TextureCache::ImageDesc& desc, uint8_t code,
|
||||
vk::ClearColorValue& clear) {
|
||||
const auto& metadata = desc.info.metadata;
|
||||
const auto format = desc.view_info.format;
|
||||
const auto format = desc.info.IsDepth()
|
||||
? TextureGetSurfaceFormatInfo(desc.info.guest_format).vk_format
|
||||
: desc.view_info.format;
|
||||
const bool cmask = metadata.kind == ImageMetadataKind::Cmask;
|
||||
if (cmask ? code == 0 : code == 0x20) {
|
||||
// Register clears belong to the color buffer; the texture pipe cannot decode them.
|
||||
|
|
@ -249,14 +252,11 @@ void TextureCache::DeleteImage(ImageId id) {
|
|||
if (image == nullptr || !image->registered) {
|
||||
return;
|
||||
}
|
||||
if (!image->depth_id) {
|
||||
std::vector<ImageId> associations;
|
||||
m_slot_images.ForEach([&](ImageId candidate, const Image& associated) {
|
||||
if (associated.depth_id == id) {
|
||||
associations.push_back(candidate);
|
||||
}
|
||||
});
|
||||
for (const auto association: associations) {
|
||||
if (image->depth_id) {
|
||||
std::erase(m_slot_images[image->depth_id].stencil_ids, id);
|
||||
} else {
|
||||
// Detach first: deleting each association also unlinks it from its depth owner.
|
||||
for (const auto association: std::exchange(image->stencil_ids, {})) {
|
||||
FreeImage(association);
|
||||
}
|
||||
}
|
||||
|
|
@ -297,9 +297,6 @@ void TextureCache::TouchImage(Image& image) {
|
|||
|
||||
void TextureCache::MarkAsMaybeDirty(ImageId id, Image& image) {
|
||||
image.MarkMaybeCpuDirty();
|
||||
if (image.NeedsMaybeCpuHash()) {
|
||||
image.SetMaybeCpuHash(image.HashGuestEdges());
|
||||
}
|
||||
UntrackImage(id);
|
||||
}
|
||||
|
||||
|
|
@ -631,6 +628,9 @@ void TextureCache::CopyImage(ImageId destination_id, ImageId source_id) {
|
|||
void TextureCache::CopyImageMip(ImageId destination_id, ImageId source_id, uint32_t mip,
|
||||
uint32_t layer) {
|
||||
RefreshCopySource(source_id);
|
||||
if (m_slot_images[destination_id].IsBufferModified()) {
|
||||
RefreshImage(destination_id);
|
||||
}
|
||||
auto& destination = m_slot_images[destination_id];
|
||||
auto& source = m_slot_images[source_id];
|
||||
TrackImage(destination_id);
|
||||
|
|
@ -751,11 +751,63 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingTyp
|
|||
return replacement_id;
|
||||
}
|
||||
|
||||
namespace BcAlias {
|
||||
|
||||
bool Matches(const ImageInfo& requested, const ImageInfo& cached) {
|
||||
return requested.guest_format != cached.guest_format &&
|
||||
requested.IsBlock() != cached.IsBlock() &&
|
||||
requested.tile_mode == cached.tile_mode &&
|
||||
requested.bytes_per_block == cached.bytes_per_block &&
|
||||
requested.samples == 1 && cached.samples == 1 &&
|
||||
(requested.data != cached.data || requested.resources != cached.resources ||
|
||||
requested.BlockExtent() != cached.BlockExtent()) &&
|
||||
!TextureCache::SameBacking(cached, requested, false);
|
||||
}
|
||||
|
||||
bool Publish(TextureCache& cache, Image& image) {
|
||||
if (!cache.CanDownload(image)) {
|
||||
return false;
|
||||
}
|
||||
// ObtainBuffer preserves padding and records ownership before the GPU-only transfer.
|
||||
auto [buffer, offset] =
|
||||
cache.m_buffer_cache.ObtainBuffer(image.info.data.address, image.info.data.size, true);
|
||||
cache.DownloadImage(image, *buffer, offset, image.info.data.size, image.info.resources.levels);
|
||||
image.ClearGpuModified();
|
||||
image.MarkBufferModified();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Resolve(TextureCache& cache, ImageId cached_id, ImageId merged_id) {
|
||||
auto& cached = cache.m_slot_images[cached_id];
|
||||
auto* merged = merged_id ? &cache.m_slot_images[merged_id] : nullptr;
|
||||
// PS5 raw block images can span several packed BC mips. Transfer their complete
|
||||
// guest bytes, including tail padding, before retiring the incompatible backing.
|
||||
if (cached.SafeToDownload()) {
|
||||
if (merged && merged->SafeToDownload() && !Publish(cache, *merged)) {
|
||||
return;
|
||||
}
|
||||
if (!Publish(cache, cached)) {
|
||||
return;
|
||||
}
|
||||
if (merged) {
|
||||
merged->MarkBufferModified();
|
||||
}
|
||||
}
|
||||
cached.binding.needs_rebind |= cached.binding.is_bound || cached.binding.is_target;
|
||||
cache.FreeImage(cached_id);
|
||||
}
|
||||
|
||||
} // namespace BcAlias
|
||||
|
||||
TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& requested,
|
||||
BindingType binding, ImageId cached_id,
|
||||
ImageId merged_id) {
|
||||
auto owner = m_slot_images.try_get(cached_id);
|
||||
if (owner == nullptr) {
|
||||
if (owner == nullptr || !owner->registered) {
|
||||
return {merged_id};
|
||||
}
|
||||
if (BcAlias::Matches(requested, owner->info) && !owner->IsCpuDirty()) {
|
||||
BcAlias::Resolve(*this, cached_id, merged_id);
|
||||
return {merged_id};
|
||||
}
|
||||
auto& cached = *owner;
|
||||
|
|
@ -1059,8 +1111,15 @@ void TextureCache::MaterializeColorClear(ImageId id, const ImageDesc& desc,
|
|||
}
|
||||
{
|
||||
std::scoped_lock lock {m_lock};
|
||||
ClearImage(m_scheduler.Current(), id, view.format,
|
||||
{vk::ImageAspectFlagBits::eColor, view.base_level, view.level_count,
|
||||
const auto& image = m_slot_images[id];
|
||||
const bool depth = image.info.IsDepth();
|
||||
if (depth) {
|
||||
const auto value = clear.color.float32[0];
|
||||
clear.depthStencil = {value, 0u};
|
||||
}
|
||||
ClearImage(m_scheduler.Current(), id, depth ? image.backing.format : view.format,
|
||||
{depth ? vk::ImageAspectFlagBits::eDepth : vk::ImageAspectFlagBits::eColor,
|
||||
view.base_level, view.level_count,
|
||||
image_first + slice, 1}, clear);
|
||||
}
|
||||
// Publish the conversion's expanded keys without treating them as guest writes
|
||||
|
|
@ -1082,12 +1141,7 @@ void TextureCache::RefreshImage(ImageId id) {
|
|||
}
|
||||
TrackImage(id);
|
||||
if (image.IsMaybeCpuDirty()) {
|
||||
const auto hash = image.HashGuestEdges();
|
||||
if (image.NeedsMaybeCpuHash()) {
|
||||
image.SetMaybeCpuHash(hash);
|
||||
return;
|
||||
}
|
||||
(void)image.ResolveMaybeCpuHash(hash);
|
||||
(void)image.ResolveMaybeCpuHash(image.HashGuestEdges());
|
||||
}
|
||||
bool cpu_dirty = image.IsBufferModified() || image.IsDefinitelyCpuDirty();
|
||||
if (image.info.metadata.compression != VideoOutCompression::Uncompressed) {
|
||||
|
|
@ -1141,6 +1195,12 @@ ImageId TextureCache::AssociateStencil(ImageId depth_id, GuestRange stencil) {
|
|||
plane.IsTiled() ? TileGetDepthPitch(plane.extent.width, 1, 0) : depth.info.pitch;
|
||||
plane.bytes_per_block = 1;
|
||||
plane.UpdateSize();
|
||||
if (record.depth_id != depth_id) {
|
||||
if (record.depth_id) {
|
||||
std::erase(m_slot_images[record.depth_id].stencil_ids, association);
|
||||
}
|
||||
depth.stencil_ids.push_back(association);
|
||||
}
|
||||
record.info = plane;
|
||||
record.depth_id = depth_id;
|
||||
record.stencil_subresources = depth.stencil_subresources;
|
||||
|
|
@ -1166,6 +1226,7 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
|
|||
std::scoped_lock lock {m_lock};
|
||||
const auto candidates =
|
||||
FindImagesInRegion(desc.info.data.address, desc.info.data.size, false);
|
||||
bool block_alias = false;
|
||||
|
||||
for (const auto id: candidates) {
|
||||
const auto& image = m_slot_images[id];
|
||||
|
|
@ -1184,6 +1245,9 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
|
|||
}
|
||||
if (SameBacking(image.info, desc.info, exact_format)) {
|
||||
result = id;
|
||||
} else if (image.SafeToDownload() &&
|
||||
BcAlias::Matches(desc.info, image.info)) {
|
||||
block_alias = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1201,6 +1265,15 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
|
|||
view_layer = overlap.layer;
|
||||
}
|
||||
}
|
||||
} else if (block_alias) {
|
||||
for (const auto candidate: candidates) {
|
||||
const auto* owner = m_slot_images.try_get(candidate);
|
||||
if (candidate != result && owner != nullptr && owner->registered &&
|
||||
!owner->IsCpuDirty() &&
|
||||
BcAlias::Matches(m_slot_images[result].info, owner->info)) {
|
||||
BcAlias::Resolve(*this, candidate, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result && !m_slot_images[result].depth_id) {
|
||||
|
|
@ -1363,7 +1436,6 @@ vk::ImageView TextureCache::FindDepthTarget(ImageId id, const ImageDesc& desc) {
|
|||
}
|
||||
TouchImage(image);
|
||||
image.MarkGpuModified();
|
||||
image.usage.depth_target = true;
|
||||
if (desc.info.HasStencil()) {
|
||||
const auto layers = image.info.resources.layers;
|
||||
EXIT_IF(layers == 0 || image.info.data.size % layers != 0 ||
|
||||
|
|
@ -1779,7 +1851,7 @@ void TextureCache::InvalidateCpuAliases(uint64_t address, uint64_t size) {
|
|||
continue;
|
||||
}
|
||||
if (owner->Overlaps(address, size)) {
|
||||
owner->InvalidateCpuWrite(address, size);
|
||||
owner->MarkCpuDirty();
|
||||
UntrackImage(id);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -1923,6 +1995,9 @@ void TextureCache::RunGarbageCollector() {
|
|||
}
|
||||
|
||||
void TextureCache::ProcessDownloadImages() {
|
||||
if (!m_readback_linear_images) {
|
||||
return;
|
||||
}
|
||||
std::scoped_lock lock {m_lock};
|
||||
for (const auto id: m_download_images) {
|
||||
const auto owner = m_slot_images.try_get(id);
|
||||
|
|
|
|||
|
|
@ -26,8 +26,15 @@ class BufferCache;
|
|||
class CommandBuffer;
|
||||
class CommandScheduler;
|
||||
class RenderExecutor;
|
||||
class TextureCache;
|
||||
struct TextureCacheTestAccess;
|
||||
|
||||
namespace BcAlias {
|
||||
[[nodiscard]] bool Matches(const ImageInfo& requested, const ImageInfo& cached);
|
||||
[[nodiscard]] bool Publish(TextureCache& cache, Image& image);
|
||||
void Resolve(TextureCache& cache, ImageId cached, ImageId merged);
|
||||
} // namespace BcAlias
|
||||
|
||||
class TextureCache {
|
||||
public:
|
||||
enum class BindingType : uint8_t { Texture, Storage, RenderTarget, DepthTarget, VideoOut };
|
||||
|
|
@ -181,6 +188,9 @@ private:
|
|||
friend struct TextureCacheTestAccess;
|
||||
friend class BufferCache;
|
||||
friend class RenderExecutor;
|
||||
friend bool BcAlias::Matches(const ImageInfo&, const ImageInfo&);
|
||||
friend bool BcAlias::Publish(TextureCache&, Image&);
|
||||
friend void BcAlias::Resolve(TextureCache&, ImageId, ImageId);
|
||||
};
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
|
|
|
|||
|
|
@ -236,7 +236,8 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
|
|||
// DCC owns the clear when both planes are enabled; single-sample CMASK stays expanded.
|
||||
desc.info.metadata.kind = has_dcc ? ImageMetadataKind::Dcc : ImageMetadataKind::Cmask;
|
||||
desc.info.metadata.range = {has_dcc ? rt.dcc_addr.addr : rt.cmask.addr, metadata_size.size};
|
||||
desc.info.metadata.clear_word = rt.clear_word0.word0;
|
||||
desc.info.metadata.clear_word =
|
||||
rt.clear_word0.word0 | (uint64_t {rt.clear_word1.word1} << 32u);
|
||||
desc.info.metadata.clear_register_valid = true;
|
||||
}
|
||||
desc.view_info.format = target_format.format;
|
||||
|
|
|
|||
|
|
@ -29,10 +29,8 @@ struct CachedImageView {
|
|||
};
|
||||
|
||||
struct ImageUsage {
|
||||
bool texture = false;
|
||||
bool storage = false;
|
||||
bool render_target = false;
|
||||
bool depth_target = false;
|
||||
bool video_out = false;
|
||||
};
|
||||
|
||||
|
|
@ -70,40 +68,25 @@ public:
|
|||
void CopyImageWithBuffer(Image& source, Buffer& buffer, TileManager& tiler);
|
||||
void CopyMip(Image& source, uint32_t mip, uint32_t layer);
|
||||
|
||||
void InvalidateCpuWrite(uint64_t vaddr, uint64_t size) {
|
||||
if (ImageRangeOverlaps(info.data.address, info.data.size, vaddr, size)) {
|
||||
m_cpu_dirty = true;
|
||||
m_maybe_cpu_dirty = false;
|
||||
m_maybe_hash_valid = false;
|
||||
} else if (ImagePageRangesOverlap(info.data.address, info.data.size, vaddr, size)) {
|
||||
m_maybe_cpu_dirty = true;
|
||||
}
|
||||
void MarkCpuDirty() {
|
||||
m_cpu_dirty = true;
|
||||
m_maybe_cpu_dirty = false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsCpuDirty() const { return m_cpu_dirty || m_maybe_cpu_dirty; }
|
||||
[[nodiscard]] bool IsDefinitelyCpuDirty() const { return m_cpu_dirty; }
|
||||
[[nodiscard]] bool IsMaybeCpuDirty() const { return m_maybe_cpu_dirty; }
|
||||
void MarkMaybeCpuDirty() {
|
||||
if (!m_cpu_dirty) {
|
||||
if (!IsCpuDirty()) {
|
||||
m_maybe_cpu_hash = HashGuestEdges();
|
||||
m_maybe_cpu_dirty = true;
|
||||
}
|
||||
}
|
||||
[[nodiscard]] bool NeedsMaybeCpuHash() const {
|
||||
return m_maybe_cpu_dirty && !m_maybe_hash_valid;
|
||||
}
|
||||
void SetMaybeCpuHash(uint64_t hash) {
|
||||
if (!NeedsMaybeCpuHash()) {
|
||||
EXIT("image cannot initialize maybe-dirty hash\n");
|
||||
}
|
||||
m_maybe_cpu_hash = hash;
|
||||
m_maybe_hash_valid = true;
|
||||
}
|
||||
[[nodiscard]] bool ResolveMaybeCpuHash(uint64_t hash) {
|
||||
if (!m_maybe_cpu_dirty || !m_maybe_hash_valid || m_cpu_dirty) {
|
||||
if (!m_maybe_cpu_dirty || m_cpu_dirty) {
|
||||
EXIT("image cannot resolve maybe-dirty hash\n");
|
||||
}
|
||||
m_maybe_cpu_dirty = false;
|
||||
m_maybe_hash_valid = false;
|
||||
m_maybe_cpu_dirty = false;
|
||||
m_cpu_dirty |= hash != m_maybe_cpu_hash;
|
||||
return m_cpu_dirty;
|
||||
}
|
||||
|
|
@ -112,9 +95,8 @@ public:
|
|||
if (!IsCpuDirty()) {
|
||||
EXIT("clean image cannot complete a refresh\n");
|
||||
}
|
||||
m_cpu_dirty = false;
|
||||
m_maybe_cpu_dirty = false;
|
||||
m_maybe_hash_valid = false;
|
||||
m_cpu_dirty = false;
|
||||
m_maybe_cpu_dirty = false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsGpuModified() const noexcept { return m_gpu_modified; }
|
||||
|
|
@ -149,6 +131,8 @@ public:
|
|||
uint64_t track_addr = 0;
|
||||
uint64_t track_addr_end = 0;
|
||||
ImageId depth_id {};
|
||||
// Stencil associations that refer to this depth image.
|
||||
std::vector<ImageId> stencil_ids;
|
||||
// The current stencil plane's mapping into the depth image, also retained by its association.
|
||||
ImageSubresourceRange stencil_subresources;
|
||||
uint64_t tick_accessed_last = 0;
|
||||
|
|
@ -165,12 +149,11 @@ private:
|
|||
|
||||
GraphicContext& m_graphics;
|
||||
CommandScheduler& m_scheduler;
|
||||
uint64_t m_maybe_cpu_hash = 0;
|
||||
bool m_cpu_dirty = false;
|
||||
bool m_maybe_cpu_dirty = false;
|
||||
bool m_maybe_hash_valid = false;
|
||||
bool m_gpu_modified = false;
|
||||
bool m_buffer_modified = false;
|
||||
uint64_t m_maybe_cpu_hash = 0;
|
||||
bool m_cpu_dirty = false;
|
||||
bool m_maybe_cpu_dirty = false;
|
||||
bool m_gpu_modified = false;
|
||||
bool m_buffer_modified = false;
|
||||
};
|
||||
|
||||
namespace ImageOps {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ struct ImageMetadataInfo {
|
|||
GuestRange range;
|
||||
ImageMetadataKind kind = ImageMetadataKind::None;
|
||||
uint32_t control = 0;
|
||||
uint32_t clear_word = 0;
|
||||
uint64_t clear_word = 0;
|
||||
VideoOutCompression compression = VideoOutCompression::Uncompressed;
|
||||
bool stencil_compressed = false;
|
||||
bool clear_register_valid = false;
|
||||
|
|
@ -362,8 +362,17 @@ struct VideoOutFormatPolicy {
|
|||
};
|
||||
|
||||
inline constexpr uint64_t VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ = 0x8100070422000000ull;
|
||||
inline constexpr uint64_t VIDEO_OUT_PIXEL_FORMAT_B10_G10_R10_A2_BT2100_PQ = 0x8100070400000000ull;
|
||||
|
||||
inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {{
|
||||
[[nodiscard]] constexpr bool IsHdrVideoOutPixelFormat(uint64_t format) {
|
||||
switch (format) {
|
||||
case VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ:
|
||||
case VIDEO_OUT_PIXEL_FORMAT_B10_G10_R10_A2_BT2100_PQ: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {{
|
||||
{0x8000000022000000ull,
|
||||
{vk::Format::eR8G8B8A8Srgb, Prospero::BufferFormat::k8_8_8_8Srgb, 4, false}},
|
||||
{0x8000000000000000ull,
|
||||
|
|
@ -374,6 +383,8 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
|
|||
{vk::Format::eA2R10G10B10UnormPack32, Prospero::BufferFormat::k10_10_10_2UNorm, 4, false}},
|
||||
{VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ,
|
||||
{vk::Format::eA2B10G10R10UnormPack32, Prospero::BufferFormat::k10_10_10_2UNorm, 4, false}},
|
||||
{VIDEO_OUT_PIXEL_FORMAT_B10_G10_R10_A2_BT2100_PQ,
|
||||
{vk::Format::eA2R10G10B10UnormPack32, Prospero::BufferFormat::k10_10_10_2UNorm, 4, false}},
|
||||
{0xc001000622000000ull,
|
||||
{vk::Format::eR16G16B16A16Sfloat, Prospero::BufferFormat::k16_16_16_16Float, 8, false}},
|
||||
{0xc001000600000000ull,
|
||||
|
|
@ -403,9 +414,10 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
|
|||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool DecodePackedColorClear(vk::Format format, uint32_t packed,
|
||||
[[nodiscard]] inline bool DecodePackedColorClear(vk::Format format, uint64_t clear_words,
|
||||
vk::ClearColorValue& clear) {
|
||||
vk::ClearColorValue next {};
|
||||
const auto packed = static_cast<uint32_t>(clear_words);
|
||||
const auto unorm8 = [](uint32_t value) { return static_cast<float>(value & 0xffu) / 255.0f; };
|
||||
const auto srgb8 = [](uint32_t value) {
|
||||
const auto encoded = static_cast<float>(value & 0xffu) / 255.0f;
|
||||
|
|
@ -423,6 +435,15 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
|
|||
(mantissa << 13u));
|
||||
};
|
||||
switch (format) {
|
||||
case vk::Format::eB10G11R11UfloatPack32:
|
||||
next.float32[0] = float16((packed & 0x7ffu) << 4u);
|
||||
next.float32[1] = float16(((packed >> 11u) & 0x7ffu) << 4u);
|
||||
next.float32[2] = float16((packed >> 22u) << 5u);
|
||||
break;
|
||||
case vk::Format::eR16G16B16A16Sfloat:
|
||||
next.float32[2] = float16(static_cast<uint32_t>(clear_words >> 32u));
|
||||
next.float32[3] = float16(static_cast<uint32_t>(clear_words >> 48u));
|
||||
[[fallthrough]];
|
||||
case vk::Format::eR16G16Sfloat:
|
||||
next.float32[1] = float16(packed >> 16u);
|
||||
[[fallthrough]];
|
||||
|
|
@ -433,9 +454,10 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
|
|||
case vk::Format::eR16Unorm:
|
||||
next.float32[0] = static_cast<float>(packed & 0xffffu) / 65535.0f;
|
||||
break;
|
||||
// A single-plane float target carries its clear as raw float bits, the same encoding the
|
||||
// depth decoder below uses. Without this the clear is discarded and the target keeps stale
|
||||
// contents.
|
||||
// Each clear register stores one raw 32-bit float channel.
|
||||
case vk::Format::eR32G32Sfloat:
|
||||
next.float32[1] = std::bit_cast<float>(static_cast<uint32_t>(clear_words >> 32u));
|
||||
[[fallthrough]];
|
||||
case vk::Format::eR32Sfloat: next.float32[0] = std::bit_cast<float>(packed); break;
|
||||
case vk::Format::eR32Uint: next.uint32[0] = packed; break;
|
||||
case vk::Format::eR32Sint: next.int32[0] = static_cast<int32_t>(packed); break;
|
||||
|
|
@ -496,7 +518,7 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
|
|||
clear = vk::ClearColorValue {};
|
||||
return true;
|
||||
}
|
||||
return DecodePackedColorClear(format, packed, clear);
|
||||
return DecodePackedColorClear(format, uint64_t {packed} | (uint64_t {packed} << 32u), clear);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool DecodePackedStencilClear(uint32_t packed, uint8_t& clear) {
|
||||
|
|
|
|||
|
|
@ -179,7 +179,9 @@ static void ValidateSampledDepthBinding(const ShaderRecompiler::IR::ImageResourc
|
|||
const ShaderTextureResource& descriptor, const Image& image,
|
||||
vk::Format view_format, uint64_t size) {
|
||||
const bool resource_ok = IsSupportedSampledDepthResource(resource);
|
||||
const bool encoding_ok = IsSupportedDepthTextureEncoding(descriptor, resource.r128);
|
||||
// Comparison sampling changes the host format, not the guest metadata encoding.
|
||||
const bool encoding_ok = descriptor.TileMode() != Prospero::TileMode::kDepth ||
|
||||
IsSupportedDepthTextureEncoding(descriptor, resource.r128);
|
||||
const bool view_ok =
|
||||
IsSupportedSampledDepthView(image.info.pixel_format, view_format, descriptor.DstSelXYZW());
|
||||
if (resource_ok && encoding_ok && view_ok) {
|
||||
|
|
@ -307,8 +309,10 @@ void ValidateStorageTexture(const ShaderRecompiler::IR::ImageResource& resource,
|
|||
const bool encoding_ok = IsSupportedStorageTextureEncoding(resource, descriptor);
|
||||
const bool uint_resource = resource.numeric_class == Prospero::TextureNumericClass::Uint;
|
||||
const bool raw_sint_storage = format == Prospero::BufferFormat::k32SInt && uint_resource &&
|
||||
resource.written && !resource.read && !resource.atomic;
|
||||
const auto numeric_class = Prospero::SampledTextureNumericClass(format);
|
||||
resource.written &&
|
||||
(resource.atomic ? !resource.atomic64 : !resource.read);
|
||||
const auto numeric_class =
|
||||
Prospero::SampledTextureNumericClass(Prospero::RemapTextureFormat(format));
|
||||
const bool raw_float_atomic = format == Prospero::BufferFormat::k32Float && uint_resource &&
|
||||
resource.atomic && !resource.atomic64;
|
||||
const bool format_ok =
|
||||
|
|
@ -564,7 +568,8 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
|
|||
const bool shader_conversion =
|
||||
surface_format.conversion_format != Prospero::BufferFormat::kInvalid;
|
||||
const bool sampled_numeric_class =
|
||||
storage || resource.numeric_class == Prospero::SampledTextureNumericClass(format);
|
||||
storage || resource.numeric_class ==
|
||||
Prospero::SampledTextureNumericClass(Prospero::RemapTextureFormat(format));
|
||||
if (!storage && resource.resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled &&
|
||||
!sampled_numeric_class) {
|
||||
EXIT("sampled image numeric class mismatch: numeric=%u format=%u addr=0x%016" PRIx64 "\n",
|
||||
|
|
@ -622,8 +627,7 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
|
|||
if (storage) {
|
||||
ValidateStorageTexture(resource, descriptor, desc.info.data.size);
|
||||
}
|
||||
if (!resource.r128 && descriptor.MetaCompress() && tile != Prospero::TileMode::kDepth &&
|
||||
!desc.info.IsDepth()) {
|
||||
if (!resource.r128 && descriptor.MetaCompress() && tile != Prospero::TileMode::kDepth) {
|
||||
TileSizeAlign metadata_size {};
|
||||
(void)TileGetDccSize(width, height, volume ? depth : image_layers,
|
||||
desc.info.bytes_per_block, physical_levels, tile, metadata_size,
|
||||
|
|
@ -872,7 +876,6 @@ void RenderExecutor::RebindImages(PreparedBindings& prepared) {
|
|||
auto& image = texture_cache.GetImage(binding.image_id);
|
||||
const bool storage = binding.desc.type == TextureCache::BindingType::Storage;
|
||||
image.usage.storage |= storage;
|
||||
image.usage.texture |= !storage;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ void DumpShaderOriginal(const char* stage_name, uint64_t shader_hash,
|
|||
}
|
||||
|
||||
bool ValidateShaderSpirv(const char* label, uint64_t shader_hash,
|
||||
const std::vector<uint32_t>& spirv) {
|
||||
const std::vector<uint32_t>& spirv, bool maintenance8_enabled) {
|
||||
if (!Config::ShaderValidationEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -160,7 +160,9 @@ bool ValidateShaderSpirv(const char* label, uint64_t shader_hash,
|
|||
static_cast<int>(position.column), static_cast<int>(position.index),
|
||||
message);
|
||||
});
|
||||
if (tools.Validate(spirv)) {
|
||||
spvtools::ValidatorOptions validator_options;
|
||||
validator_options.SetAllowOffsetTextureOperand(maintenance8_enabled);
|
||||
if (tools.Validate(spirv.data(), spirv.size(), validator_options)) {
|
||||
return true;
|
||||
}
|
||||
spvtools::SpirvTools disassembler(SPV_ENV_VULKAN_1_2);
|
||||
|
|
@ -296,14 +298,15 @@ struct PipelineCache::ProgramCache {
|
|||
std::span<const uint32_t> function_code) {
|
||||
auto result = ShaderRecompiler::CompileProgram(std::move(translated), options,
|
||||
specialization, push_data_start_dword);
|
||||
if (!ValidateShaderSpirv(options.dump_label, options.shader_hash, result.spirv)) {
|
||||
if (!ValidateShaderSpirv(options.dump_label, options.shader_hash, result.spirv,
|
||||
options.maintenance8_enabled)) {
|
||||
DumpShaderSpirv(stage_name, options.shader_hash, result.spirv);
|
||||
EXIT("%s failed hash=0x%016" PRIx64 ": SPIR-V validation failed\n", options.dump_label,
|
||||
options.shader_hash);
|
||||
}
|
||||
DumpShaderSpirv(stage_name, options.shader_hash, result.spirv);
|
||||
|
||||
const auto module = CompileSPV(result.spirv, device);
|
||||
const auto module = CompileSPV(result.spirv, graphics.device);
|
||||
EXIT_IF(module == nullptr);
|
||||
if (options.dump_ir) {
|
||||
LOGF("%s SPIR-V words=%" PRIu64 " wave_size=%u\n", options.dump_label,
|
||||
|
|
@ -384,6 +387,7 @@ struct PipelineCache::ProgramCache {
|
|||
}
|
||||
ShaderRecompiler::CompileOptions options;
|
||||
options.stage = stage;
|
||||
options.maintenance8_enabled = graphics.maintenance8_enabled;
|
||||
options.shader_hash = params.hash;
|
||||
options.user_data = user_data;
|
||||
options.back_code = params.back_code;
|
||||
|
|
@ -456,26 +460,26 @@ struct PipelineCache::ProgramCache {
|
|||
return permutation.handle;
|
||||
}
|
||||
|
||||
explicit ProgramCache(vk::Device device): device(device) {
|
||||
explicit ProgramCache(GraphicContext& graphics): graphics(graphics) {
|
||||
lookup_key.static_state.reserve(MaxStaticKeyWords);
|
||||
}
|
||||
~ProgramCache() {
|
||||
for (const auto& [key, entry]: programs) {
|
||||
(void)key;
|
||||
for (const auto& permutation: entry.permutations) {
|
||||
device.destroyShaderModule(permutation.handle.module, nullptr);
|
||||
graphics.device.destroyShaderModule(permutation.handle.module, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_map<ProgramKey, SourceEntry, ProgramKeyHash> programs;
|
||||
ProgramKey lookup_key;
|
||||
vk::Device device;
|
||||
GraphicContext& graphics;
|
||||
uint64_t next_shader_id = 0;
|
||||
};
|
||||
|
||||
PipelineCache::PipelineCache(GraphicContext& graphics)
|
||||
: m_graphics(graphics), m_program_cache(std::make_unique<ProgramCache>(graphics.device)) {
|
||||
: m_graphics(graphics), m_program_cache(std::make_unique<ProgramCache>(graphics)) {
|
||||
EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
|
||||
InitializeDriverCache();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,13 +39,7 @@ enum class CommandBufferDebugOp : uint32_t {
|
|||
DispatchDirect,
|
||||
DrawIndex,
|
||||
DrawIndexAuto,
|
||||
EopWrite,
|
||||
EopInterrupt,
|
||||
EopWriteBack,
|
||||
EopFlip,
|
||||
EopWriteBackFlip,
|
||||
EopOnlyFlip,
|
||||
DispatchIndirect,
|
||||
DispatchIndirect = 9,
|
||||
Unknown,
|
||||
};
|
||||
|
||||
|
|
@ -202,11 +196,13 @@ private:
|
|||
std::span<PreparedBindings* const> stages = {});
|
||||
[[nodiscard]] bool ResolveColorTargets(CommandBuffer& buffer,
|
||||
uint32_t render_target_slice_offset);
|
||||
[[nodiscard]] bool ConsumeMetadataColorOperation(CommandBuffer& buffer,
|
||||
uint32_t render_target_slice_offset);
|
||||
void BindImage(ImageId id, bool storage);
|
||||
void BindRenderTarget(ImageId id);
|
||||
void ResetBindings();
|
||||
[[nodiscard]] bool TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer);
|
||||
void UpdateComputeMetaClears(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer);
|
||||
[[nodiscard]] bool TryConsumeComputeImageClear(const ShaderComputeInputInfo& input,
|
||||
CommandBuffer& command, uint32_t group_x,
|
||||
uint32_t group_y, uint32_t group_z, uint32_t mode);
|
||||
|
|
|
|||
|
|
@ -49,8 +49,8 @@ static bool FillSourcesDisjoint(std::span<const ShaderRecompiler::IR::Descriptor
|
|||
return true;
|
||||
}
|
||||
|
||||
bool RenderExecutor::TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer) {
|
||||
void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer) {
|
||||
const auto& program = *input.stage.program;
|
||||
const auto& resources = *input.stage.resources;
|
||||
if (resources.buffers.size() != program.info.buffers.size()) {
|
||||
|
|
@ -60,10 +60,9 @@ bool RenderExecutor::TryConsumeComputeMetaClear(const ShaderComputeInputInfo& in
|
|||
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
|
||||
const auto& resource = program.info.buffers[i];
|
||||
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
|
||||
// A metadata resource that is also read is not proven to be a full overwrite. Execute it
|
||||
// conservatively instead of replacing the dispatch with a coarse full-surface clear.
|
||||
// Reads and transforms of metadata do not establish a clear state.
|
||||
if ((!resource.written || resource.read) && cache.IsMeta(descriptor.Base48())) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -73,13 +72,10 @@ bool RenderExecutor::TryConsumeComputeMetaClear(const ShaderComputeInputInfo& in
|
|||
if (resource.written) {
|
||||
const auto descriptor =
|
||||
DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
|
||||
if (cache.ClearMeta(descriptor.Base48())) {
|
||||
return true;
|
||||
}
|
||||
(void)cache.ClearMeta(descriptor.Base48());
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t group_x,
|
||||
|
|
@ -306,12 +302,14 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
|||
|
||||
const auto& program = *input_info.stage.program;
|
||||
const auto& resources = *input_info.stage.resources;
|
||||
if (resources.specialization_reads.empty() &&
|
||||
(TryConsumeComputeMetaClear(input_info, buffer) ||
|
||||
TryConsumeComputeImageClear(input_info, buffer, thread_group_x, thread_group_y,
|
||||
thread_group_z, mode))) {
|
||||
ResetBindings();
|
||||
return;
|
||||
if (resources.specialization_reads.empty()) {
|
||||
// Cached metadata addresses can be reused as ordinary buffers; preserve their guest writes.
|
||||
UpdateComputeMetaClears(input_info, buffer);
|
||||
if (TryConsumeComputeImageClear(input_info, buffer, thread_group_x, thread_group_y,
|
||||
thread_group_z, mode)) {
|
||||
ResetBindings();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (use_thread_dimensions) {
|
||||
|
|
|
|||
|
|
@ -608,16 +608,19 @@ enum class CbColorMode : uint8_t {
|
|||
DccDecompress = 6,
|
||||
};
|
||||
|
||||
static bool ConsumeMetadataColorOperation(const CommandBuffer& buffer) {
|
||||
bool RenderExecutor::ConsumeMetadataColorOperation(CommandBuffer& buffer,
|
||||
uint32_t render_target_slice_offset) {
|
||||
const auto& ctx = buffer.GetRegisters();
|
||||
const auto mode = ctx.GetColorControl().mode;
|
||||
// These special modes run color-buffer metadata or decompression operations. The shader is a
|
||||
// vehicle for that operation, and its exported color must not be applied as a normal draw.
|
||||
// Kyty stores expanded Vulkan images rather than compressed guest surfaces, so no equivalent
|
||||
// hardware pass is emitted. Tracked DCC clear state is materialized on attachment bind;
|
||||
// future CMask/FMask support can consume its state through the same TextureCache path.
|
||||
return mode == static_cast<uint8_t>(CbColorMode::EliminateFastClear) ||
|
||||
mode == static_cast<uint8_t>(CbColorMode::FmaskDecompress) ||
|
||||
if (mode == static_cast<uint8_t>(CbColorMode::EliminateFastClear)) {
|
||||
// FCE can precede any ordinary draw. Discover the target to materialize its native
|
||||
// clear metadata before the expanded image is sampled.
|
||||
RenderColorInfo target {};
|
||||
ResolveRenderColorTarget(buffer, target, render_target_slice_offset, 0, true);
|
||||
return true;
|
||||
}
|
||||
// Metadata/decompression shaders must not export colors into the expanded image.
|
||||
return mode == static_cast<uint8_t>(CbColorMode::FmaskDecompress) ||
|
||||
mode == static_cast<uint8_t>(CbColorMode::DccDecompress);
|
||||
}
|
||||
|
||||
|
|
@ -1038,7 +1041,7 @@ bool RenderExecutor::CopyDepthToColor(CommandBuffer& buffer, const DrawCallInfo&
|
|||
const auto& hw = buffer.GetRegisters();
|
||||
auto& depth = state.depth_info;
|
||||
auto& color = state.color_info[0];
|
||||
EXIT_NOT_IMPLEMENTED(!m_context.GetGraphics().depth_color_copy_enabled ||
|
||||
EXIT_NOT_IMPLEMENTED(!m_context.GetGraphics().maintenance8_enabled ||
|
||||
!Prospero::IsRectList(ucfg.GetPrimType()) ||
|
||||
draw.IsIndexed() || draw.index_count != 3 || draw.instance_count != 1 ||
|
||||
emit.first_vertex != 0 || emit.first_instance != 0 ||
|
||||
|
|
@ -1257,7 +1260,8 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
|
|||
return;
|
||||
}
|
||||
|
||||
if (ConsumeMetadataColorOperation(buffer) || DepthStencilCopy(buffer) ||
|
||||
if (ConsumeMetadataColorOperation(buffer, args.render_target_slice_offset) ||
|
||||
DepthStencilCopy(buffer) ||
|
||||
ResolveColorTargets(buffer, args.render_target_slice_offset)) {
|
||||
ResetBindings();
|
||||
return;
|
||||
|
|
@ -1370,7 +1374,8 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
|
|||
return;
|
||||
}
|
||||
|
||||
if (ConsumeMetadataColorOperation(buffer) || DepthStencilCopy(buffer) ||
|
||||
if (ConsumeMetadataColorOperation(buffer, args.render_target_slice_offset) ||
|
||||
DepthStencilCopy(buffer) ||
|
||||
ResolveColorTargets(buffer, args.render_target_slice_offset)) {
|
||||
ResetBindings();
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
#include "graphics/host_gpu/renderer/sync.h"
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/common.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/threads.h"
|
||||
#include "graphics/host_gpu/graphicContext.h"
|
||||
#include "graphics/host_gpu/renderer/cache/bufferCache.h"
|
||||
#include "graphics/host_gpu/renderer/render.h"
|
||||
#include "graphics/host_gpu/renderer/renderContext.h"
|
||||
|
|
@ -53,109 +49,6 @@ uint64_t ReadReferenceClock() {
|
|||
return value;
|
||||
}
|
||||
|
||||
enum class EndOfPipeWriteSize : uint32_t { Dword = 4, Qword = 8 };
|
||||
enum class EndOfPipeWriteAction { Write, WriteBack, Interrupt, InterruptWriteBack };
|
||||
|
||||
static CommandBufferDebugOp DebugOperation(EndOfPipeWriteAction action) {
|
||||
switch (action) {
|
||||
case EndOfPipeWriteAction::Write: return CommandBufferDebugOp::EopWrite;
|
||||
case EndOfPipeWriteAction::WriteBack:
|
||||
case EndOfPipeWriteAction::InterruptWriteBack: return CommandBufferDebugOp::EopWriteBack;
|
||||
case EndOfPipeWriteAction::Interrupt: return CommandBufferDebugOp::EopInterrupt;
|
||||
}
|
||||
EXIT("unsupported end-of-pipe write action\n");
|
||||
return CommandBufferDebugOp::Unknown;
|
||||
}
|
||||
|
||||
static bool TriggersInterrupt(EndOfPipeWriteAction action) {
|
||||
return action == EndOfPipeWriteAction::Interrupt ||
|
||||
action == EndOfPipeWriteAction::InterruptWriteBack;
|
||||
}
|
||||
|
||||
static void RecordEndOfPipeWrite(uint64_t submit_id, CommandBuffer& buffer, uint64_t destination,
|
||||
uint64_t value, EndOfPipeWriteSize size,
|
||||
EndOfPipeWriteAction action, int interrupt_event_id = 0,
|
||||
uint32_t context_id = 0) {
|
||||
EXIT_IF(destination == 0);
|
||||
(void)buffer.Handle();
|
||||
|
||||
const auto width = static_cast<uint32_t>(size);
|
||||
const auto value_low = static_cast<uint32_t>(value);
|
||||
const auto value_high = static_cast<uint32_t>(value >> 32u);
|
||||
const auto operation = static_cast<uint32_t>(DebugOperation(action));
|
||||
if (TriggersInterrupt(action)) {
|
||||
buffer.SetDebugInfo(operation, submit_id, width, context_id, value_low, value_high,
|
||||
destination);
|
||||
TriggerEopEventAtEndOfPipe(buffer, interrupt_event_id, context_id);
|
||||
} else {
|
||||
buffer.SetDebugInfo(operation, submit_id, width, value_low, value_high, 0, destination);
|
||||
}
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipe32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
|
||||
uint32_t value) {
|
||||
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
|
||||
EndOfPipeWriteSize::Dword, EndOfPipeWriteAction::Write);
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeGds32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
|
||||
uint32_t dw_offset, uint32_t dw_num) {
|
||||
EXIT_IF(dst_gpu_addr == nullptr);
|
||||
(void)buffer.Handle();
|
||||
buffer.SetDebugInfo(static_cast<uint32_t>(CommandBufferDebugOp::EopWrite), submit_id,
|
||||
dw_offset, dw_num, 0, 0, reinterpret_cast<uint64_t>(dst_gpu_addr));
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipe64(uint64_t submit_id, CommandBuffer& buffer, uint64_t* dst_gpu_addr,
|
||||
uint64_t value) {
|
||||
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
|
||||
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::Write);
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeWithWriteBack64(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint64_t* dst_gpu_addr, uint64_t value) {
|
||||
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
|
||||
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::WriteBack);
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeWithWriteBack32(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint32_t* dst_gpu_addr, uint32_t value) {
|
||||
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
|
||||
EndOfPipeWriteSize::Dword, EndOfPipeWriteAction::WriteBack);
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeWithInterruptWriteBack64(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint64_t* dst_gpu_addr, uint64_t value, int event_id,
|
||||
uint32_t context_id) {
|
||||
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
|
||||
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::InterruptWriteBack,
|
||||
event_id, context_id);
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeWithInterruptWriteBack32(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint32_t* dst_gpu_addr, uint32_t value, int event_id,
|
||||
uint32_t context_id) {
|
||||
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
|
||||
EndOfPipeWriteSize::Dword, EndOfPipeWriteAction::InterruptWriteBack,
|
||||
event_id, context_id);
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeWithInterrupt64(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint64_t* dst_gpu_addr, uint64_t value, int event_id,
|
||||
uint32_t context_id) {
|
||||
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
|
||||
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::Interrupt, event_id,
|
||||
context_id);
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeWithInterrupt32(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint32_t* dst_gpu_addr, uint32_t value, int event_id,
|
||||
uint32_t context_id) {
|
||||
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
|
||||
EndOfPipeWriteSize::Dword, EndOfPipeWriteAction::Interrupt, event_id,
|
||||
context_id);
|
||||
}
|
||||
|
||||
uint64_t PrepareVideoOutFlip(CommandBuffer& buffer, int handle, int index, int flip_mode,
|
||||
int64_t flip_arg) {
|
||||
for (;;) {
|
||||
|
|
@ -176,65 +69,6 @@ uint64_t PrepareVideoOutFlip(CommandBuffer& buffer, int handle, int index, int f
|
|||
}
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeWithInterruptWriteBackFlip32(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint32_t* dst_gpu_addr, uint32_t value,
|
||||
int handle, int index, int flip_mode,
|
||||
int64_t flip_arg, uint64_t request_id,
|
||||
int event_id) {
|
||||
EXIT_IF(dst_gpu_addr == nullptr);
|
||||
(void)buffer.Handle();
|
||||
buffer.SetDebugInfo(static_cast<uint32_t>(CommandBufferDebugOp::EopWriteBackFlip), submit_id,
|
||||
static_cast<uint32_t>(handle), static_cast<uint32_t>(index),
|
||||
static_cast<uint32_t>(flip_mode), value, static_cast<uint64_t>(flip_arg));
|
||||
|
||||
auto& renderer = buffer.GetContext();
|
||||
auto& scheduler = renderer.GetCommandScheduler();
|
||||
EXIT_IF(!scheduler.Active() || &buffer != &scheduler.Current());
|
||||
scheduler.DeferPriorityOperation([&renderer, event_id, request_id] {
|
||||
renderer.GetVideoOut().CompleteFlip(request_id);
|
||||
renderer.TriggerInterrupt(event_id, 0);
|
||||
});
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeWithFlip32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
|
||||
uint32_t value, int handle, int index, int flip_mode,
|
||||
int64_t flip_arg, uint64_t request_id) {
|
||||
EXIT_IF(dst_gpu_addr == nullptr);
|
||||
(void)buffer.Handle();
|
||||
buffer.SetDebugInfo(static_cast<uint32_t>(CommandBufferDebugOp::EopFlip), submit_id,
|
||||
static_cast<uint32_t>(handle), static_cast<uint32_t>(index),
|
||||
static_cast<uint32_t>(flip_mode), value, static_cast<uint64_t>(flip_arg));
|
||||
|
||||
auto& renderer = buffer.GetContext();
|
||||
auto& scheduler = renderer.GetCommandScheduler();
|
||||
EXIT_IF(!scheduler.Active() || &buffer != &scheduler.Current());
|
||||
scheduler.DeferPriorityOperation(
|
||||
[&renderer, request_id] { renderer.GetVideoOut().CompleteFlip(request_id); });
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeOnlyFlip(uint64_t submit_id, CommandBuffer& buffer, int handle, int index,
|
||||
int flip_mode, int64_t flip_arg, uint64_t request_id) {
|
||||
(void)buffer.Handle();
|
||||
buffer.SetDebugInfo(static_cast<uint32_t>(CommandBufferDebugOp::EopOnlyFlip), submit_id,
|
||||
static_cast<uint32_t>(handle), static_cast<uint32_t>(index),
|
||||
static_cast<uint32_t>(flip_mode), 0, static_cast<uint64_t>(flip_arg));
|
||||
|
||||
auto& renderer = buffer.GetContext();
|
||||
auto& scheduler = renderer.GetCommandScheduler();
|
||||
EXIT_IF(!scheduler.Active() || &buffer != &scheduler.Current());
|
||||
scheduler.DeferPriorityOperation(
|
||||
[&renderer, request_id] { renderer.GetVideoOut().CompleteFlip(request_id); });
|
||||
}
|
||||
|
||||
void TriggerEopEventAtEndOfPipe(CommandBuffer& buffer, int event_id, uint32_t context_id) {
|
||||
(void)buffer.Handle();
|
||||
auto& renderer = buffer.GetContext();
|
||||
auto& scheduler = renderer.GetCommandScheduler();
|
||||
EXIT_IF(!scheduler.Active() || &buffer != &scheduler.Current());
|
||||
scheduler.DeferPriorityOperation(
|
||||
[&renderer, event_id, context_id] { renderer.TriggerInterrupt(event_id, context_id); });
|
||||
}
|
||||
|
||||
static void InterruptEventResetFunc(LibKernel::EventQueue::KernelEqueueEvent* event) {
|
||||
EXIT_IF(event == nullptr);
|
||||
event->triggered = false;
|
||||
|
|
|
|||
|
|
@ -17,44 +17,8 @@ namespace Sync {
|
|||
uint64_t& value);
|
||||
[[nodiscard]] uint64_t ReadReferenceClock();
|
||||
|
||||
void TriggerEopEventAtEndOfPipe(CommandBuffer& buffer, int event_id, uint32_t context_id);
|
||||
|
||||
void WriteAtEndOfPipe32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
|
||||
uint32_t value);
|
||||
void WriteAtEndOfPipe64(uint64_t submit_id, CommandBuffer& buffer, uint64_t* dst_gpu_addr,
|
||||
uint64_t value);
|
||||
void WriteAtEndOfPipeGds32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
|
||||
uint32_t dw_offset, uint32_t dw_num);
|
||||
void WriteAtEndOfPipeWithWriteBack32(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint32_t* dst_gpu_addr, uint32_t value);
|
||||
void WriteAtEndOfPipeWithWriteBack64(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint64_t* dst_gpu_addr, uint64_t value);
|
||||
void WriteAtEndOfPipeWithInterrupt32(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint32_t* dst_gpu_addr, uint32_t value, int event_id,
|
||||
uint32_t context_id = 0);
|
||||
void WriteAtEndOfPipeWithInterrupt64(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint64_t* dst_gpu_addr, uint64_t value, int event_id,
|
||||
uint32_t context_id = 0);
|
||||
void WriteAtEndOfPipeWithInterruptWriteBack32(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint32_t* dst_gpu_addr, uint32_t value, int event_id,
|
||||
uint32_t context_id = 0);
|
||||
void WriteAtEndOfPipeWithInterruptWriteBack64(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint64_t* dst_gpu_addr, uint64_t value, int event_id,
|
||||
uint32_t context_id = 0);
|
||||
|
||||
[[nodiscard]] uint64_t PrepareVideoOutFlip(CommandBuffer& buffer, int handle, int index,
|
||||
int flip_mode, int64_t flip_arg);
|
||||
void WriteAtEndOfPipeOnlyFlip(uint64_t submit_id, CommandBuffer& buffer, int handle, int index,
|
||||
int flip_mode, int64_t flip_arg, uint64_t request_id);
|
||||
void WriteAtEndOfPipeWithFlip32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
|
||||
uint32_t value, int handle, int index, int flip_mode,
|
||||
int64_t flip_arg, uint64_t request_id);
|
||||
void WriteAtEndOfPipeWithInterruptWriteBackFlip32(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint32_t* dst_gpu_addr, uint32_t value,
|
||||
int handle, int index, int flip_mode,
|
||||
int64_t flip_arg, uint64_t request_id,
|
||||
int event_id);
|
||||
|
||||
int AddEqEvent(RenderContext& renderer, LibKernel::EventQueue::KernelEqueue eq, int id,
|
||||
void* udata);
|
||||
int DeleteEqEvent(RenderContext& renderer, LibKernel::EventQueue::KernelEqueue eq, int id);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,53 @@
|
|||
#version 450
|
||||
|
||||
layout(location = 0) in vec2 uv;
|
||||
layout(location = 0) out vec4 color;
|
||||
|
||||
vec3 SdrToHdr10(vec3 rgb, bool source_linear) {
|
||||
rgb = clamp(rgb, 0.0, 1.0);
|
||||
vec3 linear = source_linear ? rgb :
|
||||
mix(rgb / 12.92, pow((rgb + 0.055) / 1.055, vec3(2.4)),
|
||||
greaterThan(rgb, vec3(0.04045)));
|
||||
// Convert primaries and scale 250-nit reference white relative to 10,000 nits.
|
||||
const mat3 to_rec2020 = (250.0 / 10000.0) *
|
||||
mat3(0.627404, 0.069097, 0.016391,
|
||||
0.329283, 0.919540, 0.088013,
|
||||
0.043313, 0.011362, 0.895595);
|
||||
vec3 powered = pow(to_rec2020 * linear, vec3(2610.0 / 16384.0));
|
||||
return pow((3424.0 / 4096.0 + (2413.0 / 128.0) * powered) /
|
||||
(1.0 + (2392.0 / 128.0) * powered), vec3(2523.0 / 32.0));
|
||||
}
|
||||
|
||||
#ifdef IMGUI_HDR
|
||||
layout(set = 0, binding = 0) uniform texture2D image;
|
||||
layout(set = 1, binding = 0) uniform sampler image_sampler;
|
||||
layout(location = 0) in struct { vec4 Color; vec2 UV; } In;
|
||||
|
||||
void main() {
|
||||
vec4 sampled = In.Color * texture(sampler2D(image, image_sampler), In.UV);
|
||||
color = vec4(SdrToHdr10(sampled.rgb, false), sampled.a);
|
||||
}
|
||||
#else
|
||||
layout(location = 0) in vec2 uv;
|
||||
layout(binding = 0) uniform sampler2D source;
|
||||
layout(push_constant) uniform AlphaControl {
|
||||
uint premultiplied;
|
||||
uint flags; // bit 0: premultiplied, bit 1: HDR target, bit 2: PQ source, bit 3: linear source
|
||||
};
|
||||
|
||||
void main() {
|
||||
color = texture(source, uv);
|
||||
if (premultiplied == 0u) {
|
||||
color.rgb *= color.a;
|
||||
const uint HDR_TARGET = 2u;
|
||||
const uint PQ_SOURCE = 4u;
|
||||
vec4 sampled = texture(source, uv);
|
||||
bool premultiplied = (flags & 1u) != 0u;
|
||||
bool convert_to_hdr = (flags & (HDR_TARGET | PQ_SOURCE)) == HDR_TARGET;
|
||||
if (convert_to_hdr && premultiplied) {
|
||||
sampled.rgb = sampled.a > 0.0 ? sampled.rgb / sampled.a : vec3(0.0);
|
||||
}
|
||||
if (convert_to_hdr) {
|
||||
sampled.rgb = SdrToHdr10(sampled.rgb, (flags & 8u) != 0u);
|
||||
}
|
||||
if (!premultiplied || convert_to_hdr) {
|
||||
sampled.rgb *= sampled.a;
|
||||
}
|
||||
color = sampled;
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -28,6 +28,14 @@ vk::Format VulkanFormat(Prospero::BufferFormat guest_format);
|
|||
void RequireVulkanSuccess(vk::Result result, const char* operation);
|
||||
vk::ShaderModule CompileSPV(std::span<const uint32_t> code, vk::Device device);
|
||||
|
||||
[[nodiscard]] constexpr bool IsHdrSwapchainFormat(vk::Format format) {
|
||||
switch (format) {
|
||||
case vk::Format::eA2B10G10R10UnormPack32:
|
||||
case vk::Format::eA2R10G10B10UnormPack32: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Handle, typename... Args>
|
||||
void SetVulkanObjectNameF(vk::Device device, Handle handle, fmt::format_string<Args...> format,
|
||||
Args&&... args) {
|
||||
|
|
|
|||
|
|
@ -26,11 +26,13 @@ public:
|
|||
~Presenter();
|
||||
KYTY_CLASS_NO_COPY(Presenter);
|
||||
|
||||
[[nodiscard]] Frame& PrepareFrame(CommandBuffer& command, const ImageInfo& info);
|
||||
[[nodiscard]] Frame& PrepareFrame(CommandBuffer& command, const ImageInfo& info,
|
||||
bool hdr = false);
|
||||
[[nodiscard]] Frame& PrepareBlankFrame(uint32_t width, uint32_t height, bool opaque,
|
||||
CommandBuffer* producer = nullptr);
|
||||
[[nodiscard]] bool PresentLastFrame();
|
||||
[[nodiscard]] bool IsGuestPaused() const noexcept;
|
||||
[[nodiscard]] bool IsHdrSupported() const noexcept;
|
||||
[[nodiscard]] bool NeedsSystemOverlayRefresh() const noexcept;
|
||||
[[nodiscard]] RenderContext& Renderer() const noexcept;
|
||||
void Present(Frame& frame);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include "common/file.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "gpu_blit_shaders/gpu_imgui_hdr_spv.h"
|
||||
#include "graphics/host_gpu/graphicContext.h"
|
||||
#include "graphics/presentation/ddsImage.h"
|
||||
#include "imgui.h"
|
||||
|
|
@ -890,6 +891,11 @@ struct SystemOverlay::Impl {
|
|||
info.PipelineInfoMain.PipelineRenderingCreateInfo.colorAttachmentCount = 1;
|
||||
info.PipelineInfoMain.PipelineRenderingCreateInfo.pColorAttachmentFormats = &color_format;
|
||||
info.CheckVkResultFn = CheckVulkanResult;
|
||||
if (IsHdrSwapchainFormat(format)) {
|
||||
info.CustomShaderFragCreateInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
|
||||
info.CustomShaderFragCreateInfo.codeSize = sizeof(GPU_IMGUI_HDR_SPV);
|
||||
info.CustomShaderFragCreateInfo.pCode = GPU_IMGUI_HDR_SPV;
|
||||
}
|
||||
EXIT_IF(!ImGui_ImplVulkan_Init(&info));
|
||||
vulkan_initialized = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -272,6 +272,7 @@ public:
|
|||
void Init(uint32_t width, uint32_t height);
|
||||
FlipQueue& GetFlipQueue() { return m_flip_queue; }
|
||||
Graphics::RenderContext& Renderer() const noexcept { return m_renderer; }
|
||||
bool IsHdrSupported() const noexcept { return m_presenter.IsHdrSupported(); }
|
||||
|
||||
void VblankBegin();
|
||||
void VblankEnd();
|
||||
|
|
@ -558,14 +559,6 @@ Graphics::ImageInfo BufferAttributeGroup::ImageInfo(const VideoOutBuffer& buffer
|
|||
if (!Graphics::DecodeVideoOutPixelFormat(attribute.pixel_format, pixel_format)) {
|
||||
EXIT("unsupported video-out pixel format: 0x%016" PRIx64 "\n", attribute.pixel_format);
|
||||
}
|
||||
if (attribute.pixel_format == Graphics::VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ) {
|
||||
static std::atomic_flag warned = ATOMIC_FLAG_INIT;
|
||||
if (!warned.test_and_set(std::memory_order_relaxed)) {
|
||||
Log::WriteToConsoleAndLog(fmt::format(
|
||||
"Warning: HDR-to-SDR conversion is not implemented; displaying PQ video-out "
|
||||
"pixels unchanged (format=0x{:016x}).\n", attribute.pixel_format));
|
||||
}
|
||||
}
|
||||
const auto tile_mode = Graphics::Prospero::TileMode::kRenderTarget;
|
||||
Graphics::ImageInfo info {};
|
||||
info.data = {buffer.data_address, 0};
|
||||
|
|
@ -1071,31 +1064,31 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
|
|||
uint32_t height = 0;
|
||||
bool current = false;
|
||||
bool premultiplied_alpha = false;
|
||||
bool hdr = false;
|
||||
{
|
||||
Common::LockGuard lock(cfg->mutex);
|
||||
current = cfg->opened && !cfg->closing && cfg->generation == generation;
|
||||
if (current) {
|
||||
if (special) {
|
||||
width = cfg->width;
|
||||
height = cfg->height;
|
||||
} else {
|
||||
const auto& surface = cfg->buffers[index];
|
||||
if (!surface.Occupied()) {
|
||||
EXIT("cannot prepare flip from an unregistered surface, id=%" PRIu64
|
||||
" index=%d\n",
|
||||
request_id, index);
|
||||
}
|
||||
if (surface.group_index >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX ||
|
||||
!cfg->groups[surface.group_index].occupied) {
|
||||
EXIT("video-out surface references an unavailable attribute group, id=%" PRIu64
|
||||
" index=%d group=%d\n",
|
||||
request_id, index, surface.group_index);
|
||||
}
|
||||
source_info = cfg->groups[surface.group_index].ImageInfo(surface);
|
||||
premultiplied_alpha =
|
||||
(cfg->groups[surface.group_index].attribute.option &
|
||||
VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED) != 0;
|
||||
if (current && special) {
|
||||
width = cfg->width;
|
||||
height = cfg->height;
|
||||
} else if (current) {
|
||||
const auto& surface = cfg->buffers[index];
|
||||
if (!surface.Occupied()) {
|
||||
EXIT("cannot prepare flip from an unregistered surface, id=%" PRIu64
|
||||
" index=%d\n",
|
||||
request_id, index);
|
||||
}
|
||||
if (surface.group_index >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX ||
|
||||
!cfg->groups[surface.group_index].occupied) {
|
||||
EXIT("video-out surface references an unavailable attribute group, id=%" PRIu64
|
||||
" index=%d group=%d\n",
|
||||
request_id, index, surface.group_index);
|
||||
}
|
||||
const auto& group = cfg->groups[surface.group_index];
|
||||
source_info = group.ImageInfo(surface);
|
||||
hdr = Graphics::IsHdrVideoOutPixelFormat(group.attribute.pixel_format);
|
||||
premultiplied_alpha = (group.attribute.option &
|
||||
VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED) != 0;
|
||||
}
|
||||
}
|
||||
if (!current) {
|
||||
|
|
@ -1108,12 +1101,18 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
|
|||
}
|
||||
return;
|
||||
}
|
||||
static std::atomic_flag warned = ATOMIC_FLAG_INIT;
|
||||
if (hdr && !m_presenter.IsHdrSupported() && !warned.test_and_set(std::memory_order_relaxed)) {
|
||||
Log::WriteToConsoleAndLog(
|
||||
"Warning: HDR output is unavailable; HDR-to-SDR conversion is not implemented, "
|
||||
"displaying PQ video-out pixels unchanged.\n");
|
||||
}
|
||||
Graphics::Presenter::Frame* frame = nullptr;
|
||||
if (special) {
|
||||
frame = &m_presenter.PrepareBlankFrame(width, height, index == VIDEO_OUT_BUFFER_INDEX_BLACK,
|
||||
&buffer);
|
||||
} else {
|
||||
frame = &m_presenter.PrepareFrame(buffer, source_info);
|
||||
frame = &m_presenter.PrepareFrame(buffer, source_info, hdr);
|
||||
}
|
||||
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
|
@ -1790,11 +1789,14 @@ KYTY_SYSV_ABI int VideoOutGetOutputStatus(int handle, VideoOutOutputStatus* stat
|
|||
|
||||
int32_t attribute3 = 0;
|
||||
Loader::SystemContentParamSfoGetInt("ATTRIBUTE3", &attribute3);
|
||||
int32_t attribute = 0;
|
||||
const bool hdr_allowed = !Loader::SystemContentParamSfoGetInt("ATTRIBUTE", &attribute) ||
|
||||
(attribute & 0x20000000) != 0;
|
||||
ctx->mutex.Lock();
|
||||
// Primary output reports 4K unless param.json Video-out Info enables resolution detection.
|
||||
status->resolution =
|
||||
((attribute3 & 4) != 0 && ctx->width < 3840 && ctx->height < 2160 ? 1u : 2u);
|
||||
status->dynamicRange = 1;
|
||||
status->dynamicRange = hdr_allowed && DriverState().IsHdrSupported() ? 2u : 1u;
|
||||
status->refreshRate =
|
||||
(ctx->output_mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ || Config::GetVblankFrequency() >= 119
|
||||
? VIDEO_OUT_REFRESH_RATE_119_88HZ
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
|
@ -31,6 +32,8 @@ struct Presenter::Frame {
|
|||
vk::ImageView view = nullptr;
|
||||
uint64_t present_tick = 0;
|
||||
bool busy = false;
|
||||
bool hdr = false;
|
||||
bool blank = false;
|
||||
|
||||
void Configure(GraphicContext& graphics, vk::Extent2D extent, vk::Format format);
|
||||
void Transit(vk::CommandBuffer command, vk::ImageLayout layout, vk::AccessFlags2 access);
|
||||
|
|
@ -263,7 +266,7 @@ public:
|
|||
void Create();
|
||||
void Recreate(bool surface_lost = false);
|
||||
[[nodiscard]] bool NeedsResize() const;
|
||||
[[nodiscard]] Status AcquireNextImage();
|
||||
[[nodiscard]] Status AcquireNextImage(CommandScheduler& scheduler);
|
||||
[[nodiscard]] bool PrepareSystemOverlay();
|
||||
void RecordPresentCommands(CommandBuffer& command, Presenter::Frame* source,
|
||||
const Presenter::Layer& overlay, bool draw_system_overlay);
|
||||
|
|
@ -274,20 +277,31 @@ public:
|
|||
return static_cast<uint32_t>(m_images.size());
|
||||
}
|
||||
[[nodiscard]] vk::Format Format() const noexcept { return m_format; }
|
||||
[[nodiscard]] bool IsHdrSupported() const noexcept { return m_hdr_supported.load(); }
|
||||
bool SetHdr(bool hdr) {
|
||||
const bool changed = m_hdr_active != (hdr && IsHdrSupported());
|
||||
m_hdr_requested = hdr;
|
||||
return changed;
|
||||
}
|
||||
|
||||
private:
|
||||
vk::SurfaceFormatKHR SelectFormat();
|
||||
void Destroy();
|
||||
void DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer);
|
||||
|
||||
WindowContext& m_window;
|
||||
vk::SwapchainKHR m_handle = nullptr;
|
||||
vk::Format m_format = vk::Format::eUndefined;
|
||||
std::atomic_bool m_hdr_supported {false};
|
||||
bool m_hdr_requested = false;
|
||||
bool m_hdr_active = false;
|
||||
vk::Extent2D m_extent {};
|
||||
// Drawable pixel size observed when this swapchain was created.
|
||||
vk::Extent2D m_window_extent {};
|
||||
std::vector<vk::Image> m_images;
|
||||
std::vector<vk::ImageView> m_image_views;
|
||||
std::vector<vk::Semaphore> m_image_acquired;
|
||||
std::vector<uint64_t> m_image_acquired_ticks;
|
||||
std::vector<vk::Semaphore> m_render_complete;
|
||||
std::unique_ptr<SystemOverlay> m_system_overlay;
|
||||
vk::DescriptorSetLayout m_overlay_descriptors = nullptr;
|
||||
|
|
@ -346,6 +360,40 @@ struct Presenter::Impl {
|
|||
std::atomic<uint64_t> presented_overlay_revision {0};
|
||||
};
|
||||
|
||||
vk::SurfaceFormatKHR Swapchain::SelectFormat() {
|
||||
const auto& formats = m_window.surface_capabilities.formats;
|
||||
auto& graphics = m_window.graphic_ctx;
|
||||
const auto hdr_format = std::ranges::find_if(formats, [&](const auto& candidate) {
|
||||
return candidate.colorSpace == vk::ColorSpaceKHR::eHdr10St2084EXT &&
|
||||
IsHdrSwapchainFormat(candidate.format) &&
|
||||
static_cast<bool>(
|
||||
graphics.GetFormatProperties(candidate.format).optimalTilingFeatures &
|
||||
vk::FormatFeatureFlagBits::eBlitDst);
|
||||
});
|
||||
m_hdr_supported = Config::HdrEnabled() && hdr_format != formats.end();
|
||||
m_hdr_active = m_hdr_requested && IsHdrSupported();
|
||||
if (m_hdr_active) {
|
||||
return *hdr_format;
|
||||
}
|
||||
if (Config::HdrEnabled() && !IsHdrSupported()) {
|
||||
LOGF("HDR10 surface unavailable; using SDR output\n");
|
||||
}
|
||||
if (formats.size() == 1 && formats.front().format == vk::Format::eUndefined) {
|
||||
return {vk::Format::eR8G8B8A8Unorm, vk::ColorSpaceKHR::eSrgbNonlinear};
|
||||
}
|
||||
for (const auto& candidate: formats) {
|
||||
if (candidate.colorSpace != vk::ColorSpaceKHR::eSrgbNonlinear) {
|
||||
continue;
|
||||
}
|
||||
switch (candidate.format) {
|
||||
case vk::Format::eB8G8R8A8Unorm:
|
||||
case vk::Format::eR8G8B8A8Unorm: return candidate;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
EXIT("no supported UNORM swapchain format\n");
|
||||
}
|
||||
|
||||
void Swapchain::Create() {
|
||||
auto& graphics = m_window.graphic_ctx;
|
||||
EXIT_IF(graphics.device == nullptr);
|
||||
|
|
@ -385,18 +433,7 @@ void Swapchain::Create() {
|
|||
? vk::CompositeAlphaFlagBitsKHR::eOpaque
|
||||
: vk::CompositeAlphaFlagBitsKHR::eInherit;
|
||||
|
||||
vk::SurfaceFormatKHR format {vk::Format::eR8G8B8A8Unorm, vk::ColorSpaceKHR::eSrgbNonlinear};
|
||||
if (surface.formats.size() != 1 || surface.formats.front().format != vk::Format::eUndefined) {
|
||||
const auto it = std::find_if(surface.formats.begin(), surface.formats.end(),
|
||||
[](const vk::SurfaceFormatKHR& candidate) {
|
||||
return candidate.format == vk::Format::eB8G8R8A8Unorm ||
|
||||
candidate.format == vk::Format::eR8G8B8A8Unorm;
|
||||
});
|
||||
if (it == surface.formats.end()) {
|
||||
EXIT("no supported UNORM swapchain format\n");
|
||||
}
|
||||
format = *it;
|
||||
}
|
||||
const auto format = SelectFormat();
|
||||
m_format = format.format;
|
||||
const auto swapchain_features = graphics.GetFormatProperties(m_format).optimalTilingFeatures;
|
||||
if (!static_cast<bool>(swapchain_features & vk::FormatFeatureFlagBits::eBlitDst)) {
|
||||
|
|
@ -436,6 +473,7 @@ void Swapchain::Create() {
|
|||
RequireVulkanSuccess(graphics.device.createSwapchainKHR(&create_info, nullptr, &m_handle),
|
||||
"vkCreateSwapchainKHR");
|
||||
EXIT_IF(m_handle == nullptr);
|
||||
LOGF("Vulkan presentation: %s\n", m_hdr_active ? "HDR10 (PQ)" : "SDR");
|
||||
|
||||
m_images = EnumerateVulkan<vk::Image>(
|
||||
"vkGetSwapchainImagesKHR", [&](uint32_t* count, vk::Image* images) {
|
||||
|
|
@ -464,6 +502,7 @@ void Swapchain::Create() {
|
|||
vk::SemaphoreCreateInfo semaphore_info {};
|
||||
semaphore_info.sType = vk::StructureType::eSemaphoreCreateInfo;
|
||||
m_image_acquired.resize(m_images.size());
|
||||
m_image_acquired_ticks.assign(m_images.size(), 0);
|
||||
m_render_complete.resize(m_images.size());
|
||||
for (size_t i = 0; i < m_images.size(); i++) {
|
||||
RequireVulkanSuccess(
|
||||
|
|
@ -532,6 +571,7 @@ void Swapchain::Destroy() {
|
|||
m_images.clear();
|
||||
m_image_views.clear();
|
||||
m_image_acquired.clear();
|
||||
m_image_acquired_ticks.clear();
|
||||
m_render_complete.clear();
|
||||
}
|
||||
|
||||
|
|
@ -557,8 +597,11 @@ bool Swapchain::NeedsResize() const {
|
|||
m_window_extent.height != m_window.graphic_ctx.screen_height;
|
||||
}
|
||||
|
||||
Swapchain::Status Swapchain::AcquireNextImage() {
|
||||
Swapchain::Status Swapchain::AcquireNextImage(CommandScheduler& scheduler) {
|
||||
EXIT_IF(m_handle == nullptr || m_frame_index >= m_image_acquired.size());
|
||||
// Repeated frames can wrap the semaphore ring without acquiring a new source frame.
|
||||
// Wait for the submission that consumed this slot before signaling it again.
|
||||
scheduler.Wait(m_image_acquired_ticks[m_frame_index]);
|
||||
m_image_index = static_cast<uint32_t>(-1);
|
||||
const auto result = m_window.graphic_ctx.device.acquireNextImageKHR(
|
||||
m_handle, std::numeric_limits<uint64_t>::max(), m_image_acquired[m_frame_index], nullptr,
|
||||
|
|
@ -692,9 +735,11 @@ void Swapchain::DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& l
|
|||
write.descriptorType = vk::DescriptorType::eCombinedImageSampler;
|
||||
write.pImageInfo = ℑ
|
||||
command.pushDescriptorSetKHR(vk::PipelineBindPoint::eGraphics, m_overlay_layout, 0, 1, &write);
|
||||
const uint32_t premultiplied = layer.premultiplied_alpha;
|
||||
const uint32_t flags = (layer.premultiplied_alpha ? 1u : 0u) |
|
||||
(m_hdr_active ? 2u : 0u) | (frame.hdr ? 4u : 0u) |
|
||||
(frame.image.format == vk::Format::eR16G16B16A16Sfloat ? 8u : 0u);
|
||||
command.pushConstants(m_overlay_layout, vk::ShaderStageFlagBits::eFragment, 0,
|
||||
sizeof(premultiplied), &premultiplied);
|
||||
sizeof(flags), &flags);
|
||||
command.bindPipeline(vk::PipelineBindPoint::eGraphics, m_overlay_pipeline);
|
||||
vk::RenderingAttachmentInfo attachment {};
|
||||
attachment.imageView = m_image_views[m_image_index];
|
||||
|
|
@ -828,7 +873,9 @@ uint64_t Swapchain::Submit(CommandScheduler& scheduler) {
|
|||
SubmitInfo submit;
|
||||
submit.AddWait(m_image_acquired[m_frame_index], 1, vk::PipelineStageFlagBits::eTransfer);
|
||||
submit.AddSignal(m_render_complete[m_image_index]);
|
||||
return scheduler.Submit(submit);
|
||||
const auto tick = scheduler.Submit(submit);
|
||||
m_image_acquired_ticks[m_frame_index] = tick;
|
||||
return tick;
|
||||
}
|
||||
|
||||
Swapchain::Status Swapchain::Present() {
|
||||
|
|
@ -868,7 +915,7 @@ Presenter::Presenter(WindowContext& window): m_impl(std::make_unique<Impl>(windo
|
|||
|
||||
Presenter::~Presenter() = default;
|
||||
|
||||
Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info) {
|
||||
Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info, bool hdr) {
|
||||
KYTY_PROFILER_FUNCTION();
|
||||
EXIT_IF(buffer.IsInvalid());
|
||||
auto frame_format = info.pixel_format;
|
||||
|
|
@ -878,6 +925,8 @@ Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo
|
|||
default: break;
|
||||
}
|
||||
auto* frame = m_impl->frames.Acquire({info.extent.width, info.extent.height}, frame_format);
|
||||
frame->hdr = hdr;
|
||||
frame->blank = false;
|
||||
Common::LockGuard render_lock(m_impl->renderer.GetMutex());
|
||||
auto& image = m_impl->ResolveSurface(info);
|
||||
if (image.backing.format == vk::Format::eUndefined) {
|
||||
|
|
@ -894,6 +943,8 @@ Presenter::Frame& Presenter::PrepareBlankFrame(uint32_t width, uint32_t height,
|
|||
KYTY_PROFILER_FUNCTION();
|
||||
auto format = m_impl->frames.GetFormat();
|
||||
auto* frame = m_impl->frames.Acquire({width, height}, format);
|
||||
frame->hdr = false;
|
||||
frame->blank = true;
|
||||
Common::LockGuard render_lock(m_impl->renderer.GetMutex());
|
||||
frame->Configure(m_impl->window.graphic_ctx, {width, height}, format);
|
||||
vk::ClearColorValue clear {};
|
||||
|
|
@ -922,6 +973,10 @@ bool Presenter::IsGuestPaused() const noexcept {
|
|||
return m_impl->window.loop.paused.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
bool Presenter::IsHdrSupported() const noexcept {
|
||||
return m_impl->swapchain.IsHdrSupported();
|
||||
}
|
||||
|
||||
bool Presenter::NeedsSystemOverlayRefresh() const noexcept {
|
||||
const auto visual = GetSystemOverlayVisualState();
|
||||
return visual.active ||
|
||||
|
|
@ -967,12 +1022,16 @@ void Presenter::Impl::Present() {
|
|||
KYTY_PROFILER_FUNCTION();
|
||||
|
||||
const auto overlay_visual = GetSystemOverlayVisualState();
|
||||
// Keep black/blank flips in the current color space; only real main-bus frames switch it.
|
||||
const auto* main_frame = layers[0].frame;
|
||||
const bool hdr_changed = main_frame != nullptr && !main_frame->blank &&
|
||||
swapchain.SetHdr(main_frame->hdr);
|
||||
// Some window systems keep presenting an old swapchain after a resize.
|
||||
if (swapchain.NeedsResize()) {
|
||||
if (hdr_changed || swapchain.NeedsResize()) {
|
||||
RecoverSwapchain(Swapchain::Status::Recreate);
|
||||
}
|
||||
for (uint32_t attempt = 0; attempt < 2; attempt++) {
|
||||
auto status = swapchain.AcquireNextImage();
|
||||
auto status = swapchain.AcquireNextImage(present_scheduler);
|
||||
if (status != Swapchain::Status::Success) {
|
||||
RecoverSwapchain(status);
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ vk::PhysicalDeviceVulkan12Features WindowContext::RequiredVulkan12Features() noe
|
|||
features.shaderBufferInt64Atomics = VK_TRUE;
|
||||
features.storageBuffer8BitAccess = VK_TRUE;
|
||||
features.shaderSampledImageArrayNonUniformIndexing = VK_TRUE;
|
||||
features.shaderStorageBufferArrayNonUniformIndexing = VK_TRUE;
|
||||
return features;
|
||||
}
|
||||
|
||||
|
|
@ -284,6 +285,9 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
|
|||
check_feature(features12.shaderSampledImageArrayNonUniformIndexing,
|
||||
"shaderSampledImageArrayNonUniformIndexing",
|
||||
required_features12.shaderSampledImageArrayNonUniformIndexing);
|
||||
check_feature(features12.shaderStorageBufferArrayNonUniformIndexing,
|
||||
"shaderStorageBufferArrayNonUniformIndexing",
|
||||
required_features12.shaderStorageBufferArrayNonUniformIndexing);
|
||||
check_feature(features13.robustImageAccess, "robustImageAccess");
|
||||
check_feature(features13.dynamicRendering, "dynamicRendering",
|
||||
required_features13.dynamicRendering);
|
||||
|
|
@ -549,9 +553,9 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
|
|||
graphics.attachment_feedback_loop_enabled =
|
||||
feedback_extensions && feedback_layout.attachmentFeedbackLoopLayout &&
|
||||
feedback_dynamic.attachmentFeedbackLoopDynamicState;
|
||||
graphics.depth_color_copy_enabled = maintenance8_extension && maintenance8.maintenance8;
|
||||
LOGF("Vulkan depth/color copy support: %s\n",
|
||||
graphics.depth_color_copy_enabled ? "true" : "false");
|
||||
graphics.maintenance8_enabled = maintenance8_extension && maintenance8.maintenance8;
|
||||
LOGF("Vulkan maintenance8 support: %s\n",
|
||||
graphics.maintenance8_enabled ? "true" : "false");
|
||||
LOGF("Vulkan depth feedback support: %s\n",
|
||||
graphics.attachment_feedback_loop_enabled ? "true" : "false");
|
||||
if (graphics.mesh_shader_enabled) {
|
||||
|
|
@ -648,7 +652,7 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
|
|||
image_atomic_int64.sparseImageInt64Atomics = VK_FALSE;
|
||||
create_info.pNext = &image_atomic_int64;
|
||||
}
|
||||
if (graphics.depth_color_copy_enabled) {
|
||||
if (graphics.maintenance8_enabled) {
|
||||
maintenance8.pNext = const_cast<void*>(create_info.pNext);
|
||||
create_info.pNext = &maintenance8;
|
||||
}
|
||||
|
|
@ -685,6 +689,11 @@ static void VulkanGetExtensions(VulkanExtensions& r) {
|
|||
});
|
||||
|
||||
r.enable_validation_layers = Config::VulkanValidationEnabled();
|
||||
if (Config::HdrEnabled() &&
|
||||
HasExtension(r.available_extensions, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME) &&
|
||||
!HasExtension(r.required_extensions, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)) {
|
||||
r.required_extensions.push_back(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME);
|
||||
}
|
||||
|
||||
if (HasExtension(r.available_extensions, VK_EXT_DEBUG_UTILS_EXTENSION_NAME)) {
|
||||
r.required_extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
|
||||
|
|
|
|||
|
|
@ -789,19 +789,20 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
|
|||
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
|
||||
static_cast<uint64_t>(ir.blocks.size()), phase_ms());
|
||||
IR::RewriteToSsa(ir.blocks);
|
||||
IR::ConstantPropagationPass(ir.blocks, ir.wave_size);
|
||||
IR::ResolveControlFlowIdentities(ir);
|
||||
IR::RemoveIdentities(ir.blocks);
|
||||
IR::EliminateDeadCode(ir.blocks);
|
||||
const auto read_lane_stats = IR::EliminateReadLane(ir, ir.wave_size);
|
||||
if (read_lane_stats.rewritten_reads != 0) {
|
||||
LOGF("%s read-lane elimination: reads=%" PRIu32 "\n", GetDumpLabel(options),
|
||||
read_lane_stats.rewritten_reads);
|
||||
const auto simplify = [&ir] {
|
||||
IR::ConstantPropagationPass(ir.blocks, ir.wave_size);
|
||||
IR::ResolveControlFlowIdentities(ir);
|
||||
IR::RemoveIdentities(ir.blocks);
|
||||
IR::EliminateDeadCode(ir.blocks);
|
||||
};
|
||||
simplify();
|
||||
const auto read_lane_stats = IR::EliminateReadLane(ir, ir.wave_size);
|
||||
if (read_lane_stats.rewritten_reads != 0) {
|
||||
LOGF("%s read-lane elimination: reads=%" PRIu32 "\n", GetDumpLabel(options),
|
||||
read_lane_stats.rewritten_reads);
|
||||
}
|
||||
// Cleanup also exposes profitable folds by removing dead predicate consumers.
|
||||
simplify();
|
||||
if (decoded.has_swap_pc) {
|
||||
// Leaf returns are resolved CFG edges; native PC values must not escape into GPU data.
|
||||
for (const auto* block: ir.blocks)
|
||||
|
|
@ -817,7 +818,7 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
|
|||
MakeIrDump(cfg_dump, ir).c_str());
|
||||
}
|
||||
}
|
||||
IR::TrackResources(ir, decoded, native_cfg);
|
||||
IR::TrackResources(ir, decoded, native_cfg, options.input_info);
|
||||
TranslateResult result;
|
||||
result.program = std::move(ir);
|
||||
if (options.dump_ir) {
|
||||
|
|
@ -885,7 +886,8 @@ CompileResult CompileProgram(TranslateResult translated, const CompileOptions& o
|
|||
|
||||
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " SPIR-V EmitProgram\n",
|
||||
GetDumpLabel(options), StageName(ir.stage), ir.shader_hash);
|
||||
auto spirv = Spirv::EmitProgram(ir, options.input_info, push_data_start_dword);
|
||||
auto spirv = Spirv::EmitProgram(ir, options.input_info, push_data_start_dword,
|
||||
options.maintenance8_enabled);
|
||||
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " SPIR-V EmitProgram words=%" PRIu64
|
||||
" elapsed_ms=%" PRIu64 "\n",
|
||||
GetDumpLabel(options), StageName(ir.stage), ir.shader_hash,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ struct CompileOptions {
|
|||
uint64_t shader_hash = 0;
|
||||
bool dump_ir = true;
|
||||
bool early_dump = false;
|
||||
bool maintenance8_enabled = false;
|
||||
const char* dump_label = nullptr;
|
||||
std::span<const uint32_t> user_data;
|
||||
std::span<const uint32_t> back_code;
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ void ValidateNativeProgram(const IR::Program& program, bool lds_storage) {
|
|||
} // namespace
|
||||
|
||||
std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo input_info,
|
||||
uint32_t push_data_start_dword) {
|
||||
uint32_t push_data_start_dword, bool maintenance8_enabled) {
|
||||
using namespace Emitter;
|
||||
|
||||
if (program.stage != ShaderType::Compute && program.stage != ShaderType::Vertex &&
|
||||
|
|
@ -215,7 +215,7 @@ std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo inp
|
|||
}
|
||||
IR::ValidateProgram(program, true);
|
||||
program.bindings = {.push_data_start_dword = push_data_start_dword};
|
||||
EmitterState state(program, input_info);
|
||||
EmitterState state(program, input_info, maintenance8_enabled);
|
||||
const auto* workgroup = ShaderWorkgroupInput(program.stage, input_info);
|
||||
state.lane_count =
|
||||
workgroup != nullptr && program.wave_size == 64u && workgroup->host_subgroup_size == 32u
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ namespace Libs::Graphics::ShaderRecompiler::Spirv {
|
|||
|
||||
// Resets and writes backend IDs and descriptor indices; do not emit the same program concurrently.
|
||||
std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo input_info,
|
||||
uint32_t push_data_start_dword = 0);
|
||||
uint32_t push_data_start_dword = 0,
|
||||
bool maintenance8_enabled = false);
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::Spirv
|
||||
|
||||
|
|
|
|||
|
|
@ -269,20 +269,13 @@ uint32_t ResultVector(ValueEmitContext& ctx, uint32_t value,
|
|||
result, packed[0], packed[1], packed[2], packed[3]);
|
||||
return result;
|
||||
}
|
||||
uint32_t component[4] {};
|
||||
for (uint32_t index = 0; index < 4u; index++) {
|
||||
if (dref) {
|
||||
component[index] = index == 0u ? FloatBits(ctx, value) : ConstantU32(ctx.state, 0);
|
||||
continue;
|
||||
}
|
||||
const auto scalar = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(
|
||||
spv::OpCompositeExtract, ImageScalarType(ctx.state, value_class), scalar, value, index);
|
||||
component[index] = SampledComponentBits(ctx, scalar, value_class);
|
||||
}
|
||||
if (!dref)
|
||||
return value_class == Prospero::TextureNumericClass::Uint
|
||||
? value : Unary(ctx.state, spv::OpBitcast, TypeU32Vector(ctx.state, 4), value);
|
||||
const auto result = ctx.state.builder.AllocateId();
|
||||
const auto zero = ConstantU32(ctx.state, 0);
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(ctx.state, 4), result,
|
||||
component[0], component[1], component[2], component[3]);
|
||||
FloatBits(ctx, value), zero, zero, zero);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -323,25 +316,26 @@ uint32_t QueryDimensions(ValueEmitContext& ctx, const IR::MemoryInfo& mem,
|
|||
return vector;
|
||||
}
|
||||
|
||||
uint32_t PackedOffset(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::Inst& address,
|
||||
const ImageSampleLayout& layout, ImageDimension dimension) {
|
||||
uint32_t PackedOffset(ValueEmitContext& ctx, IR::Value value, ImageDimension dimension,
|
||||
uint32_t& mask) {
|
||||
const auto components = ImageDimensionInfoFor(dimension).spatial_components;
|
||||
const auto zero = ConstantI32(ctx.state, 0);
|
||||
if (layout.offset == NoImageComponent || mem.image_address_components <= layout.offset) {
|
||||
if (components == 1u) return zero;
|
||||
const auto result = ctx.state.builder.AllocateId();
|
||||
if (components == 3u) {
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 3),
|
||||
result, zero, zero, zero);
|
||||
} else {
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 2),
|
||||
result, zero, zero);
|
||||
uint32_t values[3] {};
|
||||
if (value.IsImmediate()) {
|
||||
// RDNA2 packs signed six-bit texel offsets into the low bits of each byte.
|
||||
for (uint32_t index = 0; index < components; index++) {
|
||||
const auto bits = (value.U32() >> (index * 8u)) & 0x3fu;
|
||||
values[index] = ConstantI32(ctx.state, static_cast<int32_t>(bits ^ 0x20u) - 0x20);
|
||||
}
|
||||
return result;
|
||||
mask |= spv::ImageOperandsConstOffsetMask;
|
||||
return components == 1u
|
||||
? values[0]
|
||||
: ctx.state.builder.Constant(spv::OpConstantComposite,
|
||||
TypeI32Vector(ctx.state, components),
|
||||
std::span(values).first(components));
|
||||
}
|
||||
const auto packed = Unary(ctx.state, spv::OpBitcast, TypeI32(ctx.state),
|
||||
AddressU32(ctx, mem, address, layout.offset));
|
||||
uint32_t values[3] = {zero, zero, zero};
|
||||
ctx.state.builder.RequireCapability(spv::CapabilityImageGatherExtended);
|
||||
mask |= spv::ImageOperandsOffsetMask;
|
||||
const auto packed = Unary(ctx.state, spv::OpBitcast, TypeI32(ctx.state), ctx.Def(value));
|
||||
for (uint32_t index = 0; index < components; index++) {
|
||||
values[index] = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(spv::OpBitFieldSExtract, TypeI32(ctx.state), values[index],
|
||||
|
|
@ -350,13 +344,8 @@ uint32_t PackedOffset(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR
|
|||
}
|
||||
if (components == 1u) return values[0];
|
||||
const auto result = ctx.state.builder.AllocateId();
|
||||
if (components == 3u) {
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 3),
|
||||
result, values[0], values[1], values[2]);
|
||||
} else {
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 2),
|
||||
result, values[0], values[1]);
|
||||
}
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, components),
|
||||
result, std::span(values).first(components));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -388,17 +377,28 @@ uint32_t InverseSwizzle(uint32_t swizzle, uint32_t component) {
|
|||
|
||||
Format::BufferFormatInfo ImageConversionFormat(const EmitterState& state,
|
||||
const IR::MemoryInfo& mem) {
|
||||
const auto format = state.program.info.images[mem.resource].conversion_format;
|
||||
const auto& image = state.program.info.images[mem.resource];
|
||||
const auto format = image.conversion_format;
|
||||
if (format == Prospero::BufferFormat::kInvalid) return {};
|
||||
const auto info = Format::GetFormatInfo(format);
|
||||
EXIT_IF(Prospero::RemapTextureFormat(format) == format || info.component_count == 0u ||
|
||||
info.component_count > 4u);
|
||||
EXIT_IF(info.type != Format::ComponentType::Uscaled &&
|
||||
(info.type != Format::ComponentType::Uint || !info.packed_bitfield ||
|
||||
((info.type != Format::ComponentType::Uint &&
|
||||
info.type != Format::ComponentType::Unorm) || !info.packed_bitfield ||
|
||||
info.byte_size != sizeof(uint32_t)));
|
||||
return info;
|
||||
}
|
||||
|
||||
uint32_t ExtractPackedImageComponent(EmitterState& state, const Format::BufferFormatInfo& info,
|
||||
uint32_t packed, uint32_t type, uint32_t component) {
|
||||
const auto result = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpBitFieldUExtract, type, result, packed,
|
||||
ConstantU32(state, info.component_bit_offset[component]),
|
||||
ConstantU32(state, info.component_bits[component]));
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t ImageGatherSource(const EmitterState& state, const IR::MemoryInfo& mem) {
|
||||
const auto component = ImageGatherComponent(mem.dmask);
|
||||
const auto info = ImageConversionFormat(state, mem);
|
||||
|
|
@ -422,13 +422,11 @@ uint32_t UnpackImageTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint
|
|||
}
|
||||
uint32_t components[4] {};
|
||||
for (uint32_t component = 0; component < info.component_count; component++) {
|
||||
components[component] = ctx.state.builder.AllocateId();
|
||||
if (info.packed_bitfield) {
|
||||
ctx.state.builder.AddFunction(spv::OpBitFieldUExtract, scalar_type,
|
||||
components[component], packed,
|
||||
ConstantU32(ctx.state, info.component_bit_offset[component]),
|
||||
ConstantU32(ctx.state, info.component_bits[component]));
|
||||
components[component] =
|
||||
ExtractPackedImageComponent(ctx.state, info, packed, scalar_type, component);
|
||||
} else {
|
||||
components[component] = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeExtract, scalar_type,
|
||||
components[component], texel, component);
|
||||
// UNorm backing preserves the guest's filtering; scaling precedes swizzle constants.
|
||||
|
|
@ -488,21 +486,74 @@ uint32_t UnpackImageGather(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uin
|
|||
return Binary(ctx.state, spv::OpFMul, vector_type, gathered, scales);
|
||||
}
|
||||
|
||||
uint32_t values[4] {};
|
||||
for (uint32_t lane = 0; lane < 4u; lane++) {
|
||||
const auto packed = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), packed, gathered,
|
||||
lane);
|
||||
values[lane] = ctx.state.builder.AllocateId();
|
||||
const auto physical = (selector - 4u) % info.component_count;
|
||||
ctx.state.builder.AddFunction(spv::OpBitFieldUExtract, TypeU32(ctx.state), values[lane],
|
||||
packed,
|
||||
ConstantU32(ctx.state, info.component_bit_offset[physical]),
|
||||
ConstantU32(ctx.state, info.component_bits[physical]));
|
||||
return ExtractPackedImageComponent(ctx.state, info, gathered, TypeU32Vector(ctx.state, 4),
|
||||
(selector - 4u) % info.component_count);
|
||||
}
|
||||
|
||||
uint32_t SamplePackedUnormLz(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t coord) {
|
||||
auto& state = ctx.state;
|
||||
const auto& image = state.program.info.images[mem.resource];
|
||||
const auto info = ImageConversionFormat(state, mem);
|
||||
const auto sampled = MakeSampledImage(state, mem.resource,
|
||||
LoadSamplerDescriptor(state, mem.sampler));
|
||||
const auto gathered = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpImageGather, TypeU32Vector(state, 4), gathered, sampled,
|
||||
coord, ConstantU32(state, 0u));
|
||||
state.builder.RequireCapability(spv::CapabilityImageQuery);
|
||||
const auto size = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpImageQuerySizeLod, TypeU32Vector(state, 2), size,
|
||||
LoadImageDescriptor(state, mem.resource), ConstantU32(state, 0u));
|
||||
const auto size_f32 = Unary(state, spv::OpConvertUToF, TypeF32Vector(state, 2), size);
|
||||
const auto half = ConstantF32Value(state, 0.5f);
|
||||
const auto halves = state.builder.Constant(spv::OpConstantComposite,
|
||||
TypeF32Vector(state, 2), half, half);
|
||||
const auto fractions = state.builder.AllocateId();
|
||||
state.builder.AddFunction(
|
||||
spv::OpExtInst, TypeF32Vector(state, 2), fractions, GlslStd450(state), GLSLstd450Fract,
|
||||
Binary(state, spv::OpFSub, TypeF32Vector(state, 2),
|
||||
Binary(state, spv::OpFMul, TypeF32Vector(state, 2), coord, size_f32), halves));
|
||||
uint32_t fraction[2];
|
||||
uint32_t complement[2];
|
||||
for (uint32_t axis = 0; axis < 2u; axis++) {
|
||||
fraction[axis] = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpCompositeExtract, TypeF32(state), fraction[axis],
|
||||
fractions, axis);
|
||||
complement[axis] = Binary(state, spv::OpFSub, TypeF32(state),
|
||||
ConstantF32Value(state, 1.0f), fraction[axis]);
|
||||
}
|
||||
const auto result = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(ctx.state, 4), result,
|
||||
values[0], values[1], values[2], values[3]);
|
||||
// Gather order is (x0, y1), (x1, y1), (x1, y0), (x0, y0).
|
||||
const auto weights = state.builder.AllocateId();
|
||||
state.builder.AddFunction(
|
||||
spv::OpCompositeConstruct, TypeF32Vector(state, 4), weights,
|
||||
Binary(state, spv::OpFMul, TypeF32(state), complement[0], fraction[1]),
|
||||
Binary(state, spv::OpFMul, TypeF32(state), fraction[0], fraction[1]),
|
||||
Binary(state, spv::OpFMul, TypeF32(state), fraction[0], complement[1]),
|
||||
Binary(state, spv::OpFMul, TypeF32(state), complement[0], complement[1]));
|
||||
uint32_t decoded[4] {};
|
||||
uint32_t selected[4] {ZeroF32(state), ZeroF32(state), ZeroF32(state), ZeroF32(state)};
|
||||
for (uint32_t component = 0; component < 4u; component++) {
|
||||
if ((mem.dmask & (1u << component)) == 0u) continue;
|
||||
const auto selector = (image.shader_swizzle >> (component * 3u)) & 7u;
|
||||
if (selector == 1u) {
|
||||
selected[component] = ConstantF32Value(state, 1.0f);
|
||||
} else if (selector >= 4u) {
|
||||
const auto physical = (selector - 4u) % info.component_count;
|
||||
if (decoded[physical] == 0u) {
|
||||
const auto texels = Unary(
|
||||
state, spv::OpConvertUToF, TypeF32Vector(state, 4),
|
||||
ExtractPackedImageComponent(state, info, gathered,
|
||||
TypeU32Vector(state, 4), physical));
|
||||
decoded[physical] = Binary(
|
||||
state, spv::OpFMul, TypeF32(state),
|
||||
Binary(state, spv::OpDot, TypeF32(state), texels, weights),
|
||||
ConstantF32Value(state, 1.0f / ((1u << info.component_bits[physical]) - 1u)));
|
||||
}
|
||||
selected[component] = decoded[physical];
|
||||
}
|
||||
}
|
||||
const auto result = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 4), result,
|
||||
selected[0], selected[1], selected[2], selected[3]);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -548,47 +599,22 @@ uint32_t EmitOneDimensionalGatherLz(ValueEmitContext& ctx, const IR::MemoryInfo&
|
|||
return result;
|
||||
}
|
||||
|
||||
uint32_t PackImageTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t texel) {
|
||||
const auto info = ImageConversionFormat(ctx.state, mem);
|
||||
if (info.format == Prospero::BufferFormat::kInvalid) return texel;
|
||||
EXIT_IF(info.type != Format::ComponentType::Uint || !info.packed_bitfield);
|
||||
|
||||
auto packed = ConstantU32(ctx.state, 0u);
|
||||
for (uint32_t component = 0; component < info.component_count; component++) {
|
||||
const auto value = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), value, texel,
|
||||
component);
|
||||
const auto maximum =
|
||||
ConstantU32(ctx.state, info.component_bits[component] == 32u
|
||||
? UINT32_MAX
|
||||
: (1u << info.component_bits[component]) - 1u);
|
||||
const auto within =
|
||||
Binary(ctx.state, spv::OpULessThan, TypeBool(ctx.state), value, maximum);
|
||||
const auto clamped = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(spv::OpSelect, TypeU32(ctx.state), clamped, within, value,
|
||||
maximum);
|
||||
const auto shifted =
|
||||
info.component_bit_offset[component] == 0u
|
||||
? clamped
|
||||
: Binary(ctx.state, spv::OpShiftLeftLogical, TypeU32(ctx.state), clamped,
|
||||
ConstantU32(ctx.state, info.component_bit_offset[component]));
|
||||
packed = Binary(ctx.state, spv::OpBitwiseOr, TypeU32(ctx.state), packed, shifted);
|
||||
}
|
||||
const auto zero = ConstantU32(ctx.state, 0u);
|
||||
const auto result = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(ctx.state, 4), result,
|
||||
packed, zero, zero, zero);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t StoreTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t data, bool integer) {
|
||||
const auto swizzle = ctx.state.program.info.images[mem.resource].shader_swizzle;
|
||||
uint32_t values[4] {};
|
||||
const auto info = ImageConversionFormat(ctx.state, mem);
|
||||
const bool packed = info.format != Prospero::BufferFormat::kInvalid;
|
||||
EXIT_IF(packed && (!info.packed_bitfield ||
|
||||
(info.type != Format::ComponentType::Uint &&
|
||||
info.type != Format::ComponentType::Unorm)));
|
||||
const auto zero = ConstantU32(ctx.state, 0);
|
||||
uint32_t values[4] {zero, zero, zero, zero};
|
||||
auto packed_value = zero;
|
||||
const auto dmask = mem.dmask != 0u ? mem.dmask : 1u;
|
||||
for (uint32_t component = 0; component < 4u; component++) {
|
||||
for (uint32_t component = 0; component < (packed ? info.component_count : 4u); component++) {
|
||||
const auto source = InverseSwizzle(swizzle, component);
|
||||
uint32_t raw = ConstantU32(ctx.state, 0);
|
||||
if (source < 4u && ((dmask >> source) & 1u) != 0u) {
|
||||
const bool selected = source < 4u && ((dmask >> source) & 1u) != 0u;
|
||||
uint32_t raw = zero;
|
||||
if (selected) {
|
||||
const auto packed_index = DmaskComponentIndex(dmask, source);
|
||||
raw = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), raw, data,
|
||||
|
|
@ -598,21 +624,39 @@ uint32_t StoreTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t d
|
|||
raw = Binary(ctx.state, spv::OpShiftRightLogical, TypeU32(ctx.state), raw,
|
||||
ConstantU32(ctx.state, 16u));
|
||||
}
|
||||
raw = Binary(ctx.state, spv::OpBitwiseAnd, TypeU32(ctx.state), raw,
|
||||
ConstantU32(ctx.state, 0xffffu));
|
||||
if (!packed) {
|
||||
raw = Binary(ctx.state, spv::OpBitwiseAnd, TypeU32(ctx.state), raw,
|
||||
ConstantU32(ctx.state, 0xffffu));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (packed) {
|
||||
if (!selected) continue;
|
||||
auto value = EncodeFormattedStoreComponent(ctx.state, info, component, raw, mem.data_bits);
|
||||
if (info.type == Format::ComponentType::Uint) {
|
||||
value = EmitUMin32(ctx.state, value,
|
||||
ConstantU32(ctx.state, (1u << info.component_bits[component]) - 1u));
|
||||
}
|
||||
if (info.component_bit_offset[component] != 0u) {
|
||||
value = Binary(ctx.state, spv::OpShiftLeftLogical, TypeU32(ctx.state), value,
|
||||
ConstantU32(ctx.state, info.component_bit_offset[component]));
|
||||
}
|
||||
packed_value = packed_value == zero ? value
|
||||
: Binary(ctx.state, spv::OpBitwiseOr, TypeU32(ctx.state), packed_value, value);
|
||||
continue;
|
||||
}
|
||||
values[component] = integer ? raw
|
||||
: mem.data_bits == 16u
|
||||
? EmitF16BitsToF32(ctx.state, raw)
|
||||
: Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), raw);
|
||||
}
|
||||
if (packed) values[0] = packed_value;
|
||||
const auto texel = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(spv::OpCompositeConstruct,
|
||||
integer ? TypeU32Vector(ctx.state, 4)
|
||||
: TypeF32Vector(ctx.state, 4),
|
||||
texel, values[0], values[1], values[2], values[3]);
|
||||
return PackImageTexel(ctx, mem, texel);
|
||||
return texel;
|
||||
}
|
||||
|
||||
template <typename Emit>
|
||||
|
|
@ -699,6 +743,87 @@ uint32_t EmitImageAccess(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t r
|
|||
result_type, EmitRun);
|
||||
}
|
||||
|
||||
template <typename Emit>
|
||||
uint32_t EmitImageResult(ValueEmitContext& ctx, const IR::Inst& inst, const Emit& emit) {
|
||||
auto& state = ctx.state;
|
||||
const auto& mem = ctx.Memory(inst);
|
||||
const auto& image = state.program.info.images[mem.resource];
|
||||
const bool gather = inst.GetOpcode() == IR::ValueOpcode::ImageGatherRaw;
|
||||
const bool dref = HasFlag(mem, Decoder::ImageSampleFlagCompare);
|
||||
const bool scalar = dref && !gather;
|
||||
const bool mixed = std::ranges::any_of(image.indirect_resources, [&](uint32_t resource) {
|
||||
return state.program.info.images[resource].numeric_class != image.numeric_class;
|
||||
});
|
||||
const auto native_type = [&](uint32_t resource) {
|
||||
return scalar ? TypeF32(state) : ImageVectorType(
|
||||
state, dref ? Prospero::TextureNumericClass::Float
|
||||
: state.program.info.images[resource].numeric_class, 4);
|
||||
};
|
||||
const auto result_bits = [&](uint32_t resource, uint32_t value) {
|
||||
auto candidate_mem = mem;
|
||||
candidate_mem.resource = resource;
|
||||
if (!scalar) {
|
||||
value = gather ? UnpackImageGather(ctx, candidate_mem, value)
|
||||
: UnpackImageTexel(ctx, candidate_mem, value);
|
||||
}
|
||||
return ResultVector(ctx, value,
|
||||
dref ? Prospero::TextureNumericClass::Float
|
||||
: state.program.info.images[resource].numeric_class,
|
||||
scalar, candidate_mem, gather);
|
||||
};
|
||||
// RDNA returns register bits. Convert per typed run only when the join mixes
|
||||
// numeric classes; homogeneous accesses keep their single conversion after it.
|
||||
const auto value = EmitImageAccess(
|
||||
ctx, inst, mixed ? TypeU32Vector(state, 4) : native_type(mem.resource),
|
||||
[&](uint32_t resource, uint32_t array_index) {
|
||||
const auto native = emit(resource, array_index, native_type(resource));
|
||||
return mixed ? result_bits(resource, native) : native;
|
||||
});
|
||||
return mixed ? value : result_bits(mem.resource, value);
|
||||
}
|
||||
|
||||
struct ImageSampler {
|
||||
uint32_t id = 0;
|
||||
bool dynamic = false;
|
||||
};
|
||||
|
||||
std::array<ImageSampler, 2> LoadImageSamplers(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
auto& state = ctx.state;
|
||||
const auto& mem = ctx.Memory(inst);
|
||||
const auto& samplers = state.program.info.samplers;
|
||||
const auto& root = samplers.at(mem.sampler);
|
||||
std::array<ImageSampler, 2> result {};
|
||||
const auto load = [&](const IR::ImageResource& image) {
|
||||
const bool integer = image.numeric_class != Prospero::TextureNumericClass::Float;
|
||||
auto& sampler = result[integer];
|
||||
if (sampler.id != 0u) return;
|
||||
uint32_t index = mem.sampler;
|
||||
if (root.integer_border != integer) {
|
||||
const auto found = std::ranges::find_if(samplers, [&](const auto& candidate) {
|
||||
return candidate.snapshot_index == root.snapshot_index &&
|
||||
candidate.integer_border == integer;
|
||||
});
|
||||
if (found == samplers.end()) ctx.Fail(inst, "has no typed sampler variant");
|
||||
index = static_cast<uint32_t>(found - samplers.begin());
|
||||
}
|
||||
uint32_t array_index = 0;
|
||||
const auto& candidates = samplers[index].indirect_resources;
|
||||
if (!candidates.empty()) {
|
||||
const auto* handle = inst.Arg(1).ResolveInstruction();
|
||||
if (handle == nullptr || handle->GetOpcode() != IR::ValueOpcode::GetSamplerResource)
|
||||
ctx.Fail(inst, "has invalid finite sampler key provenance");
|
||||
array_index = EmitIndexSwitch(
|
||||
state, ctx.Def(handle->Arg(0)), static_cast<uint32_t>(candidates.size()),
|
||||
TypeU32(state), [&](uint32_t ordinal) { return ConstantU32(state, candidates[ordinal]); });
|
||||
}
|
||||
sampler = {LoadSamplerDescriptor(state, index, array_index), array_index != 0u};
|
||||
};
|
||||
const auto& image = state.program.info.images[mem.resource];
|
||||
load(image);
|
||||
for (const auto candidate: image.indirect_resources) load(state.program.info.images[candidate]);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
|
|
@ -715,6 +840,22 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
ctx.Define(inst, QueryDimensions(ctx, mem, *address));
|
||||
return;
|
||||
}
|
||||
if (image.resource_class == IR::ImageResourceClass::Sampled &&
|
||||
image.conversion_format == Prospero::BufferFormat::k11_11_10UNorm) {
|
||||
if (op != IR::ValueOpcode::ImageSampleRaw ||
|
||||
image.dimension != ImageDimension::Dim2D ||
|
||||
mem.image_dimension != ImageDimension::Dim2D || mem.data_bits != 32u ||
|
||||
mem.image_sample_flags != Decoder::ImageSampleFlagLevelZero ||
|
||||
image.indirect_root != IR::ImageResource::NoIndirectImage ||
|
||||
!state.program.info.samplers.at(mem.sampler).indirect_resources.empty()) {
|
||||
ctx.Fail(inst, "packed UNorm sampling requires a direct 2D sample_lz");
|
||||
return;
|
||||
}
|
||||
const auto coord = CoordF32(ctx, mem, *address, Layout(mem).coord, 2u);
|
||||
ctx.Define(inst, ResultVector(ctx, SamplePackedUnormLz(ctx, mem, coord),
|
||||
Prospero::TextureNumericClass::Float, false, mem));
|
||||
return;
|
||||
}
|
||||
if (op == IR::ValueOpcode::ImageQueryLod) {
|
||||
state.builder.RequireCapability(spv::CapabilityImageQuery);
|
||||
const auto dimension = image.dimension;
|
||||
|
|
@ -740,16 +881,14 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
return;
|
||||
}
|
||||
if (op == IR::ValueOpcode::ImageRead) {
|
||||
const auto numeric_class = image.numeric_class;
|
||||
const auto condition = ctx.Arg(inst, 2);
|
||||
ctx.Define(
|
||||
inst,
|
||||
EmitValueOrDefaultIfCondition(
|
||||
state, condition, TypeU32Vector(state, 4), ConstantU32CompositeZero(state, 4),
|
||||
[&]() {
|
||||
const auto color = EmitImageAccess(
|
||||
ctx, inst, ImageVectorType(state, numeric_class, 4),
|
||||
[&](uint32_t resource, uint32_t array_index) {
|
||||
return EmitImageResult(
|
||||
ctx, inst, [&](uint32_t resource, uint32_t array_index, uint32_t result_type) {
|
||||
const auto& candidate = state.program.info.images[resource];
|
||||
const auto dimension = candidate.dimension;
|
||||
const auto& dimension_info = ImageDimensionInfoFor(dimension);
|
||||
|
|
@ -758,19 +897,17 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
const auto coord = CoordU32(ctx, mem, *address, dimension);
|
||||
if (dimension_info.multisampled != 0u) {
|
||||
state.builder.AddFunction(
|
||||
spv::OpImageFetch, ImageVectorType(state, candidate.numeric_class, 4), fetched,
|
||||
spv::OpImageFetch, result_type, fetched,
|
||||
descriptor, coord, spv::ImageOperandsSampleMask,
|
||||
AddressU32(ctx, mem, *address, dimension_info.coordinate_components));
|
||||
} else {
|
||||
state.builder.AddFunction(
|
||||
spv::OpImageFetch, ImageVectorType(state, candidate.numeric_class, 4), fetched,
|
||||
spv::OpImageFetch, result_type, fetched,
|
||||
descriptor, coord, spv::ImageOperandsLodMask,
|
||||
LodU32(ctx, mem, *address, mem.image_dimension));
|
||||
}
|
||||
return fetched;
|
||||
});
|
||||
return ResultVector(ctx, UnpackImageTexel(ctx, mem, color), numeric_class,
|
||||
false, mem);
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
|
@ -794,21 +931,28 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
const auto layout = Layout(mem);
|
||||
const auto numeric_class = image.numeric_class;
|
||||
const bool dref = HasFlag(mem, Decoder::ImageSampleFlagCompare);
|
||||
IR::Value offset_value;
|
||||
if (layout.offset != NoImageComponent) {
|
||||
const auto value = address->Arg(layout.offset).Resolve();
|
||||
offset_value = IR::ResolveActiveU32(value, inst.Arg(inst.NumArgs() - 1u).Resolve());
|
||||
if (offset_value.IsEmpty()) offset_value = value;
|
||||
}
|
||||
if (dref && state.program.info.images[mem.resource].conversion_format !=
|
||||
Prospero::BufferFormat::kInvalid) {
|
||||
ctx.Fail(inst, "uses depth comparison with a converted image");
|
||||
return;
|
||||
}
|
||||
if (op == IR::ValueOpcode::ImageGatherRaw) {
|
||||
const auto coord = CoordF32(ctx, mem, *address, layout.coord,
|
||||
dimension_info.coordinate_components, image.cube);
|
||||
if (dimension == ImageDimension::Dim1D) {
|
||||
if (dref || !HasFlag(mem, Decoder::ImageSampleFlagLevelZero) ||
|
||||
if (image.indirect_root == mem.resource || dref ||
|
||||
!HasFlag(mem, Decoder::ImageSampleFlagLevelZero) ||
|
||||
HasFlag(mem, Decoder::ImageSampleFlagOffset) ||
|
||||
HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) {
|
||||
ctx.Fail(inst, "has an unsupported 1D gather variant");
|
||||
return;
|
||||
}
|
||||
const auto coord = CoordF32(ctx, mem, *address, layout.coord,
|
||||
dimension_info.coordinate_components, image.cube);
|
||||
const auto sample = EmitOneDimensionalGatherLz(ctx, mem, coord, numeric_class);
|
||||
ctx.Define(inst, ResultVector(ctx, UnpackImageGather(ctx, mem, sample),
|
||||
numeric_class, false, mem, true));
|
||||
|
|
@ -818,9 +962,6 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
ctx.Fail(inst, "has an unsupported 1D-array gather");
|
||||
return;
|
||||
}
|
||||
const auto result_numeric_class = dref ? Prospero::TextureNumericClass::Float
|
||||
: numeric_class;
|
||||
const auto result_type = ImageVectorType(state, result_numeric_class, 4);
|
||||
uint32_t component_or_dref;
|
||||
if (dref) {
|
||||
component_or_dref = layout.dref != NoImageComponent
|
||||
|
|
@ -829,34 +970,40 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
} else {
|
||||
component_or_dref = ConstantU32(state, ImageGatherSource(state, mem));
|
||||
}
|
||||
uint32_t operand_mask = 0;
|
||||
uint32_t offset = 0;
|
||||
if (HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) {
|
||||
operand_mask = spv::ImageOperandsConstOffsetsMask;
|
||||
offset = HorizontalOffsets(state, dimension);
|
||||
} else if (layout.offset != NoImageComponent) {
|
||||
operand_mask = spv::ImageOperandsOffsetMask;
|
||||
offset = PackedOffset(ctx, mem, *address, layout, dimension);
|
||||
}
|
||||
const auto sampler_id = LoadSamplerDescriptor(state, mem.sampler);
|
||||
const auto EmitGather = [&](uint32_t mip) {
|
||||
const auto sampled = MakeSampledImage(state, mem.resource, sampler_id, mip);
|
||||
const auto sample = state.builder.AllocateId();
|
||||
const std::array operands {operand_mask, offset};
|
||||
state.builder.AddFunction(
|
||||
dref ? spv::OpImageDrefGather : spv::OpImageGather, result_type,
|
||||
sample, sampled, coord, component_or_dref,
|
||||
std::span(operands).first(operand_mask != 0u ? 2u : 0u));
|
||||
return sample;
|
||||
const auto samplers = LoadImageSamplers(ctx, inst);
|
||||
const auto EmitGather = [&](uint32_t resource, uint32_t array_index, uint32_t result_type) {
|
||||
const auto& candidate = state.program.info.images[resource];
|
||||
const auto sampler = samplers[candidate.numeric_class != Prospero::TextureNumericClass::Float];
|
||||
const auto dimension = candidate.dimension;
|
||||
const auto coord = CoordF32(
|
||||
ctx, mem, *address, layout.coord,
|
||||
ImageDimensionInfoFor(dimension).coordinate_components, candidate.cube);
|
||||
uint32_t operand_mask = 0;
|
||||
uint32_t offset = 0;
|
||||
if (HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) {
|
||||
operand_mask = spv::ImageOperandsConstOffsetsMask;
|
||||
offset = HorizontalOffsets(state, dimension);
|
||||
} else if (layout.offset != NoImageComponent) {
|
||||
offset = PackedOffset(ctx, offset_value, dimension, operand_mask);
|
||||
}
|
||||
const auto EmitMip = [&](uint32_t mip) {
|
||||
const auto sampled = MakeSampledImage(state, resource, sampler.id, mip, array_index,
|
||||
sampler.dynamic);
|
||||
const auto sample = state.builder.AllocateId();
|
||||
const std::array operands {operand_mask, offset};
|
||||
state.builder.AddFunction(
|
||||
dref ? spv::OpImageDrefGather : spv::OpImageGather, result_type,
|
||||
sample, sampled, coord, component_or_dref,
|
||||
std::span(operands).first(operand_mask != 0u ? 2u : 0u));
|
||||
return sample;
|
||||
};
|
||||
return candidate.mip_mode == IR::ImageMipMode::Dynamic && candidate.mip_count > 1u
|
||||
? EmitIndexSwitch(
|
||||
state, GatherMip(ctx, inst, mem, *address, layout, candidate.mip_count),
|
||||
candidate.mip_count, result_type, EmitMip)
|
||||
: EmitMip(0u);
|
||||
};
|
||||
const auto sample = image.mip_mode == IR::ImageMipMode::Dynamic && image.mip_count > 1u
|
||||
? EmitIndexSwitch(
|
||||
state, GatherMip(ctx, inst, mem, *address, layout,
|
||||
image.mip_count),
|
||||
image.mip_count, result_type, EmitGather)
|
||||
: EmitGather(0);
|
||||
ctx.Define(inst, ResultVector(ctx, UnpackImageGather(ctx, mem, sample),
|
||||
result_numeric_class, false, mem, true));
|
||||
ctx.Define(inst, EmitImageResult(ctx, inst, EmitGather));
|
||||
return;
|
||||
}
|
||||
const bool explicit_lod = HasFlag(mem, Decoder::ImageSampleFlagDerivative) ||
|
||||
|
|
@ -869,16 +1016,14 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
} else if (dref) {
|
||||
opcode = spv::OpImageSampleDrefImplicitLod;
|
||||
}
|
||||
uint32_t result_type = ImageVectorType(state, numeric_class, 4);
|
||||
uint32_t dref_value = 0;
|
||||
if (dref) {
|
||||
result_type = TypeF32(state);
|
||||
dref_value = ZeroF32(state);
|
||||
if (layout.dref != NoImageComponent) {
|
||||
dref_value = AddressF32(ctx, mem, *address, layout.dref);
|
||||
}
|
||||
}
|
||||
std::array<uint32_t, 3> operands {};
|
||||
std::array<uint32_t, 4> operands {};
|
||||
size_t operand_count = 0;
|
||||
if (HasFlag(mem, Decoder::ImageSampleFlagDerivative)) {
|
||||
operands[0] = spv::ImageOperandsGradMask;
|
||||
|
|
@ -898,36 +1043,34 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
operands[1] = AddressF32(ctx, mem, *address, layout.bias);
|
||||
operand_count = 2;
|
||||
}
|
||||
const auto& samplers = state.program.info.samplers.at(mem.sampler).indirect_resources;
|
||||
uint32_t sampler_index = 0;
|
||||
if (!samplers.empty()) {
|
||||
const auto* handle = inst.Arg(1).ResolveInstruction();
|
||||
if (handle == nullptr || handle->GetOpcode() != IR::ValueOpcode::GetSamplerResource)
|
||||
ctx.Fail(inst, "has invalid finite sampler key provenance");
|
||||
sampler_index = EmitIndexSwitch(
|
||||
state, ctx.Def(handle->Arg(0)), static_cast<uint32_t>(samplers.size()),
|
||||
TypeU32(state), [&](uint32_t ordinal) { return ConstantU32(state, samplers[ordinal]); });
|
||||
if (layout.offset != NoImageComponent &&
|
||||
!offset_value.IsImmediate() && !state.maintenance8_enabled) {
|
||||
ctx.Fail(inst, "requires maintenance8 for a dynamic sample texel offset");
|
||||
}
|
||||
const auto sampler_id = LoadSamplerDescriptor(state, mem.sampler, sampler_index);
|
||||
const auto EmitSample = [&](uint32_t resource, uint32_t array_index) {
|
||||
const auto samplers = LoadImageSamplers(ctx, inst);
|
||||
const auto EmitSample = [&](uint32_t resource, uint32_t array_index, uint32_t result_type) {
|
||||
const auto& candidate = state.program.info.images[resource];
|
||||
const auto sampler = samplers[candidate.numeric_class != Prospero::TextureNumericClass::Float];
|
||||
auto sample_operands = operands;
|
||||
auto sample_operand_count = operand_count;
|
||||
if (layout.offset != NoImageComponent) {
|
||||
sample_operand_count = std::max(size_t{1}, sample_operand_count);
|
||||
sample_operands[sample_operand_count++] = PackedOffset(
|
||||
ctx, offset_value, candidate.dimension, sample_operands[0]);
|
||||
}
|
||||
const auto coord =
|
||||
CoordF32(ctx, mem, *address, layout.coord,
|
||||
ImageDimensionInfoFor(candidate.dimension).coordinate_components,
|
||||
candidate.cube);
|
||||
const auto sampled = MakeSampledImage(state, resource, sampler_id, 0u, array_index,
|
||||
sampler_index != 0u);
|
||||
const auto sampled = MakeSampledImage(state, resource, sampler.id, 0u, array_index,
|
||||
sampler.dynamic);
|
||||
const auto sample = state.builder.AllocateId();
|
||||
state.builder.AddFunction(opcode, result_type, sample, sampled, coord,
|
||||
std::span(&dref_value, dref ? 1u : 0u),
|
||||
std::span<const uint32_t>(operands).first(operand_count));
|
||||
std::span<const uint32_t>(sample_operands).first(sample_operand_count));
|
||||
return sample;
|
||||
};
|
||||
auto result = EmitImageAccess(ctx, inst, result_type, EmitSample);
|
||||
if (!dref) {
|
||||
result = UnpackImageTexel(ctx, mem, result);
|
||||
}
|
||||
ctx.Define(inst, ResultVector(ctx, result, numeric_class, dref, mem));
|
||||
ctx.Define(inst, EmitImageResult(ctx, inst, EmitSample));
|
||||
return;
|
||||
}
|
||||
if (image_info.access == IR::ImageAccess::Atomic) {
|
||||
|
|
|
|||
|
|
@ -5,15 +5,17 @@
|
|||
#include <bit>
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
|
||||
uint32_t EmitIdentity(ValueEmitContext& ctx, uint32_t value);
|
||||
|
||||
// Operand signatures distinguish SPIR-V IDs (uint32_t), raw literals/values
|
||||
// (IR::Value), and instructions whose metadata or operand loads must stay lazy.
|
||||
#define EMIT_NATIVE(name, opcode, type, ...) \
|
||||
inline constexpr auto Emit##name = EmitNative<spv::opcode, IR::Type::type, __VA_ARGS__>;
|
||||
EMIT_NATIVE(BitCastU16F16, OpBitcast, U32, uint32_t)
|
||||
inline constexpr auto EmitBitCastU16F16 = EmitIdentity;
|
||||
inline constexpr auto EmitBitCastF16U16 = EmitBitCastU16F16;
|
||||
inline constexpr auto EmitConvertU32U16 = EmitBitCastU16F16;
|
||||
inline constexpr auto EmitConvertU32U8 = EmitBitCastU16F16;
|
||||
inline constexpr auto EmitBitCastU32F32 = EmitBitCastU16F16;
|
||||
EMIT_NATIVE(BitCastU32F32, OpBitcast, U32, uint32_t)
|
||||
EMIT_NATIVE(BitCastF32U32, OpBitcast, F32, uint32_t)
|
||||
EMIT_NATIVE(BitCastU64F64, OpBitcast, U64, uint32_t)
|
||||
EMIT_NATIVE(BitCastF64U64, OpBitcast, F64, uint32_t)
|
||||
|
|
@ -207,7 +209,6 @@ uint32_t EmitFPSin(EmitterState& state, uint32_t arg0);
|
|||
uint32_t EmitFPCos(EmitterState& state, uint32_t arg0);
|
||||
#undef EMIT_NATIVE
|
||||
|
||||
uint32_t EmitIdentity(ValueEmitContext& ctx, uint32_t value);
|
||||
void EmitVoid(ValueEmitContext&);
|
||||
inline constexpr auto EmitReference = EmitVoid;
|
||||
inline constexpr auto EmitReferenceU32 = EmitVoid;
|
||||
|
|
@ -321,6 +322,8 @@ inline constexpr auto EmitSharedAtomicDec32 = EmitSharedIncDec;
|
|||
inline constexpr auto EmitSharedAtomicSMin32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicUMin32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicSMax32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicUMax64 = EmitSharedAtomic64;
|
||||
inline constexpr auto EmitSharedAtomicCmpSwap64 = EmitSharedAtomic64;
|
||||
inline constexpr auto EmitSharedAtomicUMax32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicAnd32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicOr32 = EmitAtomic32;
|
||||
|
|
|
|||
|
|
@ -73,9 +73,9 @@ struct ImageDefinition {
|
|||
};
|
||||
|
||||
struct EmitterState {
|
||||
EmitterState(IR::Program& program_, ShaderStageInputInfo input_info_)
|
||||
EmitterState(IR::Program& program_, ShaderStageInputInfo input_info_, bool maintenance8_enabled_)
|
||||
: builder(program_.stage == ShaderType::Mesh ? 0x00010400u : 0x00010300u),
|
||||
program(program_), input_info(input_info_) {
|
||||
program(program_), input_info(input_info_), maintenance8_enabled(maintenance8_enabled_) {
|
||||
if (ShaderWorkgroupInput(program.stage, input_info) != nullptr) {
|
||||
lds_storage_class = spv::StorageClassWorkgroup;
|
||||
}
|
||||
|
|
@ -88,6 +88,7 @@ struct EmitterState {
|
|||
Builder builder;
|
||||
IR::Program& program;
|
||||
ShaderStageInputInfo input_info;
|
||||
bool maintenance8_enabled;
|
||||
uint32_t void_type = 0;
|
||||
uint32_t bool_type = 0;
|
||||
uint32_t u32_type = 0;
|
||||
|
|
@ -398,6 +399,8 @@ uint32_t StorageBufferPackedStride(const EmitterState& state, const IR::MemoryIn
|
|||
|
||||
Prospero::BufferFormat StorageBufferFormat(const EmitterState& state, const IR::MemoryInfo& mem);
|
||||
|
||||
bool UsesSharedIndirectBufferStore(const EmitterState& state, uint32_t resource);
|
||||
|
||||
void EmitMemoryOffsets(EmitterState& state);
|
||||
|
||||
uint32_t LdsDwordCount(const EmitterState& state);
|
||||
|
|
@ -409,6 +412,7 @@ struct MemoryResourceAccess {
|
|||
uint32_t length = 0;
|
||||
uint32_t byte_offset = 0;
|
||||
uint32_t element_bits = 32;
|
||||
bool non_uniform = false;
|
||||
spv::MemoryAccessMask memory_access = spv::MemoryAccessMaskNone;
|
||||
};
|
||||
|
||||
|
|
@ -418,7 +422,8 @@ MemoryResourceAccess PrepareMemoryResourceAccess(EmitterState& state, const IR::
|
|||
|
||||
MemoryResourceAccess PrepareStorageBufferResourceAccess(EmitterState& state,
|
||||
const IR::MemoryInfo& mem,
|
||||
BufferDefinition& buffer);
|
||||
BufferDefinition& buffer,
|
||||
uint32_t array_index = 0);
|
||||
|
||||
uint32_t EmitMemoryElementInBounds(EmitterState& state, const MemoryResourceAccess& access,
|
||||
uint32_t index);
|
||||
|
|
@ -441,6 +446,9 @@ uint32_t EmitUFloatToF32Bits(EmitterState& state, uint32_t raw, uint32_t bits);
|
|||
uint32_t NormalizeFormatComponent(EmitterState& state, const Format::BufferFormatInfo& info,
|
||||
uint32_t component, uint32_t raw);
|
||||
|
||||
uint32_t EncodeFormattedStoreComponent(EmitterState& state, const Format::BufferFormatInfo& info,
|
||||
uint32_t component, uint32_t data, uint32_t data_bits);
|
||||
|
||||
spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode);
|
||||
|
||||
uint32_t EmitAtomicOperation(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t pointer,
|
||||
|
|
|
|||
|
|
@ -249,10 +249,11 @@ uint32_t LoadBda(ValueEmitContext& ctx, uint32_t address, uint32_t active, uint3
|
|||
});
|
||||
}
|
||||
|
||||
uint32_t ByteAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem) {
|
||||
uint32_t ByteAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem,
|
||||
uint32_t prefix = 0) {
|
||||
if (mem.kind == IR::ResourceKind::Buffer) {
|
||||
const auto resource = ctx.state.program.info.buffers.at(mem.resource).descriptor_index;
|
||||
const auto prefix = ctx.state.memory_byte_offsets[resource];
|
||||
if (prefix == 0u) prefix = ctx.state.memory_byte_offsets[resource];
|
||||
const auto address = Binary(ctx.state, spv::OpIAdd, TypeU32(ctx.state),
|
||||
BufferByteAddress(ctx, inst, mem), prefix);
|
||||
// The host binding prefix must not wrap an out-of-range guest address into the buffer.
|
||||
|
|
@ -273,15 +274,10 @@ uint32_t ByteAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Memo
|
|||
return ScratchByteAddress(ctx, mem, ctx.Arg(inst, 1), ctx.Arg(inst, 2));
|
||||
}
|
||||
|
||||
uint32_t DwordIndex(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem) {
|
||||
auto address = ByteAddress(ctx, inst, mem);
|
||||
if (mem.kind == IR::ResourceKind::Lds || mem.kind == IR::ResourceKind::Gds) {
|
||||
// RDNA2 DS region addresses retain bits [15:2] after adding the byte offset.
|
||||
address = Binary(ctx.state, spv::OpBitwiseAnd, TypeU32(ctx.state), address,
|
||||
ConstantU32(ctx.state, 0xffffu));
|
||||
}
|
||||
uint32_t DwordIndex(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem,
|
||||
uint32_t prefix = 0) {
|
||||
return Binary(ctx.state, spv::OpShiftRightLogical, TypeU32(ctx.state),
|
||||
address, ConstantU32(ctx.state, 2));
|
||||
ByteAddress(ctx, inst, mem, prefix), ConstantU32(ctx.state, 2));
|
||||
}
|
||||
|
||||
uint32_t LoadWordInBounds(ValueEmitContext& ctx, const MemoryResourceAccess& resource,
|
||||
|
|
@ -540,8 +536,30 @@ void StoreLocalFlat(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
|
||||
void StoreWordPrepared(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem,
|
||||
const MemoryResourceAccess& resource, uint32_t data) {
|
||||
const auto index = DwordIndex(ctx, inst, mem);
|
||||
EmitIfCondition(ctx.state, EmitMemoryElementInBounds(ctx.state, resource, index), [&]() {
|
||||
auto& state = ctx.state;
|
||||
const auto source = mem.kind == IR::ResourceKind::Buffer
|
||||
? state.program.info.buffers[mem.resource].source : UINT32_MAX;
|
||||
const auto& sources = state.program.descriptor_sources;
|
||||
const auto* indirect = source < sources.size() && sources[source].indirect_descriptor
|
||||
? &*sources[source].indirect_descriptor : nullptr;
|
||||
uint32_t index = 0;
|
||||
uint32_t in_bounds = 0;
|
||||
if (indirect != nullptr && indirect->indexed_store) {
|
||||
// The resource plan proves stride-four, index-only stores. Check the original
|
||||
// index before any byte multiplication can wrap it into the buffer.
|
||||
const auto prefix = Binary(state, spv::OpShiftRightLogical, TypeU32(state),
|
||||
resource.byte_offset, ConstantU32(state, 2));
|
||||
const auto guest_index = ctx.Arg(inst, 1);
|
||||
const auto records = Binary(state, spv::OpISub, TypeU32(state), resource.length, prefix);
|
||||
in_bounds = AndCondition(state,
|
||||
Binary(state, spv::OpULessThanEqual, TypeBool(state), prefix, resource.length),
|
||||
Binary(state, spv::OpULessThan, TypeBool(state), guest_index, records));
|
||||
index = EmitAddU32(state, guest_index, prefix);
|
||||
} else {
|
||||
index = DwordIndex(ctx, inst, mem, resource.byte_offset);
|
||||
in_bounds = EmitMemoryElementInBounds(state, resource, index);
|
||||
}
|
||||
EmitIfCondition(state, in_bounds, [&]() {
|
||||
StoreWordInBounds(ctx, resource, index, data);
|
||||
});
|
||||
}
|
||||
|
|
@ -576,6 +594,15 @@ void StoreWord(ValueEmitContext& ctx, const IR::Inst& inst, IR::MemoryInfo mem)
|
|||
const auto selected = EmitIndirectResourceIndex(
|
||||
state, ctx.Arg(*handle, 0), buffer.indirect_mapping_offset,
|
||||
buffer.indirect_search_iterations, UINT32_MAX);
|
||||
if (UsesSharedIndirectBufferStore(state, mem.resource)) {
|
||||
EmitIfCondition(state, Binary(state, spv::OpULessThan, TypeBool(state), selected,
|
||||
ConstantU32(state, buffer.indirect_resources.size())), [&]() {
|
||||
const auto slot = EmitAddU32(state, selected, ConstantU32(state, buffer.descriptor_index));
|
||||
const auto access = PrepareStorageBufferResourceAccess(state, mem, state.storage_buffers[2], slot);
|
||||
StoreWordPrepared(ctx, inst, mem, access, data);
|
||||
});
|
||||
return;
|
||||
}
|
||||
const auto merge = state.builder.AllocateId();
|
||||
std::vector<uint32_t> labels(buffer.indirect_resources.size());
|
||||
std::vector<uint32_t> branches {spv::OpSwitch, selected, merge};
|
||||
|
|
@ -599,7 +626,12 @@ template <typename Fn>
|
|||
uint32_t EmitAtomicAccess(ValueEmitContext& ctx, const IR::Inst& inst,
|
||||
const IR::MemoryInfo& mem, Fn&& operation) {
|
||||
return EmitValueOrZeroIfCondition(ctx.state, ctx.Arg(inst, inst.NumArgs() - 1), [&]() {
|
||||
const auto index = DwordIndex(ctx, inst, mem);
|
||||
auto index = DwordIndex(ctx, inst, mem);
|
||||
if (mem.kind == IR::ResourceKind::Lds || mem.kind == IR::ResourceKind::Gds) {
|
||||
// Keep region-address truncation local to DS atomics.
|
||||
index = Binary(ctx.state, spv::OpBitwiseAnd, TypeU32(ctx.state), index,
|
||||
ConstantU32(ctx.state, 0x3fffu));
|
||||
}
|
||||
const auto resource = PrepareMemoryResourceAccess(ctx.state, mem);
|
||||
return EmitValueOrZeroIfCondition(
|
||||
ctx.state, EmitMemoryElementInBounds(ctx.state, resource, index), [&]() {
|
||||
|
|
@ -735,53 +767,6 @@ uint32_t FormattedLoad(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Me
|
|||
});
|
||||
}
|
||||
|
||||
uint32_t EncodeFormattedStoreComponent(ValueEmitContext& ctx,
|
||||
const Format::BufferFormatInfo& info,
|
||||
uint32_t component, uint32_t data, uint32_t data_bits) {
|
||||
auto& state = ctx.state;
|
||||
const auto bits = info.component_bits[component];
|
||||
if (data_bits == 16u) {
|
||||
if (info.type == Format::ComponentType::Uint) {
|
||||
data = EmitBitFieldUExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
|
||||
} else if (info.type == Format::ComponentType::Sint) {
|
||||
data = EmitBitFieldSExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
|
||||
} else {
|
||||
data = EmitBitCastU32F32(state, EmitF16BitsToF32(state, data));
|
||||
}
|
||||
}
|
||||
const bool normalized = info.type == Format::ComponentType::Unorm ||
|
||||
info.type == Format::ComponentType::Snorm;
|
||||
const bool scaled = info.type == Format::ComponentType::Uscaled ||
|
||||
info.type == Format::ComponentType::Sscaled;
|
||||
if (normalized || scaled) {
|
||||
// Two-bit packed channels are unsigned even in signed formats.
|
||||
const bool is_signed = bits != 2u && (info.type == Format::ComponentType::Snorm ||
|
||||
info.type == Format::ComponentType::Sscaled);
|
||||
const auto max_value = static_cast<float>((1u << (bits - (is_signed ? 1u : 0u))) - 1u);
|
||||
const auto lower = normalized ? (is_signed ? -1.0f : 0.0f)
|
||||
: (is_signed ? -max_value - 1.0f : 0.0f);
|
||||
const auto upper = normalized ? 1.0f : max_value;
|
||||
auto value = Select(state, TypeF32(state), EmitClassifyF32Bits(state, data).nan,
|
||||
ConstantF32Value(state, 0.0f), EmitBitCastF32U32(state, data));
|
||||
value = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(
|
||||
state, value, ConstantF32Value(state, lower), ConstantF32Value(state, upper));
|
||||
if (normalized) {
|
||||
value = EmitFPRoundEven32(
|
||||
state, EmitFPMul32(state, value, ConstantF32Value(state, max_value)));
|
||||
}
|
||||
// Float-to-integer conversion truncates scaled values toward zero.
|
||||
value = Unary(state, is_signed ? spv::OpConvertFToS : spv::OpConvertFToU,
|
||||
is_signed ? TypeI32(state) : TypeU32(state), value);
|
||||
return is_signed ? Unary(state, spv::OpBitcast, TypeU32(state), value) : value;
|
||||
}
|
||||
if (bits == 16u && info.type == Format::ComponentType::Float) {
|
||||
const auto pair = EmitCompositeConstructF32x2(
|
||||
state, EmitBitCastF32U32(state, data), ConstantF32Value(state, 0.0f));
|
||||
return EmitPackHalf2x16(state, pair);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void StoreFormattedPrepared(ValueEmitContext& ctx, const IR::Inst& inst,
|
||||
const IR::MemoryInfo& mem, const MemoryResourceAccess& resource,
|
||||
const Format::BufferFormatInfo& info, uint32_t data,
|
||||
|
|
@ -800,7 +785,7 @@ void StoreFormattedPrepared(ValueEmitContext& ctx, const IR::Inst& inst,
|
|||
value, data, component);
|
||||
}
|
||||
}
|
||||
value = EncodeFormattedStoreComponent(ctx, info, component, value, mem.data_bits);
|
||||
value = EncodeFormattedStoreComponent(ctx.state, info, component, value, mem.data_bits);
|
||||
const auto bits = info.component_bits[component];
|
||||
if (info.packed_bitfield) {
|
||||
packed = EmitBitFieldInsert(
|
||||
|
|
@ -926,10 +911,9 @@ uint32_t IndirectBufferInBounds(EmitterState& state, const IndirectBufferAccess&
|
|||
state, spv::OpIAdd, TypeU32(state), buffer.offset, ConstantU32(state, displacement));
|
||||
const auto has_payload = [&](uint32_t size) {
|
||||
return AndCondition(
|
||||
state, Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), size,
|
||||
ConstantU32(state, bytes)),
|
||||
state, Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), size, bytes),
|
||||
Binary(state, spv::OpULessThanEqual, TypeBool(state), offset,
|
||||
Binary(state, spv::OpISub, TypeU32(state), size, ConstantU32(state, bytes))));
|
||||
Binary(state, spv::OpISub, TypeU32(state), size, bytes)));
|
||||
};
|
||||
const auto structured = AndCondition(
|
||||
state, buffer.index_in_bounds,
|
||||
|
|
@ -970,7 +954,8 @@ uint32_t LoadIndirectBuffer(ValueEmitContext& ctx, const IR::Inst& inst, uint32_
|
|||
ConstantU64(state, component * 4u));
|
||||
values[component] = EmitValueOrZeroIfCondition(
|
||||
state, AndCondition(state, valid_format,
|
||||
IndirectBufferInBounds(state, buffer, component * 4u, 4u, false)),
|
||||
IndirectBufferInBounds(state, buffer, component * 4u,
|
||||
ConstantU32(state, 4u), false)),
|
||||
[&]() { return LoadBdaDword(ctx, address); });
|
||||
}
|
||||
return ConstructU32Composite(state, components, values);
|
||||
|
|
@ -980,9 +965,54 @@ uint32_t LoadIndirectFormattedX(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
auto& state = ctx.state;
|
||||
return EmitValueOrZeroIfCondition(state, ctx.Arg(inst, inst.NumArgs() - 1), [&]() {
|
||||
const auto buffer = PrepareIndirectBuffer(ctx, inst);
|
||||
constexpr auto format_count =
|
||||
static_cast<uint32_t>(Prospero::BufferFormat::k32_32_32_32Float) + 1u;
|
||||
const auto geometry = EmitIndexSwitch(
|
||||
state, buffer.format, format_count, TypeU32Composite(state, 4), [&](uint32_t format) {
|
||||
const auto info = Format::GetFormatInfo(static_cast<Prospero::BufferFormat>(format));
|
||||
return state.builder.Constant(
|
||||
spv::OpConstantComposite, TypeU32Composite(state, 4),
|
||||
ConstantU32(state, info.byte_size),
|
||||
ConstantU32(state, std::max(1u, info.component_count)),
|
||||
ConstantU32(state, info.packed_bitfield ? 0u : info.component_bits[0] / 8u),
|
||||
ConstantU32(state, info.byte_size == 0u ? 0u : std::min(4u, info.byte_size) - 1u));
|
||||
});
|
||||
const auto field = [&](uint32_t index) {
|
||||
const auto value = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, geometry, index);
|
||||
return value;
|
||||
};
|
||||
const auto bytes = field(0);
|
||||
const auto component = Binary(
|
||||
state, spv::OpUMod, TypeU32(state),
|
||||
Binary(state, spv::OpISub, TypeU32(state), buffer.selector, ConstantU32(state, 4)), field(1));
|
||||
const auto alignment = Unary(state, spv::OpUConvert, TypeU64(state), field(3));
|
||||
const auto base = Binary(state, spv::OpBitwiseAnd, TypeU64(state), buffer.address,
|
||||
Unary(state, spv::OpNot, TypeU64(state), alignment));
|
||||
const auto address = Binary(
|
||||
state, spv::OpIAdd, TypeU64(state), base,
|
||||
Unary(state, spv::OpUConvert, TypeU64(state),
|
||||
Binary(state, spv::OpIMul, TypeU32(state), component, field(2))));
|
||||
const auto memory = AndCondition(
|
||||
state, Binary(state, spv::OpINotEqual, TypeBool(state), bytes, ConstantU32(state, 0)),
|
||||
Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), buffer.selector,
|
||||
ConstantU32(state, 4)));
|
||||
const auto raw = EmitValueOrZeroIfCondition(
|
||||
state, AndCondition(state, memory, IndirectBufferInBounds(state, buffer, 0u, bytes, true)),
|
||||
[&]() {
|
||||
// Format alignment and component spacing keep every selected component within one DWORD.
|
||||
const auto word = LoadBdaDword(
|
||||
ctx, Binary(state, spv::OpBitwiseAnd, TypeU64(state), address,
|
||||
ConstantU64(state, ~uint64_t {3})));
|
||||
const auto byte = Binary(
|
||||
state, spv::OpBitwiseAnd, TypeU32(state),
|
||||
Unary(state, spv::OpUConvert, TypeU32(state), address), ConstantU32(state, 3));
|
||||
return Binary(state, spv::OpShiftRightLogical, TypeU32(state), word,
|
||||
Binary(state, spv::OpShiftLeftLogical, TypeU32(state), byte,
|
||||
ConstantU32(state, 3)));
|
||||
});
|
||||
return EmitIndexSwitch(
|
||||
state, buffer.format, static_cast<uint32_t>(Prospero::BufferFormat::k32_32_32_32Float) + 1u,
|
||||
TypeU32(state), [&](uint32_t format) {
|
||||
state, buffer.format, format_count, TypeU32(state), [&](uint32_t format) {
|
||||
const auto info = Format::GetFormatInfo(static_cast<Prospero::BufferFormat>(format));
|
||||
if (info.type == Format::ComponentType::Unknown) return ConstantU32(state, 0);
|
||||
const auto constant = Select(
|
||||
|
|
@ -993,28 +1023,16 @@ uint32_t LoadIndirectFormattedX(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
state, Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), buffer.selector,
|
||||
ConstantU32(state, 4)),
|
||||
TypeU32(state), constant, [&]() {
|
||||
const auto in_bounds = IndirectBufferInBounds(state, buffer, 0u, info.byte_size, true);
|
||||
const auto base = Binary(
|
||||
state, spv::OpBitwiseAnd, TypeU64(state), buffer.address,
|
||||
ConstantU64(state, ~(uint64_t(std::min(4u, info.byte_size)) - 1u)));
|
||||
const auto load = [&](uint32_t component, uint32_t bits) {
|
||||
const auto offset = Format::GetFormatComponentByteOffset(info, component);
|
||||
const auto address = offset == 0u
|
||||
? base
|
||||
: Binary(state, spv::OpIAdd, TypeU64(state),
|
||||
base, ConstantU64(state, offset));
|
||||
return LoadBda(ctx, address, in_bounds, bits);
|
||||
};
|
||||
const auto emit_component = [&](uint32_t component) {
|
||||
return LoadFormattedComponent(
|
||||
ctx, info, {FormattedSourceKind::Memory, component},
|
||||
[&](uint32_t source) { return load(source, 32u); }, load);
|
||||
[&](uint32_t) { return raw; },
|
||||
[&](uint32_t, uint32_t bits) {
|
||||
return Binary(state, spv::OpBitwiseAnd, TypeU32(state), raw,
|
||||
ConstantU32(state, bits == 8u ? 0xffu : 0xffffu));
|
||||
});
|
||||
};
|
||||
if (info.component_count == 1u) return emit_component(0u);
|
||||
const auto component = Binary(
|
||||
state, spv::OpUMod, TypeU32(state),
|
||||
Binary(state, spv::OpISub, TypeU32(state), buffer.selector, ConstantU32(state, 4)),
|
||||
ConstantU32(state, info.component_count));
|
||||
if (!info.packed_bitfield) return emit_component(0u);
|
||||
return EmitIndexSwitch(state, component, info.component_count, TypeU32(state),
|
||||
emit_component);
|
||||
});
|
||||
|
|
@ -1222,32 +1240,48 @@ void EmitSharedAtomic64(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||
auto& state = ctx.state;
|
||||
const auto& mem = ctx.Memory(inst);
|
||||
EnsureLdsStorage(state);
|
||||
EmitIfCondition(state, ctx.Arg(inst, 2), [&]() {
|
||||
const auto address = Binary(state, spv::OpBitwiseAnd, TypeU32(state),
|
||||
ByteAddress(ctx, inst, mem), ConstantU32(state, 0xfff8u));
|
||||
const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address,
|
||||
ConstantU32(state, 3u));
|
||||
const auto in_bounds = Binary(state, spv::OpULessThan, TypeBool(state), index,
|
||||
ConstantU32(state, LdsDwordCount(state) / 2u));
|
||||
EmitIfCondition(state, in_bounds, [&]() {
|
||||
auto native_index = index;
|
||||
auto semantics = spv::MemorySemanticsWorkgroupMemoryMask;
|
||||
if (state.lds_storage_class == spv::StorageClassStorageBuffer) {
|
||||
native_index = EmitAddU32(state, index, EmitBinaryU32(
|
||||
state, spv::OpShiftRightLogical, state.lds_base_dwords, ConstantU32(state, 1)));
|
||||
semantics = spv::MemorySemanticsUniformMemoryMask;
|
||||
}
|
||||
const auto pointer = state.builder.AllocateId();
|
||||
const auto address = Binary(state, spv::OpBitwiseAnd, TypeU32(state),
|
||||
ByteAddress(ctx, inst, mem), ConstantU32(state, 0xfff8u));
|
||||
const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address,
|
||||
ConstantU32(state, 3u));
|
||||
const auto in_bounds = Binary(state, spv::OpULessThan, TypeBool(state), index,
|
||||
ConstantU32(state, LdsDwordCount(state) / 2u));
|
||||
const auto active = EmitLogicalAndBool(state, ctx.Arg(inst, inst.NumArgs() - 1), in_bounds);
|
||||
const auto emit = [&]() {
|
||||
auto native_index = index;
|
||||
auto semantics = spv::MemorySemanticsWorkgroupMemoryMask;
|
||||
if (state.lds_storage_class == spv::StorageClassStorageBuffer) {
|
||||
native_index = EmitAddU32(state, index, EmitBinaryU32(
|
||||
state, spv::OpShiftRightLogical, state.lds_base_dwords, ConstantU32(state, 1)));
|
||||
semantics = spv::MemorySemanticsUniformMemoryMask;
|
||||
}
|
||||
const auto pointer = state.builder.AllocateId();
|
||||
state.builder.AddFunction(
|
||||
spv::OpAccessChain, TypePointer(state, state.lds_storage_class, TypeU64(state)),
|
||||
pointer, state.lds_u64_variable, ConstantU32(state, 0), native_index);
|
||||
const auto old = state.builder.AllocateId();
|
||||
const auto opcode = SpirvAtomicOpcode(inst.GetOpcode());
|
||||
const auto scope = ConstantU32(state, spv::ScopeWorkgroup);
|
||||
const auto acquire_release =
|
||||
ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | semantics);
|
||||
if (opcode == spv::OpAtomicCompareExchange) {
|
||||
// RDNA2 DS compares DATA and stores DATA2, opposite to BUFFER atomics.
|
||||
state.builder.AddFunction(
|
||||
spv::OpAccessChain, TypePointer(state, state.lds_storage_class, TypeU64(state)),
|
||||
pointer, state.lds_u64_variable, ConstantU32(state, 0), native_index);
|
||||
const auto value = ctx.Arg(inst, 1);
|
||||
state.builder.AddFunction(
|
||||
SpirvAtomicOpcode(inst.GetOpcode()), TypeU64(state), state.builder.AllocateId(),
|
||||
pointer, ConstantU32(state, spv::ScopeWorkgroup),
|
||||
ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | semantics), value);
|
||||
});
|
||||
});
|
||||
opcode, TypeU64(state), old, pointer, scope, acquire_release,
|
||||
ConstantU32(state, spv::MemorySemanticsAcquireMask | semantics),
|
||||
ctx.Arg(inst, 2), ctx.Arg(inst, 1));
|
||||
} else {
|
||||
state.builder.AddFunction(opcode, TypeU64(state), old, pointer, scope,
|
||||
acquire_release, ctx.Arg(inst, 1));
|
||||
}
|
||||
return old;
|
||||
};
|
||||
if (inst.HasUses()) {
|
||||
ctx.Define(inst, EmitValueOrDefaultIfCondition(
|
||||
state, active, TypeU64(state), ConstantU64(state, 0), emit));
|
||||
} else {
|
||||
EmitIfCondition(state, active, emit);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t EmitBufferFloatAtomic(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
#include "graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h"
|
||||
#include "graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h"
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
|
||||
|
||||
uint32_t EmitShaderDataDwordLoad(EmitterState& state, uint32_t dword_index) {
|
||||
static uint32_t EmitShaderDataDwordLoadAt(EmitterState& state, uint32_t dword_index) {
|
||||
if (state.program.bindings.UsesPushData()) {
|
||||
dword_index += state.program.bindings.push_data_start_dword;
|
||||
const auto pointer = state.builder.AllocateId();
|
||||
const auto value = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpAccessChain, TypePushConstantElementPointer(state),
|
||||
pointer, state.push_constant_variable, ConstantU32(state, 0),
|
||||
ConstantU32(state, dword_index));
|
||||
dword_index);
|
||||
state.builder.AddFunction(spv::OpLoad, TypeU32(state), value, pointer);
|
||||
return value;
|
||||
}
|
||||
|
|
@ -18,13 +17,20 @@ uint32_t EmitShaderDataDwordLoad(EmitterState& state, uint32_t dword_index) {
|
|||
const auto value = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state),
|
||||
pointer, state.shader_data_storage_variable,
|
||||
ConstantU32(state, 0), ConstantU32(state, dword_index));
|
||||
ConstantU32(state, 0), dword_index);
|
||||
state.builder.AddFunction(spv::OpLoad, TypeU32(state), value, pointer);
|
||||
return value;
|
||||
}
|
||||
return ConstantU32(state, 0);
|
||||
}
|
||||
|
||||
uint32_t EmitShaderDataDwordLoad(EmitterState& state, uint32_t dword_index) {
|
||||
if (state.program.bindings.UsesPushData()) {
|
||||
dword_index += state.program.bindings.push_data_start_dword;
|
||||
}
|
||||
return EmitShaderDataDwordLoadAt(state, ConstantU32(state, dword_index));
|
||||
}
|
||||
|
||||
uint32_t EmitBinaryU32(EmitterState& state, spv::Op opcode, uint32_t lhs, uint32_t rhs) {
|
||||
const auto ret = state.builder.AllocateId();
|
||||
state.builder.AddFunction(opcode, TypeU32(state), ret, lhs, rhs);
|
||||
|
|
@ -102,12 +108,38 @@ Prospero::BufferFormat StorageBufferFormat(const EmitterState& state, const IR::
|
|||
return state.program.info.buffers[mem.resource].descriptor_format;
|
||||
}
|
||||
|
||||
bool UsesSharedIndirectBufferStore(const EmitterState& state, uint32_t resource) {
|
||||
const auto& buffers = state.program.info.buffers;
|
||||
const auto& root = buffers[resource];
|
||||
if (root.indirect_root != resource || root.descriptor_index == UINT32_MAX ||
|
||||
root.packed_stride == 0u || root.indirect_resources.empty()) {
|
||||
return false;
|
||||
}
|
||||
uint32_t slot = root.descriptor_index;
|
||||
for (const auto candidate: root.indirect_resources) {
|
||||
if (buffers[candidate].packed_stride != root.packed_stride ||
|
||||
buffers[candidate].descriptor_index != slot++) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void EmitMemoryOffsets(EmitterState& state) {
|
||||
std::array<bool, IR::ShaderInfo::MaxBuffers> dynamic_offsets {};
|
||||
for (uint32_t resource = 0; resource < state.program.info.buffers.size(); ++resource) {
|
||||
if (!UsesSharedIndirectBufferStore(state, resource)) continue;
|
||||
for (const auto candidate: state.program.info.buffers[resource].indirect_resources) {
|
||||
dynamic_offsets[state.program.info.buffers[candidate].descriptor_index] = true;
|
||||
}
|
||||
}
|
||||
const auto count = state.program.bindings.descriptor_counts[static_cast<size_t>(IR::DescriptorBindingKind::Buffers)];
|
||||
for (uint32_t i = 0; i < count; i += 4u) {
|
||||
const auto lanes = std::min(4u, count - i);
|
||||
if (std::all_of(dynamic_offsets.begin() + i, dynamic_offsets.begin() + i + lanes,
|
||||
[](bool dynamic) { return dynamic; })) continue;
|
||||
const auto word =
|
||||
EmitShaderDataDwordLoad(state, state.program.bindings.memory_offset_dword + i / 4u);
|
||||
for (uint32_t lane = 0; lane < std::min(4u, count - i); ++lane) {
|
||||
for (uint32_t lane = 0; lane < lanes; ++lane) {
|
||||
if (dynamic_offsets[i + lane]) continue;
|
||||
state.memory_byte_offsets[i + lane] = EmitNative<spv::OpBitFieldUExtract, IR::Type::U32>(
|
||||
state, word, ConstantU32(state, lane * 8u), ConstantU32(state, 8u));
|
||||
}
|
||||
|
|
@ -152,7 +184,8 @@ void EnsureLdsStorage(EmitterState& state) {
|
|||
|
||||
MemoryResourceAccess PrepareStorageBufferResourceAccess(EmitterState& state,
|
||||
const IR::MemoryInfo& mem,
|
||||
BufferDefinition& buffer) {
|
||||
BufferDefinition& buffer,
|
||||
uint32_t array_index) {
|
||||
if (buffer.variable == 0) {
|
||||
ExitDescriptorBindingFailure(state, IR::DescriptorBindingKind::Buffers, mem.resource,
|
||||
"storage buffer descriptor array was not emitted");
|
||||
|
|
@ -160,15 +193,34 @@ MemoryResourceAccess PrepareStorageBufferResourceAccess(EmitterState& state,
|
|||
if (buffer.pointer_type == 0) {
|
||||
buffer.pointer_type = TypePointer(state, spv::StorageClassStorageBuffer, buffer.type);
|
||||
}
|
||||
const auto array_index =
|
||||
state.program.info.buffers.at(mem.resource).descriptor_index;
|
||||
const bool non_uniform = array_index != 0u;
|
||||
const auto static_index = state.program.info.buffers.at(mem.resource).descriptor_index;
|
||||
if (!non_uniform) array_index = ConstantU32(state, static_index);
|
||||
MemoryResourceAccess access {
|
||||
.kind = mem.kind,
|
||||
.non_uniform = non_uniform,
|
||||
.memory_access = mem.coherent ? spv::MemoryAccessVolatileMask : spv::MemoryAccessMaskNone};
|
||||
access.object_pointer = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpAccessChain, buffer.pointer_type, access.object_pointer,
|
||||
buffer.variable, ConstantU32(state, array_index));
|
||||
access.byte_offset = state.memory_byte_offsets[array_index];
|
||||
buffer.variable, array_index);
|
||||
if (non_uniform) {
|
||||
state.builder.RequireExtension("SPV_EXT_descriptor_indexing");
|
||||
state.builder.RequireCapability(spv::CapabilityShaderNonUniform);
|
||||
state.builder.RequireCapability(spv::CapabilityStorageBufferArrayNonUniformIndexing);
|
||||
state.builder.AddAnnotation(spv::OpDecorate, access.object_pointer, spv::DecorationNonUniform);
|
||||
const auto offsets_start = state.program.bindings.memory_offset_dword +
|
||||
(state.program.bindings.UsesPushData() ? state.program.bindings.push_data_start_dword : 0u);
|
||||
const auto word = EmitShaderDataDwordLoadAt(state, EmitAddU32(
|
||||
state, ConstantU32(state, offsets_start),
|
||||
Binary(state, spv::OpShiftRightLogical, TypeU32(state), array_index, ConstantU32(state, 2))));
|
||||
const auto shift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state),
|
||||
Binary(state, spv::OpBitwiseAnd, TypeU32(state), array_index, ConstantU32(state, 3)),
|
||||
ConstantU32(state, 3));
|
||||
access.byte_offset = EmitNative<spv::OpBitFieldUExtract, IR::Type::U32>(
|
||||
state, word, shift, ConstantU32(state, 8));
|
||||
} else {
|
||||
access.byte_offset = state.memory_byte_offsets[static_index];
|
||||
}
|
||||
access.length = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpArrayLength, TypeU32(state), access.length,
|
||||
access.object_pointer, 0);
|
||||
|
|
@ -260,6 +312,9 @@ uint32_t EmitStorageBufferElementPointer(EmitterState& state,
|
|||
const auto pointer = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpAccessChain, pointer_type, pointer, access.object_pointer,
|
||||
ConstantU32(state, 0), index);
|
||||
if (access.non_uniform) {
|
||||
state.builder.AddAnnotation(spv::OpDecorate, pointer, spv::DecorationNonUniform);
|
||||
}
|
||||
return pointer;
|
||||
}
|
||||
|
||||
|
|
@ -362,8 +417,54 @@ uint32_t NormalizeFormatComponent(EmitterState& state, const Format::BufferForma
|
|||
}
|
||||
}
|
||||
|
||||
uint32_t EncodeFormattedStoreComponent(EmitterState& state, const Format::BufferFormatInfo& info,
|
||||
uint32_t component, uint32_t data, uint32_t data_bits) {
|
||||
const auto bits = info.component_bits[component];
|
||||
if (data_bits == 16u) {
|
||||
if (info.type == Format::ComponentType::Uint) {
|
||||
data = EmitBitFieldUExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
|
||||
} else if (info.type == Format::ComponentType::Sint) {
|
||||
data = EmitBitFieldSExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
|
||||
} else {
|
||||
data = EmitBitCastU32F32(state, EmitF16BitsToF32(state, data));
|
||||
}
|
||||
}
|
||||
const bool normalized = info.type == Format::ComponentType::Unorm ||
|
||||
info.type == Format::ComponentType::Snorm;
|
||||
const bool scaled = info.type == Format::ComponentType::Uscaled ||
|
||||
info.type == Format::ComponentType::Sscaled;
|
||||
if (normalized || scaled) {
|
||||
// Two-bit packed channels are unsigned even in signed formats.
|
||||
const bool is_signed = bits != 2u && (info.type == Format::ComponentType::Snorm ||
|
||||
info.type == Format::ComponentType::Sscaled);
|
||||
const auto max_value = static_cast<float>((1u << (bits - (is_signed ? 1u : 0u))) - 1u);
|
||||
const auto lower = normalized ? (is_signed ? -1.0f : 0.0f)
|
||||
: (is_signed ? -max_value - 1.0f : 0.0f);
|
||||
const auto upper = normalized ? 1.0f : max_value;
|
||||
auto value = Select(state, TypeF32(state), EmitClassifyF32Bits(state, data).nan,
|
||||
ConstantF32Value(state, 0.0f), EmitBitCastF32U32(state, data));
|
||||
value = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(
|
||||
state, value, ConstantF32Value(state, lower), ConstantF32Value(state, upper));
|
||||
if (normalized) {
|
||||
value = EmitFPRoundEven32(
|
||||
state, EmitFPMul32(state, value, ConstantF32Value(state, max_value)));
|
||||
}
|
||||
// Float-to-integer conversion truncates scaled values toward zero.
|
||||
value = Unary(state, is_signed ? spv::OpConvertFToS : spv::OpConvertFToU,
|
||||
is_signed ? TypeI32(state) : TypeU32(state), value);
|
||||
return is_signed ? Unary(state, spv::OpBitcast, TypeU32(state), value) : value;
|
||||
}
|
||||
if (bits == 16u && info.type == Format::ComponentType::Float) {
|
||||
const auto pair = EmitCompositeConstructF32x2(
|
||||
state, EmitBitCastF32U32(state, data), ConstantF32Value(state, 0.0f));
|
||||
return EmitPackHalf2x16(state, pair);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
|
||||
switch (opcode) {
|
||||
case IR::ValueOpcode::SharedAtomicCmpSwap64:
|
||||
case IR::ValueOpcode::ImageAtomicCompareSwap32:
|
||||
case IR::ValueOpcode::BufferAtomicCmpSwap32: return spv::OpAtomicCompareExchange;
|
||||
case IR::ValueOpcode::ImageAtomicSwap32:
|
||||
|
|
@ -391,7 +492,8 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
|
|||
case IR::ValueOpcode::ImageAtomicUMax32:
|
||||
case IR::ValueOpcode::ImageAtomicUMax64:
|
||||
case IR::ValueOpcode::BufferAtomicUMax32:
|
||||
case IR::ValueOpcode::SharedAtomicUMax32: return spv::OpAtomicUMax;
|
||||
case IR::ValueOpcode::SharedAtomicUMax32:
|
||||
case IR::ValueOpcode::SharedAtomicUMax64: return spv::OpAtomicUMax;
|
||||
case IR::ValueOpcode::ImageAtomicAnd32:
|
||||
case IR::ValueOpcode::ImageAtomicAnd64:
|
||||
case IR::ValueOpcode::BufferAtomicAnd32:
|
||||
|
|
|
|||
|
|
@ -351,6 +351,9 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
|||
if (a16) {
|
||||
inst.image_sample_flags |= ImageSampleFlagA16;
|
||||
}
|
||||
if ((sample != nullptr || gather != nullptr) && (word0 & (1u << 12u)) != 0u) {
|
||||
inst.image_sample_flags |= ImageSampleFlagUnnormalized;
|
||||
}
|
||||
inst.image_dimension = dimension;
|
||||
inst.image_r128 = r128;
|
||||
inst.image_nsa_dwords = nsa_dwords;
|
||||
|
|
|
|||
|
|
@ -116,7 +116,8 @@ constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
|
|||
{0x3fu, Opcode::DS_ORDERED_COUNT, 1, 32},
|
||||
{0x40u, Opcode::DS_ADD_U64, 2, 32}, {0x4au, Opcode::DS_OR_B64, 2, 32},
|
||||
{0x4du, Opcode::DS_WRITE_B64, 2, 32}, {0x4eu, Opcode::DS_WRITE2_B64, 4, 32},
|
||||
{0x4fu, Opcode::DS_WRITE2ST64_B64, 4, 32}, {0x76u, Opcode::DS_READ_B64, 2, 32},
|
||||
{0x4fu, Opcode::DS_WRITE2ST64_B64, 4, 32}, {0x68u, Opcode::DS_MAX_RTN_U64, 2, 32},
|
||||
{0x70u, Opcode::DS_CMPST_RTN_B64, 2, 32}, {0x76u, Opcode::DS_READ_B64, 2, 32},
|
||||
{0x77u, Opcode::DS_READ2_B64, 4, 32}, {0x78u, Opcode::DS_READ2ST64_B64, 4, 32},
|
||||
{0xa0u, Opcode::DS_WRITE_B8_D16_HI, 1, 8},
|
||||
{0xa1u, Opcode::DS_WRITE_B16_D16_HI, 1, 16},
|
||||
|
|
@ -192,6 +193,8 @@ bool IsDsAtomicOpcode(Opcode opcode) {
|
|||
case Opcode::DS_MIN_RTN_U32:
|
||||
case Opcode::DS_MAX_U32:
|
||||
case Opcode::DS_MAX_RTN_U32:
|
||||
case Opcode::DS_MAX_RTN_U64:
|
||||
case Opcode::DS_CMPST_RTN_B64:
|
||||
case Opcode::DS_AND_B32:
|
||||
case Opcode::DS_AND_RTN_B32:
|
||||
case Opcode::DS_OR_B32:
|
||||
|
|
@ -211,6 +214,7 @@ uint32_t DsSourceCount(Opcode opcode) {
|
|||
case Opcode::DS_WRITE2ST64_B32:
|
||||
case Opcode::DS_WRITE2_B64:
|
||||
case Opcode::DS_WRITE2ST64_B64:
|
||||
case Opcode::DS_CMPST_RTN_B64:
|
||||
case Opcode::DS_MSKOR_B32: return 3u;
|
||||
case Opcode::DS_MIN_F32:
|
||||
case Opcode::DS_MAX_F32:
|
||||
|
|
|
|||
|
|
@ -708,6 +708,8 @@ std::string InstructionToString(const Instruction& inst) {
|
|||
case Opcode::DS_MIN_RTN_U32:
|
||||
case Opcode::DS_MAX_U32:
|
||||
case Opcode::DS_MAX_RTN_U32:
|
||||
case Opcode::DS_MAX_RTN_U64:
|
||||
case Opcode::DS_CMPST_RTN_B64:
|
||||
case Opcode::DS_AND_B32:
|
||||
case Opcode::DS_AND_RTN_B32:
|
||||
case Opcode::DS_OR_B32:
|
||||
|
|
|
|||
|
|
@ -624,6 +624,8 @@ enum class Opcode {
|
|||
DS_MIN_RTN_U32,
|
||||
DS_MAX_U32,
|
||||
DS_MAX_RTN_U32,
|
||||
DS_MAX_RTN_U64,
|
||||
DS_CMPST_RTN_B64,
|
||||
DS_AND_B32,
|
||||
DS_AND_RTN_B32,
|
||||
DS_OR_B32,
|
||||
|
|
@ -762,6 +764,7 @@ enum ImageSampleFlag : uint32_t {
|
|||
ImageSampleFlagCd = 1u << 8u,
|
||||
ImageSampleFlagGatherHorizontal = 1u << 9u,
|
||||
ImageSampleFlagAdjust = 1u << 10u,
|
||||
ImageSampleFlagUnnormalized = 1u << 11u,
|
||||
};
|
||||
|
||||
enum class ImageDimension : uint32_t {
|
||||
|
|
|
|||
|
|
@ -578,11 +578,13 @@ void Translator::DS_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode opc
|
|||
bool returns_value) {
|
||||
const auto memory = MemoryInfoFromDecoded(inst);
|
||||
const auto address = ReadU32(inst.src0);
|
||||
const IR::Value value =
|
||||
memory.data_dwords == 2u ? IR::Value(ReadU64(inst.src1)) : IR::Value(ReadU32(inst.src1));
|
||||
const auto read = [&](const Decoder::Operand& operand) -> IR::Value {
|
||||
return memory.data_dwords == 2u ? IR::Value(ReadU64(operand)) : IR::Value(ReadU32(operand));
|
||||
};
|
||||
const auto value = read(inst.src1);
|
||||
const auto flags = AddMemoryInfo(memory, inst.pc);
|
||||
const auto result = inst.src_count == 3u
|
||||
? ir.Emit(opcode, {address, value, ReadU32(inst.src2), ir.GetExec()}, flags)
|
||||
? ir.Emit(opcode, {address, value, read(inst.src2), ir.GetExec()}, flags)
|
||||
: ir.Emit(opcode, {address, value, ir.GetExec()}, flags);
|
||||
if (returns_value) {
|
||||
WriteOperand(inst.dst, result);
|
||||
|
|
@ -687,7 +689,8 @@ void Translator::IMAGE_SAMPLE(const Decoder::Instruction& inst) {
|
|||
const auto resource = GetImageResource(memory);
|
||||
const auto sampler = GetSamplerResource(memory);
|
||||
const auto address = MakeImageAddress(inst, MemorySourceAt(inst, 0));
|
||||
const auto result = ir.Emit(IR::ValueOpcode::ImageSampleRaw, {resource, sampler, address},
|
||||
const auto result = ir.Emit(IR::ValueOpcode::ImageSampleRaw,
|
||||
{resource, sampler, address, ir.GetExec()},
|
||||
AddMemoryInfo(memory, inst.pc));
|
||||
const bool dref = (memory.image_sample_flags & Decoder::ImageSampleFlagCompare) != 0u;
|
||||
if (dref && memory.data_bits != 16u) {
|
||||
|
|
@ -714,7 +717,7 @@ void Translator::IMAGE_GATHER(const Decoder::Instruction& inst) {
|
|||
has_lod ? resource.Instruction()->Arg(1) : IR::Value(0u),
|
||||
has_lod ? resource.Instruction()->Arg(3) : IR::Value(0u),
|
||||
has_lod ? sampler.Instruction()->Arg(1) : IR::Value(0u),
|
||||
has_lod ? sampler.Instruction()->Arg(2) : IR::Value(0u)},
|
||||
has_lod ? sampler.Instruction()->Arg(2) : IR::Value(0u), ir.GetExec()},
|
||||
AddMemoryInfo(memory, inst.pc));
|
||||
for (uint32_t index = 0; index < memory.data_dwords; index++) {
|
||||
WriteOperand(OffsetOperand(inst.dst, index),
|
||||
|
|
@ -1072,6 +1075,10 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
|||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax32, false);
|
||||
case Decoder::Opcode::DS_MAX_RTN_U32:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax32, true);
|
||||
case Decoder::Opcode::DS_MAX_RTN_U64:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax64, true);
|
||||
case Decoder::Opcode::DS_CMPST_RTN_B64:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicCmpSwap64, true);
|
||||
case Decoder::Opcode::DS_AND_B32:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicAnd32, false);
|
||||
case Decoder::Opcode::DS_AND_RTN_B32:
|
||||
|
|
|
|||
|
|
@ -948,9 +948,14 @@ void IncludeInstructionVectorRegisters(const Decoder::Instruction& inst, uint32_
|
|||
switch (inst.opcode) {
|
||||
case Decoder::Opcode::DS_ADD_U64:
|
||||
case Decoder::Opcode::DS_OR_B64:
|
||||
case Decoder::Opcode::DS_MAX_RTN_U64:
|
||||
case Decoder::Opcode::DS_WRITE_B64:
|
||||
case Decoder::Opcode::DS_WRITE_B96:
|
||||
case Decoder::Opcode::DS_WRITE_B128: include_vector(inst.src1, inst.data_dwords); break;
|
||||
case Decoder::Opcode::DS_CMPST_RTN_B64:
|
||||
include_vector(inst.src1, 2u);
|
||||
include_vector(inst.src2, 2u);
|
||||
break;
|
||||
case Decoder::Opcode::DS_WRITE2_B32:
|
||||
case Decoder::Opcode::DS_WRITE2ST64_B32:
|
||||
case Decoder::Opcode::DS_WRITE2_B64:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Xbyak {
|
||||
|
|
@ -27,6 +28,11 @@ class CodeGenerator;
|
|||
|
||||
namespace Libs::Graphics::ShaderRecompiler::IR {
|
||||
|
||||
class SrtWalker;
|
||||
|
||||
// Finite descriptor domains also bound the arithmetic used to prove their indices.
|
||||
constexpr uint32_t MaxIndirectDescriptorProbes = 65536u;
|
||||
|
||||
enum class ResourceKind {
|
||||
None,
|
||||
ScalarBuffer,
|
||||
|
|
@ -501,9 +507,23 @@ struct ShaderInfo {
|
|||
struct DescriptorSource {
|
||||
struct IndirectDescriptor {
|
||||
struct SelectorRead {
|
||||
struct CountedRows {
|
||||
uint32_t source = UINT32_MAX;
|
||||
uint32_t stride = 0; // Byte spacing between selector rows, not their length.
|
||||
|
||||
bool operator==(const CountedRows& other) const = default;
|
||||
};
|
||||
uint32_t source = UINT32_MAX;
|
||||
uint32_t stride = 0;
|
||||
uint32_t offset = 0;
|
||||
uint32_t maximum = UINT32_MAX;
|
||||
uint32_t bit_offset = 0;
|
||||
uint32_t bit_mask = UINT32_MAX;
|
||||
bool global = false;
|
||||
// Four DWORD bitmap words per record; set-bit ranks subtract from the last key.
|
||||
bool bitset = false;
|
||||
std::optional<CountedRows> rows;
|
||||
std::vector<std::pair<uint32_t, uint32_t>> byte_ranges;
|
||||
|
||||
bool operator==(const SelectorRead& other) const = default;
|
||||
};
|
||||
|
|
@ -514,8 +534,11 @@ struct DescriptorSource {
|
|||
uint32_t table_stride = 0;
|
||||
uint32_t table_record_bytes = 0;
|
||||
bool table_scalar = false;
|
||||
bool indexed_store = false;
|
||||
bool signed_count = false;
|
||||
uint32_t workgroup_axis = UINT32_MAX;
|
||||
Value key_count;
|
||||
// First selector index, or first descriptor key for a direct or bitmap range.
|
||||
Value selector_first;
|
||||
Value selector_mask;
|
||||
std::vector<uint32_t> sources;
|
||||
|
|
@ -575,6 +598,9 @@ struct ResourcePlan {
|
|||
|
||||
std::vector<Entry> values;
|
||||
uint64_t generation = 0;
|
||||
const SrtWalker* session = nullptr;
|
||||
Value active_mask;
|
||||
uint8_t read_policy = 0;
|
||||
};
|
||||
|
||||
ResourcePlan();
|
||||
|
|
@ -593,6 +619,7 @@ struct ResourcePlan {
|
|||
std::vector<MemoryInfo> memory_info;
|
||||
std::vector<DescriptorSource> descriptor_sources;
|
||||
std::vector<ResourceBlock> control_flow;
|
||||
std::unordered_map<const Inst*, std::vector<uint32_t>> control_phi_predecessors;
|
||||
std::vector<SrtRead> srt_reads;
|
||||
bool requires_specialization_memory = false;
|
||||
std::vector<std::pair<uint64_t, uint64_t>> source_reads;
|
||||
|
|
@ -605,9 +632,9 @@ struct ResourcePlan {
|
|||
// GPU-thread scratch for nested clean/EXEC memos, activity and material keys.
|
||||
mutable std::deque<EvaluationContext> evaluation_contexts;
|
||||
mutable uint32_t evaluation_value_count = 0;
|
||||
mutable uint32_t evaluation_depth = 0;
|
||||
mutable std::vector<uint8_t> active_sources;
|
||||
mutable std::vector<uint8_t> visited_blocks;
|
||||
mutable std::vector<uint8_t> reachable_blocks;
|
||||
mutable std::vector<uint32_t> pending_blocks;
|
||||
mutable std::vector<uint32_t> material_keys;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -175,6 +175,8 @@ SharedAccess SharedAccessOf(ValueOpcode opcode) {
|
|||
case ValueOpcode::SharedAtomicUMin32:
|
||||
case ValueOpcode::SharedAtomicSMax32:
|
||||
case ValueOpcode::SharedAtomicUMax32:
|
||||
case ValueOpcode::SharedAtomicUMax64:
|
||||
case ValueOpcode::SharedAtomicCmpSwap64:
|
||||
case ValueOpcode::SharedAtomicAnd32:
|
||||
case ValueOpcode::SharedAtomicOr32:
|
||||
case ValueOpcode::SharedAtomicOr64:
|
||||
|
|
@ -190,6 +192,8 @@ uint32_t SharedComponentCount(ValueOpcode opcode) {
|
|||
switch (opcode) {
|
||||
case ValueOpcode::SharedAtomicIAdd64:
|
||||
case ValueOpcode::SharedAtomicOr64:
|
||||
case ValueOpcode::SharedAtomicUMax64:
|
||||
case ValueOpcode::SharedAtomicCmpSwap64:
|
||||
case ValueOpcode::LoadSharedU32x2:
|
||||
case ValueOpcode::WriteSharedU32x2: return 2u;
|
||||
case ValueOpcode::LoadSharedU32x3:
|
||||
|
|
|
|||
|
|
@ -303,6 +303,8 @@ VALUE_OPCODE(SharedAtomicSMin32, U32, U32, U32, U1)
|
|||
VALUE_OPCODE(SharedAtomicUMin32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicSMax32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicUMax32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicUMax64, U64, U32, U64, U1)
|
||||
VALUE_OPCODE(SharedAtomicCmpSwap64, U64, U32, U64, U64, U1)
|
||||
VALUE_OPCODE(SharedAtomicAnd32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicOr32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicOr64, Void, U32, U64, U1)
|
||||
|
|
@ -317,8 +319,8 @@ VALUE_OPCODE(BvhIntersect, U32x4, U32x4, ImageAddress, U1)
|
|||
VALUE_OPCODE(ImageQueryLod, U32x4, ImageResource, SamplerResource, ImageAddress)
|
||||
VALUE_OPCODE(ImageRead, U32x4, ImageResource, ImageAddress, U1)
|
||||
VALUE_OPCODE(ImageWrite, Void, ImageResource, ImageAddress, U32x4, U1)
|
||||
VALUE_OPCODE(ImageSampleRaw, U32x4, ImageResource, SamplerResource, ImageAddress)
|
||||
VALUE_OPCODE(ImageGatherRaw, U32x4, ImageResource, SamplerResource, ImageAddress, U32, U32, U32, U32)
|
||||
VALUE_OPCODE(ImageSampleRaw, U32x4, ImageResource, SamplerResource, ImageAddress, U1)
|
||||
VALUE_OPCODE(ImageGatherRaw, U32x4, ImageResource, SamplerResource, ImageAddress, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicCompareSwap32, U32, ImageResource, ImageAddress, U32, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicSwap32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicSwap64, U64, ImageResource, ImageAddress, U64, U1)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,66 @@ void Replace(Inst& inst, Value value) {
|
|||
inst.ReplaceUsesWith(value.Resolve());
|
||||
}
|
||||
|
||||
bool HasOneUse(const Inst& inst) {
|
||||
// Replacement aliases preserve external Values but emit no additional operation.
|
||||
return std::ranges::count_if(inst.Uses(), [](const Use& use) {
|
||||
return use.user->GetOpcode() != ValueOpcode::Identity;
|
||||
}) == 1;
|
||||
}
|
||||
|
||||
class FoldWorklist {
|
||||
public:
|
||||
struct Location {
|
||||
Block::iterator instruction;
|
||||
uint64_t visited = 0;
|
||||
bool queued = false;
|
||||
};
|
||||
|
||||
void Register(Block::iterator instruction) {
|
||||
locations.emplace(&*instruction, Location {instruction});
|
||||
Queue(&*instruction);
|
||||
}
|
||||
|
||||
void Queue(Inst* inst) {
|
||||
const auto found = locations.find(inst);
|
||||
if (found != locations.end() && !found->second.queued) {
|
||||
found->second.queued = true;
|
||||
pending.push_back(inst);
|
||||
}
|
||||
}
|
||||
|
||||
void QueueUsers(const std::vector<Use>& users) {
|
||||
++generation;
|
||||
affected.clear();
|
||||
for (const auto& use: users) affected.push_back(use.user);
|
||||
// Pattern folds inspect nested operands even when intermediate operations
|
||||
// stay dynamic. Visit consumers once per change, including cyclic Phi edges.
|
||||
for (size_t i = 0; i < affected.size(); ++i) {
|
||||
auto* inst = affected[i];
|
||||
const auto found = locations.find(inst);
|
||||
if (found == locations.end() || found->second.visited == generation) continue;
|
||||
found->second.visited = generation;
|
||||
Queue(inst);
|
||||
for (const auto& use: inst->Uses()) affected.push_back(use.user);
|
||||
}
|
||||
}
|
||||
|
||||
Block::iterator Prepend(Block& block, Block::iterator before, ValueOpcode opcode,
|
||||
std::initializer_list<Value> args = {}) {
|
||||
const auto result = block.PrependNewInst(before, opcode, args);
|
||||
Register(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unordered_map<Inst*, Location> locations;
|
||||
std::vector<Inst*> pending;
|
||||
std::unordered_set<Inst*> lowered_ancillary;
|
||||
|
||||
private:
|
||||
std::vector<Inst*> affected;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
template <typename Function>
|
||||
bool FoldU32(Inst& inst, Function function) {
|
||||
const auto lhs = Arg(inst, 0);
|
||||
|
|
@ -195,8 +255,42 @@ bool FoldCompositeExtract(Inst& inst, ValueOpcode construct, size_t components)
|
|||
return false;
|
||||
}
|
||||
|
||||
void FoldZeroComparison(Block& block, Block::iterator instruction, FoldWorklist& worklist) {
|
||||
auto& inst = *instruction;
|
||||
Value value;
|
||||
for (uint32_t arg = 0; arg < 2u; ++arg)
|
||||
if (Arg(inst, arg) == Value(0u)) value = Arg(inst, arg ^ 1u);
|
||||
const auto* source = value.TryInstruction();
|
||||
if (source == nullptr) return;
|
||||
const bool equal = inst.GetOpcode() == ValueOpcode::IEqual32;
|
||||
const auto boolean_select = [](const Inst* select) {
|
||||
return select != nullptr && select->GetOpcode() == ValueOpcode::SelectU32 &&
|
||||
IsImmediate(Arg(*select, 1), Type::U32) &&
|
||||
IsImmediate(Arg(*select, 2), Type::U32);
|
||||
};
|
||||
if (boolean_select(source)) {
|
||||
const bool when_true = (Arg(*source, 1).U32() == 0u) == equal;
|
||||
const bool when_false = (Arg(*source, 2).U32() == 0u) == equal;
|
||||
if (when_true == when_false) Replace(inst, Value(when_true));
|
||||
else if (when_true) Replace(inst, Arg(*source, 0));
|
||||
else Replace(inst, Value(&*worklist.Prepend(block, instruction, ValueOpcode::LogicalNot,
|
||||
{Arg(*source, 0)})));
|
||||
} else if (source->GetOpcode() == ValueOpcode::BitwiseOr32 &&
|
||||
(boolean_select(Arg(*source, 0).TryInstruction()) ||
|
||||
boolean_select(Arg(*source, 1).TryInstruction()))) {
|
||||
// SCC encoded as a DWORD needs no select or bitwise operation when only
|
||||
// its zero predicate is consumed. Keep arbitrary DWORDs as zero comparisons.
|
||||
const auto lhs = worklist.Prepend(block, instruction, inst.GetOpcode(),
|
||||
{Arg(*source, 0), Value(0u)});
|
||||
const auto rhs = worklist.Prepend(block, instruction, inst.GetOpcode(),
|
||||
{Arg(*source, 1), Value(0u)});
|
||||
Replace(inst, Value(&*worklist.Prepend(block, instruction,
|
||||
equal ? ValueOpcode::LogicalAnd : ValueOpcode::LogicalOr, {Value(&*lhs), Value(&*rhs)})));
|
||||
}
|
||||
}
|
||||
|
||||
void FoldInstruction(Block& block, Block::iterator instruction,
|
||||
std::unordered_set<Inst*>& lowered_ancillary) {
|
||||
FoldWorklist& worklist) {
|
||||
auto& inst = *instruction;
|
||||
switch (inst.GetOpcode()) {
|
||||
case ValueOpcode::StoreBufferU32: {
|
||||
|
|
@ -208,7 +302,7 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
|||
case ValueOpcode::SelectU1:
|
||||
if (!FoldSelect(inst) && IsImmediate(Arg(inst, 2), Type::U1) &&
|
||||
!Arg(inst, 2).U1()) {
|
||||
auto result = block.PrependNewInst(instruction, ValueOpcode::LogicalAnd,
|
||||
auto result = worklist.Prepend(block, instruction, ValueOpcode::LogicalAnd,
|
||||
{Arg(inst, 0), Arg(inst, 1)});
|
||||
Replace(inst, Value(&*result));
|
||||
}
|
||||
|
|
@ -224,8 +318,8 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
|||
case 0u: Replace(inst, Value(false)); break;
|
||||
case 0x3ffu: Replace(inst, Value(true)); break;
|
||||
case 3u: {
|
||||
const auto result = block.PrependNewInst(
|
||||
instruction, ValueOpcode::FPIsNan32, {Arg(inst, 0)});
|
||||
const auto result = worklist.Prepend(
|
||||
block, instruction, ValueOpcode::FPIsNan32, {Arg(inst, 0)});
|
||||
Replace(inst, Value(&*result));
|
||||
break;
|
||||
}
|
||||
|
|
@ -280,12 +374,12 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
|||
if (offset.U32() >= field.start && offset.U32() < field.end &&
|
||||
count.U32() <= field.end - offset.U32()) {
|
||||
// Preserve extraction and sign extension while exposing only the used field.
|
||||
const auto input = block.PrependNewInst(
|
||||
instruction, ValueOpcode::GetBuiltin,
|
||||
const auto input = worklist.Prepend(
|
||||
block, instruction, ValueOpcode::GetBuiltin,
|
||||
{Value(static_cast<uint32_t>(field.kind)), Value(0u)});
|
||||
inst.SetArg(0, Value(&*input));
|
||||
inst.SetArg(1, Value(offset.U32() - field.start));
|
||||
lowered_ancillary.insert(source);
|
||||
worklist.lowered_ancillary.insert(source);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -510,8 +604,20 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
|||
});
|
||||
return;
|
||||
case ValueOpcode::BitwiseAnd32:
|
||||
if (!FoldU32(inst, [](uint32_t a, uint32_t b) { return a & b; })) {
|
||||
ReplaceBinaryIdentity(inst, Type::U32, 0xffffffffu);
|
||||
if (FoldU32(inst, [](uint32_t a, uint32_t b) { return a & b; }) ||
|
||||
ReplaceBinaryIdentity(inst, Type::U32, UINT32_MAX)) return;
|
||||
for (uint32_t arm = 0; arm < 2u; ++arm) {
|
||||
const auto mask = Arg(inst, arm);
|
||||
const auto* shifted = Arg(inst, arm ^ 1u).TryInstruction();
|
||||
if (!IsImmediate(mask, Type::U32) || shifted == nullptr ||
|
||||
shifted->GetOpcode() != ValueOpcode::ShiftLeftLogical32 || !HasOneUse(*shifted)) continue;
|
||||
const auto shift = Arg(*shifted, 1);
|
||||
if (!IsImmediate(shift, Type::U32) || shift.U32() >= 32u) continue;
|
||||
const auto masked = worklist.Prepend(block, instruction, ValueOpcode::BitwiseAnd32,
|
||||
{Arg(*shifted, 0), Value(mask.U32() >> shift.U32())});
|
||||
Replace(inst, Value(&*worklist.Prepend(block, instruction, ValueOpcode::ShiftLeftLogical32,
|
||||
{Value(&*masked), shift})));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
case ValueOpcode::BitwiseAnd64:
|
||||
|
|
@ -577,10 +683,12 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
|||
FoldU32(inst, [](uint32_t a, uint32_t b) { return std::max(a, b); });
|
||||
return;
|
||||
case ValueOpcode::IEqual32:
|
||||
FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a == b; });
|
||||
if (!FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a == b; }))
|
||||
FoldZeroComparison(block, instruction, worklist);
|
||||
return;
|
||||
case ValueOpcode::INotEqual32:
|
||||
FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a != b; });
|
||||
if (!FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a != b; }))
|
||||
FoldZeroComparison(block, instruction, worklist);
|
||||
return;
|
||||
case ValueOpcode::ULessThan32:
|
||||
FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a < b; });
|
||||
|
|
@ -642,19 +750,43 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
|||
const auto lhs = Arg(inst, 0);
|
||||
const auto rhs = Arg(inst, 1);
|
||||
const auto simplify = [&](Value assumption, Value expression) {
|
||||
const auto* disjunction = expression.TryInstruction();
|
||||
if (disjunction == nullptr || disjunction->GetOpcode() != ValueOpcode::LogicalOr) {
|
||||
return false;
|
||||
const auto* test = expression.TryInstruction();
|
||||
if (test == nullptr) return false;
|
||||
const auto replace = [&](Value value) {
|
||||
inst.SetArg(assumption == lhs ? 1u : 0u, value);
|
||||
return true;
|
||||
};
|
||||
const auto negate = [&](Value value) {
|
||||
return Value(&*worklist.Prepend(block, instruction, ValueOpcode::LogicalNot, {value}));
|
||||
};
|
||||
const bool inverse = test->GetOpcode() == ValueOpcode::LogicalNot;
|
||||
if (inverse) {
|
||||
test = Arg(*test, 0).TryInstruction();
|
||||
if (test == nullptr) return false;
|
||||
if (test->GetOpcode() == ValueOpcode::LogicalAnd && HasOneUse(*test))
|
||||
for (uint32_t arg = 0; arg < 2u; ++arg)
|
||||
if (Arg(*test, arg) == assumption)
|
||||
return replace(negate(Arg(*test, arg ^ 1u)));
|
||||
}
|
||||
for (uint32_t i = 0; i < 2u; ++i) {
|
||||
const auto* inverse = disjunction->Arg(i).Resolve().TryInstruction();
|
||||
if (inverse != nullptr && inverse->GetOpcode() == ValueOpcode::LogicalNot &&
|
||||
inverse->Arg(0).Resolve() == assumption) {
|
||||
inst.SetArg(assumption == lhs ? 1u : 0u,
|
||||
disjunction->Arg(i ^ 1u));
|
||||
return true;
|
||||
if (test->GetOpcode() == ValueOpcode::IEqual32 ||
|
||||
test->GetOpcode() == ValueOpcode::INotEqual32) {
|
||||
for (uint32_t arg = 0; arg < 2u; ++arg) {
|
||||
if (Arg(*test, arg) != Value(0u)) continue;
|
||||
const auto original = Arg(*test, arg ^ 1u);
|
||||
const auto active = ResolveActiveU32(original, assumption);
|
||||
if (!active.IsEmpty() && active != original) {
|
||||
const auto comparison = Value(&*worklist.Prepend(block, instruction,
|
||||
test->GetOpcode(), {active, Value(0u)}));
|
||||
return replace(inverse ? negate(comparison) : comparison);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inverse || test->GetOpcode() != ValueOpcode::LogicalOr) return false;
|
||||
for (uint32_t arg = 0; arg < 2u; ++arg) {
|
||||
const auto* negation = Arg(*test, arg).TryInstruction();
|
||||
if (negation != nullptr && negation->GetOpcode() == ValueOpcode::LogicalNot &&
|
||||
Arg(*negation, 0) == assumption) return replace(Arg(*test, arg ^ 1u));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (simplify(lhs, rhs) || simplify(rhs, lhs)) return;
|
||||
|
|
@ -673,6 +805,25 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
|||
Replace(inst, lhs.U1() ? lhs : rhs);
|
||||
} else if (IsImmediate(rhs, Type::U1)) {
|
||||
Replace(inst, rhs.U1() ? rhs : lhs);
|
||||
} else {
|
||||
const auto* left = lhs.TryInstruction();
|
||||
const auto* right = rhs.TryInstruction();
|
||||
if (left == nullptr || right == nullptr ||
|
||||
left->GetOpcode() != ValueOpcode::LogicalAnd ||
|
||||
right->GetOpcode() != ValueOpcode::LogicalAnd ||
|
||||
(!HasOneUse(*left) && !HasOneUse(*right))) return;
|
||||
// Factor a shared execution predicate without retaining more operations.
|
||||
for (uint32_t a = 0; a < 2u; ++a) {
|
||||
for (uint32_t b = 0; b < 2u; ++b) {
|
||||
const auto common = Arg(*left, a);
|
||||
if (common != Arg(*right, b)) continue;
|
||||
const auto either = Value(&*worklist.Prepend(block, instruction,
|
||||
ValueOpcode::LogicalOr, {Arg(*left, a ^ 1u), Arg(*right, b ^ 1u)}));
|
||||
Replace(inst, Value(&*worklist.Prepend(block, instruction,
|
||||
ValueOpcode::LogicalAnd, {common, either})));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
|
@ -706,14 +857,13 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
|||
}
|
||||
}
|
||||
|
||||
// In wave32, extracting this invocation's bit from a ballot recovers its predicate.
|
||||
// Keep that identity through scalar EXEC operations and their loop-carried mask Phis.
|
||||
// Full and empty masks have a constant predicate in either wave size. In wave32,
|
||||
// ballot projection also survives scalar EXEC operations and loop-carried mask Phis.
|
||||
class LaneMaskProjection {
|
||||
public:
|
||||
explicit LaneMaskProjection(std::unordered_set<Inst*>& lowered_ancillary)
|
||||
: m_lowered_ancillary(lowered_ancillary) {}
|
||||
explicit LaneMaskProjection(FoldWorklist& worklist): m_worklist(worklist) {}
|
||||
|
||||
void Fold(Inst& inst) {
|
||||
void Fold(Inst& inst, uint32_t wave_size) {
|
||||
if (inst.GetOpcode() != ValueOpcode::INotEqual32 || !Immediate(Arg(inst, 1), 0u)) return;
|
||||
const auto* bit = Arg(inst, 0).TryInstruction();
|
||||
if (bit == nullptr || bit->GetOpcode() != ValueOpcode::BitwiseAnd32 ||
|
||||
|
|
@ -725,9 +875,15 @@ public:
|
|||
!Immediate(Arg(*index, 1), 31u)) return;
|
||||
const auto* lane = Arg(*index, 0).TryInstruction();
|
||||
if (lane == nullptr || lane->GetOpcode() != ValueOpcode::LaneId) return;
|
||||
const auto mask = Arg(*shift, 0);
|
||||
if (Immediate(mask, 0u) || Immediate(mask, UINT32_MAX)) {
|
||||
Replace(inst, Value(mask.U32() != 0u));
|
||||
return;
|
||||
}
|
||||
if (wave_size != 32u) return;
|
||||
m_visited.clear();
|
||||
m_grounded = false;
|
||||
if (CanProject(Arg(*shift, 0)) && m_grounded) Replace(inst, Project(Arg(*shift, 0)));
|
||||
if (CanProject(mask) && m_grounded) Replace(inst, Project(mask));
|
||||
}
|
||||
|
||||
private:
|
||||
|
|
@ -772,19 +928,18 @@ private:
|
|||
if (const auto found = m_values.find(source); found != m_values.end()) return found->second.Resolve();
|
||||
if (const auto predicate = BallotPredicate(*source); !predicate.IsEmpty()) return predicate;
|
||||
auto* block = source->Parent();
|
||||
const auto where = std::find_if(block->begin(), block->end(),
|
||||
[source](const Inst& candidate) { return &candidate == source; });
|
||||
const auto where = m_worklist.locations.at(source).instruction;
|
||||
const auto op = source->GetOpcode() == ValueOpcode::BitwiseAnd32 ? ValueOpcode::LogicalAnd
|
||||
: source->GetOpcode() == ValueOpcode::BitwiseOr32 ? ValueOpcode::LogicalOr
|
||||
: source->GetOpcode() == ValueOpcode::BitwiseNot32 ? ValueOpcode::LogicalNot
|
||||
: source->GetOpcode() == ValueOpcode::SelectU32 ? ValueOpcode::SelectU1
|
||||
: ValueOpcode::Phi;
|
||||
auto result = op == ValueOpcode::Phi ? block->PrependNewInst(where, op)
|
||||
auto result = op == ValueOpcode::Phi ? m_worklist.Prepend(*block, where, op)
|
||||
: op == ValueOpcode::LogicalNot
|
||||
? block->PrependNewInst(where, op, {Value(false)})
|
||||
? m_worklist.Prepend(*block, where, op, {Value(false)})
|
||||
: op == ValueOpcode::SelectU1
|
||||
? block->PrependNewInst(where, op, {Arg(*source, 0), Value(false), Value(false)})
|
||||
: block->PrependNewInst(where, op, {Value(false), Value(false)});
|
||||
? m_worklist.Prepend(*block, where, op, {Arg(*source, 0), Value(false), Value(false)})
|
||||
: m_worklist.Prepend(*block, where, op, {Value(false), Value(false)});
|
||||
m_values.emplace(source, Value(&*result));
|
||||
if (op == ValueOpcode::Phi) result->SetFlags(Type::U1);
|
||||
for (size_t arg = op == ValueOpcode::SelectU1 ? 1u : 0u; arg < source->NumArgs(); ++arg) {
|
||||
|
|
@ -792,11 +947,11 @@ private:
|
|||
if (op == ValueOpcode::Phi) result->AddPhiOperand(source->PhiBlock(arg), projected);
|
||||
else result->SetArg(arg, projected);
|
||||
}
|
||||
FoldInstruction(*block, result, m_lowered_ancillary);
|
||||
FoldInstruction(*block, result, m_worklist);
|
||||
return Value(&*result).Resolve();
|
||||
}
|
||||
|
||||
std::unordered_set<Inst*>& m_lowered_ancillary;
|
||||
FoldWorklist& m_worklist;
|
||||
std::unordered_set<const Inst*> m_visited;
|
||||
std::unordered_map<const Inst*, Value> m_values;
|
||||
bool m_grounded = false;
|
||||
|
|
@ -805,29 +960,59 @@ private:
|
|||
} // namespace
|
||||
|
||||
void ConstantPropagationPass(const BlockList& blocks, uint32_t wave_size) {
|
||||
std::unordered_set<Inst*> lowered_ancillary;
|
||||
LaneMaskProjection mask_projection(lowered_ancillary);
|
||||
for (auto* block: blocks) {
|
||||
for (auto inst = block->begin(); inst != block->end(); ++inst) {
|
||||
if (wave_size == 32u) mask_projection.Fold(*inst);
|
||||
FoldInstruction(*block, inst, lowered_ancillary);
|
||||
FoldWorklist worklist;
|
||||
LaneMaskProjection mask_projection(worklist);
|
||||
for (auto* block: blocks)
|
||||
for (auto inst = block->begin(); inst != block->end(); ++inst) worklist.Register(inst);
|
||||
std::vector<Use> users;
|
||||
std::vector<Value> operands;
|
||||
const auto drain = [&] {
|
||||
for (size_t i = 0; i < worklist.pending.size(); ++i) {
|
||||
auto* inst = worklist.pending[i];
|
||||
auto& location = worklist.locations.at(inst);
|
||||
location.queued = false;
|
||||
const auto opcode = inst->GetOpcode();
|
||||
if (opcode == ValueOpcode::Identity || opcode == ValueOpcode::Void) continue;
|
||||
const auto where = location.instruction;
|
||||
users.assign(inst->Uses().begin(), inst->Uses().end());
|
||||
operands.clear();
|
||||
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) operands.push_back(inst->Arg(arg));
|
||||
mask_projection.Fold(*inst, wave_size);
|
||||
FoldInstruction(*inst->Parent(), where, worklist);
|
||||
bool changed = opcode != inst->GetOpcode() || operands.size() != inst->NumArgs();
|
||||
for (size_t arg = 0; !changed && arg < operands.size(); ++arg)
|
||||
changed = operands[arg] != inst->Arg(arg);
|
||||
if (changed) {
|
||||
worklist.QueueUsers(users);
|
||||
worklist.Queue(inst);
|
||||
}
|
||||
}
|
||||
}
|
||||
worklist.pending.clear();
|
||||
};
|
||||
drain();
|
||||
// Normalize retained PHI/select values only after every supported field read has
|
||||
// been lowered; direct raw consumers remain unsupported.
|
||||
for (auto* source: lowered_ancillary) {
|
||||
const bool retained_only = std::ranges::all_of(source->Uses(), [](const Use& use) {
|
||||
const auto& user = *use.user;
|
||||
if (!user.HasUses() && !user.MayHaveSideEffects()) {
|
||||
return true;
|
||||
bool normalized;
|
||||
do {
|
||||
normalized = false;
|
||||
for (auto* source: worklist.lowered_ancillary) {
|
||||
if (source->GetOpcode() == ValueOpcode::Identity) continue;
|
||||
const bool retained_only = std::ranges::all_of(source->Uses(), [](const Use& use) {
|
||||
const auto& user = *use.user;
|
||||
if (!user.HasUses() && !user.MayHaveSideEffects()) {
|
||||
return true;
|
||||
}
|
||||
return user.GetOpcode() == ValueOpcode::Phi ||
|
||||
(user.GetOpcode() == ValueOpcode::SelectU32 && use.operand == 2u);
|
||||
});
|
||||
if (retained_only) {
|
||||
worklist.QueueUsers(source->Uses());
|
||||
Replace(*source, Value(0u));
|
||||
normalized = true;
|
||||
}
|
||||
return user.GetOpcode() == ValueOpcode::Phi ||
|
||||
(user.GetOpcode() == ValueOpcode::SelectU32 && use.operand == 2u);
|
||||
});
|
||||
if (retained_only) {
|
||||
Replace(*source, Value(0u));
|
||||
}
|
||||
}
|
||||
drain();
|
||||
} while (normalized);
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ namespace Libs::Graphics::ShaderRecompiler::IR {
|
|||
namespace {
|
||||
|
||||
constexpr uint64_t AddressMask = 0x0000ffffffffffffull;
|
||||
constexpr uint64_t MaxIndirectDescriptorProbes = 65536u;
|
||||
|
||||
bool SpecializationFail(std::string_view message) {
|
||||
std::fprintf(stderr, "shader resource specialization failed: %.*s\n",
|
||||
|
|
@ -106,16 +105,46 @@ Prospero::BufferFormat ImageConversionFormat(Prospero::BufferFormat format) {
|
|||
: Prospero::BufferFormat::kInvalid;
|
||||
}
|
||||
|
||||
enum class SamplerClass : uint8_t { Float, Integer, PointInteger };
|
||||
bool ValidatePackedUnormSampling(const ResourcePlan& program, const ResourceSnapshot& snapshot,
|
||||
uint32_t image_index) {
|
||||
ShaderTextureResource texture;
|
||||
std::copy_n(snapshot.images[image_index].dwords.begin(), std::size(texture.fields),
|
||||
texture.fields);
|
||||
if (texture.Type() != Prospero::ImageType::kColor2D ||
|
||||
texture.BaseLevel() != texture.LastLevel() || texture.MinLod() != 0u ||
|
||||
texture.CornerSample()) {
|
||||
return SpecializationFail("packed UNorm sampling requires a centered, single-mip 2D image");
|
||||
}
|
||||
for (const auto& pair: program.info.sampled_pairs) {
|
||||
if (pair.image != image_index) continue;
|
||||
if (pair.sampler >= snapshot.samplers.size() ||
|
||||
!program.info.samplers[pair.sampler].indirect_resources.empty()) {
|
||||
return SpecializationFail("packed UNorm sampling requires a direct sampler");
|
||||
}
|
||||
ShaderSamplerResource sampler;
|
||||
std::copy_n(snapshot.samplers[pair.sampler].dwords.begin(), std::size(sampler.fields),
|
||||
sampler.fields);
|
||||
const auto linear = static_cast<uint32_t>(Prospero::SamplerFilter::kBilinear);
|
||||
const auto clamp = static_cast<uint32_t>(Prospero::SamplerClampMode::kClampLastTexel);
|
||||
if (sampler.XyMagFilter() != linear || sampler.XyMinFilter() != linear ||
|
||||
sampler.ClampX() != clamp || sampler.ClampY() != clamp ||
|
||||
sampler.ForceUnormCoords() || sampler.MaxAnisoRatio() != 0u ||
|
||||
sampler.FilterMode() != 0u || sampler.MinLod() != 0u || sampler.ForceSrgb() ||
|
||||
sampler.TruncCoord() || (sampler.fields[0] & (1u << 19u)) != 0u) { // MC_COORD_TRUNC
|
||||
return SpecializationFail(fmt::format(
|
||||
"packed UNorm image {} requires normalized bilinear clamp sampling (sampler {})",
|
||||
image_index, pair.sampler));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
enum class SamplerClass : uint8_t { Float, Integer };
|
||||
|
||||
SamplerClass ClassifySampler(const ImageResource& image) {
|
||||
if (image.numeric_class == Prospero::TextureNumericClass::Sint ||
|
||||
(image.numeric_class == Prospero::TextureNumericClass::Uint &&
|
||||
image.conversion_format != Prospero::BufferFormat::kInvalid)) {
|
||||
return SamplerClass::PointInteger;
|
||||
}
|
||||
return image.numeric_class == Prospero::TextureNumericClass::Uint ? SamplerClass::Integer
|
||||
: SamplerClass::Float;
|
||||
return image.numeric_class == Prospero::TextureNumericClass::Uint ||
|
||||
image.numeric_class == Prospero::TextureNumericClass::Sint
|
||||
? SamplerClass::Integer : SamplerClass::Float;
|
||||
}
|
||||
|
||||
bool DescriptorIsCube(const DescriptorValue& descriptor) {
|
||||
|
|
@ -176,16 +205,14 @@ void MarkCleanReads(const std::unordered_set<const Inst*>& planned_reads,
|
|||
}
|
||||
}
|
||||
|
||||
bool ReadScalarTable(uint64_t base, uint64_t size, uint64_t dynamic_offset,
|
||||
const SrtRuntime& runtime, std::span<uint32_t> words) {
|
||||
const auto offset = dynamic_offset & ~uint64_t {3};
|
||||
bool ReadTable(uint64_t base, uint64_t size, uint64_t offset,
|
||||
const SrtRuntime& runtime, std::span<uint32_t> words) {
|
||||
const auto count = std::min<uint64_t>(words.size(), offset < size ? (size - offset) / 4u : 0u);
|
||||
std::ranges::fill(words.subspan(count), 0u);
|
||||
if (count == 0u) {
|
||||
return true;
|
||||
}
|
||||
base &= AddressMask & ~uint64_t {3};
|
||||
if (offset > AddressMask - base) {
|
||||
if (base > AddressMask || offset > AddressMask - base) {
|
||||
return false;
|
||||
}
|
||||
const auto address = base + offset;
|
||||
|
|
@ -195,15 +222,24 @@ bool ReadScalarTable(uint64_t base, uint64_t size, uint64_t dynamic_offset,
|
|||
runtime.read_specialization_memory(runtime.userdata, address, prefix);
|
||||
}
|
||||
|
||||
bool NormalizeIndirectStoreBuffer(DescriptorValue& value) {
|
||||
bool ReadScalarTable(uint64_t base, uint64_t size, uint64_t offset,
|
||||
const SrtRuntime& runtime, std::span<uint32_t> words) {
|
||||
return ReadTable(base & AddressMask & ~uint64_t {3}, size, offset & ~uint64_t {3}, runtime, words);
|
||||
}
|
||||
|
||||
bool NormalizeIndirectStoreBuffer(DescriptorValue& value, bool indexed, bool formatted) {
|
||||
ShaderBufferResource descriptor;
|
||||
if (!DecodeBufferDescriptor(value, descriptor)) return false;
|
||||
if (descriptor.Type() != 0u || descriptor.Format() == Prospero::BufferFormat::kInvalid ||
|
||||
descriptor.Base48() == 0u || descriptor.GetSize() == 0u) {
|
||||
value.dwords.fill(0u);
|
||||
} else if (descriptor.Stride() != 1u || descriptor.SwizzleEnabled() || descriptor.AddTid() ||
|
||||
descriptor.OutOfBounds() != 3u) {
|
||||
return SpecializationFail("indirect buffer stores require linear byte bounds");
|
||||
} else if (descriptor.Stride() != (indexed ? 4u : 1u) || descriptor.SwizzleEnabled() ||
|
||||
descriptor.AddTid() || descriptor.OutOfBounds() != (indexed ? 0u : 3u)) {
|
||||
return SpecializationFail("indirect buffer stores require exact linear DWORD or byte bounds");
|
||||
} else if (formatted && (descriptor.Format() != Prospero::BufferFormat::k32UInt ||
|
||||
descriptor.DstSelXYZW() != DstSel(4, 0, 0, 0) ||
|
||||
(descriptor.Base48() & 3u) != 0u)) {
|
||||
return SpecializationFail("indirect formatted X stores require an aligned 32-bit UInt identity mapping");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -230,6 +266,11 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
|||
auto& keys = program.material_keys;
|
||||
const auto children_begin = descriptors.size();
|
||||
const auto root_resource = specializations[resource_index];
|
||||
const auto evaluate_count = [&](uint32_t& count) {
|
||||
if (!clean.Evaluate(indirect.key_count, count)) return false;
|
||||
if (indirect.signed_count && std::bit_cast<int32_t>(count) < 0) count = 0;
|
||||
return true;
|
||||
};
|
||||
const auto intern_candidate = [&](const DescriptorValue& candidate, uint32_t& ordinal) {
|
||||
ordinal = 0u;
|
||||
if (candidate == descriptors[resource_index]) return true;
|
||||
|
|
@ -244,14 +285,19 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
|||
}
|
||||
return true;
|
||||
};
|
||||
const auto read_keys = [&](const ShaderBufferResource& material, uint64_t first,
|
||||
const auto read_selector = [&](uint64_t base, uint64_t size, uint64_t offset,
|
||||
std::span<uint32_t> words) {
|
||||
return selector->global ? ReadTable(base, size, offset, runtime, words)
|
||||
: ReadScalarTable(base, size, offset, runtime, words);
|
||||
};
|
||||
const auto read_keys = [&](uint64_t base, uint64_t size, uint64_t first,
|
||||
uint64_t step, uint64_t count) {
|
||||
keys.resize(count);
|
||||
if (step == 4u) {
|
||||
return ReadScalarTable(material.Base48(), material.GetSize(), first, runtime, keys);
|
||||
return read_selector(base, size, first, keys);
|
||||
}
|
||||
for (auto& key: keys) {
|
||||
if (!ReadScalarTable(material.Base48(), material.GetSize(), first, runtime, {&key, 1}))
|
||||
if (!read_selector(base, size, first, {&key, 1}))
|
||||
return false;
|
||||
first += step;
|
||||
}
|
||||
|
|
@ -320,29 +366,133 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
|||
}
|
||||
if (selector == nullptr) {
|
||||
uint32_t key_count = 0;
|
||||
uint32_t first_key = 0;
|
||||
if (indirect.workgroup_axis != UINT32_MAX) {
|
||||
if (indirect.workgroup_axis >= runtime.workgroup_counts.size()) return false;
|
||||
key_count = runtime.workgroup_counts[indirect.workgroup_axis];
|
||||
if (key_count == 0u) return false;
|
||||
} else {
|
||||
if (table_value.dword_count != 2u || !clean.Evaluate(indirect.key_count, key_count))
|
||||
if (table_value.dword_count != 2u || !evaluate_count(key_count))
|
||||
return false;
|
||||
if (std::bit_cast<int32_t>(key_count) <= 0) key_count = 0;
|
||||
}
|
||||
if (!indirect.selector_first.IsEmpty() && !clean.Evaluate(indirect.selector_first, first_key))
|
||||
return false;
|
||||
if (indirect.table_stride == 0u || key_count > MaxIndirectDescriptorProbes ||
|
||||
(key_count != 0u && uint64_t {indirect.table_offset} + indirect.table_immediate +
|
||||
(indirect.selector_first.IsEmpty() && key_count != 0u &&
|
||||
uint64_t {indirect.table_offset} + indirect.table_immediate +
|
||||
uint64_t {key_count - 1u} * indirect.table_stride + descriptor_bytes >
|
||||
UINT32_MAX + 1ull)) {
|
||||
return false;
|
||||
}
|
||||
keys.resize(key_count);
|
||||
std::iota(keys.begin(), keys.end(), 0u);
|
||||
} else if (!indirect.selector_first.IsEmpty()) {
|
||||
std::iota(keys.begin(), keys.end(), first_key);
|
||||
if (!keys.empty() && keys.back() < keys.front())
|
||||
std::rotate(keys.begin(), keys.begin() + (UINT32_MAX - first_key + 1ull), keys.end());
|
||||
} else if (selector->rows) {
|
||||
const auto& rows = *selector->rows;
|
||||
DescriptorValue count_value;
|
||||
ShaderBufferResource counts, material;
|
||||
if (!clean.EvaluateDescriptor(rows.source, count_value) ||
|
||||
!DecodeBufferDescriptor(count_value, counts) ||
|
||||
!DecodeBufferDescriptor(material_value, material) || selector->stride != 4u ||
|
||||
selector->offset != 0u || rows.stride == 0u || (rows.stride & 3u) != 0u ||
|
||||
counts.GetSize() / 4u > MaxIndirectDescriptorProbes) return false;
|
||||
std::vector<uint32_t> lengths(counts.GetSize() / 4u);
|
||||
if (!ReadScalarTable(counts.Base48(), counts.GetSize(), 0u, runtime, lengths)) return false;
|
||||
uint32_t total = 0;
|
||||
for (size_t row = 0; row < lengths.size(); ++row) {
|
||||
const auto length = lengths[row];
|
||||
if (length > MaxIndirectDescriptorProbes - total ||
|
||||
(length != 0u && uint64_t {row} * rows.stride + uint64_t {length} * 4u >
|
||||
UINT32_MAX + 1ull)) return false;
|
||||
total += length;
|
||||
}
|
||||
keys.reserve(total);
|
||||
std::vector<uint32_t> batch;
|
||||
for (size_t row = 0; row < lengths.size();) {
|
||||
if (lengths[row] == 0u) { ++row; continue; }
|
||||
const auto first = uint64_t {row} * rows.stride / 4u;
|
||||
auto end = first + lengths[row];
|
||||
size_t next = row + 1u;
|
||||
for (; next < lengths.size(); ++next) {
|
||||
if (lengths[next] == 0u) continue;
|
||||
const auto candidate_end =
|
||||
std::max(end, uint64_t {next} * rows.stride / 4u + lengths[next]);
|
||||
if (candidate_end - first > MaxIndirectDescriptorProbes) break;
|
||||
end = candidate_end;
|
||||
}
|
||||
// Coalesce small gaps, but interpret only selected prefixes. The strict
|
||||
// reader captures the entire fetched span for writable-alias rejection.
|
||||
batch.resize(end - first);
|
||||
if (!ReadScalarTable(material.Base48(), material.GetSize(), first * 4u, runtime, batch))
|
||||
return false;
|
||||
for (; row < next; ++row) {
|
||||
if (lengths[row] == 0u) continue;
|
||||
const auto begin = batch.begin() + (uint64_t {row} * rows.stride / 4u - first);
|
||||
keys.insert(keys.end(), begin, begin + lengths[row]);
|
||||
}
|
||||
}
|
||||
} else if (selector->bitset) {
|
||||
ShaderBufferResource material;
|
||||
uint32_t first = 0, count = 0;
|
||||
std::array<uint32_t, 4> masks {};
|
||||
uint32_t first = 0, count = 0;
|
||||
if (!DecodeBufferDescriptor(material_value, material) ||
|
||||
!clean.Evaluate(indirect.selector_first, first) ||
|
||||
!clean.Evaluate(indirect.key_count, count) || count > MaxIndirectDescriptorProbes)
|
||||
!evaluate_count(count) || count != masks.size() * 32u ||
|
||||
selector->stride != sizeof(masks) || selector->offset != 0u ||
|
||||
material.Type() != 0u || material.Stride() != sizeof(masks) ||
|
||||
material.SwizzleEnabled() || material.AddTid() || material.OutOfBounds() != 0u ||
|
||||
(material.Base48() & 3u) != 0u) return false;
|
||||
if (material.Format() != Prospero::BufferFormat::kInvalid) {
|
||||
const auto words = material.GetSize() / sizeof(uint32_t);
|
||||
// Bitmap storage can grow with resolution while its candidate domain stays
|
||||
// at 128 keys. Bound scratch memory, preserving each coherent read's capture.
|
||||
for (uint64_t first_word = 0; first_word < words; first_word += keys.size()) {
|
||||
keys.resize(std::min<uint64_t>(words - first_word, MaxIndirectDescriptorProbes));
|
||||
if (!ReadScalarTable(material.Base48(), material.GetSize(), first_word * 4u,
|
||||
runtime, keys)) return false;
|
||||
for (size_t word = 0; word < keys.size(); ++word) masks[word % masks.size()] |= keys[word];
|
||||
if (std::ranges::all_of(masks, [](uint32_t mask) { return mask == UINT32_MAX; })) break;
|
||||
}
|
||||
keys.clear();
|
||||
for (uint32_t word = 0; word < masks.size(); ++word) {
|
||||
auto mask = masks[word];
|
||||
while (mask != 0u) {
|
||||
const auto rank = word * 32u + std::countr_zero(mask);
|
||||
keys.push_back(first + count - 1u - rank);
|
||||
mask &= mask - 1u;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!selector->byte_ranges.empty()) {
|
||||
if (material_value.dword_count != 2u) return false;
|
||||
const auto base = (uint64_t {material_value.dwords[1]} << 32u) | material_value.dwords[0];
|
||||
std::array<bool, 256> selected {};
|
||||
for (const auto [offset, size]: selector->byte_ranges) {
|
||||
if (size > MaxIndirectDescriptorProbes || ((offset | size) & 3u) != 0u) return false;
|
||||
keys.resize(size / 4u);
|
||||
if (!ReadScalarTable(base, UINT64_MAX, offset, runtime, keys)) return false;
|
||||
for (const auto word: keys)
|
||||
for (uint32_t shift = 0; shift < 32u; shift += 8u)
|
||||
selected[(word >> shift) & 255u] = true;
|
||||
}
|
||||
keys.clear();
|
||||
for (uint32_t key = 0; key < selected.size(); ++key)
|
||||
if (selected[key]) keys.push_back(key);
|
||||
} else if (!indirect.selector_first.IsEmpty()) {
|
||||
ShaderBufferResource material;
|
||||
uint64_t base = 0, size = UINT64_MAX;
|
||||
uint32_t first = 0, count = 0;
|
||||
if (material_value.dword_count == 2u) {
|
||||
if (selector->stride == 0u) return false;
|
||||
base = (uint64_t {material_value.dwords[1]} << 32u) | material_value.dwords[0];
|
||||
} else {
|
||||
if (!DecodeBufferDescriptor(material_value, material)) return false;
|
||||
base = material.Base48();
|
||||
size = material.GetSize();
|
||||
}
|
||||
if (!clean.Evaluate(indirect.selector_first, first) ||
|
||||
!evaluate_count(count) || count > MaxIndirectDescriptorProbes)
|
||||
return false;
|
||||
// SMEM uses its instruction's byte stride; V# stride only supplies its bounds.
|
||||
const auto step = selector->stride != 0u ? selector->stride : material.Stride();
|
||||
|
|
@ -352,16 +502,16 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
|||
uint64_t {first} + count > material.NumRecords())) return false;
|
||||
const auto end = count != 0u
|
||||
? (uint64_t {first} + count - 1u) * step + selector->offset + 4u : 0u;
|
||||
if (end > uint64_t {UINT32_MAX} + 1u || end > material.GetSize())
|
||||
if (end > uint64_t {UINT32_MAX} + 1u || end > size)
|
||||
return false;
|
||||
if (!read_keys(material, uint64_t {first} * step + selector->offset, step, count))
|
||||
if (!read_keys(base, size, uint64_t {first} * step + selector->offset, step, count))
|
||||
return false;
|
||||
} else if (!indirect.selector_mask.IsEmpty()) {
|
||||
uint32_t mask = 0;
|
||||
uint32_t count = 0;
|
||||
if (material_value.dword_count != 2u || table_value.dword_count != 2u ||
|
||||
!clean.Evaluate(indirect.selector_mask, mask) ||
|
||||
!clean.Evaluate(indirect.key_count, count) || count == 0u || count > 32u) {
|
||||
!evaluate_count(count) || count == 0u || count > 32u) {
|
||||
return false;
|
||||
}
|
||||
if (count < 32u) mask &= (1u << count) - 1u;
|
||||
|
|
@ -374,13 +524,31 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
|||
static_cast<uint64_t>(index) * selector->stride;
|
||||
if (offset > UINT32_MAX) return false;
|
||||
uint32_t key = 0;
|
||||
if (!ReadScalarTable(material_base, UINT64_MAX, static_cast<uint32_t>(offset),
|
||||
runtime, {&key, 1})) return false;
|
||||
if (!read_selector(material_base, UINT64_MAX, static_cast<uint32_t>(offset),
|
||||
{&key, 1})) return false;
|
||||
keys.push_back(key);
|
||||
mask &= mask - 1u;
|
||||
}
|
||||
} else return false;
|
||||
if (selector != nullptr) {
|
||||
} else {
|
||||
ShaderBufferResource material;
|
||||
if (!DecodeBufferDescriptor(material_value, material) || selector->stride < 4u ||
|
||||
!std::has_single_bit(selector->stride) || (selector->offset & 3u) != 0u)
|
||||
return false;
|
||||
// Empty selector bounds denote the complete U32 congruence class. SMEM
|
||||
// adds its aligned immediate after the dynamic offset, without U32 wrap.
|
||||
const auto domain_count = (uint64_t {1} << 32u) / selector->stride;
|
||||
const auto available = material.GetSize() > selector->offset
|
||||
? material.GetSize() - selector->offset : 0u;
|
||||
const auto count = std::min(domain_count,
|
||||
available < 4u ? 0u : (available - 4u) / selector->stride + 1u);
|
||||
if (count > MaxIndirectDescriptorProbes ||
|
||||
!read_keys(material.Base48(), material.GetSize(), selector->offset, selector->stride, count)) return false;
|
||||
if (count < domain_count) keys.push_back(0u); // An out-of-bounds scalar read returns zero.
|
||||
}
|
||||
if (selector != nullptr && selector->byte_ranges.empty()) {
|
||||
if (selector->bit_offset != 0u || selector->bit_mask != UINT32_MAX || selector->maximum != UINT32_MAX)
|
||||
for (auto& key: keys)
|
||||
key = std::min((key >> selector->bit_offset) & selector->bit_mask, selector->maximum);
|
||||
std::ranges::sort(keys);
|
||||
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
|
||||
}
|
||||
|
|
@ -433,7 +601,7 @@ struct SamplerPlan {
|
|||
uint32_t source;
|
||||
SamplerClass type;
|
||||
};
|
||||
std::array<std::array<uint32_t, 3>, ShaderInfo::MaxSamplers> mapping;
|
||||
std::array<std::array<uint32_t, 2>, ShaderInfo::MaxSamplers> mapping;
|
||||
std::array<Binding, ShaderInfo::MaxSamplers> bindings;
|
||||
uint32_t sampler_count = 0;
|
||||
};
|
||||
|
|
@ -520,6 +688,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
|||
if (base.atomic &&
|
||||
(base.atomic64 ? format != Prospero::BufferFormat::k32_32UInt
|
||||
: format != Prospero::BufferFormat::k32UInt &&
|
||||
format != Prospero::BufferFormat::k32SInt &&
|
||||
format != Prospero::BufferFormat::k32Float)) {
|
||||
return SpecializationFail(
|
||||
fmt::format("atomic image descriptor {} uses unsupported format {}", i,
|
||||
|
|
@ -540,8 +709,15 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
|||
image.shader_swizzle = DescriptorImageSwizzle(descriptor);
|
||||
}
|
||||
const bool raw_sint_storage = storage && format == Prospero::BufferFormat::k32SInt &&
|
||||
base.written && !base.read && !base.atomic;
|
||||
image.numeric_class = Prospero::SampledTextureNumericClass(format);
|
||||
base.written && (base.atomic ? !base.atomic64 : !base.read);
|
||||
image.numeric_class = Prospero::SampledTextureNumericClass(
|
||||
Prospero::RemapTextureFormat(format));
|
||||
if (!storage && format == Prospero::BufferFormat::k11_11_10UNorm) {
|
||||
if (image.indirect_root != ImageResource::NoIndirectImage) {
|
||||
return SpecializationFail("packed UNorm sampling requires a direct image");
|
||||
}
|
||||
if (!ValidatePackedUnormSampling(program, snapshot, i)) return false;
|
||||
}
|
||||
if (storage) {
|
||||
if ((!raw_sint_storage && image.numeric_class == Prospero::TextureNumericClass::Sint) ||
|
||||
image.numeric_class == Prospero::TextureNumericClass::Unsupported) {
|
||||
|
|
@ -609,8 +785,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
|||
}
|
||||
const bool same_coordinates = image.dimension == image_class.dimension &&
|
||||
image.cube == image_class.cube;
|
||||
if (image.numeric_class != image_class.numeric_class ||
|
||||
(!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension))) ||
|
||||
if ((!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension))) ||
|
||||
image.mip_count != image_class.mip_count ||
|
||||
image.conversion_format != image_class.conversion_format ||
|
||||
image.shader_swizzle != image_class.shader_swizzle) {
|
||||
|
|
@ -635,6 +810,10 @@ bool BuildSamplerPlan(const ShaderInfo& base, SamplerPlan& plan) {
|
|||
return false;
|
||||
}
|
||||
usage[pair.sampler] |= 1u << static_cast<uint32_t>(ClassifySampler(base.images[pair.image]));
|
||||
for (const auto candidate: base.images[pair.image].indirect_resources) {
|
||||
if (candidate >= base.images.size()) return false;
|
||||
usage[pair.sampler] |= 1u << static_cast<uint32_t>(ClassifySampler(base.images[candidate]));
|
||||
}
|
||||
for (const auto candidate: base.samplers[pair.sampler].indirect_resources) {
|
||||
if (candidate >= base.samplers.size()) return false;
|
||||
usage[candidate] |= usage[pair.sampler];
|
||||
|
|
@ -751,55 +930,16 @@ static bool IsFullDispatchPredicate(Value value, uint32_t depth = 0) {
|
|||
return axis.IsImmediate() && axis.U32() < 3 && extent->Arg(0).Resolve() == axis;
|
||||
}
|
||||
|
||||
// Nonnegative affine coefficients for constant, local and workgroup coordinates. Reject modular
|
||||
// arithmetic that could wrap; runtime coverage also bounds the largest invocation index.
|
||||
static std::optional<std::array<uint64_t, 3>> FillIndex(Value value, uint32_t axis, Value guard,
|
||||
uint32_t depth = 0) {
|
||||
value = ResolveActiveU32(value, guard);
|
||||
if (depth > 32 || value.GetType() != Type::U32) {
|
||||
return {};
|
||||
}
|
||||
if (value.IsImmediate()) {
|
||||
return std::array<uint64_t, 3> {value.U32(), 0, 0};
|
||||
}
|
||||
const auto* inst = value.TryInstruction();
|
||||
if (inst == nullptr) {
|
||||
return {};
|
||||
}
|
||||
const auto op = inst->GetOpcode();
|
||||
if (op == ValueOpcode::GetBuiltin && inst->Arg(1) == Value(axis)) {
|
||||
if (inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId))) {
|
||||
return std::array<uint64_t, 3> {0, 1, 0};
|
||||
}
|
||||
if (inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::WorkgroupId))) {
|
||||
return std::array<uint64_t, 3> {0, 0, 1};
|
||||
}
|
||||
}
|
||||
if (op != ValueOpcode::IAdd32 && op != ValueOpcode::IMul32 &&
|
||||
op != ValueOpcode::ShiftLeftLogical32) {
|
||||
return {};
|
||||
}
|
||||
auto left = FillIndex(inst->Arg(0), axis, guard, depth + 1);
|
||||
auto right = FillIndex(inst->Arg(1), axis, guard, depth + 1);
|
||||
if (!left || !right) {
|
||||
return {};
|
||||
}
|
||||
if (op == ValueOpcode::IMul32 && ((*right)[1] != 0 || (*right)[2] != 0)) {
|
||||
std::swap(left, right);
|
||||
}
|
||||
if (op != ValueOpcode::IAdd32 && ((*right)[1] != 0 || (*right)[2] != 0)) {
|
||||
return {};
|
||||
}
|
||||
if (op == ValueOpcode::ShiftLeftLogical32) {
|
||||
if ((*right)[0] >= 32) return {};
|
||||
(*right)[0] = uint64_t {1} << (*right)[0];
|
||||
}
|
||||
for (uint32_t i = 0; i < left->size(); ++i) {
|
||||
(*left)[i] =
|
||||
op == ValueOpcode::IAdd32 ? (*left)[i] + (*right)[i] : (*left)[i] * (*right)[0];
|
||||
if ((*left)[i] > UINT32_MAX) return {};
|
||||
}
|
||||
return left;
|
||||
static std::optional<std::array<uint64_t, 3>> FillIndex(Value value, uint32_t axis, Value guard) {
|
||||
return AffineIndex<3>(value, [&](Value at) { return ResolveActiveU32(at, guard); },
|
||||
[&](Value at) -> std::optional<size_t> {
|
||||
const auto* inst = at.TryInstruction();
|
||||
if (inst == nullptr || inst->GetOpcode() != ValueOpcode::GetBuiltin ||
|
||||
inst->Arg(1) != Value(axis)) return {};
|
||||
if (inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId))) return 1u;
|
||||
if (inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::WorkgroupId))) return 2u;
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
static UniformFillPlan AnalyzeUniformFill(const Program& program) {
|
||||
|
|
@ -985,43 +1125,79 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
|
|||
for (const auto& read: program.srt_reads) {
|
||||
plan.srt_reads.push_back({Clone(read.value), read.flat_offset});
|
||||
}
|
||||
// A proven uniform factor can decide a branch even when its other lanes are unknown.
|
||||
// Keep only that Boolean structure, never the varying shader dependency graph.
|
||||
Value unknown;
|
||||
// Retain integer control expressions with opaque varying leaves. Their identities
|
||||
// matter when an EXEC factor also guards a selected value; never clone float work.
|
||||
bool capture_control_reads = false;
|
||||
bool uniform_control = false;
|
||||
const auto control_value_count = plan.value_storage.size();
|
||||
struct ControlValue {
|
||||
Value value;
|
||||
bool complete = false;
|
||||
};
|
||||
std::unordered_map<const Inst*, ControlValue> predicates;
|
||||
std::function<Value(Value)> ClonePredicate = [&](Value value) -> Value {
|
||||
value = value.Resolve();
|
||||
if (value.IsEmpty()) return {};
|
||||
const auto* inst = value.TryInstruction();
|
||||
if (inst == nullptr) {
|
||||
uniform_control |= value.GetType() == Type::U1;
|
||||
return ValidateRuntimeValue(program, value, RuntimeValueType::Integer) ? value : Value {};
|
||||
}
|
||||
const auto [found, inserted] = predicates.try_emplace(inst);
|
||||
auto& entry = found->second;
|
||||
auto& result = entry.value;
|
||||
const auto opaque = [&]() -> Value {
|
||||
if (value.GetType() != Type::U1 && value.GetType() != Type::U32) return {};
|
||||
return result = Value(&plan.value_storage.emplace_back(
|
||||
value.GetType() == Type::U1 ? ValueOpcode::UndefU1 : ValueOpcode::UndefU32));
|
||||
};
|
||||
// A recursive projection remains opaque, including unstructured loop Phis.
|
||||
if (!inserted) return entry.complete || !result.IsEmpty() ? result : opaque();
|
||||
const auto finish = [&](Value projected) {
|
||||
entry.complete = true;
|
||||
return result = projected;
|
||||
};
|
||||
if (ValidateRuntimeValue(program, value, RuntimeValueType::Integer)) {
|
||||
uniform_control = true;
|
||||
capture_control_reads |=
|
||||
!ValidateRuntimeValue(program, value, RuntimeValueType::ImmutableInteger);
|
||||
return Clone(value);
|
||||
return finish(Clone(value));
|
||||
}
|
||||
const auto* inst = value.TryInstruction();
|
||||
if (inst == nullptr) return {};
|
||||
const auto op = inst->GetOpcode();
|
||||
if (op == ValueOpcode::ConditionRef || op == ValueOpcode::LogicalNot) {
|
||||
const auto operand = ClonePredicate(inst->Arg(0));
|
||||
if (operand.IsEmpty() || op == ValueOpcode::ConditionRef) return operand;
|
||||
auto& node = plan.value_storage.emplace_back(op);
|
||||
node.SetArg(0, operand);
|
||||
return Value(&node);
|
||||
}
|
||||
if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr) return {};
|
||||
auto left = ClonePredicate(inst->Arg(0));
|
||||
auto right = ClonePredicate(inst->Arg(1));
|
||||
if (left.IsEmpty() && right.IsEmpty()) return {};
|
||||
if (left.IsEmpty() || right.IsEmpty()) {
|
||||
if (unknown.IsEmpty()) unknown = Value(&plan.value_storage.emplace_back(ValueOpcode::UndefU1));
|
||||
if (left.IsEmpty()) left = unknown;
|
||||
if (right.IsEmpty()) right = unknown;
|
||||
if (op == ValueOpcode::ConditionRef) return finish(ClonePredicate(inst->Arg(0)));
|
||||
if (op == ValueOpcode::Phi) {
|
||||
if (inst->Parent() == nullptr || inst->Parent()->terminator.loop_header ||
|
||||
inst->NumPhiBlocks() != inst->NumArgs() || inst->NumArgs() == 0u ||
|
||||
(value.GetType() != Type::U1 && value.GetType() != Type::U32)) return finish(opaque());
|
||||
std::vector<uint32_t> predecessors;
|
||||
for (size_t index = 0; index < inst->NumArgs(); ++index) {
|
||||
const auto predecessor = std::ranges::find(program.blocks, inst->PhiBlock(index));
|
||||
if (predecessor == program.blocks.end()) return finish(opaque());
|
||||
predecessors.push_back(static_cast<uint32_t>(predecessor - program.blocks.begin()));
|
||||
}
|
||||
auto& node = plan.value_storage.emplace_back(op, inst->Flags<uint64_t>());
|
||||
plan.control_phi_predecessors.emplace(&node, std::move(predecessors));
|
||||
for (size_t index = 0; index < inst->NumArgs(); ++index)
|
||||
node.AddPhiOperand(nullptr, ClonePredicate(inst->Arg(index)));
|
||||
return finish(Value(&node));
|
||||
}
|
||||
if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr &&
|
||||
op != ValueOpcode::LogicalNot && op != ValueOpcode::IEqual32 &&
|
||||
op != ValueOpcode::INotEqual32 && op != ValueOpcode::ULessThanEqual32 &&
|
||||
op != ValueOpcode::SelectU1 && op != ValueOpcode::SelectU32) return finish(opaque());
|
||||
auto& node = plan.value_storage.emplace_back(op);
|
||||
node.SetArg(0, left);
|
||||
node.SetArg(1, right);
|
||||
return Value(&node);
|
||||
for (size_t index = 0; index < inst->NumArgs(); ++index)
|
||||
node.SetArg(index, ClonePredicate(inst->Arg(index)));
|
||||
return finish(Value(&node));
|
||||
};
|
||||
plan.control_flow = ResourceControlFlow(program, ClonePredicate);
|
||||
if (!uniform_control) {
|
||||
plan.control_flow.clear();
|
||||
plan.control_phi_predecessors.clear();
|
||||
auto first = std::next(plan.value_storage.begin(), control_value_count);
|
||||
for (auto it = first; it != plan.value_storage.end(); ++it) it->Invalidate();
|
||||
plan.value_storage.erase(first, plan.value_storage.end());
|
||||
}
|
||||
plan.uniform_fill = AnalyzeUniformFill(program);
|
||||
for (uint32_t i = 0; i < plan.uniform_fill.fill.words; ++i) {
|
||||
plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]);
|
||||
|
|
@ -1041,8 +1217,11 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
|
|||
for (const auto& source: plan.descriptor_sources) {
|
||||
if (!source.indirect_descriptor.has_value()) continue;
|
||||
const auto& indirect = *source.indirect_descriptor;
|
||||
if (indirect.selector)
|
||||
if (indirect.selector) {
|
||||
MarkCleanReads(planned_reads, Source(plan, indirect.selector->source));
|
||||
if (indirect.selector->rows)
|
||||
MarkCleanReads(planned_reads, Source(plan, indirect.selector->rows->source));
|
||||
}
|
||||
MarkCleanReads(planned_reads, nullptr, indirect.selector_mask);
|
||||
MarkCleanReads(planned_reads, nullptr, indirect.selector_first);
|
||||
MarkCleanReads(planned_reads, nullptr, indirect.key_count);
|
||||
|
|
@ -1126,7 +1305,10 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
|||
if (!MaterializeIndirectDescriptor(
|
||||
program, *source->indirect_descriptor, i, 4u, observed, clean, snapshot,
|
||||
snapshot.buffers, specialization.buffers, ShaderInfo::MaxBuffers,
|
||||
NormalizeIndirectStoreBuffer))
|
||||
[&](DescriptorValue& value) {
|
||||
return NormalizeIndirectStoreBuffer(
|
||||
value, source->indirect_descriptor->indexed_store, base.formatted);
|
||||
}))
|
||||
return false;
|
||||
}
|
||||
} else if (!evaluate(base.source, snapshot.buffers[i], base.written)) {
|
||||
|
|
@ -1301,7 +1483,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
|||
samplers.push_back(samplers[binding.source]);
|
||||
}
|
||||
samplers[index].snapshot_index = binding.source;
|
||||
samplers[index].force_point_filtering = binding.type == SamplerClass::PointInteger;
|
||||
samplers[index].force_point_filtering = binding.type == SamplerClass::Integer;
|
||||
samplers[index].integer_border = binding.type != SamplerClass::Float;
|
||||
}
|
||||
for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) {
|
||||
|
|
@ -1329,6 +1511,16 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
|||
inst.SetFlags(image_remap[inst.Flags<uint32_t>()]);
|
||||
continue;
|
||||
}
|
||||
if (inst.GetOpcode() == ValueOpcode::StoreBufferU32) {
|
||||
auto& memory = memory_info[inst.Flags<MemoryFlags>().index];
|
||||
if (memory.kind == ResourceKind::Buffer && memory.formatted &&
|
||||
program.descriptor_sources[buffers[memory.resource].source].indirect_descriptor) {
|
||||
// Candidate validation proved an index-only k32UInt identity store.
|
||||
// Reuse finite raw-store dispatch and its original-index bounds check.
|
||||
memory.formatted = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (BufferAccessOf(inst.GetOpcode()) == BufferAccess::Read) {
|
||||
const auto& memory = memory_info[inst.Flags<MemoryFlags>().index];
|
||||
if (memory.kind == ResourceKind::Buffer &&
|
||||
|
|
@ -1406,6 +1598,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
|||
}
|
||||
EXIT_IF(image.indirect_root == memory.resource &&
|
||||
inst.GetOpcode() != ValueOpcode::ImageSampleRaw &&
|
||||
inst.GetOpcode() != ValueOpcode::ImageGatherRaw &&
|
||||
inst.GetOpcode() != ValueOpcode::ImageRead);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -6,7 +6,8 @@
|
|||
namespace Libs::Graphics::ShaderRecompiler::IR {
|
||||
|
||||
// Resolves native descriptor sources, plans their scalar reads, and assigns dense resource bindings.
|
||||
void TrackResources(Program& program, const Decoder::Program& decoded, const CFG::Graph& native_cfg);
|
||||
void TrackResources(Program& program, const Decoder::Program& decoded, const CFG::Graph& native_cfg,
|
||||
ShaderStageInputInfo input = {});
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ bool IsRuntimeUniformOp(ValueOpcode op) {
|
|||
case ValueOpcode::IMul64:
|
||||
case ValueOpcode::UMulHi:
|
||||
case ValueOpcode::UMin32:
|
||||
case ValueOpcode::UMax32:
|
||||
case ValueOpcode::ShiftLeftLogical32:
|
||||
case ValueOpcode::ShiftLeftLogical64:
|
||||
case ValueOpcode::ShiftRightLogical32:
|
||||
|
|
@ -177,8 +178,9 @@ bool IsRuntimeUniformOp(ValueOpcode op) {
|
|||
|
||||
class RuntimeValidator {
|
||||
public:
|
||||
explicit RuntimeValidator(const ResourcePlan& program, RuntimeValueType type)
|
||||
: m_program(program), m_type(type) {}
|
||||
explicit RuntimeValidator(const ResourcePlan& program, RuntimeValueType type,
|
||||
bool wave_uniform = false)
|
||||
: m_program(program), m_type(type), m_wave_uniform(wave_uniform) {}
|
||||
|
||||
bool Run(Value value) { return Validate(value); }
|
||||
|
||||
|
|
@ -277,6 +279,12 @@ private:
|
|||
}
|
||||
return finish(true);
|
||||
}
|
||||
if (m_wave_uniform && op == ValueOpcode::GetBuiltin) {
|
||||
const auto axis = inst->NumArgs() == 2u ? inst->Arg(1).Resolve() : Value {};
|
||||
return finish(m_program.stage == ShaderType::Compute && inst->NumArgs() == 2u &&
|
||||
inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::WorkgroupId)) &&
|
||||
axis.IsImmediate() && axis.GetType() == Type::U32 && axis.U32() < 3u);
|
||||
}
|
||||
if (op == ValueOpcode::Phi) {
|
||||
if (m_type != RuntimeValueType::Any && !ValidateArguments(*inst, false)) {
|
||||
return finish(false);
|
||||
|
|
@ -356,6 +364,7 @@ private:
|
|||
|
||||
const ResourcePlan& m_program;
|
||||
RuntimeValueType m_type;
|
||||
bool m_wave_uniform;
|
||||
Value m_active_mask;
|
||||
std::unordered_set<const Inst*> m_visiting;
|
||||
std::unordered_set<const Inst*> m_validated_dependencies;
|
||||
|
|
@ -490,12 +499,24 @@ void CompileSrtWalker(ResourcePlan& program) {
|
|||
}
|
||||
|
||||
SrtWalker::SrtWalker(const ResourcePlan& program, const SrtRuntime& runtime,
|
||||
SrtWalker* clean_evaluator, Value active_mask)
|
||||
SrtWalker* clean_evaluator)
|
||||
: m_program(program), m_runtime(runtime), m_clean_evaluator(clean_evaluator),
|
||||
m_active_mask(active_mask.Resolve()),
|
||||
m_context(AcquireContext(program)) {}
|
||||
m_session(this),
|
||||
m_read_policy(clean_evaluator != nullptr ? 2u : 0u), m_context(AcquireContext()) {}
|
||||
|
||||
SrtWalker::~SrtWalker() { --m_program.evaluation_depth; }
|
||||
SrtWalker::SrtWalker(SrtWalker& parent, Value active_mask, bool clean,
|
||||
SrtWalker* clean_evaluator)
|
||||
: m_program(parent.m_program), m_runtime(clean ? CleanRuntime(parent.m_runtime) : parent.m_runtime),
|
||||
m_clean_evaluator(clean_evaluator), m_active_mask(active_mask.Resolve()),
|
||||
m_reachable_blocks(parent.m_reachable_blocks), m_session(parent.m_session),
|
||||
m_read_policy((parent.m_read_policy & 1u) | (clean ? 1u : 0u) |
|
||||
(clean_evaluator != nullptr ? 2u : 0u)), m_context(AcquireContext()) {}
|
||||
|
||||
SrtWalker::~SrtWalker() {
|
||||
if (m_session != this) return;
|
||||
for (auto& context: m_program.evaluation_contexts)
|
||||
if (context.session == this) context.session = nullptr;
|
||||
}
|
||||
|
||||
bool SrtWalker::Evaluate(Value value, uint32_t& result) {
|
||||
uint64_t wide = 0;
|
||||
|
|
@ -506,15 +527,29 @@ bool SrtWalker::Evaluate(Value value, uint32_t& result) {
|
|||
return true;
|
||||
}
|
||||
|
||||
ResourcePlan::EvaluationContext& SrtWalker::AcquireContext(const ResourcePlan& program) {
|
||||
if (program.evaluation_depth == program.evaluation_contexts.size()) {
|
||||
program.evaluation_contexts.emplace_back();
|
||||
ResourcePlan::EvaluationContext& SrtWalker::AcquireContext() {
|
||||
ResourcePlan::EvaluationContext* unused = nullptr;
|
||||
for (auto& context: m_program.evaluation_contexts) {
|
||||
if (context.session == m_session && context.active_mask == m_active_mask &&
|
||||
context.read_policy == m_read_policy) return context;
|
||||
if (context.session == nullptr && unused == nullptr) unused = &context;
|
||||
}
|
||||
auto& context = program.evaluation_contexts[program.evaluation_depth++];
|
||||
context.generation += 2;
|
||||
auto& context = unused != nullptr ? *unused : m_program.evaluation_contexts.emplace_back();
|
||||
context.session = m_session;
|
||||
context.active_mask = m_active_mask;
|
||||
context.read_policy = m_read_policy;
|
||||
context.generation += 4;
|
||||
return context;
|
||||
}
|
||||
|
||||
void SrtWalker::ResetUnknowns() {
|
||||
for (auto& context: m_program.evaluation_contexts) {
|
||||
if (context.session != m_session) continue;
|
||||
for (auto& entry: context.values)
|
||||
if (entry.generation == (context.generation | 2u)) entry.generation = 0;
|
||||
}
|
||||
}
|
||||
|
||||
float SrtWalker::Float32(uint64_t bits) {
|
||||
return std::bit_cast<float>(static_cast<uint32_t>(bits));
|
||||
}
|
||||
|
|
@ -552,8 +587,10 @@ bool SrtWalker::EvaluateWide(Value value, uint64_t& result) {
|
|||
result = m_context.values[index].value;
|
||||
return true;
|
||||
}
|
||||
// The low generation bit marks an instruction that is still being evaluated.
|
||||
if (m_context.values[index].generation == (m_context.generation | 1u)) {
|
||||
// Visiting and completed-unknown entries both stop repeated evaluation in this
|
||||
// context. Only completed unknowns are retried when reachability becomes tighter.
|
||||
if (m_context.values[index].generation == (m_context.generation | 1u) ||
|
||||
m_context.values[index].generation == (m_context.generation | 2u)) {
|
||||
return false;
|
||||
}
|
||||
m_context.values[index].generation = m_context.generation | 1u;
|
||||
|
|
@ -562,7 +599,7 @@ bool SrtWalker::EvaluateWide(Value value, uint64_t& result) {
|
|||
// Recursive evaluation may grow the dense memo vector.
|
||||
auto& memo = m_context.values[index];
|
||||
if (!evaluated) {
|
||||
memo.generation = 0;
|
||||
memo.generation = m_context.generation | 2u;
|
||||
return false;
|
||||
}
|
||||
memo.value = out;
|
||||
|
|
@ -576,6 +613,19 @@ bool SrtWalker::Arg(const Inst& inst, size_t index, uint64_t& result) {
|
|||
}
|
||||
|
||||
bool SrtWalker::EvaluatePhi(const Inst& inst, uint64_t& result) {
|
||||
if (const auto control = m_program.control_phi_predecessors.find(&inst);
|
||||
control != m_program.control_phi_predecessors.end()) {
|
||||
bool found = false;
|
||||
for (size_t index = 0; index < inst.NumArgs(); ++index) {
|
||||
if (m_reachable_blocks != nullptr &&
|
||||
!m_reachable_blocks->at(control->second[index])) continue;
|
||||
uint64_t incoming = 0;
|
||||
if (!Arg(inst, index, incoming) || (found && incoming != result)) return false;
|
||||
result = incoming;
|
||||
found = true;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
const auto value = ResolveInvariantPhi(m_program, Value(const_cast<Inst*>(&inst)));
|
||||
return !value.IsEmpty() && EvaluateWide(value, result);
|
||||
}
|
||||
|
|
@ -673,7 +723,8 @@ bool SrtWalker::EvaluateRawRead(const Inst& inst, uint64_t& result) {
|
|||
? static_cast<uint64_t>(static_cast<uint32_t>(records))
|
||||
: static_cast<uint64_t>(stride) * static_cast<uint32_t>(records);
|
||||
if (byte_offset > size || size - byte_offset < sizeof(uint32_t)) {
|
||||
return false;
|
||||
result = 0u;
|
||||
return true;
|
||||
}
|
||||
address = (base & ~uint64_t {3}) + byte_offset;
|
||||
} else {
|
||||
|
|
@ -721,9 +772,8 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
|
|||
if (!m_active_mask.IsEmpty() && inst.Arg(1).Resolve() == m_active_mask) {
|
||||
return EvaluateWide(inst.Arg(0), result);
|
||||
}
|
||||
const auto clean_runtime = CleanRuntime(m_runtime);
|
||||
SrtWalker clean_active(m_program, clean_runtime, nullptr, inst.Arg(1));
|
||||
SrtWalker active(m_program, m_runtime, &clean_active, inst.Arg(1));
|
||||
SrtWalker clean_active(*this, inst.Arg(1), true);
|
||||
SrtWalker active(*this, inst.Arg(1), false, &clean_active);
|
||||
return active.EvaluateWide(inst.Arg(0), result);
|
||||
}
|
||||
case ValueOpcode::BitCastU32F32:
|
||||
|
|
@ -793,8 +843,11 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
|
|||
}
|
||||
return false;
|
||||
case ValueOpcode::UMin32:
|
||||
case ValueOpcode::UMax32:
|
||||
if (binary()) {
|
||||
result = std::min(static_cast<uint32_t>(a), static_cast<uint32_t>(b));
|
||||
result = inst.GetOpcode() == ValueOpcode::UMin32
|
||||
? std::min(static_cast<uint32_t>(a), static_cast<uint32_t>(b))
|
||||
: std::max(static_cast<uint32_t>(a), static_cast<uint32_t>(b));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -1007,7 +1060,13 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
|
|||
}
|
||||
return false;
|
||||
case ValueOpcode::ULessThanEqual32:
|
||||
if (binary()) {
|
||||
// A zero lower bound accepts every U32, including a varying lane value.
|
||||
if (!Arg(inst, 0, a)) return false;
|
||||
if (a == 0u) {
|
||||
result = 1u;
|
||||
return true;
|
||||
}
|
||||
if (Arg(inst, 1, b)) {
|
||||
result = static_cast<uint32_t>(a) <= static_cast<uint32_t>(b);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1031,9 +1090,21 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
|
|||
result = 0u;
|
||||
return true;
|
||||
}
|
||||
if (!Arg(inst, 1, b) || (b != 0u && !left)) return false;
|
||||
result = b != 0u;
|
||||
return true;
|
||||
if (Arg(inst, 1, b) && (b == 0u || left)) {
|
||||
result = b != 0u;
|
||||
return true;
|
||||
}
|
||||
if (!left) {
|
||||
// Only lanes satisfying the left EXEC factor can make the conjunction
|
||||
// true. Use a separate memo so this assumption cannot escape its RHS.
|
||||
SrtWalker active(m_clean_evaluator != nullptr ? *m_clean_evaluator : *this,
|
||||
inst.Arg(0));
|
||||
if (active.EvaluateWide(inst.Arg(1), b) && b == 0u) {
|
||||
result = 0u;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case ValueOpcode::LogicalOr: {
|
||||
const bool left = Arg(inst, 0, a);
|
||||
|
|
@ -1112,35 +1183,54 @@ bool SrtWalker::RefreshFlatBuffer(std::vector<uint32_t>& flat) {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
auto& visited = m_program.visited_blocks;
|
||||
auto& reachable = m_program.reachable_blocks;
|
||||
auto& pending = m_program.pending_blocks;
|
||||
visited.assign(m_program.control_flow.size(), 1u);
|
||||
ResetUnknowns();
|
||||
m_reachable_blocks = &visited;
|
||||
if (m_clean_evaluator != nullptr) {
|
||||
m_clean_evaluator->ResetUnknowns();
|
||||
m_clean_evaluator->m_reachable_blocks = &visited;
|
||||
}
|
||||
for (;;) {
|
||||
reachable.assign(m_program.control_flow.size(), 0u);
|
||||
pending.clear();
|
||||
pending.push_back(0u);
|
||||
while (!pending.empty()) {
|
||||
const auto index = pending.back();
|
||||
pending.pop_back();
|
||||
if (reachable.at(index)) continue;
|
||||
reachable[index] = 1u;
|
||||
const auto& block = m_program.control_flow[index];
|
||||
uint32_t condition = 0;
|
||||
auto& predicate = m_clean_evaluator != nullptr ? *m_clean_evaluator : *this;
|
||||
if (!block.condition.IsEmpty() &&
|
||||
(m_runtime.read_specialization_memory != nullptr || !m_program.capture_specialization_reads) &&
|
||||
predicate.Evaluate(block.condition, condition)) {
|
||||
pending.push_back(block.successors[condition != 0u ? 0u : 1u]);
|
||||
} else {
|
||||
pending.insert(pending.end(), block.successors.begin(), block.successors.end());
|
||||
}
|
||||
}
|
||||
if (reachable == visited) break;
|
||||
visited.swap(reachable);
|
||||
if (m_program.control_phi_predecessors.empty()) break;
|
||||
// Only unknown values need another evaluation: successful Phi facts remain
|
||||
// true as their possible predecessors shrink. Keep uniform-read memos stable.
|
||||
ResetUnknowns();
|
||||
if (m_clean_evaluator != nullptr) m_clean_evaluator->ResetUnknowns();
|
||||
}
|
||||
flat.assign(m_program.srt_reads.size(), 0u);
|
||||
active.assign(m_program.descriptor_sources.size(), 1u);
|
||||
for (const auto& block: m_program.control_flow) {
|
||||
for (const auto& block: m_program.control_flow)
|
||||
for (const auto source: block.sources) active.at(source) = 0u;
|
||||
}
|
||||
auto& visited = m_program.visited_blocks;
|
||||
auto& pending = m_program.pending_blocks;
|
||||
visited.assign(m_program.control_flow.size(), 0u);
|
||||
pending.clear();
|
||||
pending.push_back(0u);
|
||||
while (!pending.empty()) {
|
||||
const auto index = pending.back();
|
||||
pending.pop_back();
|
||||
if (visited.at(index)) continue;
|
||||
visited[index] = 1u;
|
||||
for (uint32_t index = 0; index < visited.size(); ++index) {
|
||||
if (!visited[index]) continue;
|
||||
const auto& block = m_program.control_flow[index];
|
||||
for (const auto source: block.sources) active[source] = 1u;
|
||||
for (const auto slot: block.srt_reads) {
|
||||
for (const auto slot: block.srt_reads)
|
||||
if (!refresh(slot)) return false;
|
||||
}
|
||||
uint32_t condition = 0;
|
||||
auto& predicate = m_clean_evaluator != nullptr ? *m_clean_evaluator : *this;
|
||||
if (!block.condition.IsEmpty() &&
|
||||
(m_runtime.read_specialization_memory != nullptr || !m_program.capture_specialization_reads) &&
|
||||
predicate.Evaluate(block.condition, condition)) {
|
||||
pending.push_back(block.successors[condition != 0u ? 0u : 1u]);
|
||||
} else {
|
||||
pending.insert(pending.end(), block.successors.begin(), block.successors.end());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1149,5 +1239,9 @@ bool ValidateRuntimeValue(const ResourcePlan& program, Value value, RuntimeValue
|
|||
return RuntimeValidator(program, type).Run(value);
|
||||
}
|
||||
|
||||
bool ValidateWaveUniformValue(const ResourcePlan& program, Value value) {
|
||||
return RuntimeValidator(program, RuntimeValueType::Any, true).Run(value);
|
||||
}
|
||||
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
#include "graphics/shader/recompiler/ir/ShaderIR.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <span>
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::IR {
|
||||
|
|
@ -45,15 +46,60 @@ enum class RuntimeValueType { Any, Integer, ImmutableInteger };
|
|||
|
||||
bool ValidateRuntimeValue(const ResourcePlan& program, Value value,
|
||||
RuntimeValueType type = RuntimeValueType::Any);
|
||||
// Uniform across a compute wave, including values derived from its workgroup ID.
|
||||
// This does not imply that the host can evaluate the value.
|
||||
bool ValidateWaveUniformValue(const ResourcePlan& program, Value value);
|
||||
void CompileSrtWalker(ResourcePlan& program);
|
||||
// Uses the strict reader for values that affect shader specialization.
|
||||
SrtRuntime CleanRuntime(SrtRuntime runtime);
|
||||
|
||||
// Nonnegative affine coefficients: element zero is constant, remaining elements are
|
||||
// caller-selected leaves. Runtime range proofs must additionally exclude U32 wrap.
|
||||
template <size_t Size>
|
||||
std::optional<std::array<uint64_t, Size>> AffineIndex(
|
||||
Value value, const auto& resolve, const auto& leaf, uint32_t depth = 0) {
|
||||
value = resolve(value);
|
||||
if (depth > 32u || value.GetType() != Type::U32) return {};
|
||||
std::array<uint64_t, Size> result {};
|
||||
if (value.IsImmediate()) {
|
||||
result[0] = value.U32();
|
||||
return result;
|
||||
}
|
||||
if (const auto index = leaf(value)) {
|
||||
result[*index] = 1u;
|
||||
return result;
|
||||
}
|
||||
const auto* inst = value.TryInstruction();
|
||||
if (inst == nullptr) return {};
|
||||
const auto op = inst->GetOpcode();
|
||||
if (op != ValueOpcode::IAdd32 && op != ValueOpcode::IMul32 &&
|
||||
op != ValueOpcode::ShiftLeftLogical32) return {};
|
||||
auto left = AffineIndex<Size>(inst->Arg(0), resolve, leaf, depth + 1u);
|
||||
auto right = AffineIndex<Size>(inst->Arg(1), resolve, leaf, depth + 1u);
|
||||
if (!left || !right) return {};
|
||||
const auto constant = [](const auto& coefficients) {
|
||||
return std::ranges::all_of(std::span(coefficients).subspan(1u),
|
||||
[](uint64_t coefficient) { return coefficient == 0u; });
|
||||
};
|
||||
if (op == ValueOpcode::IMul32 && !constant(*right)) std::swap(left, right);
|
||||
if (op != ValueOpcode::IAdd32 && !constant(*right)) return {};
|
||||
if (op == ValueOpcode::ShiftLeftLogical32) {
|
||||
if ((*right)[0] >= 32u) return {};
|
||||
(*right)[0] = uint64_t {1} << (*right)[0];
|
||||
}
|
||||
for (size_t index = 0; index < Size; ++index) {
|
||||
(*left)[index] = op == ValueOpcode::IAdd32
|
||||
? (*left)[index] + (*right)[index] : (*left)[index] * (*right)[0];
|
||||
if ((*left)[index] > UINT32_MAX) return {};
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
// One memoized evaluation session shared by the entire shader resource refresh.
|
||||
class SrtWalker {
|
||||
public:
|
||||
SrtWalker(const ResourcePlan& program, const SrtRuntime& runtime,
|
||||
SrtWalker* clean_evaluator = nullptr, Value active_mask = {});
|
||||
SrtWalker* clean_evaluator = nullptr);
|
||||
~SrtWalker();
|
||||
SrtWalker(const SrtWalker&) = delete;
|
||||
SrtWalker& operator=(const SrtWalker&) = delete;
|
||||
|
|
@ -64,7 +110,10 @@ public:
|
|||
bool RefreshFlatBuffer(std::vector<uint32_t>& flat);
|
||||
|
||||
private:
|
||||
static ResourcePlan::EvaluationContext& AcquireContext(const ResourcePlan& program);
|
||||
SrtWalker(SrtWalker& parent, Value active_mask, bool clean = false,
|
||||
SrtWalker* clean_evaluator = nullptr);
|
||||
ResourcePlan::EvaluationContext& AcquireContext();
|
||||
void ResetUnknowns();
|
||||
static float Float32(uint64_t bits);
|
||||
bool EvaluateWide(Value value, uint64_t& result);
|
||||
bool Arg(const Inst& inst, size_t index, uint64_t& result);
|
||||
|
|
@ -77,6 +126,9 @@ private:
|
|||
SrtRuntime m_runtime;
|
||||
SrtWalker* m_clean_evaluator = nullptr;
|
||||
Value m_active_mask;
|
||||
const std::vector<uint8_t>* m_reachable_blocks = nullptr;
|
||||
const SrtWalker* m_session;
|
||||
uint8_t m_read_policy;
|
||||
ResourcePlan::EvaluationContext& m_context;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@ constexpr uint64_t DEFAULT_FLEXIBLE_MEMORY_SIZE = 1ull * 1024ull * 1024ull * 102
|
|||
static uint64_t g_flexible_memory_size = DEFAULT_FLEXIBLE_MEMORY_SIZE;
|
||||
static bool g_flexible_memory_size_frozen = false;
|
||||
static Graphics::RenderContext* g_gpu_resources = nullptr;
|
||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||
static void (*g_test_gpu_range)(bool, uint64_t, size_t) = nullptr;
|
||||
#endif
|
||||
|
||||
static Graphics::RenderContext& GetGpuResources() {
|
||||
EXIT_IF(g_gpu_resources == nullptr);
|
||||
|
|
@ -83,6 +86,9 @@ static bool IsGpuAddressRange(uint64_t vaddr, uint64_t size) {
|
|||
}
|
||||
|
||||
static void MapGpuRange(uint64_t vaddr, uint64_t size) {
|
||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||
if (g_test_gpu_range != nullptr) g_test_gpu_range(true, vaddr, size);
|
||||
#endif
|
||||
if (g_gpu_resources == nullptr || !IsGpuAddressRange(vaddr, size)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -90,6 +96,9 @@ static void MapGpuRange(uint64_t vaddr, uint64_t size) {
|
|||
}
|
||||
|
||||
static void UnmapGpuRange(uint64_t vaddr, uint64_t size) {
|
||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||
if (g_test_gpu_range != nullptr) g_test_gpu_range(false, vaddr, size);
|
||||
#endif
|
||||
if (g_gpu_resources == nullptr || !IsGpuAddressRange(vaddr, size)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -962,7 +971,11 @@ static bool IsInPrtAperture(uint64_t address, uint64_t size = 1) {
|
|||
}
|
||||
|
||||
bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size) {
|
||||
return g_guest_address_space->TryReadSparseBacking(vaddr, data, size);
|
||||
return g_guest_address_space->CopySparseMemory<false>(vaddr, data, size);
|
||||
}
|
||||
|
||||
bool CopySparseMemory(uint64_t vaddr, void* data, uint64_t size) {
|
||||
return g_guest_address_space->CopySparseMemory<true>(vaddr, data, size);
|
||||
}
|
||||
|
||||
static bool SelfTestSub64SharedPlaceholderAlias() {
|
||||
|
|
@ -2524,20 +2537,7 @@ static bool FreeGuestMemoryOwner(uint64_t vaddr, uint64_t size) {
|
|||
g_virtual_ranges->Remove(vaddr, size);
|
||||
}
|
||||
|
||||
static int UnmapMemoryRange(uint64_t vaddr, size_t len) {
|
||||
if (len == 0 || UINT64_MAX - vaddr < len) {
|
||||
return KERNEL_ERROR_EINVAL;
|
||||
}
|
||||
|
||||
VirtualRanges::Range range {};
|
||||
if (!g_virtual_ranges->Query(vaddr, 0, &range)) {
|
||||
return KERNEL_ERROR_EACCES;
|
||||
}
|
||||
const auto chunk_len = std::min<uint64_t>(len, range.size - (vaddr - range.start));
|
||||
if (chunk_len < len) {
|
||||
const int ret = UnmapMemoryRange(vaddr, chunk_len);
|
||||
return ret == OK ? UnmapMemoryRange(vaddr + chunk_len, len - chunk_len) : ret;
|
||||
}
|
||||
static int UnmapMemoryOwner(const VirtualRanges::Range& range, uint64_t vaddr, size_t len) {
|
||||
if (IsReservedRangeType(range.type)) {
|
||||
if (!g_guest_address_space->ReleaseFree(vaddr, len)) {
|
||||
return KERNEL_ERROR_EACCES;
|
||||
|
|
@ -2601,6 +2601,13 @@ static int UnmapMemoryRange(uint64_t vaddr, size_t len) {
|
|||
return OK;
|
||||
}
|
||||
|
||||
static int UnmapMemoryRange(const VirtualRanges::Range& range, uint64_t vaddr, size_t len) {
|
||||
UnmapGpuRange(vaddr, len);
|
||||
const int result = UnmapMemoryOwner(range, vaddr, len);
|
||||
if (result != OK && IsCommittedRangeType(range.type)) MapGpuRange(vaddr, len);
|
||||
return result;
|
||||
}
|
||||
|
||||
// The ABI entry point and batch operations hold g_memory_operation_mutex.
|
||||
static int UnmapMemory(uint64_t vaddr, size_t len) {
|
||||
LOGF("\t start = 0x%016" PRIx64 "\n"
|
||||
|
|
@ -2614,8 +2621,11 @@ static int UnmapMemory(uint64_t vaddr, size_t len) {
|
|||
if (!g_virtual_ranges->QuerySpan(vaddr, len, &ranges)) {
|
||||
return KERNEL_ERROR_EACCES;
|
||||
}
|
||||
UnmapGpuRange(vaddr, len);
|
||||
return UnmapMemoryRange(vaddr, len);
|
||||
for (const auto& range: ranges) {
|
||||
const int result = UnmapMemoryRange(range, range.start, range.size);
|
||||
if (result != OK) return result;
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI KernelMunmap(uint64_t vaddr, size_t len) {
|
||||
|
|
@ -2624,6 +2634,31 @@ int KYTY_SYSV_ABI KernelMunmap(uint64_t vaddr, size_t len) {
|
|||
return UnmapMemory(vaddr, len);
|
||||
}
|
||||
|
||||
static bool IsAmmMemoryRange(uint64_t vaddr, size_t size) {
|
||||
constexpr uint64_t page_size = 0x4000;
|
||||
return size != 0 && ((vaddr | size) & (page_size - 1)) == 0 &&
|
||||
vaddr >= kExtendedMemoryBase && vaddr < kExtendedMemoryBase + kExtendedMemorySize &&
|
||||
size <= kExtendedMemoryBase + kExtendedMemorySize - vaddr;
|
||||
}
|
||||
|
||||
int UnmapAmmMemory(uint64_t vaddr, size_t size) {
|
||||
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
|
||||
if (!IsAmmMemoryRange(vaddr, size)) return KERNEL_ERROR_EINVAL;
|
||||
|
||||
const auto end = vaddr + size;
|
||||
while (vaddr < end) {
|
||||
VirtualRanges::Range range {};
|
||||
// AMM unmap accepts holes, including a completely unmapped range.
|
||||
if (!g_virtual_ranges->Query(vaddr, 1, &range) || range.start >= end) break;
|
||||
vaddr = std::max(vaddr, range.start);
|
||||
const auto chunk = std::min(end, range.start + range.size) - vaddr;
|
||||
const int result = UnmapMemoryRange(range, vaddr, chunk);
|
||||
if (result != OK) return result;
|
||||
vaddr += chunk;
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
|
||||
size_t KYTY_SYSV_ABI KernelGetDirectMemorySize() {
|
||||
PRINT_NAME();
|
||||
|
||||
|
|
@ -3225,11 +3260,7 @@ int KYTY_SYSV_ABI KernelMapDirectMemory2(void** addr, size_t len, int type, int
|
|||
int MapAutomaticMemory(uint64_t vaddr, size_t size, int type, int prot) {
|
||||
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
|
||||
constexpr uint64_t page_size = 0x4000;
|
||||
if (size == 0 || (vaddr & (page_size - 1)) != 0 || (size & (page_size - 1)) != 0 ||
|
||||
vaddr < kExtendedMemoryBase || vaddr >= kExtendedMemoryBase + kExtendedMemorySize ||
|
||||
size > kExtendedMemoryBase + kExtendedMemorySize - vaddr) {
|
||||
return KERNEL_ERROR_EINVAL;
|
||||
}
|
||||
if (!IsAmmMemoryRange(vaddr, size)) return KERNEL_ERROR_EINVAL;
|
||||
|
||||
PhysicalMemory::PhysicalRanges ranges;
|
||||
if (!g_physical_memory->ReserveAutomatic(size, &ranges)) {
|
||||
|
|
@ -3567,6 +3598,10 @@ int KYTY_SYSV_ABI KernelReserveVirtualRange(void** addr, size_t len, int flags,
|
|||
}
|
||||
|
||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||
void TestSetGpuRangeCallback(void (*callback)(bool, uint64_t, size_t)) {
|
||||
g_test_gpu_range = callback;
|
||||
}
|
||||
|
||||
void TestBeforeNextBackingMap(callback_func_t callback) {
|
||||
g_test_before_backing_map = callback;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,10 +112,12 @@ void SetFlexibleMemorySize(uint64_t size);
|
|||
int AllocateDirectMemory(int64_t search_start, int64_t search_end, size_t size, size_t alignment,
|
||||
int memory_type, int64_t* phys_addr_out, bool automatic = false);
|
||||
int MapAutomaticMemory(uint64_t vaddr, size_t size, int type, int prot);
|
||||
int UnmapAmmMemory(uint64_t vaddr, size_t size);
|
||||
bool TryWriteBacking(uint64_t vaddr, const void* data, uint64_t size);
|
||||
bool TryReadBacking(uint64_t vaddr, void* data, uint64_t size);
|
||||
bool TryReadBufferBacking(uint64_t vaddr, void* data, uint64_t size);
|
||||
bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size);
|
||||
bool CopySparseMemory(uint64_t vaddr, void* data, uint64_t size);
|
||||
[[nodiscard]] uint64_t ClampRangeSize(uint64_t vaddr, uint64_t size);
|
||||
void WriteBacking(uint64_t vaddr, const void* data, uint64_t size) noexcept;
|
||||
void InvalidateMemory(uint64_t vaddr, uint64_t size);
|
||||
|
|
@ -191,6 +193,7 @@ bool ProtectGuestHostMemory(uint64_t vaddr, uint64_t size, Common::VirtualMemory
|
|||
bool FreeGuestMemory(uint64_t vaddr, uint64_t size);
|
||||
|
||||
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
|
||||
void TestSetGpuRangeCallback(void (*callback)(bool mapped, uint64_t addr, size_t size));
|
||||
void TestBeforeNextBackingMap(callback_func_t callback);
|
||||
void TestSetBackingReadCallback(callback_func_t callback);
|
||||
void TestFailNextPhysicalMemoryUnmap();
|
||||
|
|
|
|||
|
|
@ -28,6 +28,35 @@ static bool CoalescePlaceholders(uint64_t vaddr, uint64_t size) {
|
|||
}
|
||||
#endif
|
||||
|
||||
template <typename Regions, typename Source, typename Gap>
|
||||
static void CopySparseRange(const Regions& regions, uint64_t vaddr, void* data, uint64_t size,
|
||||
Source&& source, Gap&& gap) {
|
||||
const auto end = vaddr + size;
|
||||
auto mapping = regions.lower_bound(vaddr);
|
||||
if (mapping != regions.begin()) {
|
||||
auto previous = std::prev(mapping);
|
||||
if (previous->second.vaddr + previous->second.size > vaddr) {
|
||||
mapping = previous;
|
||||
}
|
||||
}
|
||||
|
||||
auto* destination = static_cast<uint8_t*>(data);
|
||||
auto cursor = vaddr;
|
||||
for (; mapping != regions.end() && mapping->second.vaddr < end; ++mapping) {
|
||||
const auto& entry = mapping->second;
|
||||
const auto start = std::max(vaddr, entry.vaddr);
|
||||
const auto bytes = std::min(end, entry.vaddr + entry.size) - start;
|
||||
if (cursor < start) {
|
||||
gap(cursor, destination + (cursor - vaddr), start - cursor);
|
||||
}
|
||||
std::memcpy(destination + (start - vaddr), source(entry, start), bytes);
|
||||
cursor = start + bytes;
|
||||
}
|
||||
if (cursor < end) {
|
||||
gap(cursor, destination + (cursor - vaddr), end - cursor);
|
||||
}
|
||||
}
|
||||
|
||||
class GuestBackingStore {
|
||||
public:
|
||||
enum class FailureReason {
|
||||
|
|
@ -247,7 +276,8 @@ public:
|
|||
return true;
|
||||
}
|
||||
|
||||
bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size) {
|
||||
template <typename Gap>
|
||||
bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size, Gap&& gap) {
|
||||
if (!IsAvailable() || data == nullptr || size == 0 || UINT64_MAX - vaddr < size) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -258,31 +288,11 @@ public:
|
|||
g_test_backing_read(vaddr, size);
|
||||
}
|
||||
#endif
|
||||
const auto end = vaddr + size;
|
||||
auto mapping = m_maps.lower_bound(vaddr);
|
||||
if (mapping != m_maps.begin()) {
|
||||
auto previous = std::prev(mapping);
|
||||
if (previous->second.vaddr + previous->second.size > vaddr) {
|
||||
mapping = previous;
|
||||
}
|
||||
}
|
||||
|
||||
auto* destination = static_cast<uint8_t*>(data);
|
||||
auto cursor = vaddr;
|
||||
for (; mapping != m_maps.end() && mapping->second.vaddr < end; ++mapping) {
|
||||
const auto& entry = mapping->second;
|
||||
const auto start = std::max(vaddr, entry.vaddr);
|
||||
const auto bytes = std::min(end, entry.vaddr + entry.size) - start;
|
||||
const auto offset = entry.backing_offset + start - entry.vaddr;
|
||||
if (cursor < start) {
|
||||
std::memset(destination + (cursor - vaddr), 0, start - cursor);
|
||||
}
|
||||
std::memcpy(destination + (start - vaddr), m_backing_base + offset, bytes);
|
||||
cursor = start + bytes;
|
||||
}
|
||||
if (cursor < end) {
|
||||
std::memset(destination + (cursor - vaddr), 0, end - cursor);
|
||||
}
|
||||
CopySparseRange(m_maps, vaddr, data, size,
|
||||
[this](const auto& entry, uint64_t start) {
|
||||
return m_backing_base + (entry.backing_offset + start - entry.vaddr);
|
||||
},
|
||||
gap);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -773,22 +783,41 @@ public:
|
|||
return m_backing->TryReadBacking(vaddr, data, size);
|
||||
}
|
||||
|
||||
bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size) {
|
||||
if (!Owns(vaddr, size)) {
|
||||
template <bool copy_private>
|
||||
bool CopySparseMemory(uint64_t vaddr, void* data, uint64_t size) {
|
||||
if (data == nullptr || !Owns(vaddr, size)) {
|
||||
return false;
|
||||
}
|
||||
std::shared_lock lock(m_mutex);
|
||||
auto mapping = m_mapped.upper_bound(vaddr);
|
||||
if (mapping != m_mapped.begin()) {
|
||||
--mapping;
|
||||
}
|
||||
for (; mapping != m_mapped.end() && mapping->second.vaddr < vaddr + size; ++mapping) {
|
||||
const auto& region = mapping->second;
|
||||
if (region.type == MappingType::Private && region.vaddr + region.size > vaddr) {
|
||||
return false;
|
||||
if constexpr (!copy_private) {
|
||||
auto mapping = m_mapped.upper_bound(vaddr);
|
||||
if (mapping != m_mapped.begin()) {
|
||||
--mapping;
|
||||
}
|
||||
for (; mapping != m_mapped.end() && mapping->second.vaddr < vaddr + size; ++mapping) {
|
||||
const auto& region = mapping->second;
|
||||
if (region.type == MappingType::Private && region.vaddr + region.size > vaddr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return m_backing->TryReadSparseBacking(vaddr, data, size);
|
||||
return m_backing->TryReadSparseBacking(
|
||||
vaddr, data, size, [&](uint64_t start, uint8_t* destination, uint64_t bytes) {
|
||||
const auto zero = [](uint64_t, uint8_t* output, uint64_t length) {
|
||||
std::memset(output, 0, length);
|
||||
};
|
||||
if constexpr (copy_private) {
|
||||
// Backing gaps can contain private mappings or reservations only.
|
||||
CopySparseRange(m_mapped, start, destination, bytes,
|
||||
[](const auto& region, uint64_t address) {
|
||||
EXIT_IF(region.type != MappingType::Private);
|
||||
return reinterpret_cast<const void*>(address);
|
||||
},
|
||||
zero);
|
||||
} else {
|
||||
zero(start, destination, bytes);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool BackingContains(uint64_t vaddr, uint64_t size) { return m_backing->Contains(vaddr, size); }
|
||||
|
|
|
|||
|
|
@ -180,6 +180,12 @@
|
|||
<property name="text"><string>Accurate texture synchronization</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="QCheckBox" name="checkBox_hdr">
|
||||
<property name="toolTip"><string>Enable HDR10 output for supported games. Requires HDR enabled on the host display.</string></property>
|
||||
<property name="text"><string>Enable HDR display support</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
|
@ -323,6 +329,23 @@
|
|||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="network_tab">
|
||||
<attribute name="title"><string>Network</string></attribute>
|
||||
<layout class="QVBoxLayout" name="networkTabLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_network_connected">
|
||||
<property name="toolTip"><string>Let games use your computer's local network connection.</string></property>
|
||||
<property name="text"><string>Enable network connection</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="networkSpacer">
|
||||
<property name="orientation"><enum>Qt::Vertical</enum></property>
|
||||
<property name="sizeHint" stdset="0"><size><width>20</width><height>40</height></size></property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="debug_tab">
|
||||
<attribute name="title"><string>Debug</string></attribute>
|
||||
<layout class="QVBoxLayout" name="debugTabLayout">
|
||||
|
|
|
|||
|
|
@ -117,12 +117,14 @@ public:
|
|||
PresentMode present_mode = PresentMode::Mailbox;
|
||||
int gpu_index = -1;
|
||||
bool fullscreen_enabled = false;
|
||||
bool hdr_enabled = false;
|
||||
bool hide_cursor_enabled = false;
|
||||
bool readback_linear_images = false;
|
||||
bool sync_raw_image_buffers = false;
|
||||
bool tessellation_enabled = false;
|
||||
bool trophy_enabled = true;
|
||||
bool skip_notice_screen = false;
|
||||
bool network_connected = false;
|
||||
int vblank_frequency = 60;
|
||||
int console_language = DEFAULT_CONSOLE_LANGUAGE;
|
||||
bool vulkan_validation_enabled = false;
|
||||
|
|
@ -152,12 +154,14 @@ public:
|
|||
present_mode = other.present_mode;
|
||||
gpu_index = other.gpu_index;
|
||||
fullscreen_enabled = other.fullscreen_enabled;
|
||||
hdr_enabled = other.hdr_enabled;
|
||||
hide_cursor_enabled = other.hide_cursor_enabled;
|
||||
readback_linear_images = other.readback_linear_images;
|
||||
sync_raw_image_buffers = other.sync_raw_image_buffers;
|
||||
tessellation_enabled = other.tessellation_enabled;
|
||||
trophy_enabled = other.trophy_enabled;
|
||||
skip_notice_screen = other.skip_notice_screen;
|
||||
network_connected = other.network_connected;
|
||||
vblank_frequency = other.vblank_frequency;
|
||||
console_language = other.console_language;
|
||||
vulkan_validation_enabled = other.vulkan_validation_enabled;
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@ QVariantMap Configuration::GameSettings() const {
|
|||
KYTY_CFG_SET(present_mode);
|
||||
KYTY_CFG_SET(gpu_index);
|
||||
KYTY_CFG_SET(fullscreen_enabled);
|
||||
KYTY_CFG_SET(hdr_enabled);
|
||||
KYTY_CFG_SET(hide_cursor_enabled);
|
||||
KYTY_CFG_SET(readback_linear_images);
|
||||
KYTY_CFG_SET(sync_raw_image_buffers);
|
||||
KYTY_CFG_SET(tessellation_enabled);
|
||||
KYTY_CFG_SET(trophy_enabled);
|
||||
KYTY_CFG_SET(skip_notice_screen);
|
||||
KYTY_CFG_SET(network_connected);
|
||||
KYTY_CFG_SET(vblank_frequency);
|
||||
KYTY_CFG_SET(console_language);
|
||||
KYTY_CFG_SET(vulkan_validation_enabled);
|
||||
|
|
@ -55,12 +57,14 @@ void Configuration::ReadGameSettingsValues(const Settings& s) {
|
|||
present_mode = PresentMode::Mailbox;
|
||||
}
|
||||
KYTY_CFG_GET(fullscreen_enabled);
|
||||
KYTY_CFG_GET(hdr_enabled);
|
||||
KYTY_CFG_GET(hide_cursor_enabled);
|
||||
KYTY_CFG_GET(readback_linear_images);
|
||||
KYTY_CFG_GET(sync_raw_image_buffers);
|
||||
KYTY_CFG_GET(tessellation_enabled);
|
||||
trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool();
|
||||
KYTY_CFG_GET(skip_notice_screen);
|
||||
KYTY_CFG_GET(network_connected);
|
||||
vblank_frequency = s.value("vblank_frequency", vblank_frequency).toInt();
|
||||
console_language = s.value("console_language", console_language).toInt();
|
||||
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
|
||||
|
|
|
|||
|
|
@ -273,11 +273,13 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
|
|||
info.gpu_index >= 0 && info.gpu_index < m_ui->comboBox_gpu->count() - 1 ? info.gpu_index + 1
|
||||
: 0);
|
||||
m_ui->checkBox_fullscreen->setChecked(info.fullscreen_enabled);
|
||||
m_ui->checkBox_hdr->setChecked(info.hdr_enabled);
|
||||
m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled);
|
||||
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
|
||||
m_ui->checkBox_sync_raw_image_buffers->setChecked(info.sync_raw_image_buffers);
|
||||
m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled);
|
||||
m_ui->checkBox_skip_notice_screen->setChecked(info.skip_notice_screen);
|
||||
m_ui->checkBox_network_connected->setChecked(info.network_connected);
|
||||
m_ui->checkBox_trophy_notifications->setChecked(info.trophy_enabled);
|
||||
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
|
||||
m_ui->comboBox_console_language->clear();
|
||||
|
|
@ -434,11 +436,13 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
|
|||
TextToEnum<Configuration::PresentMode>(ui.comboBox_present_mode->currentText());
|
||||
info.gpu_index = ui.comboBox_gpu->currentIndex() - 1;
|
||||
info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked();
|
||||
info.hdr_enabled = ui.checkBox_hdr->isChecked();
|
||||
info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked();
|
||||
info.readback_linear_images = ui.checkBox_readback->isChecked();
|
||||
info.sync_raw_image_buffers = ui.checkBox_sync_raw_image_buffers->isChecked();
|
||||
info.tessellation_enabled = ui.checkBox_tessellation->isChecked();
|
||||
info.skip_notice_screen = ui.checkBox_skip_notice_screen->isChecked();
|
||||
info.network_connected = ui.checkBox_network_connected->isChecked();
|
||||
info.trophy_enabled = ui.checkBox_trophy_notifications->isChecked();
|
||||
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
|
||||
info.console_language = ui.comboBox_console_language->currentIndex();
|
||||
|
|
|
|||
|
|
@ -245,6 +245,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
|||
if (info.fullscreen_enabled) {
|
||||
args << "--fullscreen";
|
||||
}
|
||||
if (info.hdr_enabled) {
|
||||
args << "--hdr";
|
||||
}
|
||||
if (info.hide_cursor_enabled) {
|
||||
args << "--hide-cursor";
|
||||
}
|
||||
|
|
@ -252,6 +255,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
|||
args << "--sync-raw-image-buffers" << BoolArg(info.sync_raw_image_buffers);
|
||||
args << "--trophy-notifications" << BoolArg(info.trophy_enabled);
|
||||
args << "--skip-notice-screen" << BoolArg(info.skip_notice_screen);
|
||||
args << "--network-connected" << BoolArg(info.network_connected);
|
||||
if (info.tessellation_enabled) {
|
||||
args << "--tessellation";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -868,6 +868,10 @@ static uint64_t AprReadFileRecordSize(uint64_t file_offset) {
|
|||
return (file_offset >> 32u) != 0 ? READ_FILE_RECORD_SIZE_EXT : READ_FILE_RECORD_SIZE;
|
||||
}
|
||||
|
||||
static uint64_t WriteAddressRecordSize(uint64_t value) {
|
||||
return value <= 3 ? 8 : value < (uint64_t {1} << 34) ? 12 : 16;
|
||||
}
|
||||
|
||||
static uint64_t AprReadGatherRecordSize(uint64_t file_offset) {
|
||||
return file_offset > 0x3ffffu ? READ_GATHER_RECORD_SIZE_EXT : READ_GATHER_RECORD_SIZE;
|
||||
}
|
||||
|
|
@ -1207,7 +1211,7 @@ static int ExecuteCommand(const CommandBufferState::Command& entry) {
|
|||
const auto& command = *payload;
|
||||
int result = OK;
|
||||
if (command.kind == AmmCommandKind::Unmap) {
|
||||
result = LibKernel::Memory::KernelMunmap(command.va, command.size);
|
||||
result = LibKernel::Memory::UnmapAmmMemory(command.va, command.size);
|
||||
} else {
|
||||
result = ExecuteAmmMapCommand(command);
|
||||
}
|
||||
|
|
@ -1617,11 +1621,10 @@ static uint64_t KYTY_SYSV_ABI MeasureCommandSizeFixed32(uint64_t, uint64_t, uint
|
|||
return 0x20;
|
||||
}
|
||||
|
||||
static uint64_t KYTY_SYSV_ABI MeasureCommandSizeWriteAddressOnCompletion(volatile uint64_t*,
|
||||
uint64_t) {
|
||||
static uint64_t KYTY_SYSV_ABI MeasureCommandSizeWriteAddress(volatile uint64_t*, uint64_t value) {
|
||||
PRINT_NAME();
|
||||
|
||||
return 0x20;
|
||||
return WriteAddressRecordSize(value);
|
||||
}
|
||||
|
||||
static uint64_t KYTY_SYSV_ABI
|
||||
|
|
@ -1950,7 +1953,7 @@ static int KYTY_SYSV_ABI CommandBufferWaitOnCounter(void* command_buffer, uint8_
|
|||
}
|
||||
|
||||
static int AppendWriteAddressCommand(void* command_buffer, volatile uint64_t* address,
|
||||
uint64_t value) {
|
||||
uint64_t value, uint64_t record_size) {
|
||||
if (command_buffer == nullptr || address == nullptr ||
|
||||
(reinterpret_cast<uint64_t>(address) & 7) != 0) {
|
||||
return LibKernel::KERNEL_ERROR_EINVAL;
|
||||
|
|
@ -1958,7 +1961,7 @@ static int AppendWriteAddressCommand(void* command_buffer, volatile uint64_t* ad
|
|||
return AppendCommandRecord(
|
||||
reinterpret_cast<uint64_t>(command_buffer),
|
||||
CommandBufferState::WriteAddressCommand {reinterpret_cast<uint64_t>(address), value},
|
||||
0x20)
|
||||
record_size)
|
||||
? OK
|
||||
: LibKernel::KERNEL_ERROR_EBUSY;
|
||||
}
|
||||
|
|
@ -1967,7 +1970,7 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddress(void* command_buffer, volatil
|
|||
uint64_t value, uint32_t) {
|
||||
PRINT_NAME();
|
||||
|
||||
return AppendWriteAddressCommand(command_buffer, address, value);
|
||||
return AppendWriteAddressCommand(command_buffer, address, value, WriteAddressRecordSize(value));
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI CommandBufferWriteAddressOnCompletion(void* command_buffer,
|
||||
|
|
@ -1975,7 +1978,7 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressOnCompletion(void*
|
|||
uint64_t value) {
|
||||
PRINT_NAME();
|
||||
|
||||
return AppendWriteAddressCommand(command_buffer, address, value);
|
||||
return AppendWriteAddressCommand(command_buffer, address, value, WriteAddressRecordSize(value));
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI CommandBufferWriteCounter(void* command_buffer, uint8_t index,
|
||||
|
|
@ -2007,14 +2010,14 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressFromTimeCounter(void*
|
|||
uint32_t) {
|
||||
PRINT_NAME();
|
||||
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromTimeCounterOnCompletion(
|
||||
void* command_buffer, volatile uint64_t* address) {
|
||||
PRINT_NAME();
|
||||
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounter(void* command_buffer,
|
||||
|
|
@ -2022,14 +2025,14 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounter(void*
|
|||
uint32_t) {
|
||||
PRINT_NAME();
|
||||
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounterOnCompletion(
|
||||
void* command_buffer, volatile uint64_t* address, uint8_t) {
|
||||
PRINT_NAME();
|
||||
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounterPair(void* command_buffer,
|
||||
|
|
@ -2037,14 +2040,14 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounterPair(void*
|
|||
uint8_t, uint32_t) {
|
||||
PRINT_NAME();
|
||||
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounterPairOnCompletion(
|
||||
void* command_buffer, volatile uint64_t* address, uint8_t) {
|
||||
PRINT_NAME();
|
||||
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
|
||||
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI AprCommandBufferReadFileGather(void* command_buffer, uint64_t, uint64_t,
|
||||
|
|
@ -2452,14 +2455,14 @@ LIB_DEFINE(InitAmpr_1) {
|
|||
LIB_FUNC("vWU-odnS+fU", Ampr::MeasureCommandSizeReadFile);
|
||||
LIB_FUNC("sSAUCCU1dv4", Ampr::MeasureCommandSizeWriteKernelEventQueue);
|
||||
LIB_FUNC("Zi3dBUjgyXI", Ampr::MeasureCommandSizeWriteKernelEventQueue);
|
||||
LIB_FUNC("C+IEj+BsAFM", Ampr::MeasureCommandSizeWriteAddressOnCompletion);
|
||||
LIB_FUNC("C+IEj+BsAFM", Ampr::MeasureCommandSizeWriteAddress);
|
||||
LIB_FUNC("x7SQEXfeovg", Ampr::MeasureCommandSizeWriteAddressFromTimeCounterOnCompletion);
|
||||
LIB_FUNC("32AcaTaBPSY", Ampr::MeasureCommandSizeWriteAddressFromCounterOnCompletion);
|
||||
LIB_FUNC("vxC58+DRk-U", Ampr::MeasureCommandSizeWriteAddressFromCounterPairOnCompletion);
|
||||
LIB_FUNC("4muPEJ-x5N8", Ampr::MeasureCommandSizeWriteCounterOnCompletion);
|
||||
LIB_FUNC("0BMj1hgG+kE", Ampr::MeasureCommandSizeFixed32);
|
||||
LIB_FUNC("ClnsFLLLcss", Ampr::MeasureCommandSizeFixed32);
|
||||
LIB_FUNC("4fgtGfXDrFc", Ampr::MeasureCommandSizeFixed32);
|
||||
LIB_FUNC("4fgtGfXDrFc", Ampr::MeasureCommandSizeWriteAddress);
|
||||
LIB_FUNC("gAtc79UTt5E", Ampr::MeasureCommandSizeFixed32);
|
||||
LIB_FUNC("JYd9g9L+TmE", Ampr::MeasureCommandSizeFixed32);
|
||||
LIB_FUNC("2Hw8gjMdwSY", Ampr::MeasureCommandSizeFixed32);
|
||||
|
|
|
|||
|
|
@ -3117,6 +3117,7 @@ LIB_DEFINE(InitLibKernel_1_FS) {
|
|||
LIB_FUNC("mBd4AfLP+u8", FileSystem::KernelPwritev);
|
||||
LIB_FUNC("eV9wAD2riIA", FileSystem::KernelStat);
|
||||
LIB_FUNC("kBwCPsYX-m4", FileSystem::KernelFstat);
|
||||
LIB_FUNC("VW3TVZiM4-E", FileSystem::KernelFtruncate);
|
||||
LIB_FUNC("AUXVxWeJU-A", FileSystem::KernelUnlink);
|
||||
LIB_FUNC("52NcYU9+lEo", FileSystem::KernelRename);
|
||||
LIB_FUNC("taRWhTJFTgE", FileSystem::KernelGetdirentries);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,459 @@
|
|||
#include "common/abi.h"
|
||||
#include "libs/libs.h"
|
||||
#include "libs/videoDec2Decoder.h"
|
||||
#include "loader/symbolDatabase.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
#include <optional>
|
||||
#include <shared_mutex>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace Libs::Vdecsw {
|
||||
|
||||
LIB_VERSION("Vdecsw", 1, "Vdecsw", 1, 1);
|
||||
|
||||
namespace Decoder = VideoDec2::Decoder;
|
||||
|
||||
enum class Error : uint32_t {
|
||||
ApiFail = 0x0100,
|
||||
StructSize,
|
||||
ArgumentPointer,
|
||||
DecoderInstance,
|
||||
MemorySize,
|
||||
MemoryPointer,
|
||||
FrameBufferSize,
|
||||
FrameBufferPointer,
|
||||
FrameBufferAlignment,
|
||||
AccessUnitSize = 0x010d,
|
||||
AccessUnitPointer,
|
||||
OutputInfo,
|
||||
ComputeQueue,
|
||||
InputQueueFull = 0x0113,
|
||||
OutputBufferFull,
|
||||
InputQueueEmpty = 0x0116,
|
||||
DecodePending,
|
||||
OutputBufferEmpty,
|
||||
ConfigInfo = 0x0200,
|
||||
ComputePipeId,
|
||||
ComputeQueueId,
|
||||
ResourceType,
|
||||
CodecType,
|
||||
InputQueueDepth = 0x0206,
|
||||
DpbFrameCount = 0x0209,
|
||||
FrameWidthHeight,
|
||||
ExtraConfigInfo,
|
||||
AccessUnit = 0x0301,
|
||||
OversizeDecode,
|
||||
};
|
||||
|
||||
constexpr int32_t Fail(Error error) {
|
||||
return static_cast<int32_t>(0x81510000u | static_cast<uint32_t>(error));
|
||||
}
|
||||
|
||||
struct ComputeMemoryInfo {
|
||||
size_t this_size;
|
||||
size_t cpu_gpu_memory_size;
|
||||
void* cpu_gpu_memory;
|
||||
};
|
||||
|
||||
struct ComputeConfigInfo {
|
||||
size_t this_size;
|
||||
uint16_t pipe_id;
|
||||
uint16_t queue_id;
|
||||
bool check_memory_type;
|
||||
uint8_t reserved0;
|
||||
uint16_t reserved1;
|
||||
};
|
||||
|
||||
struct ComputeQueue {
|
||||
uint16_t pipe_id;
|
||||
uint16_t queue_id;
|
||||
uint32_t decoders = 0;
|
||||
};
|
||||
|
||||
struct ConfigInfo {
|
||||
size_t this_size;
|
||||
uint32_t resource_type;
|
||||
uint32_t codec_type;
|
||||
uint32_t profile;
|
||||
uint32_t max_level;
|
||||
int32_t max_width;
|
||||
int32_t max_height;
|
||||
int32_t max_dpb_frames;
|
||||
uint32_t input_queue_depth;
|
||||
ComputeQueue* compute_queue;
|
||||
uint64_t cpu_affinity_mask;
|
||||
int32_t cpu_thread_priority;
|
||||
bool optimize_progressive;
|
||||
bool check_memory_type;
|
||||
uint8_t reserved0;
|
||||
int8_t extra_dpb_frames;
|
||||
void* extra_config;
|
||||
bool disable_sync_input;
|
||||
uint32_t max_pending_sync;
|
||||
};
|
||||
|
||||
struct MemoryInfo {
|
||||
size_t this_size;
|
||||
size_t cpu_memory_size;
|
||||
void* cpu_memory;
|
||||
size_t gpu_memory_size;
|
||||
void* gpu_memory;
|
||||
size_t cpu_gpu_memory_size;
|
||||
void* cpu_gpu_memory;
|
||||
size_t max_frame_buffer_size;
|
||||
uint32_t frame_buffer_alignment;
|
||||
uint32_t reserved0;
|
||||
};
|
||||
|
||||
struct InputData {
|
||||
size_t this_size;
|
||||
void* au_data;
|
||||
size_t au_size;
|
||||
uint64_t pts;
|
||||
uint64_t dts;
|
||||
uint64_t attached_data;
|
||||
};
|
||||
|
||||
struct InputResult {
|
||||
size_t this_size;
|
||||
void* decoded_au;
|
||||
uint32_t output_frame_count;
|
||||
uint32_t reserved0;
|
||||
};
|
||||
|
||||
struct FrameBuffer {
|
||||
size_t this_size;
|
||||
void* data;
|
||||
size_t size;
|
||||
};
|
||||
|
||||
struct OutputInfo {
|
||||
size_t this_size;
|
||||
bool valid;
|
||||
bool last_frame;
|
||||
bool error_frame;
|
||||
uint8_t picture_count;
|
||||
uint32_t codec_type;
|
||||
uint32_t width;
|
||||
uint32_t pitch;
|
||||
uint32_t height;
|
||||
bool discarded_frame;
|
||||
void* frame_buffer;
|
||||
size_t frame_buffer_size;
|
||||
uint32_t format;
|
||||
uint32_t pitch_in_bytes;
|
||||
};
|
||||
|
||||
static_assert(sizeof(ComputeMemoryInfo) == 24);
|
||||
static_assert(sizeof(ComputeConfigInfo) == 16);
|
||||
static_assert(sizeof(ConfigInfo) == 80);
|
||||
static_assert(offsetof(ConfigInfo, extra_dpb_frames) == 63);
|
||||
static_assert(offsetof(ConfigInfo, disable_sync_input) == 72);
|
||||
static_assert(sizeof(MemoryInfo) == 72);
|
||||
static_assert(sizeof(InputData) == 48);
|
||||
static_assert(sizeof(InputResult) == 24);
|
||||
static_assert(sizeof(FrameBuffer) == 24);
|
||||
static_assert(sizeof(OutputInfo) == 56);
|
||||
static_assert(offsetof(OutputInfo, discarded_frame) == 28);
|
||||
static_assert(offsetof(OutputInfo, frame_buffer) == 32);
|
||||
|
||||
struct State;
|
||||
static std::shared_mutex g_registry_mutex;
|
||||
static std::unordered_set<ComputeQueue*> g_queues;
|
||||
static std::unordered_set<State*> g_decoders;
|
||||
|
||||
struct State {
|
||||
ConfigInfo config;
|
||||
Decoder::Instance* decoder;
|
||||
std::mutex mutex;
|
||||
std::deque<InputResult> completed_inputs;
|
||||
std::optional<FrameBuffer> output_buffer;
|
||||
|
||||
~State() {
|
||||
Decoder::Destroy(decoder);
|
||||
--config.compute_queue->decoders;
|
||||
}
|
||||
};
|
||||
|
||||
// The shared registry lock keeps guest-backed state alive until each API call returns.
|
||||
// Deletion takes the exclusive lock before releasing that memory to the guest.
|
||||
struct StateAccess {
|
||||
std::shared_lock<std::shared_mutex> lock {g_registry_mutex};
|
||||
State* value;
|
||||
explicit StateAccess(State* handle): value(g_decoders.contains(handle) ? handle : nullptr) {}
|
||||
explicit operator bool() const { return value != nullptr; }
|
||||
State* operator->() const { return value; }
|
||||
};
|
||||
|
||||
static int32_t Result(Decoder::Result result) {
|
||||
switch (result) {
|
||||
case Decoder::Result::Ok: return 0;
|
||||
case Decoder::Result::ApiFail: return Fail(Error::ApiFail);
|
||||
case Decoder::Result::AccessUnit: return Fail(Error::AccessUnit);
|
||||
case Decoder::Result::FrameBufferSize: return Fail(Error::FrameBufferSize);
|
||||
case Decoder::Result::OversizeDecode: return Fail(Error::OversizeDecode);
|
||||
case Decoder::Result::InputFull: return Fail(Error::OutputBufferFull);
|
||||
case Decoder::Result::NoFrame: return Fail(Error::DecodePending);
|
||||
}
|
||||
return Fail(Error::ApiFail);
|
||||
}
|
||||
|
||||
static int32_t ValidateConfig(const ConfigInfo* config) {
|
||||
if (config == nullptr) return Fail(Error::ArgumentPointer);
|
||||
if (config->this_size != sizeof(ConfigInfo)) return Fail(Error::StructSize);
|
||||
if (config->resource_type != 1) return Fail(Error::ResourceType);
|
||||
if (config->codec_type != 1) return Fail(Error::CodecType);
|
||||
if (config->reserved0 != 0) return Fail(Error::ConfigInfo);
|
||||
if (config->extra_config != nullptr) return Fail(Error::ExtraConfigInfo);
|
||||
if (config->input_queue_depth < 1 || config->input_queue_depth > 8)
|
||||
return Fail(Error::InputQueueDepth);
|
||||
if (config->max_dpb_frames < 1 || config->extra_dpb_frames < 0)
|
||||
return Fail(Error::DpbFrameCount);
|
||||
if (config->max_width <= 0 || config->max_height <= 0) return Fail(Error::FrameWidthHeight);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static size_t FrameSize(const ConfigInfo& config) {
|
||||
// AVC output: 256-byte pitch, 16-line maximum height, and 4096 bytes of metadata.
|
||||
const auto pitch = (static_cast<size_t>(config.max_width) + 255u) & ~size_t {255};
|
||||
const auto height = (static_cast<size_t>(config.max_height) + 15u) & ~size_t {15};
|
||||
return pitch * height * 3 / 2 + 4096;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI QueryComputeMemoryInfo(ComputeMemoryInfo* memory) {
|
||||
PRINT_NAME();
|
||||
if (memory == nullptr) return Fail(Error::ArgumentPointer);
|
||||
if (memory->this_size != sizeof(ComputeMemoryInfo)) return Fail(Error::StructSize);
|
||||
memory->cpu_gpu_memory_size = sizeof(ComputeQueue);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI AllocateComputeQueue(const ComputeConfigInfo* config,
|
||||
const ComputeMemoryInfo* memory,
|
||||
ComputeQueue** output) {
|
||||
PRINT_NAME();
|
||||
if (config == nullptr || memory == nullptr || output == nullptr)
|
||||
return Fail(Error::ArgumentPointer);
|
||||
if (config->this_size != sizeof(ComputeConfigInfo) ||
|
||||
memory->this_size != sizeof(ComputeMemoryInfo))
|
||||
return Fail(Error::StructSize);
|
||||
if (config->reserved0 != 0 || config->reserved1 != 0) return Fail(Error::ConfigInfo);
|
||||
if (config->pipe_id > 4) return Fail(Error::ComputePipeId);
|
||||
if (config->queue_id > 7) return Fail(Error::ComputeQueueId);
|
||||
if (memory->cpu_gpu_memory_size < sizeof(ComputeQueue)) return Fail(Error::MemorySize);
|
||||
if (memory->cpu_gpu_memory == nullptr ||
|
||||
reinterpret_cast<uintptr_t>(memory->cpu_gpu_memory) % alignof(ComputeQueue) != 0)
|
||||
return Fail(Error::MemoryPointer);
|
||||
std::scoped_lock lock(g_registry_mutex);
|
||||
for (const auto* queue: g_queues)
|
||||
if ((queue->pipe_id == config->pipe_id && queue->queue_id == config->queue_id) ||
|
||||
queue == memory->cpu_gpu_memory)
|
||||
return Fail(Error::ComputeQueue);
|
||||
auto* queue = new (memory->cpu_gpu_memory) ComputeQueue {config->pipe_id, config->queue_id};
|
||||
g_queues.insert(queue);
|
||||
*output = queue;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI ReleaseComputeQueue(ComputeQueue* queue) {
|
||||
PRINT_NAME();
|
||||
std::scoped_lock lock(g_registry_mutex);
|
||||
if (!g_queues.contains(queue) || queue->decoders != 0) return Fail(Error::ComputeQueue);
|
||||
g_queues.erase(queue);
|
||||
queue->~ComputeQueue();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI QueryDecoderMemoryInfo(const ConfigInfo* config, MemoryInfo* memory) {
|
||||
PRINT_NAME();
|
||||
if (const auto error = ValidateConfig(config); error != 0) return error;
|
||||
if (memory == nullptr) return Fail(Error::ArgumentPointer);
|
||||
if (memory->this_size != sizeof(MemoryInfo)) return Fail(Error::StructSize);
|
||||
memory->cpu_memory_size = sizeof(State);
|
||||
// The FFmpeg backend needs no guest GPU workspace.
|
||||
memory->gpu_memory_size = 0;
|
||||
memory->cpu_gpu_memory_size = 0;
|
||||
memory->max_frame_buffer_size = FrameSize(*config);
|
||||
memory->frame_buffer_alignment = 256;
|
||||
memory->reserved0 = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI CreateDecoder(const ConfigInfo* config, const MemoryInfo* memory,
|
||||
State** output) {
|
||||
PRINT_NAME();
|
||||
if (const auto error = ValidateConfig(config); error != 0) return error;
|
||||
if (memory == nullptr || output == nullptr) return Fail(Error::ArgumentPointer);
|
||||
if (memory->this_size != sizeof(MemoryInfo)) return Fail(Error::StructSize);
|
||||
if (memory->cpu_memory_size < sizeof(State)) return Fail(Error::MemorySize);
|
||||
if (memory->cpu_memory == nullptr ||
|
||||
reinterpret_cast<uintptr_t>(memory->cpu_memory) % alignof(State) != 0)
|
||||
return Fail(Error::MemoryPointer);
|
||||
std::scoped_lock lock(g_registry_mutex);
|
||||
if (!g_queues.contains(config->compute_queue)) return Fail(Error::ComputeQueue);
|
||||
if (g_decoders.contains(static_cast<State*>(memory->cpu_memory)))
|
||||
return Fail(Error::MemoryPointer);
|
||||
auto* decoder = Decoder::Create(
|
||||
{config->codec_type, config->max_width, config->max_height,
|
||||
std::max(2u, static_cast<uint32_t>(config->max_dpb_frames) + config->extra_dpb_frames)});
|
||||
if (decoder == nullptr) return Fail(Error::ApiFail);
|
||||
auto* state = new (memory->cpu_memory) State {*config, decoder};
|
||||
++config->compute_queue->decoders;
|
||||
g_decoders.insert(state);
|
||||
*output = state;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI DeleteDecoder(State* handle) {
|
||||
PRINT_NAME();
|
||||
std::scoped_lock lock(g_registry_mutex);
|
||||
if (g_decoders.erase(handle) == 0) return Fail(Error::DecoderInstance);
|
||||
handle->~State();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI SetDecodeInput(State* handle, const InputData* input) {
|
||||
PRINT_NAME();
|
||||
const StateAccess state(handle);
|
||||
if (!state) return Fail(Error::DecoderInstance);
|
||||
if (input == nullptr) return Fail(Error::ArgumentPointer);
|
||||
if (input->this_size != sizeof(InputData)) return Fail(Error::StructSize);
|
||||
if (input->au_data == nullptr) return Fail(Error::AccessUnitPointer);
|
||||
if (input->au_size == 0) return Fail(Error::AccessUnitSize);
|
||||
std::scoped_lock lock(state->mutex);
|
||||
const auto& config = state->config;
|
||||
if (!config.disable_sync_input &&
|
||||
state->completed_inputs.size() >=
|
||||
uint64_t {config.input_queue_depth} + config.max_pending_sync)
|
||||
return Fail(Error::InputQueueFull);
|
||||
uint32_t produced = 0;
|
||||
const auto result = Decoder::Submit(
|
||||
state->decoder,
|
||||
{input->au_data, input->au_size, input->pts, input->dts, input->attached_data}, &produced);
|
||||
if (result != Decoder::Result::Ok) return Result(result);
|
||||
if (!config.disable_sync_input)
|
||||
state->completed_inputs.push_back({sizeof(InputResult), input->au_data, produced, 0});
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI TrySyncDecodeInput(State* handle, InputResult* output) {
|
||||
PRINT_NAME();
|
||||
const StateAccess state(handle);
|
||||
if (!state) return Fail(Error::DecoderInstance);
|
||||
if (output == nullptr) return Fail(Error::ArgumentPointer);
|
||||
if (output->this_size != sizeof(InputResult)) return Fail(Error::StructSize);
|
||||
std::scoped_lock lock(state->mutex);
|
||||
if (state->config.disable_sync_input) return Fail(Error::ApiFail);
|
||||
if (state->completed_inputs.empty()) return Fail(Error::InputQueueEmpty);
|
||||
*output = state->completed_inputs.front();
|
||||
state->completed_inputs.pop_front();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI SetDecodeOutput(State* handle, FrameBuffer* buffer) {
|
||||
PRINT_NAME();
|
||||
const StateAccess state(handle);
|
||||
if (!state) return Fail(Error::DecoderInstance);
|
||||
if (buffer == nullptr) return Fail(Error::ArgumentPointer);
|
||||
if (buffer->this_size != sizeof(FrameBuffer)) return Fail(Error::StructSize);
|
||||
if (buffer->data == nullptr) return Fail(Error::FrameBufferPointer);
|
||||
if (reinterpret_cast<uintptr_t>(buffer->data) % 256 != 0)
|
||||
return Fail(Error::FrameBufferAlignment);
|
||||
if (buffer->size < FrameSize(state->config)) return Fail(Error::FrameBufferSize);
|
||||
std::scoped_lock lock(state->mutex);
|
||||
if (state->output_buffer) return Fail(Error::OutputBufferFull);
|
||||
state->output_buffer = *buffer;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI TrySyncDecodeOutput(State* handle, OutputInfo* output) {
|
||||
PRINT_NAME();
|
||||
const StateAccess state(handle);
|
||||
if (!state) return Fail(Error::DecoderInstance);
|
||||
if (output == nullptr) return Fail(Error::ArgumentPointer);
|
||||
if (output->this_size != sizeof(OutputInfo)) return Fail(Error::StructSize);
|
||||
std::scoped_lock lock(state->mutex);
|
||||
if (!state->output_buffer) return Fail(Error::OutputBufferEmpty);
|
||||
const auto& buffer = *state->output_buffer;
|
||||
Decoder::Output decoded {};
|
||||
const auto result = Decoder::Receive(state->decoder, {buffer.data, buffer.size}, &decoded);
|
||||
if (result != Decoder::Result::Ok) return Result(result);
|
||||
*output = {sizeof(OutputInfo),
|
||||
true,
|
||||
decoded.last_frame,
|
||||
decoded.error_frame,
|
||||
1,
|
||||
decoded.codec_type,
|
||||
decoded.width,
|
||||
decoded.pitch,
|
||||
decoded.height,
|
||||
false,
|
||||
decoded.buffer,
|
||||
decoded.buffer_size,
|
||||
0,
|
||||
decoded.pitch};
|
||||
state->output_buffer.reset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI FinalizeDecodeSequence(State* handle) {
|
||||
PRINT_NAME();
|
||||
const StateAccess state(handle);
|
||||
if (!state) return Fail(Error::DecoderInstance);
|
||||
std::scoped_lock lock(state->mutex);
|
||||
return Result(Decoder::Finalize(state->decoder));
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI ResetDecoder(State* handle) {
|
||||
PRINT_NAME();
|
||||
const StateAccess state(handle);
|
||||
if (!state) return Fail(Error::DecoderInstance);
|
||||
std::scoped_lock lock(state->mutex);
|
||||
Decoder::Reset(state->decoder);
|
||||
state->completed_inputs.clear();
|
||||
state->output_buffer.reset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t KYTY_SYSV_ABI GetAvcPictureInfo(const OutputInfo* output,
|
||||
Decoder::AvcPictureInfo* first,
|
||||
Decoder::AvcPictureInfo* second) {
|
||||
PRINT_NAME();
|
||||
if (output == nullptr || first == nullptr) return Fail(Error::ArgumentPointer);
|
||||
if (output->this_size != sizeof(OutputInfo) || first->this_size != sizeof(*first) ||
|
||||
(second != nullptr && second->this_size != sizeof(*second)))
|
||||
return Fail(Error::StructSize);
|
||||
if (!output->valid || output->codec_type != 1 || output->picture_count == 0)
|
||||
return Fail(Error::OutputInfo);
|
||||
Decoder::PictureInfo decoded {};
|
||||
if (!Decoder::GetPictureInfo(output->frame_buffer, &decoded) || decoded.codec_type != 1)
|
||||
return Fail(Error::OutputInfo);
|
||||
*first = Decoder::MakeAvcPictureInfo(decoded, sizeof(*first));
|
||||
if (second != nullptr) *second = {.this_size = sizeof(*second)};
|
||||
return 0;
|
||||
}
|
||||
|
||||
LIB_DEFINE(InitVdecsw_1) {
|
||||
PRINT_NAME_ENABLE(true);
|
||||
LIB_FUNC("0moTubWCsTM", QueryComputeMemoryInfo);
|
||||
LIB_FUNC("hIgrg5h4V6s", AllocateComputeQueue);
|
||||
LIB_FUNC("A+2M7EivuOU", QueryDecoderMemoryInfo);
|
||||
LIB_FUNC("+L5ArV1tPGA", CreateDecoder);
|
||||
LIB_FUNC("ecUtPX+dBYk", DeleteDecoder);
|
||||
LIB_FUNC("fX-zOOefbbs", ReleaseComputeQueue);
|
||||
LIB_FUNC("aqMiF0AgUYI", SetDecodeInput);
|
||||
LIB_FUNC("veb-YBrOqo0", ResetDecoder);
|
||||
LIB_FUNC("kMBw37oH8nI", TrySyncDecodeOutput);
|
||||
LIB_FUNC("rgtMCOpyBSc", SetDecodeOutput);
|
||||
LIB_FUNC("ihNT-uuEAr4", GetAvcPictureInfo);
|
||||
LIB_FUNC("5Y6nZqIZvBg", FinalizeDecodeSequence);
|
||||
LIB_FUNC("l4sQYy5wPkc", TrySyncDecodeInput);
|
||||
}
|
||||
|
||||
} // namespace Libs::Vdecsw
|
||||
|
|
@ -111,69 +111,7 @@ struct Videodec2FrameBuffer {
|
|||
bool is_accepted;
|
||||
};
|
||||
|
||||
struct Videodec2AvcPictureInfo {
|
||||
size_t this_size;
|
||||
bool is_valid;
|
||||
|
||||
uint64_t pts_data;
|
||||
uint64_t dts_data;
|
||||
uint64_t attached_data;
|
||||
|
||||
uint8_t idr_picture_flag;
|
||||
uint8_t profile_idc;
|
||||
uint8_t level_idc;
|
||||
uint32_t pic_width_in_mbs_minus1;
|
||||
uint32_t pic_height_in_map_units_minus1;
|
||||
uint8_t frame_mbs_only_flag;
|
||||
|
||||
uint8_t frame_cropping_flag;
|
||||
uint32_t frame_crop_left_offset;
|
||||
uint32_t frame_crop_right_offset;
|
||||
uint32_t frame_crop_top_offset;
|
||||
uint32_t frame_crop_bottom_offset;
|
||||
|
||||
uint8_t aspect_ratio_info_present_flag;
|
||||
uint8_t aspect_ratio_idc;
|
||||
uint16_t sar_width;
|
||||
uint16_t sar_height;
|
||||
|
||||
uint8_t video_signal_type_present_flag;
|
||||
uint8_t video_format;
|
||||
uint8_t video_full_range_flag;
|
||||
uint8_t colour_description_present_flag;
|
||||
uint8_t colour_primaries;
|
||||
uint8_t transfer_characteristics;
|
||||
uint8_t matrix_coefficients;
|
||||
|
||||
uint8_t timing_info_present_flag;
|
||||
uint32_t num_units_in_tick;
|
||||
uint32_t time_scale;
|
||||
uint8_t fixed_frame_rate_flag;
|
||||
|
||||
uint8_t bitstream_restriction_flag;
|
||||
uint8_t max_dec_frame_buffering;
|
||||
|
||||
uint8_t pic_struct_present_flag;
|
||||
uint8_t pic_struct;
|
||||
uint8_t field_pic_flag;
|
||||
uint8_t bottom_field_flag;
|
||||
|
||||
uint8_t sequence_parameter_set_present_flag;
|
||||
uint8_t picture_parameter_set_present_flag;
|
||||
uint8_t au_delimiter_present_flag;
|
||||
uint8_t end_of_sequence_present_flag;
|
||||
uint8_t end_of_stream_present_flag;
|
||||
uint8_t filler_data_present_flag;
|
||||
uint8_t picture_timing_sei_present_flag;
|
||||
uint8_t buffering_period_sei_present_flag;
|
||||
|
||||
uint8_t constraint_set0_flag;
|
||||
uint8_t constraint_set1_flag;
|
||||
uint8_t constraint_set2_flag;
|
||||
uint8_t constraint_set3_flag;
|
||||
uint8_t constraint_set4_flag;
|
||||
uint8_t constraint_set5_flag;
|
||||
};
|
||||
using Videodec2AvcPictureInfo = Decoder::AvcPictureInfo;
|
||||
|
||||
struct Videodec2ComputeMemoryInfo {
|
||||
size_t this_size;
|
||||
|
|
@ -235,6 +173,8 @@ static void FillNoPictureOutput(const Videodec2FrameBuffer* frame_buffer,
|
|||
static int32_t MapDecoderResult(Decoder::Result result) {
|
||||
switch (result) {
|
||||
case Decoder::Result::Ok: return OK;
|
||||
case Decoder::Result::InputFull:
|
||||
case Decoder::Result::NoFrame:
|
||||
case Decoder::Result::ApiFail: return VIDEODEC2_ERROR_API_FAIL;
|
||||
case Decoder::Result::AccessUnit: return VIDEODEC2_ERROR_ACCESS_UNIT;
|
||||
case Decoder::Result::FrameBufferSize: return VIDEODEC2_ERROR_FRAME_BUFFER_SIZE;
|
||||
|
|
@ -599,40 +539,7 @@ static int32_t KYTY_SYSV_ABI GetPictureInfo(const Videodec2OutputInfo* output_in
|
|||
return VIDEODEC2_ERROR_STRUCT_SIZE;
|
||||
}
|
||||
|
||||
Videodec2AvcPictureInfo picture {};
|
||||
picture.this_size = requested_size;
|
||||
picture.is_valid = true;
|
||||
picture.pts_data = decoded.pts;
|
||||
picture.dts_data = decoded.dts;
|
||||
picture.attached_data = decoded.attached_data;
|
||||
picture.idr_picture_flag = decoded.key_frame ? 1 : 0;
|
||||
picture.profile_idc = static_cast<uint8_t>(decoded.profile);
|
||||
picture.level_idc = static_cast<uint8_t>(decoded.level);
|
||||
picture.pic_width_in_mbs_minus1 = (decoded.width + 15u) / 16u - 1u;
|
||||
picture.pic_height_in_map_units_minus1 = (decoded.height + 15u) / 16u - 1u;
|
||||
picture.frame_mbs_only_flag = 1;
|
||||
picture.frame_cropping_flag = decoded.crop_left != 0 || decoded.crop_right != 0 ||
|
||||
decoded.crop_top != 0 || decoded.crop_bottom != 0
|
||||
? 1
|
||||
: 0;
|
||||
picture.frame_crop_left_offset = decoded.crop_left;
|
||||
picture.frame_crop_right_offset = decoded.crop_right;
|
||||
picture.frame_crop_top_offset = decoded.crop_top;
|
||||
picture.frame_crop_bottom_offset = decoded.crop_bottom;
|
||||
picture.aspect_ratio_info_present_flag =
|
||||
decoded.sar_width != 0 && decoded.sar_height != 0 ? 1 : 0;
|
||||
picture.aspect_ratio_idc = picture.aspect_ratio_info_present_flag ? 255 : 0;
|
||||
picture.sar_width = decoded.sar_width;
|
||||
picture.sar_height = decoded.sar_height;
|
||||
picture.video_signal_type_present_flag = 1;
|
||||
picture.video_format = 5;
|
||||
picture.video_full_range_flag = decoded.color_range == 2 ? 1 : 0;
|
||||
picture.colour_description_present_flag =
|
||||
decoded.color_primaries != 0 || decoded.color_trc != 0 || decoded.color_space != 0 ? 1
|
||||
: 0;
|
||||
picture.colour_primaries = decoded.color_primaries;
|
||||
picture.transfer_characteristics = decoded.color_trc;
|
||||
picture.matrix_coefficients = decoded.color_space;
|
||||
const auto picture = Decoder::MakeAvcPictureInfo(decoded, requested_size);
|
||||
std::memcpy(first_picture_info, &picture, requested_size);
|
||||
} else {
|
||||
const auto result = fill_common(first_picture_info, true);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ namespace VideoDec2 {
|
|||
LIB_DEFINE(InitVideoDec2_1);
|
||||
} // namespace VideoDec2
|
||||
|
||||
namespace Vdecsw {
|
||||
LIB_DEFINE(InitVdecsw_1);
|
||||
} // namespace Vdecsw
|
||||
|
||||
namespace LibMouse {
|
||||
LIB_DEFINE(InitMouse_1);
|
||||
} // namespace LibMouse
|
||||
|
|
@ -148,6 +152,7 @@ void InitAll(Loader::SymbolDatabase* s) {
|
|||
LibUlt::InitUlt_1(s);
|
||||
InitUserService_1(s);
|
||||
VideoDec2::InitVideoDec2_1(s);
|
||||
Vdecsw::InitVdecsw_1(s);
|
||||
LibGen5::InitVideoOut_1(s);
|
||||
LibGen5::VrrStatus::InitVideoOutVrrStatus_1(s);
|
||||
InitWebBrowserDialog_1(s);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@
|
|||
#else
|
||||
#include <arpa/inet.h>
|
||||
#include <fcntl.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <net/if.h>
|
||||
#include <net/route.h>
|
||||
#include <netdb.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <sys/select.h>
|
||||
|
|
@ -41,10 +44,12 @@
|
|||
#include <cstring>
|
||||
#include <fmt/format.h>
|
||||
#include <fmt/ranges.h>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
|
@ -3386,7 +3391,6 @@ constexpr int NET_CTL_STATE_DISCONNECTED = 0;
|
|||
constexpr int NET_CTL_STATE_IPOBTAINED = 3;
|
||||
constexpr int NET_CTL_DEVICE_WIRED = 0;
|
||||
constexpr int NET_CTL_DEVICE_WIRELESS = 1;
|
||||
constexpr int NET_CTL_LINK_DISCONNECTED = 0;
|
||||
constexpr int NET_CTL_LINK_CONNECTED = 1;
|
||||
constexpr int NET_CTL_EVENT_DISCONNECTED = 1;
|
||||
constexpr int NET_CTL_EVENT_IPOBTAINED = 3;
|
||||
|
|
@ -3448,8 +3452,6 @@ struct NetCtlCallbackSlot {
|
|||
|
||||
static Common::Mutex g_net_ctl_callbacks_mutex;
|
||||
static std::array<NetCtlCallbackSlot, 8> g_net_ctl_callbacks;
|
||||
static std::atomic_bool g_net_ctl_connected = false;
|
||||
static std::atomic_bool g_net_ctl_status_initialized = false;
|
||||
|
||||
struct HostNetworkInfo {
|
||||
bool connected = false;
|
||||
|
|
@ -3463,22 +3465,16 @@ struct HostNetworkInfo {
|
|||
};
|
||||
|
||||
static void CopyIpv4String(char* dst, size_t dst_size, const sockaddr* addr) {
|
||||
#if defined(_WIN32)
|
||||
if (dst == nullptr || dst_size == 0 || addr == nullptr || addr->sa_family != AF_INET) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* in = reinterpret_cast<const sockaddr_in*>(addr);
|
||||
inet_ntop(AF_INET, &in->sin_addr, dst, static_cast<socklen_t>(dst_size));
|
||||
#else
|
||||
(void)dst;
|
||||
(void)dst_size;
|
||||
(void)addr;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void CopyIpv4Netmask(char* dst, size_t dst_size, uint8_t prefix_len) {
|
||||
#if defined(_WIN32)
|
||||
static void CopyIpv4Netmask(char* dst, size_t dst_size, uint8_t prefix_len) {
|
||||
if (dst == nullptr || dst_size == 0 || prefix_len > 32) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -3487,26 +3483,22 @@ static void CopyIpv4Netmask(char* dst, size_t dst_size, uint8_t prefix_len) {
|
|||
in_addr in {};
|
||||
in.S_un.S_addr = htonl(mask);
|
||||
inet_ntop(AF_INET, &in, dst, static_cast<socklen_t>(dst_size));
|
||||
#else
|
||||
(void)dst;
|
||||
(void)dst_size;
|
||||
(void)prefix_len;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
static HostNetworkInfo QueryHostNetworkInfo() {
|
||||
HostNetworkInfo info;
|
||||
#if defined(_WIN32)
|
||||
ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST;
|
||||
ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS;
|
||||
ULONG size = 0;
|
||||
auto ret = GetAdaptersAddresses(AF_UNSPEC, flags, nullptr, nullptr, &size);
|
||||
auto ret = GetAdaptersAddresses(AF_INET, flags, nullptr, nullptr, &size);
|
||||
if (ret != ERROR_BUFFER_OVERFLOW || size == 0) {
|
||||
return info;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> buffer(size);
|
||||
auto* adapters = reinterpret_cast<IP_ADAPTER_ADDRESSES*>(buffer.data());
|
||||
ret = GetAdaptersAddresses(AF_UNSPEC, flags, nullptr, adapters, &size);
|
||||
ret = GetAdaptersAddresses(AF_INET, flags, nullptr, adapters, &size);
|
||||
if (ret != NO_ERROR) {
|
||||
return info;
|
||||
}
|
||||
|
|
@ -3557,40 +3549,76 @@ static HostNetworkInfo QueryHostNetworkInfo() {
|
|||
}
|
||||
return info;
|
||||
}
|
||||
} else if (addr->sa_family == AF_INET6) {
|
||||
const auto* in6 = reinterpret_cast<const sockaddr_in6*>(addr);
|
||||
if (!IN6_IS_ADDR_LOOPBACK(&in6->sin6_addr) &&
|
||||
!IN6_IS_ADDR_UNSPECIFIED(&in6->sin6_addr)) {
|
||||
info.connected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
#else
|
||||
info.connected = true;
|
||||
ifaddrs* addresses = nullptr;
|
||||
if (getifaddrs(&addresses) != 0) {
|
||||
return info;
|
||||
}
|
||||
const auto usable = [](const ifaddrs* address) {
|
||||
return address->ifa_addr != nullptr && address->ifa_addr->sa_family == AF_INET &&
|
||||
(address->ifa_flags & (IFF_UP | IFF_RUNNING | IFF_LOOPBACK)) ==
|
||||
(IFF_UP | IFF_RUNNING) &&
|
||||
reinterpret_cast<const sockaddr_in*>(address->ifa_addr)->sin_addr.s_addr != 0;
|
||||
};
|
||||
const ifaddrs* selected = nullptr;
|
||||
for (auto* address = addresses; address != nullptr; address = address->ifa_next) {
|
||||
if (usable(address)) {
|
||||
selected = address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::ifstream routes("/proc/net/route");
|
||||
std::string line;
|
||||
uint32_t best_metric = std::numeric_limits<uint32_t>::max();
|
||||
uint32_t gateway = 0;
|
||||
bool has_default_route = false;
|
||||
while (std::getline(routes, line)) {
|
||||
std::istringstream fields(line);
|
||||
std::string name;
|
||||
uint32_t destination = 0, candidate_gateway = 0, flags = 0, refs = 0, uses = 0;
|
||||
uint32_t metric = 0, mask = 0;
|
||||
if (!(fields >> name >> std::hex >> destination >> candidate_gateway >> flags >>
|
||||
std::dec >> refs >> uses >> metric >> std::hex >> mask) ||
|
||||
destination != 0 || mask != 0 || (flags & (RTF_UP | RTF_REJECT)) != RTF_UP ||
|
||||
(has_default_route && metric >= best_metric)) {
|
||||
continue;
|
||||
}
|
||||
for (auto* address = addresses; address != nullptr; address = address->ifa_next) {
|
||||
if (usable(address) && name == address->ifa_name) {
|
||||
selected = address;
|
||||
gateway = candidate_gateway;
|
||||
best_metric = metric;
|
||||
has_default_route = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selected != nullptr) {
|
||||
CopyIpv4String(info.ip_address, sizeof(info.ip_address), selected->ifa_addr);
|
||||
CopyIpv4String(info.netmask, sizeof(info.netmask), selected->ifa_netmask);
|
||||
info.connected = info.ip_address[0] != '\0';
|
||||
if (has_default_route) {
|
||||
in_addr address {gateway};
|
||||
inet_ntop(AF_INET, &address, info.default_route, sizeof(info.default_route));
|
||||
}
|
||||
}
|
||||
freeifaddrs(addresses);
|
||||
return info;
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool NetCtlConnected() {
|
||||
if (!g_net_ctl_status_initialized.load()) {
|
||||
g_net_ctl_connected = false;
|
||||
g_net_ctl_status_initialized = true;
|
||||
LOGF("\t host network connected = false (forced offline)\n");
|
||||
}
|
||||
|
||||
return g_net_ctl_connected.load();
|
||||
return Config::IsNetworkConnected() && QueryHostNetworkInfo().connected;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetCtlInit() {
|
||||
PRINT_NAME();
|
||||
|
||||
g_net_ctl_connected = false;
|
||||
g_net_ctl_status_initialized = true;
|
||||
LOGF("\t host network connected = false (forced offline)\n");
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
|
|
@ -3651,7 +3679,7 @@ int KYTY_SYSV_ABI NetCtlGetStateV6(int* state) {
|
|||
return NET_CTL_ERROR_INVALID_ADDR;
|
||||
}
|
||||
|
||||
*state = (NetCtlConnected() ? NET_CTL_STATE_IPOBTAINED : NET_CTL_STATE_DISCONNECTED);
|
||||
*state = NET_CTL_STATE_DISCONNECTED;
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
|
@ -3720,24 +3748,28 @@ int KYTY_SYSV_ABI NetCtlGetResult(int event_type, int* error_code) {
|
|||
int KYTY_SYSV_ABI NetCtlGetInfo(int code, NetCtlInfo* info) {
|
||||
PRINT_NAME();
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(info == nullptr);
|
||||
|
||||
if (info == nullptr) {
|
||||
return NET_CTL_ERROR_INVALID_ADDR;
|
||||
}
|
||||
if (code < 1 || code > 21) {
|
||||
return NET_CTL_ERROR_INVALID_CODE;
|
||||
}
|
||||
if (!Config::IsNetworkConnected()) {
|
||||
return NET_CTL_ERROR_NOT_CONNECTED;
|
||||
}
|
||||
const auto host = QueryHostNetworkInfo();
|
||||
if (!host.connected || (code == 16 && host.default_route[0] == '\0')) {
|
||||
return NET_CTL_ERROR_NOT_CONNECTED;
|
||||
}
|
||||
memset(info, 0, sizeof(NetCtlInfo));
|
||||
|
||||
// Online/host-backed info responses are left below for testing, but disabled
|
||||
// to preserve the disconnected-console behavior.
|
||||
return NET_CTL_ERROR_NOT_CONNECTED;
|
||||
|
||||
switch (code) {
|
||||
case 1:
|
||||
info->device =
|
||||
(QueryHostNetworkInfo().wireless ? NET_CTL_DEVICE_WIRELESS : NET_CTL_DEVICE_WIRED);
|
||||
info->device = (host.wireless ? NET_CTL_DEVICE_WIRELESS : NET_CTL_DEVICE_WIRED);
|
||||
break;
|
||||
case 2: info->ether_addr = QueryHostNetworkInfo().ether_addr; break;
|
||||
case 2: info->ether_addr = host.ether_addr; break;
|
||||
case 3: info->mtu = 1500; break;
|
||||
case 4:
|
||||
info->link = (NetCtlConnected() ? NET_CTL_LINK_CONNECTED : NET_CTL_LINK_DISCONNECTED);
|
||||
break;
|
||||
case 4: info->link = NET_CTL_LINK_CONNECTED; break;
|
||||
case 5: break;
|
||||
case 6: break;
|
||||
case 7: info->wifi_security = 0; break;
|
||||
|
|
@ -3748,28 +3780,25 @@ int KYTY_SYSV_ABI NetCtlGetInfo(int code, NetCtlInfo* info) {
|
|||
case 12: break;
|
||||
case 13: break;
|
||||
case 14:
|
||||
std::strncpy(info->ip_address, QueryHostNetworkInfo().ip_address,
|
||||
std::strncpy(info->ip_address, host.ip_address,
|
||||
sizeof(info->ip_address) - 1);
|
||||
break;
|
||||
case 15:
|
||||
std::strncpy(info->netmask, QueryHostNetworkInfo().netmask, sizeof(info->netmask) - 1);
|
||||
std::strncpy(info->netmask, host.netmask, sizeof(info->netmask) - 1);
|
||||
break;
|
||||
case 16:
|
||||
std::strncpy(info->default_route, QueryHostNetworkInfo().default_route,
|
||||
std::strncpy(info->default_route, host.default_route,
|
||||
sizeof(info->default_route) - 1);
|
||||
break;
|
||||
case 17:
|
||||
std::strncpy(info->primary_dns, QueryHostNetworkInfo().primary_dns,
|
||||
sizeof(info->primary_dns) - 1);
|
||||
std::strncpy(info->primary_dns, host.primary_dns, sizeof(info->primary_dns) - 1);
|
||||
break;
|
||||
case 18:
|
||||
std::strncpy(info->secondary_dns, QueryHostNetworkInfo().secondary_dns,
|
||||
sizeof(info->secondary_dns) - 1);
|
||||
std::strncpy(info->secondary_dns, host.secondary_dns, sizeof(info->secondary_dns) - 1);
|
||||
break;
|
||||
case 19: info->http_proxy_config = NET_CTL_HTTP_PROXY_OFF; break;
|
||||
case 20: break;
|
||||
case 21: info->http_proxy_port = 0; break;
|
||||
default: LOGF("\t unknown NetCtl info code: %d\n", code); return NET_CTL_ERROR_INVALID_CODE;
|
||||
}
|
||||
|
||||
return OK;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ LIB_NAME("Ngs2", "Ngs2");
|
|||
|
||||
constexpr int32_t NGS2_ERROR_INVALID_OUT_ADDRESS =
|
||||
static_cast<int32_t>(0x804a8010u);
|
||||
constexpr int32_t NGS2_ERROR_INVALID_NUM_WAVEFORM_BLOCKS =
|
||||
static_cast<int32_t>(0x804a8053u);
|
||||
constexpr int32_t NGS2_ERROR_FULL_WAVEFORM_BLOCKS =
|
||||
static_cast<int32_t>(0x804a8054u);
|
||||
constexpr int32_t NGS2_ERROR_INVALID_WAVEFORM_DATA =
|
||||
static_cast<int32_t>(0x804a8430u);
|
||||
constexpr int32_t NGS2_ERROR_INVALID_WAVEFORM_FORMAT =
|
||||
|
|
@ -549,6 +553,7 @@ struct Ngs2VoiceInternal {
|
|||
uint64_t sample_step = 0;
|
||||
uint64_t decoded_samples = 0;
|
||||
uint64_t decoded_bytes = 0;
|
||||
uint64_t source_generation = 0;
|
||||
uint32_t duration_remaining = 0;
|
||||
uint32_t skip_remaining = 0;
|
||||
const uint8_t* waveform_end = nullptr;
|
||||
|
|
@ -574,6 +579,19 @@ struct Ngs2VoiceInternal {
|
|||
filters.clear();
|
||||
}
|
||||
|
||||
void ClearWaveformBlocks() {
|
||||
++source_generation;
|
||||
blocks.clear();
|
||||
sample_phase = 0;
|
||||
duration_remaining = 0;
|
||||
skip_remaining = 0;
|
||||
compressed_input.clear();
|
||||
frame_cursor = 0;
|
||||
frame_samples = 0;
|
||||
next_frame_samples = 0;
|
||||
waveform_end = nullptr;
|
||||
}
|
||||
|
||||
void SetupSampler(const Ngs2WaveformFormat& format) {
|
||||
const bool deferred = format.waveform_type == 0 && format.num_channels == 0 &&
|
||||
format.sample_rate == 0 && format.config_data == 0 &&
|
||||
|
|
@ -582,23 +600,15 @@ struct Ngs2VoiceInternal {
|
|||
(format.frame_margin != 0 || format.frame_offset != 0 ||
|
||||
format.sample_rate == 0 || format.num_channels == 0));
|
||||
ResetSetupState();
|
||||
channels = format.num_channels;
|
||||
sample_rate = format.sample_rate;
|
||||
sample_phase = 0;
|
||||
sample_step = uint64_t(sample_rate) << 32u;
|
||||
decoded_samples = 0;
|
||||
decoded_bytes = 0;
|
||||
duration_remaining = 0;
|
||||
skip_remaining = 0;
|
||||
waveform_end = nullptr;
|
||||
blocks.clear();
|
||||
ClearWaveformBlocks();
|
||||
channels = format.num_channels;
|
||||
sample_rate = format.sample_rate;
|
||||
sample_step = uint64_t(sample_rate) << 32u;
|
||||
decoded_samples = 0;
|
||||
decoded_bytes = 0;
|
||||
decoder.reset();
|
||||
decoded_frame.clear();
|
||||
next_decoded_frame.clear();
|
||||
compressed_input.clear();
|
||||
frame_cursor = 0;
|
||||
frame_samples = 0;
|
||||
next_frame_samples = 0;
|
||||
accepts_blocks = true;
|
||||
if (deferred) {
|
||||
return;
|
||||
|
|
@ -625,6 +635,63 @@ struct Ngs2VoiceInternal {
|
|||
}
|
||||
}
|
||||
|
||||
int AddWaveformBlocks(const uint8_t* data, uint32_t flags, uint32_t count,
|
||||
const Ngs2WaveformBlock* waveform_blocks) {
|
||||
const auto capacity = rack->type == Ngs2RackType::Sampler
|
||||
? rack->option.sampler.max_waveform_blocks
|
||||
: rack->option.custom_sampler.max_waveform_blocks;
|
||||
if (count > capacity) {
|
||||
return NGS2_ERROR_INVALID_NUM_WAVEFORM_BLOCKS;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(!accepts_blocks || (flags & ~0x17u) != 0);
|
||||
const bool clear = (flags & 4u) != 0;
|
||||
const bool only_data = (flags & 2u) != 0;
|
||||
const bool join = only_data && (flags & 0x10u) == 0;
|
||||
EXIT_NOT_IMPLEMENTED(only_data && decoder == nullptr);
|
||||
const auto retained = clear ? 0 : blocks.size();
|
||||
Block tail = retained == 0 ? Block {} : blocks.back();
|
||||
std::vector<Block> pending;
|
||||
pending.reserve(count);
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
auto info = waveform_blocks[i];
|
||||
if (!only_data && info.num_samples == 0 && info.data_size == 0) {
|
||||
continue;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(!only_data && info.num_samples == 0);
|
||||
if (only_data) {
|
||||
info.num_repeats = 0;
|
||||
info.num_skip_samples = 0;
|
||||
info.num_samples = 0;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(info.num_repeats != 0 && rack->type != Ngs2RackType::Sampler);
|
||||
EXIT_NOT_IMPLEMENTED(decoder == nullptr &&
|
||||
(uint64_t(info.num_skip_samples) + info.num_samples) *
|
||||
channels * sizeof(int16_t) >
|
||||
info.data_size);
|
||||
Block block {data + info.data_offset, info};
|
||||
auto* previous = pending.empty() ? (retained == 0 ? nullptr : &tail)
|
||||
: &pending.back();
|
||||
if (join && previous != nullptr &&
|
||||
previous->data + previous->info.data_size == block.data) {
|
||||
previous->info.data_size += block.info.data_size;
|
||||
} else {
|
||||
pending.push_back(block);
|
||||
}
|
||||
}
|
||||
// Validate the complete addition before clearing, extending, or closing the stream.
|
||||
if (retained + pending.size() > capacity) {
|
||||
return NGS2_ERROR_FULL_WAVEFORM_BLOCKS;
|
||||
}
|
||||
if (clear) {
|
||||
ClearWaveformBlocks();
|
||||
} else if (retained != 0) {
|
||||
blocks.back().info.data_size = tail.info.data_size;
|
||||
}
|
||||
blocks.insert(blocks.end(), pending.begin(), pending.end());
|
||||
accepts_blocks = (flags & 1u) != 0;
|
||||
return OK;
|
||||
}
|
||||
|
||||
void SetMatrix(uint32_t index, const float* levels, uint32_t count) {
|
||||
EXIT_NOT_IMPLEMENTED(index >= matrices.size());
|
||||
matrices[index].assign(levels, levels + count);
|
||||
|
|
@ -1425,9 +1492,10 @@ static_assert(offsetof(Ngs2VoiceCallbackInfo, size) == 40 &&
|
|||
offsetof(Ngs2VoiceCallbackInfo, repeats) == 44 &&
|
||||
offsetof(Ngs2VoiceCallbackInfo, attributes) == 48);
|
||||
|
||||
static void Ngs2FinishBlock(Ngs2VoiceInternal& voice) {
|
||||
auto& block = voice.blocks.front();
|
||||
const bool repeat = block.info.num_repeats != 0;
|
||||
static bool Ngs2FinishBlock(Ngs2VoiceInternal& voice) {
|
||||
const auto generation = voice.source_generation;
|
||||
auto& block = voice.blocks.front();
|
||||
const bool repeat = block.info.num_repeats != 0;
|
||||
voice.waveform_end = voice.WaveformData();
|
||||
if (repeat) {
|
||||
if (block.info.num_repeats != UINT32_MAX) {
|
||||
|
|
@ -1458,32 +1526,41 @@ static void Ngs2FinishBlock(Ngs2VoiceInternal& voice) {
|
|||
reinterpret_cast<void KYTY_SYSV_ABI (*)(const Ngs2VoiceCallbackInfo*)>(voice.callback)(
|
||||
&info);
|
||||
}
|
||||
return voice.source_generation == generation;
|
||||
}
|
||||
|
||||
static void Ngs2AdvancePcm(Ngs2VoiceInternal& voice) {
|
||||
const auto output_rate = uint64_t(voice.rack->ngs->option.sample_rate) << 32u;
|
||||
// Completion callbacks may interrupt advancement across multiple blocks.
|
||||
static bool Ngs2DecodePcmFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
|
||||
uint32_t& frame_samples) {
|
||||
// PCM has no encoded frame size. Use one system grain as a bounded staging
|
||||
// quantum, keeping source decoding separate from resampled output consumption.
|
||||
const auto quantum = std::max(voice.rack->ngs->option.num_grain_samples, 1u);
|
||||
frame.resize(size_t(quantum) * voice.channels);
|
||||
frame_samples = 0;
|
||||
while (voice.state == Ngs2VoicePlayState::Playing && !voice.blocks.empty() &&
|
||||
voice.sample_phase >= output_rate) {
|
||||
auto& block = voice.blocks.front();
|
||||
const auto available = block.info.num_samples - block.cursor;
|
||||
const auto advance = std::min(voice.sample_phase / output_rate,
|
||||
uint64_t(available));
|
||||
block.cursor += static_cast<uint32_t>(advance);
|
||||
voice.decoded_samples += advance;
|
||||
voice.decoded_bytes += advance * voice.channels * sizeof(int16_t);
|
||||
voice.sample_phase -= advance * output_rate;
|
||||
if (block.cursor == block.info.num_samples) {
|
||||
Ngs2FinishBlock(voice);
|
||||
frame_samples < quantum) {
|
||||
auto& block = voice.blocks.front();
|
||||
const auto count = std::min(quantum - frame_samples, block.info.num_samples - block.cursor);
|
||||
const auto* pcm = reinterpret_cast<const int16_t*>(voice.WaveformData());
|
||||
for (size_t i = 0; i < size_t(count) * voice.channels; ++i) {
|
||||
frame[size_t(frame_samples) * voice.channels + i] = pcm[i] / 32768.0f;
|
||||
}
|
||||
block.cursor += count;
|
||||
frame_samples += count;
|
||||
voice.decoded_samples += count;
|
||||
voice.decoded_bytes += size_t(count) * voice.channels * sizeof(int16_t);
|
||||
if (block.cursor == block.info.num_samples && !Ngs2FinishBlock(voice)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (voice.blocks.empty() && !voice.accepts_blocks) {
|
||||
voice.sample_phase = 0;
|
||||
}
|
||||
return frame_samples != 0;
|
||||
}
|
||||
|
||||
static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
|
||||
uint32_t& frame_samples) {
|
||||
if (voice.decoder == nullptr) {
|
||||
return Ngs2DecodePcmFrame(voice, frame, frame_samples);
|
||||
}
|
||||
frame_samples = 0;
|
||||
while (voice.state == Ngs2VoicePlayState::Playing && !voice.blocks.empty()) {
|
||||
auto& block = voice.blocks.front();
|
||||
if (block.info.num_samples != 0 && block.data_cursor == 0) {
|
||||
|
|
@ -1491,7 +1568,9 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
|
|||
voice.skip_remaining = block.info.num_skip_samples;
|
||||
}
|
||||
if (voice.duration_remaining == 0) {
|
||||
Ngs2FinishBlock(voice);
|
||||
if (!Ngs2FinishBlock(voice)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const auto skip =
|
||||
|
|
@ -1512,7 +1591,9 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
|
|||
block.data_cursor += bytes;
|
||||
voice.decoded_bytes += bytes;
|
||||
if (voice.compressed_input.size() < info.next_frame_size) {
|
||||
Ngs2FinishBlock(voice);
|
||||
if (!Ngs2FinishBlock(voice)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
@ -1529,7 +1610,9 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
|
|||
block.data + block.info.data_size);
|
||||
voice.decoded_bytes += block.info.data_size - block.data_cursor;
|
||||
block.data_cursor = block.info.data_size;
|
||||
Ngs2FinishBlock(voice);
|
||||
if (!Ngs2FinishBlock(voice)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(result.result != OK || result.input_consumed == 0);
|
||||
|
|
@ -1547,13 +1630,16 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
|
|||
EXIT_NOT_IMPLEMENTED(decoded_samples > voice.duration_remaining);
|
||||
frame_samples = decoded_samples;
|
||||
voice.duration_remaining -= decoded_samples;
|
||||
voice.decoded_samples += decoded_samples;
|
||||
}
|
||||
if (voice.duration_remaining == 0 ||
|
||||
(block.data_cursor == block.info.data_size && voice.compressed_input.empty())) {
|
||||
if (voice.duration_remaining == 0) {
|
||||
voice.compressed_input.clear();
|
||||
}
|
||||
Ngs2FinishBlock(voice);
|
||||
if (!Ngs2FinishBlock(voice)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (decoded_samples != 0) {
|
||||
return true;
|
||||
|
|
@ -1562,8 +1648,10 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
|
|||
return false;
|
||||
}
|
||||
|
||||
static bool Ngs2CurrentAtracFrame(Ngs2VoiceInternal& voice) {
|
||||
static bool Ngs2CurrentFrame(Ngs2VoiceInternal& voice) {
|
||||
if (voice.frame_cursor == voice.frame_samples) {
|
||||
voice.frame_cursor = 0;
|
||||
voice.frame_samples = 0;
|
||||
if (voice.next_frame_samples != 0) {
|
||||
std::swap(voice.decoded_frame, voice.next_decoded_frame);
|
||||
voice.frame_samples = voice.next_frame_samples;
|
||||
|
|
@ -1571,21 +1659,21 @@ static bool Ngs2CurrentAtracFrame(Ngs2VoiceInternal& voice) {
|
|||
} else if (!Ngs2DecodeFrame(voice, voice.decoded_frame, voice.frame_samples)) {
|
||||
return false;
|
||||
}
|
||||
voice.frame_cursor = 0;
|
||||
}
|
||||
return voice.state == Ngs2VoicePlayState::Playing &&
|
||||
voice.frame_cursor < voice.frame_samples;
|
||||
}
|
||||
|
||||
static void Ngs2AdvanceAtrac9(Ngs2VoiceInternal& voice) {
|
||||
static void Ngs2AdvanceSamples(Ngs2VoiceInternal& voice) {
|
||||
const auto output_rate = uint64_t(voice.rack->ngs->option.sample_rate) << 32u;
|
||||
while (voice.state == Ngs2VoicePlayState::Playing && voice.sample_phase >= output_rate) {
|
||||
if (!Ngs2CurrentAtracFrame(voice)) {
|
||||
if (!Ngs2CurrentFrame(voice)) {
|
||||
break;
|
||||
}
|
||||
++voice.frame_cursor;
|
||||
++voice.decoded_samples;
|
||||
voice.sample_phase -= output_rate;
|
||||
const auto advance = std::min(voice.sample_phase / output_rate,
|
||||
uint64_t(voice.frame_samples - voice.frame_cursor));
|
||||
voice.frame_cursor += static_cast<uint32_t>(advance);
|
||||
voice.sample_phase -= advance * output_rate;
|
||||
}
|
||||
if (voice.blocks.empty() && voice.frame_cursor == voice.frame_samples &&
|
||||
voice.next_frame_samples == 0 && !voice.accepts_blocks) {
|
||||
|
|
@ -1594,42 +1682,14 @@ static void Ngs2AdvanceAtrac9(Ngs2VoiceInternal& voice) {
|
|||
}
|
||||
|
||||
static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
|
||||
uint32_t output = 0;
|
||||
const auto generation = voice.source_generation;
|
||||
uint32_t output = 0;
|
||||
const auto output_rate = uint64_t(voice.rack->ngs->option.sample_rate) << 32u;
|
||||
while (voice.state == Ngs2VoicePlayState::Playing && output < grain &&
|
||||
(!voice.blocks.empty() || voice.frame_cursor < voice.frame_samples ||
|
||||
voice.next_frame_samples != 0)) {
|
||||
if (voice.decoder == nullptr) {
|
||||
Ngs2AdvancePcm(voice);
|
||||
if (voice.state != Ngs2VoicePlayState::Playing || voice.blocks.empty()) {
|
||||
break;
|
||||
}
|
||||
const auto& block = voice.blocks.front();
|
||||
const auto* pcm = reinterpret_cast<const int16_t*>(voice.WaveformData());
|
||||
const auto* next = pcm;
|
||||
if (block.cursor + 1 < block.info.num_samples) {
|
||||
next += voice.channels;
|
||||
} else if (block.info.num_repeats != 0) {
|
||||
next = reinterpret_cast<const int16_t*>(block.data) +
|
||||
block.info.num_skip_samples * voice.channels;
|
||||
} else if (voice.blocks.size() > 1) {
|
||||
const auto& next_block = voice.blocks[1];
|
||||
next = reinterpret_cast<const int16_t*>(next_block.data) +
|
||||
next_block.info.num_skip_samples * voice.channels;
|
||||
}
|
||||
const float fraction = static_cast<float>(voice.sample_phase) /
|
||||
static_cast<float>(output_rate);
|
||||
for (uint32_t c = 0; c < voice.channels; ++c) {
|
||||
voice.samples[c * grain + output] =
|
||||
std::lerp(static_cast<float>(pcm[c]), static_cast<float>(next[c]), fraction) /
|
||||
32768.0f;
|
||||
}
|
||||
voice.sample_phase += voice.sample_step;
|
||||
++output;
|
||||
continue;
|
||||
}
|
||||
Ngs2AdvanceAtrac9(voice);
|
||||
if (!Ngs2CurrentAtracFrame(voice)) {
|
||||
Ngs2AdvanceSamples(voice);
|
||||
if (voice.source_generation != generation || !Ngs2CurrentFrame(voice)) {
|
||||
break;
|
||||
}
|
||||
if (voice.frame_cursor + 1 == voice.frame_samples && voice.sample_phase != 0) {
|
||||
|
|
@ -1639,7 +1699,8 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
|
|||
}
|
||||
Ngs2DecodeFrame(voice, voice.next_decoded_frame, voice.next_frame_samples);
|
||||
}
|
||||
if (voice.state != Ngs2VoicePlayState::Playing ||
|
||||
if (voice.source_generation != generation ||
|
||||
voice.state != Ngs2VoicePlayState::Playing ||
|
||||
voice.frame_cursor >= voice.frame_samples) {
|
||||
break;
|
||||
}
|
||||
|
|
@ -1657,10 +1718,13 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
|
|||
voice.sample_phase += voice.sample_step;
|
||||
++output;
|
||||
}
|
||||
if (voice.decoder == nullptr) {
|
||||
Ngs2AdvancePcm(voice);
|
||||
} else {
|
||||
Ngs2AdvanceAtrac9(voice);
|
||||
if (voice.source_generation == generation) {
|
||||
Ngs2AdvanceSamples(voice);
|
||||
}
|
||||
if (voice.source_generation != generation) {
|
||||
// A callback replaced the source, possibly changing its channel layout.
|
||||
voice.samples.assign(size_t(grain) * voice.channels, 0.0f);
|
||||
output = 0;
|
||||
}
|
||||
voice.has_samples = output != 0;
|
||||
}
|
||||
|
|
@ -2370,41 +2434,10 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
|
|||
const Ngs2WaveformBlock* blocks;
|
||||
};
|
||||
const auto& blocks = *reinterpret_cast<const BlocksParam*>(param);
|
||||
EXIT_NOT_IMPLEMENTED(!voice->accepts_blocks ||
|
||||
(blocks.flags & ~0x17u) != 0);
|
||||
if ((blocks.flags & 0x4u) != 0) {
|
||||
voice->blocks.clear();
|
||||
voice->sample_phase = 0;
|
||||
voice->duration_remaining = 0;
|
||||
voice->skip_remaining = 0;
|
||||
voice->compressed_input.clear();
|
||||
voice->frame_cursor = 0;
|
||||
voice->frame_samples = 0;
|
||||
voice->next_frame_samples = 0;
|
||||
voice->waveform_end = nullptr;
|
||||
}
|
||||
voice->accepts_blocks = (blocks.flags & 1u) != 0;
|
||||
const bool only_data = (blocks.flags & 2u) != 0;
|
||||
EXIT_NOT_IMPLEMENTED(only_data && voice->decoder == nullptr);
|
||||
for (uint32_t i = 0; i < blocks.count; ++i) {
|
||||
auto block = blocks.blocks[i];
|
||||
if (!only_data && block.num_samples == 0 && block.data_size == 0) {
|
||||
continue;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(!only_data && block.num_samples == 0);
|
||||
if (only_data) {
|
||||
block.num_repeats = 0;
|
||||
block.num_skip_samples = 0;
|
||||
block.num_samples = 0;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(block.num_repeats != 0 &&
|
||||
voice->rack->type != Ngs2RackType::Sampler);
|
||||
EXIT_NOT_IMPLEMENTED(
|
||||
voice->decoder == nullptr &&
|
||||
(uint64_t(block.num_skip_samples) + block.num_samples) *
|
||||
voice->channels * 2 >
|
||||
block.data_size);
|
||||
voice->blocks.push_back({blocks.data + block.data_offset, block});
|
||||
const auto result = voice->AddWaveformBlocks(
|
||||
blocks.data, blocks.flags, blocks.count, blocks.blocks);
|
||||
if (result != OK) {
|
||||
return result;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -2511,6 +2544,15 @@ int KYTY_SYSV_ABI Ngs2VoiceRunCommands(uintptr_t voice_handle, const void* comma
|
|||
return OK;
|
||||
}
|
||||
|
||||
template <typename State>
|
||||
static void Ngs2FillSamplerState(const Ngs2VoiceInternal& voice, State& state) {
|
||||
state.voice_state.state_flags = Ngs2GetStateFlags(&voice);
|
||||
state.num_decoded_samples = voice.decoded_samples;
|
||||
state.decoded_data_size = voice.decoded_bytes;
|
||||
state.waveform_data = voice.WaveformData();
|
||||
state.user_data = voice.blocks.empty() ? 0 : voice.blocks.front().info.user_data;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI Ngs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* state,
|
||||
size_t state_size) {
|
||||
PRINT_NAME();
|
||||
|
|
@ -2547,7 +2589,7 @@ int KYTY_SYSV_ABI Ngs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* stat
|
|||
EXIT_NOT_IMPLEMENTED(state_size != configured_size);
|
||||
std::memset(state, 0, state_size);
|
||||
auto* sampler = reinterpret_cast<Ngs2CustomSamplerVoiceState*>(state);
|
||||
sampler->voice_state.state_flags = Ngs2GetStateFlags(voice);
|
||||
Ngs2FillSamplerState(*voice, *sampler);
|
||||
break;
|
||||
}
|
||||
case Ngs2RackType::Sampler: {
|
||||
|
|
@ -2568,12 +2610,7 @@ int KYTY_SYSV_ABI Ngs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* stat
|
|||
sampler->envelope_height = 1.0f;
|
||||
sampler->peak_height = 0.0f;
|
||||
sampler->reserved = 0;
|
||||
sampler->num_decoded_samples = voice->decoded_samples;
|
||||
sampler->decoded_data_size = voice->decoded_bytes;
|
||||
sampler->waveform_data = voice->WaveformData();
|
||||
if (!voice->blocks.empty()) {
|
||||
sampler->user_data = voice->blocks.front().info.user_data;
|
||||
}
|
||||
Ngs2FillSamplerState(*voice, *sampler);
|
||||
break;
|
||||
}
|
||||
default: EXIT("unknown type: %s\n", magic_enum::enum_name(voice->rack->type));
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
|
@ -77,6 +78,7 @@ public:
|
|||
explicit Instance(const Config& config): m_config(config) {}
|
||||
|
||||
~Instance() {
|
||||
ClearFrames();
|
||||
ClearPictureMetadata();
|
||||
if (m_sws != nullptr) {
|
||||
sws_freeContext(m_sws);
|
||||
|
|
@ -119,116 +121,138 @@ public:
|
|||
|
||||
[[nodiscard]] uint32_t CodecType() const { return m_config.codec_type; }
|
||||
|
||||
[[nodiscard]] Result Submit(const Input& input, uint32_t* produced) {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
return SubmitInput(input, produced);
|
||||
}
|
||||
|
||||
[[nodiscard]] Result Receive(const FrameBuffer& frame_buffer, Output* output) {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
return ReceiveOutput(frame_buffer, output);
|
||||
}
|
||||
|
||||
[[nodiscard]] Result Finalize() {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
if (m_finalized) return Result::Ok;
|
||||
const auto drained = DrainFrames();
|
||||
if (drained != Result::Ok) return drained;
|
||||
const int result = avcodec_send_packet(m_codec, nullptr);
|
||||
if (result == AVERROR(EAGAIN)) return Result::InputFull;
|
||||
if (result < 0 && result != AVERROR_EOF) return Result::ApiFail;
|
||||
m_finalized = true;
|
||||
return DrainFrames();
|
||||
}
|
||||
|
||||
[[nodiscard]] Result DecodeInput(const Input& input, const FrameBuffer& frame_buffer,
|
||||
Output* output) {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
*output = {};
|
||||
|
||||
AVPacket* packet = av_packet_alloc();
|
||||
AVFrame* frame = av_frame_alloc();
|
||||
if (packet == nullptr || frame == nullptr ||
|
||||
input.size > static_cast<size_t>(std::numeric_limits<int>::max())) {
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
return Result::ApiFail;
|
||||
uint32_t produced = 0;
|
||||
auto result = SubmitInput(input, &produced);
|
||||
if (result == Result::InputFull) {
|
||||
// The combined API supplies an output buffer together with each AU.
|
||||
result = ReceiveOutput(frame_buffer, output);
|
||||
if (result != Result::Ok) return result;
|
||||
return SubmitInput(input, &produced);
|
||||
}
|
||||
|
||||
int result = av_new_packet(packet, static_cast<int>(input.size));
|
||||
if (result < 0) {
|
||||
LOGF("Videodec2: av_new_packet failed: %s (%d)\n", AvErrorString(result), result);
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
return Result::ApiFail;
|
||||
}
|
||||
std::memcpy(packet->data, input.data, input.size);
|
||||
packet->pts = ToAvTimestamp(input.pts);
|
||||
packet->dts = ToAvTimestamp(input.dts);
|
||||
|
||||
packet->opaque_ref = av_buffer_alloc(sizeof(PacketMetadata));
|
||||
if (packet->opaque_ref == nullptr) {
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
return Result::ApiFail;
|
||||
}
|
||||
const PacketMetadata metadata {input.pts, input.dts, input.attached_data};
|
||||
std::memcpy(packet->opaque_ref->data, &metadata, sizeof(metadata));
|
||||
|
||||
bool have_pending_frame = false;
|
||||
result = avcodec_send_packet(m_codec, packet);
|
||||
if (result == AVERROR(EAGAIN)) {
|
||||
result = avcodec_receive_frame(m_codec, frame);
|
||||
if (result < 0) {
|
||||
LOGF("Videodec2: decoder rejected an AU while no output was available: %s (%d)\n",
|
||||
AvErrorString(result), result);
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
return Result::AccessUnit;
|
||||
}
|
||||
have_pending_frame = true;
|
||||
result = avcodec_send_packet(m_codec, packet);
|
||||
}
|
||||
if (result < 0) {
|
||||
LOGF("Videodec2: avcodec_send_packet failed: %s (%d)\n", AvErrorString(result), result);
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
return Result::AccessUnit;
|
||||
}
|
||||
|
||||
Result decode_result = Result::Ok;
|
||||
if (!have_pending_frame) {
|
||||
result = avcodec_receive_frame(m_codec, frame);
|
||||
if (result != AVERROR(EAGAIN) && result != AVERROR_EOF) {
|
||||
if (result < 0) {
|
||||
LOGF("Videodec2: avcodec_receive_frame failed: %s (%d)\n",
|
||||
AvErrorString(result), result);
|
||||
decode_result = Result::AccessUnit;
|
||||
} else {
|
||||
decode_result = CopyFrame(frame, frame_buffer, output);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
decode_result = CopyFrame(frame, frame_buffer, output);
|
||||
}
|
||||
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
return decode_result;
|
||||
if (result != Result::Ok) return result;
|
||||
result = ReceiveOutput(frame_buffer, output);
|
||||
return result == Result::NoFrame ? Result::Ok : result;
|
||||
}
|
||||
|
||||
[[nodiscard]] Result FlushOutput(const FrameBuffer& frame_buffer, Output* output) {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
*output = {};
|
||||
|
||||
AVFrame* frame = av_frame_alloc();
|
||||
if (frame == nullptr) {
|
||||
return Result::ApiFail;
|
||||
}
|
||||
|
||||
// Guest Flush collects available pictures between AUs while retaining reference frames.
|
||||
const int receive_result = avcodec_receive_frame(m_codec, frame);
|
||||
if (receive_result == AVERROR(EAGAIN) || receive_result == AVERROR_EOF) {
|
||||
av_frame_free(&frame);
|
||||
return Result::Ok;
|
||||
}
|
||||
if (receive_result < 0) {
|
||||
LOGF("Videodec2: receiving a flushed frame failed: %s (%d)\n",
|
||||
AvErrorString(receive_result), receive_result);
|
||||
av_frame_free(&frame);
|
||||
return Result::ApiFail;
|
||||
}
|
||||
|
||||
const auto result = CopyFrame(frame, frame_buffer, output);
|
||||
av_frame_free(&frame);
|
||||
return result;
|
||||
const auto result = ReceiveOutput(frame_buffer, output);
|
||||
return result == Result::NoFrame ? Result::Ok : result;
|
||||
}
|
||||
|
||||
void ResetDecoder() {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
avcodec_flush_buffers(m_codec);
|
||||
ClearFrames();
|
||||
ClearPictureMetadata();
|
||||
m_finalized = m_eof = false;
|
||||
}
|
||||
|
||||
private:
|
||||
struct PendingFrame {
|
||||
AVFrame* frame;
|
||||
PictureInfo picture;
|
||||
};
|
||||
|
||||
void ClearFrames() {
|
||||
for (auto& pending: m_frames) av_frame_free(&pending.frame);
|
||||
m_frames.clear();
|
||||
}
|
||||
|
||||
[[nodiscard]] Result DrainFrames(uint32_t* produced = nullptr) {
|
||||
while (!m_eof && m_frames.size() < m_config.max_pending_frames) {
|
||||
AVFrame* frame = av_frame_alloc();
|
||||
if (frame == nullptr) return Result::ApiFail;
|
||||
const int result = avcodec_receive_frame(m_codec, frame);
|
||||
if (result < 0) {
|
||||
av_frame_free(&frame);
|
||||
if (result == AVERROR_EOF) m_eof = true;
|
||||
if (result == AVERROR(EAGAIN) || m_eof) return Result::Ok;
|
||||
LOGF("Video decoder: avcodec_receive_frame failed: %s (%d)\n",
|
||||
AvErrorString(result), result);
|
||||
return Result::AccessUnit;
|
||||
}
|
||||
m_frames.push_back({frame, MakePictureInfo(frame)});
|
||||
if (produced != nullptr) ++*produced;
|
||||
}
|
||||
return Result::Ok;
|
||||
}
|
||||
|
||||
[[nodiscard]] Result SubmitInput(const Input& input, uint32_t* produced) {
|
||||
*produced = 0;
|
||||
if (m_finalized) return Result::ApiFail;
|
||||
const auto drained = DrainFrames();
|
||||
if (drained != Result::Ok) return drained;
|
||||
if (m_frames.size() >= m_config.max_pending_frames) return Result::InputFull;
|
||||
if (input.size > static_cast<size_t>(std::numeric_limits<int>::max())) return Result::ApiFail;
|
||||
AVPacket* packet = av_packet_alloc();
|
||||
if (packet == nullptr) return Result::ApiFail;
|
||||
int result = av_new_packet(packet, static_cast<int>(input.size));
|
||||
if (result < 0) {
|
||||
av_packet_free(&packet);
|
||||
return Result::ApiFail;
|
||||
}
|
||||
std::memcpy(packet->data, input.data, input.size);
|
||||
packet->pts = ToAvTimestamp(input.pts);
|
||||
packet->dts = ToAvTimestamp(input.dts);
|
||||
packet->opaque_ref = av_buffer_alloc(sizeof(PacketMetadata));
|
||||
if (packet->opaque_ref == nullptr) {
|
||||
av_packet_free(&packet);
|
||||
return Result::ApiFail;
|
||||
}
|
||||
const PacketMetadata metadata {input.pts, input.dts, input.attached_data};
|
||||
std::memcpy(packet->opaque_ref->data, &metadata, sizeof(metadata));
|
||||
result = avcodec_send_packet(m_codec, packet);
|
||||
av_packet_free(&packet);
|
||||
if (result == AVERROR(EAGAIN)) return Result::InputFull; // The AU was not accepted.
|
||||
if (result < 0) {
|
||||
LOGF("Video decoder: submitting an AU failed: %s (%d)\n", AvErrorString(result), result);
|
||||
return Result::AccessUnit;
|
||||
}
|
||||
// Once accepted, a full output queue cannot turn this AU into a retry.
|
||||
return DrainFrames(produced);
|
||||
}
|
||||
|
||||
[[nodiscard]] Result ReceiveOutput(const FrameBuffer& frame_buffer, Output* output) {
|
||||
*output = {};
|
||||
const auto drained = DrainFrames();
|
||||
if (drained != Result::Ok) return drained;
|
||||
if (m_frames.empty()) return Result::NoFrame;
|
||||
auto& pending = m_frames.front();
|
||||
const auto result = CopyFrame(pending.frame, pending.picture, frame_buffer, output);
|
||||
if (result != Result::Ok) return result;
|
||||
output->last_frame = m_finalized && m_eof && m_frames.size() == 1u;
|
||||
av_frame_free(&pending.frame);
|
||||
m_frames.pop_front();
|
||||
return Result::Ok;
|
||||
}
|
||||
|
||||
[[nodiscard]] PictureInfo MakePictureInfo(const AVFrame* frame) const {
|
||||
PictureInfo result {};
|
||||
if (frame->opaque_ref != nullptr && frame->opaque_ref->size >= sizeof(PacketMetadata)) {
|
||||
|
|
@ -268,8 +292,8 @@ private:
|
|||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] Result CopyFrame(const AVFrame* frame, const FrameBuffer& frame_buffer,
|
||||
Output* output) {
|
||||
[[nodiscard]] Result CopyFrame(const AVFrame* frame, const PictureInfo& picture,
|
||||
const FrameBuffer& frame_buffer, Output* output) {
|
||||
if (frame->width <= 0 || frame->height <= 0) {
|
||||
return Result::ApiFail;
|
||||
}
|
||||
|
|
@ -329,7 +353,7 @@ private:
|
|||
|
||||
{
|
||||
std::scoped_lock lock(g_picture_mutex);
|
||||
g_picture_infos[frame_buffer.data] = {this, MakePictureInfo(frame)};
|
||||
g_picture_infos[frame_buffer.data] = {this, picture};
|
||||
m_picture_buffers.insert(frame_buffer.data);
|
||||
}
|
||||
return Result::Ok;
|
||||
|
|
@ -346,6 +370,9 @@ private:
|
|||
m_picture_buffers.clear();
|
||||
}
|
||||
|
||||
std::deque<PendingFrame> m_frames;
|
||||
bool m_finalized = false;
|
||||
bool m_eof = false;
|
||||
Config m_config;
|
||||
AVCodecContext* m_codec = nullptr;
|
||||
SwsContext* m_sws = nullptr;
|
||||
|
|
@ -358,7 +385,7 @@ bool IsCodecSupported(uint32_t codec_type) {
|
|||
}
|
||||
|
||||
Instance* Create(const Config& config) {
|
||||
if (!IsCodecSupported(config.codec_type)) {
|
||||
if (!IsCodecSupported(config.codec_type) || config.max_pending_frames < 2u) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* instance = new Instance(config);
|
||||
|
|
@ -377,6 +404,18 @@ uint32_t GetCodecType(const Instance* instance) {
|
|||
return instance->CodecType();
|
||||
}
|
||||
|
||||
Result Submit(Instance* instance, const Input& input, uint32_t* produced) {
|
||||
return instance->Submit(input, produced);
|
||||
}
|
||||
|
||||
Result Receive(Instance* instance, const FrameBuffer& frame_buffer, Output* output) {
|
||||
return instance->Receive(frame_buffer, output);
|
||||
}
|
||||
|
||||
Result Finalize(Instance* instance) {
|
||||
return instance->Finalize();
|
||||
}
|
||||
|
||||
Result Decode(Instance* instance, const Input& input, const FrameBuffer& frame_buffer,
|
||||
Output* output) {
|
||||
return instance->DecodeInput(input, frame_buffer, output);
|
||||
|
|
@ -390,6 +429,44 @@ void Reset(Instance* instance) {
|
|||
instance->ResetDecoder();
|
||||
}
|
||||
|
||||
AvcPictureInfo MakeAvcPictureInfo(const PictureInfo& decoded, size_t size) {
|
||||
AvcPictureInfo picture {};
|
||||
picture.this_size = size;
|
||||
picture.is_valid = true;
|
||||
picture.pts_data = decoded.pts;
|
||||
picture.dts_data = decoded.dts;
|
||||
picture.attached_data = decoded.attached_data;
|
||||
picture.idr_picture_flag = decoded.key_frame ? 1 : 0;
|
||||
picture.profile_idc = static_cast<uint8_t>(decoded.profile);
|
||||
picture.level_idc = static_cast<uint8_t>(decoded.level);
|
||||
picture.pic_width_in_mbs_minus1 = (decoded.width + 15u) / 16u - 1u;
|
||||
picture.pic_height_in_map_units_minus1 = (decoded.height + 15u) / 16u - 1u;
|
||||
picture.frame_mbs_only_flag = 1;
|
||||
picture.frame_cropping_flag = decoded.crop_left != 0 || decoded.crop_right != 0 ||
|
||||
decoded.crop_top != 0 || decoded.crop_bottom != 0
|
||||
? 1
|
||||
: 0;
|
||||
picture.frame_crop_left_offset = decoded.crop_left;
|
||||
picture.frame_crop_right_offset = decoded.crop_right;
|
||||
picture.frame_crop_top_offset = decoded.crop_top;
|
||||
picture.frame_crop_bottom_offset = decoded.crop_bottom;
|
||||
picture.aspect_ratio_info_present_flag =
|
||||
decoded.sar_width != 0 && decoded.sar_height != 0 ? 1 : 0;
|
||||
picture.aspect_ratio_idc = picture.aspect_ratio_info_present_flag ? 255 : 0;
|
||||
picture.sar_width = decoded.sar_width;
|
||||
picture.sar_height = decoded.sar_height;
|
||||
picture.video_signal_type_present_flag = 1;
|
||||
picture.video_format = 5;
|
||||
picture.video_full_range_flag = decoded.color_range == 2 ? 1 : 0;
|
||||
picture.colour_description_present_flag =
|
||||
decoded.color_primaries != 0 || decoded.color_trc != 0 || decoded.color_space != 0 ? 1
|
||||
: 0;
|
||||
picture.colour_primaries = decoded.color_primaries;
|
||||
picture.transfer_characteristics = decoded.color_trc;
|
||||
picture.matrix_coefficients = decoded.color_space;
|
||||
return picture;
|
||||
}
|
||||
|
||||
bool GetPictureInfo(void* frame_buffer, PictureInfo* picture_info) {
|
||||
std::scoped_lock lock(g_picture_mutex);
|
||||
const auto it = g_picture_infos.find(frame_buffer);
|
||||
|
|
|
|||
|
|
@ -14,12 +14,15 @@ enum class Result {
|
|||
AccessUnit,
|
||||
FrameBufferSize,
|
||||
OversizeDecode,
|
||||
InputFull,
|
||||
NoFrame,
|
||||
};
|
||||
|
||||
struct Config {
|
||||
uint32_t codec_type = 0;
|
||||
int32_t max_width = -1;
|
||||
int32_t max_height = -1;
|
||||
uint32_t codec_type = 0;
|
||||
int32_t max_width = -1;
|
||||
int32_t max_height = -1;
|
||||
uint32_t max_pending_frames = 2;
|
||||
};
|
||||
|
||||
struct Input {
|
||||
|
|
@ -37,6 +40,7 @@ struct FrameBuffer {
|
|||
|
||||
struct Output {
|
||||
bool valid = false;
|
||||
bool last_frame = false;
|
||||
bool error_frame = false;
|
||||
bool buffer_accepted = false;
|
||||
uint32_t codec_type = 0;
|
||||
|
|
@ -69,12 +73,83 @@ struct PictureInfo {
|
|||
bool key_frame = false;
|
||||
};
|
||||
|
||||
struct AvcPictureInfo {
|
||||
size_t this_size;
|
||||
bool is_valid;
|
||||
|
||||
uint64_t pts_data;
|
||||
uint64_t dts_data;
|
||||
uint64_t attached_data;
|
||||
|
||||
uint8_t idr_picture_flag;
|
||||
uint8_t profile_idc;
|
||||
uint8_t level_idc;
|
||||
uint32_t pic_width_in_mbs_minus1;
|
||||
uint32_t pic_height_in_map_units_minus1;
|
||||
uint8_t frame_mbs_only_flag;
|
||||
|
||||
uint8_t frame_cropping_flag;
|
||||
uint32_t frame_crop_left_offset;
|
||||
uint32_t frame_crop_right_offset;
|
||||
uint32_t frame_crop_top_offset;
|
||||
uint32_t frame_crop_bottom_offset;
|
||||
|
||||
uint8_t aspect_ratio_info_present_flag;
|
||||
uint8_t aspect_ratio_idc;
|
||||
uint16_t sar_width;
|
||||
uint16_t sar_height;
|
||||
|
||||
uint8_t video_signal_type_present_flag;
|
||||
uint8_t video_format;
|
||||
uint8_t video_full_range_flag;
|
||||
uint8_t colour_description_present_flag;
|
||||
uint8_t colour_primaries;
|
||||
uint8_t transfer_characteristics;
|
||||
uint8_t matrix_coefficients;
|
||||
|
||||
uint8_t timing_info_present_flag;
|
||||
uint32_t num_units_in_tick;
|
||||
uint32_t time_scale;
|
||||
uint8_t fixed_frame_rate_flag;
|
||||
|
||||
uint8_t bitstream_restriction_flag;
|
||||
uint8_t max_dec_frame_buffering;
|
||||
|
||||
uint8_t pic_struct_present_flag;
|
||||
uint8_t pic_struct;
|
||||
uint8_t field_pic_flag;
|
||||
uint8_t bottom_field_flag;
|
||||
|
||||
uint8_t sequence_parameter_set_present_flag;
|
||||
uint8_t picture_parameter_set_present_flag;
|
||||
uint8_t au_delimiter_present_flag;
|
||||
uint8_t end_of_sequence_present_flag;
|
||||
uint8_t end_of_stream_present_flag;
|
||||
uint8_t filler_data_present_flag;
|
||||
uint8_t picture_timing_sei_present_flag;
|
||||
uint8_t buffering_period_sei_present_flag;
|
||||
|
||||
uint8_t constraint_set0_flag;
|
||||
uint8_t constraint_set1_flag;
|
||||
uint8_t constraint_set2_flag;
|
||||
uint8_t constraint_set3_flag;
|
||||
uint8_t constraint_set4_flag;
|
||||
uint8_t constraint_set5_flag;
|
||||
};
|
||||
|
||||
static_assert(sizeof(AvcPictureInfo) == 120);
|
||||
|
||||
[[nodiscard]] AvcPictureInfo MakeAvcPictureInfo(const PictureInfo& decoded, size_t size);
|
||||
|
||||
class Instance;
|
||||
|
||||
[[nodiscard]] bool IsCodecSupported(uint32_t codec_type);
|
||||
[[nodiscard]] Instance* Create(const Config& config);
|
||||
void Destroy(Instance* instance);
|
||||
[[nodiscard]] uint32_t GetCodecType(const Instance* instance);
|
||||
[[nodiscard]] Result Submit(Instance* instance, const Input& input, uint32_t* produced);
|
||||
[[nodiscard]] Result Receive(Instance* instance, const FrameBuffer& frame_buffer, Output* output);
|
||||
[[nodiscard]] Result Finalize(Instance* instance);
|
||||
[[nodiscard]] Result Decode(Instance* instance, const Input& input, const FrameBuffer& frame_buffer,
|
||||
Output* output);
|
||||
[[nodiscard]] Result Flush(Instance* instance, const FrameBuffer& frame_buffer, Output* output);
|
||||
|
|
|
|||
|
|
@ -1,59 +1,126 @@
|
|||
#include "emulator.h"
|
||||
|
||||
#include "common/file.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "emulator.h"
|
||||
#include "kernel/fileSystem.h"
|
||||
#include "libs/errno.h"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#elif defined(__linux__)
|
||||
#include <linux/close_range.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace Emulator {
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
static std::wstring QuoteArgument(std::wstring_view argument) {
|
||||
std::wstring quoted = L"\"";
|
||||
size_t slashes = 0;
|
||||
for (wchar_t c: argument) {
|
||||
if (c == L'\\') {
|
||||
++slashes;
|
||||
continue;
|
||||
}
|
||||
quoted.append(c == L'"' ? slashes * 2 + 1 : slashes, L'\\');
|
||||
quoted += c;
|
||||
slashes = 0;
|
||||
}
|
||||
quoted.append(slashes * 2, L'\\');
|
||||
return quoted + L'"';
|
||||
}
|
||||
#endif
|
||||
|
||||
int LoadExec(const char* path, const char* const argv[]) {
|
||||
using namespace Libs::SystemService;
|
||||
if (path == nullptr || !std::string_view(path).starts_with("/app0/") ||
|
||||
!Common::File::IsFileExisting(Libs::LibKernel::FileSystem::GetRealFilename(path))) {
|
||||
return SYSTEM_SERVICE_ERROR_PARAMETER;
|
||||
}
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX
|
||||
std::ifstream command("/proc/self/cmdline", std::ios::binary);
|
||||
std::vector<std::string> arguments;
|
||||
for (std::string argument; std::getline(command, argument, '\0') && argument != "--guest-exec";) {
|
||||
arguments.push_back(std::move(argument));
|
||||
}
|
||||
if (arguments.empty()) {
|
||||
return SYSTEM_SERVICE_ERROR_INTERNAL;
|
||||
}
|
||||
arguments.insert(arguments.end(), {"--guest-exec", path});
|
||||
size_t bytes = 0;
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS || defined(__linux__)
|
||||
std::vector<std::string> arguments {"--guest-exec", path};
|
||||
size_t bytes = 0;
|
||||
for (size_t i = 0; argv != nullptr && argv[i] != nullptr; ++i) {
|
||||
const auto size = strnlen(argv[i], 4096 - bytes) + 1;
|
||||
if (size > 4096 - bytes) {
|
||||
return SYSTEM_SERVICE_ERROR_PARAMETER;
|
||||
}
|
||||
bytes += size;
|
||||
arguments.emplace_back(argv[i]);
|
||||
arguments.emplace_back(argv[i], size - 1);
|
||||
}
|
||||
LOGF("LoadExec: %s\n", path);
|
||||
Log::Flush();
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
wchar_t executable[32768] {};
|
||||
const auto length = GetModuleFileNameW(nullptr, executable, 32768);
|
||||
if (length == 0 || length >= 32768) {
|
||||
return SYSTEM_SERVICE_ERROR_INTERNAL;
|
||||
}
|
||||
if (__wargv == nullptr) {
|
||||
return SYSTEM_SERVICE_ERROR_INTERNAL;
|
||||
}
|
||||
std::wstring command = QuoteArgument(executable);
|
||||
for (int i = 1; i < __argc && __wargv[i] != nullptr; ++i) {
|
||||
if (std::wstring_view(__wargv[i]) == L"--guest-exec") {
|
||||
break;
|
||||
}
|
||||
command += L" " + QuoteArgument(__wargv[i]);
|
||||
}
|
||||
for (const auto& argument: arguments) {
|
||||
const int size =
|
||||
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, argument.c_str(), -1, nullptr, 0);
|
||||
if (size == 0) {
|
||||
return SYSTEM_SERVICE_ERROR_PARAMETER;
|
||||
}
|
||||
std::wstring wide(size, L'\0');
|
||||
if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, argument.c_str(), -1, wide.data(),
|
||||
size) == 0) {
|
||||
return SYSTEM_SERVICE_ERROR_PARAMETER;
|
||||
}
|
||||
wide.resize(size - 1);
|
||||
command += L" " + QuoteArgument(wide);
|
||||
}
|
||||
STARTUPINFOW startup {};
|
||||
startup.cb = sizeof(startup);
|
||||
PROCESS_INFORMATION process {};
|
||||
if (!CreateProcessW(executable, command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW,
|
||||
nullptr, nullptr, &startup, &process)) {
|
||||
LOGF("LoadExec: process creation failed: %lu\n", GetLastError());
|
||||
return SYSTEM_SERVICE_ERROR_INTERNAL;
|
||||
}
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
std::fflush(nullptr);
|
||||
std::quick_exit(0);
|
||||
#else
|
||||
std::ifstream command("/proc/self/cmdline", std::ios::binary);
|
||||
std::vector<std::string> host_arguments;
|
||||
for (std::string argument;
|
||||
std::getline(command, argument, '\0') && argument != "--guest-exec";) {
|
||||
host_arguments.push_back(std::move(argument));
|
||||
}
|
||||
if (host_arguments.empty()) {
|
||||
return SYSTEM_SERVICE_ERROR_INTERNAL;
|
||||
}
|
||||
host_arguments.insert(host_arguments.end(), arguments.begin(), arguments.end());
|
||||
std::vector<char*> host_argv;
|
||||
for (auto& argument: arguments) {
|
||||
for (auto& argument: host_arguments) {
|
||||
host_argv.push_back(argument.data());
|
||||
}
|
||||
host_argv.push_back(nullptr);
|
||||
LOGF("LoadExec: %s\n", path);
|
||||
Log::Flush();
|
||||
if (close_range(3, ~0u, CLOSE_RANGE_UNSHARE | CLOSE_RANGE_CLOEXEC) == 0) {
|
||||
execvp(host_argv[0], host_argv.data());
|
||||
}
|
||||
LOGF("LoadExec failed: %s\n", std::strerror(errno));
|
||||
#endif
|
||||
return SYSTEM_SERVICE_ERROR_INTERNAL;
|
||||
#else
|
||||
return SYSTEM_SERVICE_ERROR_UNAVAILABLE;
|
||||
|
|
|
|||
|
|
@ -164,6 +164,8 @@ void Psf::OpenJson(const std::filesystem::path& file_name) {
|
|||
add_string_param("contentId", "CONTENT_ID");
|
||||
add_string_param("contentVersion", "APP_VER");
|
||||
add_string_param("appVersion", "APP_VER");
|
||||
AddParamInt("ATTRIBUTE", 0);
|
||||
add_int_param("attribute", "ATTRIBUTE");
|
||||
add_int_param("attribute3", "ATTRIBUTE3");
|
||||
add_int_param("userDefinedParam1", "USER_DEFINED_PARAM_1");
|
||||
add_int_param("userDefinedParam2", "USER_DEFINED_PARAM_2");
|
||||
|
|
|
|||
12
src/main.cpp
12
src/main.cpp
|
|
@ -61,6 +61,7 @@ static void PrintUsage() {
|
|||
::printf(
|
||||
" --gpu <index> Vulkan physical device index. Default: auto.\n");
|
||||
::printf(" --fullscreen Run in borderless desktop fullscreen.\n");
|
||||
::printf(" --hdr Enable HDR10 output when supported.\n");
|
||||
::printf(
|
||||
" --hide-cursor Hide the cursor after 2 s idle. Default: off.\n");
|
||||
::printf(" --no-splash Disable the startup splash screen.\n");
|
||||
|
|
@ -69,6 +70,7 @@ static void PrintUsage() {
|
|||
::printf(" --vblank-frequency <num> Virtual vblank frequency. Default: 60.\n");
|
||||
::printf(" --console-language <0-29> Console language. Default: 1 (English US).\n");
|
||||
::printf(" --vulkan-validation <true|false> Enable Vulkan validation.\n");
|
||||
::printf(" --network-connected <true|false> Use the host local network (default: false).\n");
|
||||
::printf(" --gpu-assisted-validation <t|f> Bounds-check shader accesses on the GPU.\n"
|
||||
" Implies --vulkan-validation; very slow.\n");
|
||||
::printf(" --shader-validation <true|false> Enable shader validation.\n");
|
||||
|
|
@ -215,6 +217,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
|||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--hdr") {
|
||||
options.config.hdr_enabled = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--hide-cursor") {
|
||||
options.config.hide_cursor_enabled = true;
|
||||
continue;
|
||||
|
|
@ -382,6 +389,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
|||
::printf("invalid console language: %s\n", value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--network-connected") {
|
||||
if (!ParseBool(value, options.config.network_connected)) {
|
||||
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--vulkan-validation") {
|
||||
if (!ParseBool(value, options.config.vulkan_validation_enabled)) {
|
||||
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@
|
|||
#include <vector>
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <arpa/inet.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <net/if.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
|
@ -898,6 +901,11 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
|
|||
using WriteCounter = int (KYTY_SYSV_ABI *)(void *, uint8_t, uint8_t, uint64_t,
|
||||
uint8_t, uint32_t);
|
||||
using WriteAddress = int (KYTY_SYSV_ABI *)(void *, volatile uint64_t *, uint64_t);
|
||||
using WriteImmediately = int (KYTY_SYSV_ABI *)(void *, volatile uint64_t *, uint64_t,
|
||||
uint32_t);
|
||||
using MeasureWriteAddress = uint64_t (KYTY_SYSV_ABI *)(volatile uint64_t *, uint64_t);
|
||||
using MeasureWriteImmediately = uint64_t (KYTY_SYSV_ABI *)(volatile uint64_t *, uint64_t,
|
||||
uint32_t);
|
||||
using ReadFile = int (KYTY_SYSV_ABI *)(void *, uint64_t, uint64_t, uint32_t,
|
||||
void *, uint64_t, uint64_t);
|
||||
using WriteKernelEvent = int (KYTY_SYSV_ABI *)(void *, uint64_t, uint64_t, uint64_t,
|
||||
|
|
@ -916,6 +924,9 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
|
|||
const auto wait_counter = reinterpret_cast<WaitCounter>(find("cQb8Zr8Q0Y0"));
|
||||
const auto write_counter = reinterpret_cast<WriteCounter>(find("jK+yuYCI7MA"));
|
||||
const auto write_address = reinterpret_cast<WriteAddress>(find("sJXyWHjP-F8"));
|
||||
const auto write_immediately = reinterpret_cast<WriteImmediately>(find("j0+3uJMxYJY"));
|
||||
const auto measure_write_address = reinterpret_cast<MeasureWriteAddress>(find("C+IEj+BsAFM"));
|
||||
const auto measure_write_immediately = reinterpret_cast<MeasureWriteImmediately>(find("4fgtGfXDrFc"));
|
||||
const auto read_file = reinterpret_cast<ReadFile>(find("mQ16-QdKv7k"));
|
||||
const auto write_event = reinterpret_cast<WriteKernelEvent>(find("H896Pt-yB4I"));
|
||||
const auto submit_apr = reinterpret_cast<SubmitApr>(find("ASoW5WE-UPo"));
|
||||
|
|
@ -927,7 +938,7 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
|
|||
std::array<uint32_t, 256> data {};
|
||||
};
|
||||
std::array<Buffer, 4> buffers;
|
||||
const auto reset = [&](size_t apr_count) {
|
||||
const auto reset = [&](size_t apr_count, uint32_t data_size = sizeof(Buffer::data)) {
|
||||
for (size_t i = 0; i < buffers.size(); ++i) {
|
||||
auto &buffer = buffers[i];
|
||||
construct(buffer.header.data());
|
||||
|
|
@ -936,10 +947,60 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
|
|||
} else {
|
||||
construct_amm(buffer.header.data());
|
||||
}
|
||||
Check(set_buffer(buffer.header.data(), buffer.data.data(), sizeof(buffer.data)) == OK,
|
||||
Check(set_buffer(buffer.header.data(), buffer.data.data(), data_size) == OK,
|
||||
"initialize AMPR command buffer");
|
||||
}
|
||||
};
|
||||
struct LiteralWrite { uint64_t value; uint32_t size; };
|
||||
constexpr std::array literal_writes {
|
||||
LiteralWrite{0, 8}, LiteralWrite{3, 8}, LiteralWrite{4, 12},
|
||||
LiteralWrite{(uint64_t{1} << 34) - 1, 12},
|
||||
LiteralWrite{uint64_t{1} << 34, 16}, LiteralWrite{UINT64_MAX, 16}};
|
||||
for (const auto &literal : literal_writes) {
|
||||
for (const bool immediately : std::array{false, true}) {
|
||||
reset(1, literal.size);
|
||||
auto *command_buffer = buffers[0].header.data();
|
||||
uint64_t fence = ~literal.value;
|
||||
const auto append = [&](uint64_t value) {
|
||||
return immediately ? write_immediately(command_buffer, &fence, value, 1)
|
||||
: write_address(command_buffer, &fence, value);
|
||||
};
|
||||
Check(measure_write_address(&fence, literal.value) == literal.size &&
|
||||
measure_write_immediately(&fence, literal.value, 1) == literal.size &&
|
||||
append(literal.value) == OK && offset(command_buffer) == literal.size,
|
||||
"literal write fits exactly its measured command size");
|
||||
Check(append(~literal.value) == Libs::LibKernel::KERNEL_ERROR_EBUSY &&
|
||||
offset(command_buffer) == literal.size,
|
||||
"rejected write preserves the full command buffer offset");
|
||||
Result result {1234, 5678};
|
||||
uint32_t submission = 0;
|
||||
Check(submit_apr(command_buffer, 3, &result, &submission) == OK &&
|
||||
wait_apr(submission) == OK && result.result == OK &&
|
||||
fence == literal.value,
|
||||
"bounded write preserves all sixty-four literal bits");
|
||||
}
|
||||
}
|
||||
reset(1, 768);
|
||||
std::array<std::array<char, 3>, 16> batch_output {};
|
||||
std::array<uint64_t, 16> batch_fences {};
|
||||
auto *batch = buffers[0].header.data();
|
||||
for (size_t i = 0; i < batch_fences.size(); ++i) {
|
||||
Check(read_file(batch, reinterpret_cast<uint64_t>(&buffers[0].header[3]),
|
||||
reinterpret_cast<uint64_t>(&buffers[0].header[4]), file_id,
|
||||
batch_output[i].data(), batch_output[i].size(), 0) == OK &&
|
||||
write_address(batch, &batch_fences[i], i + 1) == OK,
|
||||
"sixteen APR read/fence pairs fit the guest's 768-byte command buffer");
|
||||
}
|
||||
Result batch_result {1234, 5678};
|
||||
uint32_t batch_submission = 0;
|
||||
Check(submit_apr(batch, 3, &batch_result, &batch_submission) == OK &&
|
||||
wait_apr(batch_submission) == OK && batch_result.result == OK,
|
||||
"complete the bounded APR read batch");
|
||||
for (size_t i = 0; i < batch_fences.size(); ++i) {
|
||||
Check(batch_fences[i] == i + 1 &&
|
||||
std::memcmp(batch_output[i].data(), "APR", 3) == 0,
|
||||
"every read publishes its data and completion, including requests fourteen and fifteen");
|
||||
}
|
||||
// WaitCompare order: ==, unsigned >/<, !=, wrapped >=, signed >/<.
|
||||
struct Comparison { uint8_t compare; uint64_t blocked, reference, released; };
|
||||
constexpr std::array comparisons {
|
||||
|
|
@ -1401,6 +1462,84 @@ void CheckSocketReceiveBuffer(int reader, int writer) {
|
|||
}
|
||||
#endif
|
||||
|
||||
#if defined(__linux__)
|
||||
void KYTY_SYSV_ABI RecordNetCtlEvent(int event, void* arg) {
|
||||
auto& events = *static_cast<std::array<int, 2>*>(arg);
|
||||
++events[0];
|
||||
events[1] = event;
|
||||
}
|
||||
|
||||
void TestNetCtlHostIpv4() {
|
||||
using namespace Libs::Network::NetCtl;
|
||||
ifaddrs* interfaces = nullptr;
|
||||
Check(getifaddrs(&interfaces) == 0, "enumerate host IPv4 addresses");
|
||||
std::vector<uint32_t> host_addresses;
|
||||
for (auto* interface = interfaces; interface != nullptr; interface = interface->ifa_next) {
|
||||
if (interface->ifa_addr == nullptr || interface->ifa_addr->sa_family != AF_INET ||
|
||||
(interface->ifa_flags & IFF_UP) == 0 || (interface->ifa_flags & IFF_RUNNING) == 0 ||
|
||||
(interface->ifa_flags & IFF_LOOPBACK) != 0) {
|
||||
continue;
|
||||
}
|
||||
const auto address = reinterpret_cast<const sockaddr_in*>(interface->ifa_addr)->sin_addr;
|
||||
if (address.s_addr != 0 && (ntohl(address.s_addr) >> 24) != 127) {
|
||||
host_addresses.push_back(address.s_addr);
|
||||
}
|
||||
}
|
||||
freeifaddrs(interfaces);
|
||||
|
||||
Config::ConfigOptions options;
|
||||
options.printf_direction = Config::LogDirection::Silent;
|
||||
Check(!options.network_connected, "host networking requires explicit opt-in");
|
||||
alignas(uint32_t) std::array<char, 256> storage {};
|
||||
auto* info = reinterpret_cast<NetCtlInfo*>(storage.data());
|
||||
constexpr int NotConnected = static_cast<int>(0x80412108u);
|
||||
Check(NetCtlGetInfo(14, nullptr) == static_cast<int>(0x80412107u) &&
|
||||
NetCtlGetInfo(0, info) == static_cast<int>(0x80412106u) &&
|
||||
NetCtlGetInfo(22, info) == static_cast<int>(0x80412106u),
|
||||
"NetCtl reports invalid output addresses and information codes");
|
||||
for (bool enabled : {false, true}) {
|
||||
options.network_connected = enabled;
|
||||
Config::Load(options);
|
||||
Check(NetCtlInit() == OK, "initialize NetCtl for selected network mode");
|
||||
const bool connected = enabled && !host_addresses.empty();
|
||||
int state = -1;
|
||||
Check(NetCtlGetState(&state) == OK && state == (connected ? 3 : 0),
|
||||
"NetCtl IPv4 state matches opt-in and host connectivity");
|
||||
Check(NetCtlGetStateV6(&state) == OK && state == 0,
|
||||
"IPv4 connectivity does not advertise IPv6 support");
|
||||
|
||||
Check(NetCtlGetInfo(14, info) == (connected ? OK : NotConnected),
|
||||
"NetCtl IPv4 query respects network mode and host state");
|
||||
in_addr address {};
|
||||
if (connected) {
|
||||
Check(inet_pton(AF_INET, storage.data(), &address) == 1 &&
|
||||
std::find(host_addresses.begin(), host_addresses.end(), address.s_addr) !=
|
||||
host_addresses.end(),
|
||||
"NetCtl reports an IPv4 address assigned to an active host interface");
|
||||
}
|
||||
Check(NetCtlGetInfo(15, info) == (connected ? OK : NotConnected) &&
|
||||
(!connected || inet_pton(AF_INET, storage.data(), &address) == 1),
|
||||
"NetCtl returns the active IPv4 netmask only in connected mode");
|
||||
const int gateway_result = NetCtlGetInfo(16, info);
|
||||
Check(gateway_result == NotConnected ||
|
||||
(connected && gateway_result == OK &&
|
||||
inet_pton(AF_INET, storage.data(), &address) == 1),
|
||||
"NetCtl reports a valid IPv4 gateway or its absence");
|
||||
|
||||
std::array<int, 2> events {};
|
||||
int callback_id = -1;
|
||||
Check(NetCtlRegisterCallback(RecordNetCtlEvent, &events, &callback_id) == OK &&
|
||||
NetCtlCheckCallback() == OK && events[0] == 1 &&
|
||||
events[1] == (connected ? 3 : 1) && NetCtlCheckCallback() == OK && events[0] == 1,
|
||||
"NetCtl delivers the matching connection event once while state is unchanged");
|
||||
Check(NetCtlUnregisterCallback(callback_id) == OK, "release NetCtl callback");
|
||||
NetCtlTerm();
|
||||
}
|
||||
options.network_connected = false;
|
||||
Config::Load(options);
|
||||
}
|
||||
#endif
|
||||
|
||||
void CheckEtherAddressFormatting() {
|
||||
Loader::SymbolDatabase symbols;
|
||||
Libs::LibNet::InitNet_1_Net(&symbols);
|
||||
|
|
@ -1732,6 +1871,9 @@ int main(int, char**) {
|
|||
FileSystem::Shutdown();
|
||||
CheckSocketWakeup();
|
||||
CheckEtherAddressFormatting();
|
||||
#if defined(__linux__)
|
||||
TestNetCtlHostIpv4();
|
||||
#endif
|
||||
TestNpWebApi2Memory();
|
||||
TestNpCommerceDialog();
|
||||
graphics.reset();
|
||||
|
|
|
|||
|
|
@ -24,9 +24,11 @@ struct Fixture {
|
|||
|
||||
Fixture(uint32_t rate, uint32_t channels = 1, Ngs2RackType type = Ngs2RackType::Sampler) {
|
||||
system.option.sample_rate = 48000;
|
||||
system.option.num_grain_samples = 256;
|
||||
rack.ngs = &system;
|
||||
rack.type = type;
|
||||
voice.rack = &rack;
|
||||
SetCapacity(4);
|
||||
struct Setup {
|
||||
Ngs2VoiceParamHeader header {40, 0, 0x10000000};
|
||||
Ngs2WaveformFormat format;
|
||||
|
|
@ -39,20 +41,51 @@ struct Fixture {
|
|||
voice.state = Ngs2VoicePlayState::Playing;
|
||||
}
|
||||
|
||||
void Queue(const int16_t* data, uint32_t frames, uint32_t flags, uint32_t repeats = 0,
|
||||
uint32_t skip = 0) {
|
||||
Ngs2WaveformBlock block {
|
||||
0, size_t(frames + skip) * voice.channels * sizeof(int16_t), repeats, skip, frames, 0,
|
||||
123};
|
||||
void SetCapacity(uint32_t capacity) {
|
||||
if (rack.type == Ngs2RackType::Sampler) {
|
||||
rack.option.sampler.max_waveform_blocks = capacity;
|
||||
} else {
|
||||
rack.option.custom_sampler.max_waveform_blocks = capacity;
|
||||
rack.option.custom_sampler.custom_rack_option.state_size = sizeof(Ngs2CustomSamplerVoiceState);
|
||||
}
|
||||
}
|
||||
|
||||
Ngs2SamplerVoiceState State() {
|
||||
Ngs2SamplerVoiceState state {};
|
||||
if (rack.type == Ngs2RackType::CustomSampler) {
|
||||
Ngs2CustomSamplerVoiceState custom {};
|
||||
Check(Ngs2VoiceGetState(reinterpret_cast<uintptr_t>(&voice), &custom.voice_state,
|
||||
sizeof(custom)) == OK, "custom sampler state failed");
|
||||
state.voice_state = custom.voice_state;
|
||||
state.num_decoded_samples = custom.num_decoded_samples;
|
||||
state.decoded_data_size = custom.decoded_data_size;
|
||||
state.waveform_data = custom.waveform_data;
|
||||
state.user_data = custom.user_data;
|
||||
} else {
|
||||
Check(Ngs2VoiceGetState(reinterpret_cast<uintptr_t>(&voice), &state.voice_state,
|
||||
sizeof(state)) == OK, "sampler state failed");
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
int QueueBlocks(const void* data, const Ngs2WaveformBlock* blocks, uint32_t count,
|
||||
uint32_t flags) {
|
||||
struct Blocks {
|
||||
Ngs2VoiceParamHeader header {32, 0, 0x10000001};
|
||||
const void* data;
|
||||
uint32_t flags, count;
|
||||
const Ngs2WaveformBlock* blocks;
|
||||
} param {{32, 0, 0x10000001}, data, flags, 1, &block};
|
||||
} param {{32, 0, 0x10000001}, data, flags, count, blocks};
|
||||
param.header.id = rack.type == Ngs2RackType::Sampler ? 0x10000001 : 0x40010001;
|
||||
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&voice), ¶m.header) == OK,
|
||||
"sampler queue failed");
|
||||
return Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&voice), ¶m.header);
|
||||
}
|
||||
|
||||
void Queue(const int16_t* data, uint32_t frames, uint32_t flags, uint32_t repeats = 0,
|
||||
uint32_t skip = 0) {
|
||||
const Ngs2WaveformBlock block {
|
||||
0, size_t(frames + skip) * voice.channels * sizeof(int16_t), repeats, skip, frames, 0,
|
||||
123};
|
||||
Check(QueueBlocks(data, &block, 1, flags) == OK, "sampler queue failed");
|
||||
}
|
||||
|
||||
void Render(uint32_t frames) {
|
||||
|
|
@ -61,6 +94,190 @@ struct Fixture {
|
|||
}
|
||||
};
|
||||
|
||||
void TestWaveformQueueCapacity() {
|
||||
constexpr int32_t full = static_cast<int32_t>(0x804a8054u);
|
||||
constexpr int32_t invalid_count = static_cast<int32_t>(0x804a8053u);
|
||||
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
|
||||
for (uint32_t capacity: {1u, 4u, 16u}) {
|
||||
Fixture f(24000, 1, rack);
|
||||
f.SetCapacity(capacity);
|
||||
f.system.option.num_grain_samples = 1;
|
||||
const int16_t pcm[] = {8192, 16384};
|
||||
const int16_t replacement[] = {24576, 4096};
|
||||
std::vector<Ngs2WaveformBlock> blocks(capacity + 1);
|
||||
for (uint32_t i = 0; i < blocks.size(); ++i) {
|
||||
blocks[i] = {0, sizeof(pcm), 0, 0, 2, 0, i + 1};
|
||||
}
|
||||
Check(f.QueueBlocks(pcm, blocks.data(), capacity, 1) == OK,
|
||||
"configured waveform capacity was not available");
|
||||
f.Render(1);
|
||||
const auto phase = f.voice.sample_phase;
|
||||
Check(f.QueueBlocks(replacement, blocks.data(), 1, 0) == full,
|
||||
"full waveform queue accepted another guest buffer");
|
||||
Check(f.QueueBlocks(replacement, blocks.data(), capacity + 1, 4) == invalid_count,
|
||||
"oversized waveform request did not reject before CLEAR");
|
||||
Check(f.voice.blocks.size() == capacity && f.voice.accepts_blocks &&
|
||||
f.voice.sample_phase == phase && f.voice.decoded_samples == 1 &&
|
||||
f.voice.decoded_bytes == sizeof(int16_t),
|
||||
"rejected waveform request changed playback or stream continuity");
|
||||
for (uint32_t i = 0; i < capacity; ++i) {
|
||||
Check(f.voice.blocks[i].data == reinterpret_cast<const uint8_t*>(pcm) &&
|
||||
f.voice.blocks[i].info.user_data == i + 1 &&
|
||||
f.voice.blocks[i].cursor == (i == 0 ? 1u : 0u),
|
||||
"rejected waveform request replaced an outstanding block");
|
||||
}
|
||||
f.Render(3);
|
||||
Check(f.voice.samples == std::vector<float> {0.375f, 0.5f,
|
||||
capacity == 1 ? 0.5f : 0.375f} &&
|
||||
f.voice.blocks.size() == capacity - 1,
|
||||
"queue rejection changed partially played source audio");
|
||||
Check(f.QueueBlocks(replacement, blocks.data(), 1, 1) == OK &&
|
||||
f.voice.blocks.size() == capacity,
|
||||
"completed waveform block did not release its queue slot");
|
||||
const auto progress = f.State();
|
||||
Check(f.QueueBlocks(replacement, blocks.data(), 1, 5) == OK &&
|
||||
f.voice.blocks.size() == 1 && f.voice.sample_phase == 0 &&
|
||||
f.voice.decoded_samples == progress.num_decoded_samples &&
|
||||
f.voice.decoded_bytes == progress.decoded_data_size,
|
||||
"CLEAR failed to replace a full queue while preserving progress");
|
||||
f.Render(2);
|
||||
Check(f.voice.samples == std::vector<float> {0.75f, 0.4375f},
|
||||
"CLEAR retained an old source or fractional playback position");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PcmRing {
|
||||
static constexpr uint32_t block_frames = 7, block_count = 64;
|
||||
Fixture& fixture;
|
||||
std::array<std::array<int16_t, block_frames * 2>, 4> buffers {};
|
||||
uint32_t next_block = 0, completed = 0;
|
||||
|
||||
void QueueNext(uint32_t slot) {
|
||||
auto& buffer = buffers[slot];
|
||||
for (uint32_t i = 0; i < block_frames; ++i) {
|
||||
buffer[2 * i] = static_cast<int16_t>(100 + (next_block * block_frames + i) * 16);
|
||||
buffer[2 * i + 1] = -buffer[2 * i];
|
||||
}
|
||||
const Ngs2WaveformBlock block {0, sizeof(buffer), 0, 0, block_frames, 0, slot};
|
||||
++next_block;
|
||||
Check(fixture.QueueBlocks(buffer.data(), &block, 1, next_block == block_count ? 0 : 1) == OK,
|
||||
"released ring buffer could not be queued again");
|
||||
}
|
||||
};
|
||||
|
||||
void KYTY_SYSV_ABI RefillPcmRing(const Ngs2VoiceCallbackInfo* info) {
|
||||
auto& ring = *reinterpret_cast<PcmRing*>(info->data);
|
||||
const auto slot = ring.completed++ % ring.buffers.size();
|
||||
Check(info->flag == 1 && info->attributes == 0 && info->user == slot &&
|
||||
info->block_data == ring.buffers[slot].data() &&
|
||||
info->size == sizeof(ring.buffers[slot]),
|
||||
"ring completion callback released the wrong source buffer");
|
||||
for (const auto& block: ring.fixture.voice.blocks) {
|
||||
Check(block.data != reinterpret_cast<const uint8_t*>(ring.buffers[slot].data()),
|
||||
"completion callback retained the buffer about to be overwritten");
|
||||
}
|
||||
if (ring.next_block < PcmRing::block_count) {
|
||||
ring.QueueNext(static_cast<uint32_t>(slot));
|
||||
}
|
||||
}
|
||||
|
||||
void TestWaveformRingReuse() {
|
||||
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
|
||||
Fixture f(48000, 2, rack);
|
||||
PcmRing ring {f};
|
||||
f.voice.callback = reinterpret_cast<uintptr_t>(&RefillPcmRing);
|
||||
f.voice.callback_data = reinterpret_cast<uintptr_t>(&ring);
|
||||
f.voice.callback_flags = 1;
|
||||
for (uint32_t slot = 0; slot < ring.buffers.size(); ++slot) {
|
||||
ring.QueueNext(slot);
|
||||
}
|
||||
constexpr std::array<uint32_t, 6> grains {1, 16, 3, 29, 2, 11};
|
||||
constexpr auto total_frames = PcmRing::block_frames * PcmRing::block_count;
|
||||
uint32_t rendered = 0, grain_index = 0;
|
||||
while (rendered < total_frames) {
|
||||
const auto grain = std::min(grains[grain_index++ % grains.size()], total_frames - rendered);
|
||||
f.Render(grain);
|
||||
for (uint32_t i = 0; i < grain; ++i) {
|
||||
const float expected = (100 + (rendered + i) * 16) / 32768.0f;
|
||||
Check(f.voice.samples[i] == expected && f.voice.samples[grain + i] == -expected,
|
||||
"ring buffer reuse replayed overwritten data or mixed stereo channels");
|
||||
}
|
||||
rendered += grain;
|
||||
const auto decoded = f.State().num_decoded_samples;
|
||||
Check(f.voice.blocks.size() <= ring.buffers.size() &&
|
||||
decoded >= rendered && decoded - rendered < 256 &&
|
||||
f.voice.decoded_bytes == decoded * 4 &&
|
||||
ring.completed == decoded / PcmRing::block_frames,
|
||||
"ring playback lost queue capacity or decoded progress");
|
||||
if (decoded < total_frames) {
|
||||
const auto slot = (decoded / PcmRing::block_frames) % ring.buffers.size();
|
||||
Check(f.voice.WaveformData() == reinterpret_cast<const uint8_t*>(
|
||||
ring.buffers[slot].data() + (decoded % PcmRing::block_frames) * 2),
|
||||
"ring read address did not track the active source buffer");
|
||||
}
|
||||
}
|
||||
Check(f.voice.blocks.empty() && f.voice.state == Ngs2VoicePlayState::Empty &&
|
||||
ring.completed == PcmRing::block_count,
|
||||
"finite ring stream did not release all buffers and finish");
|
||||
}
|
||||
}
|
||||
|
||||
void TestProgressDrivenWaveformRing() {
|
||||
const auto source_sample = [](uint64_t frame) {
|
||||
return static_cast<int16_t>(static_cast<int32_t>((frame * 43) % 32768) - 16384);
|
||||
};
|
||||
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
|
||||
Fixture f(44100, 2, rack);
|
||||
std::array<std::array<int16_t, 1024 * 2>, 4> buffers {};
|
||||
uint64_t submitted = 0, decoded_bytes = 0, rendered = 0;
|
||||
uint32_t slot = 0;
|
||||
const auto refill = [&](uint32_t frames) {
|
||||
auto& buffer = buffers[slot];
|
||||
for (const auto& block: f.voice.blocks) {
|
||||
Check(block.data != reinterpret_cast<const uint8_t*>(buffer.data()),
|
||||
"decoded progress allowed the guest to overwrite an outstanding ring buffer");
|
||||
}
|
||||
for (uint32_t i = 0; i < frames; ++i) {
|
||||
buffer[i * 2] = source_sample(submitted + i);
|
||||
buffer[i * 2 + 1] = -buffer[i * 2];
|
||||
}
|
||||
const Ngs2WaveformBlock block {0, frames * 4u, 0, 0, frames, 0, slot};
|
||||
Check(f.QueueBlocks(buffer.data(), &block, 1, 1) == OK,
|
||||
"progress-driven ring exhausted its waveform slots");
|
||||
submitted += frames;
|
||||
slot = (slot + 1) % buffers.size();
|
||||
};
|
||||
refill(1024);
|
||||
for (uint32_t grain = 0; grain < 128; ++grain) {
|
||||
f.Render(256);
|
||||
for (uint32_t i = 0; i < 256; ++i) {
|
||||
const uint64_t phase = (rendered + i) * 44100;
|
||||
const auto frame = phase / 48000;
|
||||
const float fraction = static_cast<float>(phase % 48000) / 48000.0f;
|
||||
const float expected = std::lerp(static_cast<float>(source_sample(frame)),
|
||||
static_cast<float>(source_sample(frame + 1)), fraction) /
|
||||
32768.0f;
|
||||
Check(std::abs(f.voice.samples[i] - expected) < 0.00001f &&
|
||||
std::abs(f.voice.samples[256 + i] + expected) < 0.00001f,
|
||||
"progress-driven ring replayed overwritten music samples");
|
||||
}
|
||||
rendered += 256;
|
||||
const auto state = f.State();
|
||||
const auto delta = state.decoded_data_size - decoded_bytes;
|
||||
Check(state.decoded_data_size >= decoded_bytes && delta <= sizeof(buffers[0]) &&
|
||||
delta % 4 == 0 && state.num_decoded_samples * 4 == state.decoded_data_size &&
|
||||
state.num_decoded_samples <= submitted,
|
||||
"published sampler progress does not describe decoded source data");
|
||||
decoded_bytes = state.decoded_data_size;
|
||||
if (delta != 0) {
|
||||
refill(static_cast<uint32_t>(delta / 4));
|
||||
}
|
||||
}
|
||||
Check(decoded_bytes > 28 * sizeof(buffers[0]), "ring test did not exercise repeated buffer reuse");
|
||||
}
|
||||
}
|
||||
|
||||
void TestStreamingResample() {
|
||||
Fixture f(44100, 2);
|
||||
std::vector<int16_t> pcm(441 * 2);
|
||||
|
|
@ -178,6 +395,7 @@ void TestAtrac9ResampleAcrossFrames() {
|
|||
f->voice.next_decoded_frame[0] = 1.0f;
|
||||
f->voice.next_decoded_frame[1] = 1.5f;
|
||||
f->voice.next_frame_samples = 2;
|
||||
f->voice.decoded_samples = 4;
|
||||
f->voice.accepts_blocks = false;
|
||||
return f;
|
||||
};
|
||||
|
|
@ -218,6 +436,7 @@ void TestAtrac9ResampleAfterStarvation() {
|
|||
f.voice.decoded_frame[0] = 0.0f;
|
||||
f.voice.decoded_frame[1] = 0.5f;
|
||||
f.voice.frame_samples = 2;
|
||||
f.voice.decoded_samples = 2;
|
||||
f.Render(4);
|
||||
Check(f.voice.samples == std::vector<float> {0.0f, 0.25f, 0.5f, 0.5f} &&
|
||||
f.voice.decoded_samples == 2,
|
||||
|
|
@ -230,6 +449,7 @@ void TestAtrac9ResampleAfterStarvation() {
|
|||
f.voice.next_decoded_frame[0] = 1.0f;
|
||||
f.voice.next_decoded_frame[1] = 1.5f;
|
||||
f.voice.next_frame_samples = 2;
|
||||
f.voice.decoded_samples += 2;
|
||||
f.Render(4);
|
||||
Check(f.voice.samples == std::vector<float> {1.0f, 1.25f, 1.5f, 1.5f} &&
|
||||
f.voice.decoded_samples == 4,
|
||||
|
|
@ -773,8 +993,8 @@ void TestLoopCallbacksAndExit() {
|
|||
Ngs2VoiceParamHeader exit_loop {sizeof(Ngs2VoiceParamHeader), 0, 0x10000004};
|
||||
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&active.voice), &exit_loop);
|
||||
active.voice.SetEvent(32);
|
||||
active.Render(3);
|
||||
Check(active.voice.samples == std::vector<float> {0.25f, 0.5f, 0.75f} &&
|
||||
active.Render(5);
|
||||
Check(active.voice.samples == std::vector<float> {0.25f, 0.5f, 0.25f, 0.5f, 0.75f} &&
|
||||
active.voice.blocks.empty() && log.events.size() == 1,
|
||||
"exiting infinite loop lost its final iteration or ignored callback flags");
|
||||
|
||||
|
|
@ -858,8 +1078,69 @@ void TestAtrac9Looping() {
|
|||
"compressed loop exit failed to finish the active iteration");
|
||||
}
|
||||
|
||||
void TestAtrac9WaveformJoining() {
|
||||
std::array<uint8_t, 192> encoded {};
|
||||
for (size_t i = 0; i < 4; ++i) {
|
||||
std::copy(ATRAC9_MONO_BLOCK.begin(), ATRAC9_MONO_BLOCK.end(),
|
||||
encoded.begin() + i * ATRAC9_MONO_BLOCK.size());
|
||||
if (i != 0) encoded[i * ATRAC9_MONO_BLOCK.size()] |= 0x80;
|
||||
}
|
||||
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
|
||||
Fixture reference(24000, 1, rack);
|
||||
reference.voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0});
|
||||
reference.voice.state = Ngs2VoicePlayState::Playing;
|
||||
const Ngs2WaveformBlock whole {0, encoded.size(), 0, 0, 512, 0, 123};
|
||||
Check(reference.QueueBlocks(encoded.data(), &whole, 1, 0) == OK,
|
||||
"compressed reference block could not be queued");
|
||||
reference.Render(1024);
|
||||
for (bool nojoin: {false, true}) {
|
||||
Fixture f(24000, 1, rack);
|
||||
f.SetCapacity(1);
|
||||
f.voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0});
|
||||
f.voice.state = Ngs2VoicePlayState::Playing;
|
||||
LoopCallbacks callbacks;
|
||||
f.voice.callback = reinterpret_cast<uintptr_t>(&RecordLoopCallback);
|
||||
f.voice.callback_data = reinterpret_cast<uintptr_t>(&callbacks);
|
||||
f.voice.callback_flags = 1;
|
||||
const Ngs2WaveformBlock first {0, 36, 0, 0, 512, 0, 123};
|
||||
const Ngs2WaveformBlock rest {36, encoded.size() - 36, 99, 99, 99, 0, 123};
|
||||
Check(f.QueueBlocks(encoded.data(), &first, 1, 1) == OK,
|
||||
"compressed stream start could not be queued");
|
||||
const uint32_t flags = nojoin ? 0x12 : 2;
|
||||
if (nojoin) {
|
||||
Check(f.QueueBlocks(encoded.data(), &rest, 1, flags) ==
|
||||
static_cast<int32_t>(0x804a8054u) &&
|
||||
f.voice.blocks.size() == 1 && f.voice.blocks.front().info.data_size == 36 &&
|
||||
f.voice.accepts_blocks,
|
||||
"NOJOINT bypassed capacity or changed the full stream");
|
||||
f.SetCapacity(2);
|
||||
}
|
||||
Check(f.QueueBlocks(encoded.data(), &rest, 1, flags) == OK &&
|
||||
f.voice.blocks.size() == (nojoin ? 2u : 1u),
|
||||
"consecutive ONLY_DATA blocks did not follow the joining flag");
|
||||
f.Render(1024);
|
||||
Check(f.voice.samples == reference.voice.samples && f.voice.decoded_samples == 512 &&
|
||||
f.voice.decoded_bytes == encoded.size() && f.voice.blocks.empty() &&
|
||||
callbacks.events.size() == (nojoin ? 2u : 1u),
|
||||
"compressed joining changed audio, progress, or block completion callbacks");
|
||||
}
|
||||
Fixture full(24000, 1, rack);
|
||||
full.SetCapacity(2);
|
||||
full.voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0});
|
||||
const Ngs2WaveformBlock initial[] = {{0, 36, 0, 0, 512}, {36, 36, 0, 0, 512}};
|
||||
const Ngs2WaveformBlock addition[] = {{72, 36}, {144, 48}};
|
||||
Check(full.QueueBlocks(encoded.data(), initial, 2, 1) == OK &&
|
||||
full.QueueBlocks(encoded.data(), addition, 2, 2) ==
|
||||
static_cast<int32_t>(0x804a8054u) &&
|
||||
full.voice.blocks.size() == 2 && full.voice.blocks.back().info.data_size == 36 &&
|
||||
full.voice.accepts_blocks,
|
||||
"overflow after a tentative join partially extended or closed the stream");
|
||||
}
|
||||
}
|
||||
|
||||
void TestWaveformReadAddress() {
|
||||
Fixture f(48000, 2);
|
||||
f.system.option.num_grain_samples = 1;
|
||||
const int16_t pcm[] = {-100, -100, 8192, 4096, 16384, 8192};
|
||||
f.Queue(pcm, 2, 1, 1, 1);
|
||||
const auto next_data = [&]() {
|
||||
|
|
@ -887,11 +1168,12 @@ struct CallbackEvent {
|
|||
Ngs2VoiceInternal* voice;
|
||||
uint32_t event;
|
||||
uint32_t calls = 0;
|
||||
uint32_t trigger = 1;
|
||||
};
|
||||
|
||||
void KYTY_SYSV_ABI ApplyCallbackEvent(const Ngs2VoiceCallbackInfo* info) {
|
||||
auto& context = *reinterpret_cast<CallbackEvent*>(info->data);
|
||||
if (++context.calls == 1) {
|
||||
if (++context.calls == context.trigger) {
|
||||
context.voice->SetEvent(context.event);
|
||||
}
|
||||
}
|
||||
|
|
@ -910,14 +1192,15 @@ void TestCallbackStopsConsumption() {
|
|||
f.Render(2);
|
||||
Check(f.voice.state != Ngs2VoicePlayState::Playing && context.calls == 1,
|
||||
"completion callback failed to interrupt playback");
|
||||
Check(f.voice.samples[0] == 0.25f && f.voice.samples[1] == 0.0f &&
|
||||
Check(f.voice.samples[0] == 0.0f && f.voice.samples[1] == 0.0f &&
|
||||
f.voice.decoded_samples == 1 && f.voice.blocks.size() == 1 &&
|
||||
f.voice.blocks.front().cursor == 0,
|
||||
"callback pause/stop consumed the next queued block");
|
||||
if (event == 16) {
|
||||
f.voice.SetEvent(32);
|
||||
f.Render(1);
|
||||
Check(f.voice.samples[0] == 0.5f && f.voice.decoded_samples == 2,
|
||||
f.Render(2);
|
||||
Check(f.voice.samples == std::vector<float> {0.25f, 0.5f} &&
|
||||
f.voice.decoded_samples == 2,
|
||||
"callback pause failed to preserve queued audio for resume");
|
||||
}
|
||||
}
|
||||
|
|
@ -936,15 +1219,95 @@ void TestCallbackPauseDuringLargeStep() {
|
|||
f.voice.callback_data = reinterpret_cast<uintptr_t>(&context);
|
||||
f.voice.callback_flags = 1;
|
||||
f.Render(2);
|
||||
Check(context.calls == 1 && f.voice.decoded_samples == 1 &&
|
||||
f.voice.sample_phase == (uint64_t(72000) << 32u),
|
||||
Check(context.calls == 1 && f.voice.decoded_samples == 1 && f.voice.sample_phase == 0 &&
|
||||
!f.voice.has_samples,
|
||||
"pause advanced across additional blocks in a large resampling step");
|
||||
f.voice.SetEvent(32);
|
||||
f.Render(1);
|
||||
Check(std::abs(f.voice.samples[0] - 0.4375f) < 1e-7f && context.calls == 3,
|
||||
f.Render(2);
|
||||
Check(f.voice.samples[0] == 0.25f && std::abs(f.voice.samples[1] - 0.4375f) < 1e-7f &&
|
||||
context.calls == 3,
|
||||
"resume lost fractional position or read past the next short block");
|
||||
}
|
||||
|
||||
void TestCallbackPauseDuringLookahead() {
|
||||
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
|
||||
Fixture f(24000, 1, rack);
|
||||
f.system.option.num_grain_samples = 1;
|
||||
int16_t pcm[] = {8192, 16384};
|
||||
f.Queue(pcm, 1, 1);
|
||||
f.Queue(pcm + 1, 1, 0);
|
||||
CallbackEvent context {&f.voice, 16, 0, 2};
|
||||
f.voice.callback = reinterpret_cast<uintptr_t>(&ApplyCallbackEvent);
|
||||
f.voice.callback_data = reinterpret_cast<uintptr_t>(&context);
|
||||
f.voice.callback_flags = 1;
|
||||
f.Render(1);
|
||||
Check(f.voice.samples[0] == 0.25f, "initial source sample was not rendered");
|
||||
f.Render(2);
|
||||
Check(!f.voice.has_samples && f.voice.state == Ngs2VoicePlayState::Paused &&
|
||||
context.calls == 2 && f.State().num_decoded_samples == 2 &&
|
||||
f.voice.sample_phase == (uint64_t(24000) << 32u),
|
||||
"lookahead callback pause consumed output or lost decoded progress");
|
||||
pcm[0] = pcm[1] = -32768;
|
||||
f.voice.SetEvent(32);
|
||||
f.Render(3);
|
||||
Check(f.voice.samples == std::vector<float> {0.375f, 0.5f, 0.5f} &&
|
||||
f.voice.state == Ngs2VoicePlayState::Empty,
|
||||
"lookahead pause failed to preserve owned samples or fractional playback position");
|
||||
}
|
||||
}
|
||||
|
||||
struct CallbackReplacement {
|
||||
Fixture& fixture;
|
||||
bool setup;
|
||||
uint32_t trigger, calls = 0;
|
||||
const std::array<int16_t, 4> pcm {16384, 8192, 4096, 2048};
|
||||
};
|
||||
|
||||
void KYTY_SYSV_ABI ReplaceCallbackSource(const Ngs2VoiceCallbackInfo* info) {
|
||||
auto& replacement = *reinterpret_cast<CallbackReplacement*>(info->data);
|
||||
if (++replacement.calls != replacement.trigger) {
|
||||
return;
|
||||
}
|
||||
auto& f = replacement.fixture;
|
||||
f.voice.callback_flags = 0;
|
||||
if (replacement.setup) {
|
||||
f.voice.SetupSampler({0x12, 1, 48000});
|
||||
}
|
||||
f.Queue(replacement.pcm.data(), 2, replacement.setup ? 0 : 4);
|
||||
f.voice.SetEvent(1);
|
||||
}
|
||||
|
||||
void TestCallbackReplacesSource() {
|
||||
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
|
||||
for (bool setup: {false, true}) {
|
||||
for (uint32_t trigger: {1u, 2u}) {
|
||||
Fixture f(24000, 2, rack);
|
||||
f.system.option.num_grain_samples = 1;
|
||||
CallbackReplacement replacement {f, setup, trigger};
|
||||
const int16_t pcm[] = {8192, 4096};
|
||||
for (uint32_t i = 0; i < trigger; ++i) {
|
||||
f.Queue(pcm, 1, 1);
|
||||
}
|
||||
f.voice.callback = reinterpret_cast<uintptr_t>(&ReplaceCallbackSource);
|
||||
f.voice.callback_data = reinterpret_cast<uintptr_t>(&replacement);
|
||||
f.voice.callback_flags = 1;
|
||||
f.Render(4);
|
||||
Check(!f.voice.has_samples && f.voice.samples.size() == (setup ? 4u : 8u) &&
|
||||
std::ranges::all_of(f.voice.samples, [](float value) { return value == 0; }) &&
|
||||
f.voice.blocks.size() == 1 && f.voice.blocks.front().cursor == 0,
|
||||
"callback source replacement used an old frame or channel layout");
|
||||
f.Render(4);
|
||||
const std::vector<float> expected = setup
|
||||
? std::vector<float> {0.5f, 0.25f, 0, 0}
|
||||
: std::vector<float> {0.5f, 0.3125f, 0.125f, 0.125f,
|
||||
0.25f, 0.15625f, 0.0625f, 0.0625f};
|
||||
Check(f.voice.samples == expected && f.voice.state == Ngs2VoicePlayState::Empty,
|
||||
"callback replacement failed to play the new source on the next render");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TestUpstreamCustomSamplerControls() {
|
||||
Fixture f(24000, 1, Ngs2RackType::CustomSampler);
|
||||
const int16_t pcm[] = {8192, 16384, 24576, 4096};
|
||||
|
|
@ -953,7 +1316,7 @@ void TestUpstreamCustomSamplerControls() {
|
|||
Check(f.voice.samples[0] == 0.25f, "custom sampler source rate was rejected");
|
||||
f.Queue(pcm + 2, 2, 4);
|
||||
f.Render(1);
|
||||
Check(f.voice.samples[0] == 0.75f && f.voice.blocks.size() == 1,
|
||||
Check(f.voice.samples[0] == 0.75f && f.voice.blocks.empty() && f.voice.frame_samples == 2,
|
||||
"replacement blocks retained old data or fractional position");
|
||||
struct Pitch {
|
||||
Ngs2VoiceParamHeader header {16, 0, 0x40010005};
|
||||
|
|
@ -1058,6 +1421,9 @@ void TestFiniteFilterTail() {
|
|||
} // namespace
|
||||
|
||||
int main() {
|
||||
TestProgressDrivenWaveformRing();
|
||||
TestWaveformQueueCapacity();
|
||||
TestWaveformRingReuse();
|
||||
TestAtrac9Vibration();
|
||||
TestPcm16RenderOutput();
|
||||
TestSubmixerSetupAndRouting();
|
||||
|
|
@ -1069,6 +1435,7 @@ int main() {
|
|||
TestDeferredSamplerSetup();
|
||||
TestLoopCallbacksAndExit();
|
||||
TestAtrac9Looping();
|
||||
TestAtrac9WaveformJoining();
|
||||
TestWaveformReadAddress();
|
||||
TestFilterTailDuringStarvation();
|
||||
TestFiniteFilterTail();
|
||||
|
|
@ -1079,6 +1446,8 @@ int main() {
|
|||
TestUpstreamCustomSamplerControls();
|
||||
TestCallbackStopsConsumption();
|
||||
TestCallbackPauseDuringLargeStep();
|
||||
TestCallbackPauseDuringLookahead();
|
||||
TestCallbackReplacesSource();
|
||||
TestStreamingResample();
|
||||
TestPitchLoopAndSkip();
|
||||
TestPitchClamps();
|
||||
|
|
|
|||
|
|
@ -616,11 +616,11 @@ void TestWrittenDescriptorPredicateReads(bool memory_condition) {
|
|||
ResourceSpecialization specialization;
|
||||
for (const bool clean : {false, true}) {
|
||||
reads = {.clean = clean};
|
||||
// A failed predicate explores both edges and the writable descriptor retries its clean read.
|
||||
// A failed predicate explores both edges; its dirty read also rejects the writable descriptor.
|
||||
const bool expected = !memory_condition || clean;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) == expected &&
|
||||
reads.ordinary == (memory_condition ? 0u : 1u) &&
|
||||
reads.strict == (memory_condition ? (clean ? 1u : 2u) : 0u),
|
||||
reads.strict == (memory_condition ? 1u : 0u),
|
||||
"writable descriptor changed reader policy or bypassed a dirty memory predicate");
|
||||
if (!expected) continue;
|
||||
Check(snapshot.buffers[0].dwords[0] == 0x8000u &&
|
||||
|
|
@ -640,6 +640,145 @@ void TestFailedMaterializationRejectsStage() {
|
|||
"missing runtime user data did not reject the cached stage");
|
||||
}
|
||||
|
||||
void TestSharedControlPredicates() {
|
||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||
using Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind;
|
||||
constexpr uint32_t depth = 8;
|
||||
for (const auto operation : {ValueOpcode::LogicalAnd, ValueOpcode::LogicalOr}) {
|
||||
Program program;
|
||||
program.user_data_count = 1;
|
||||
program.srt_plan_complete = program.resource_tracking_complete = true;
|
||||
auto &block = AddValueBlock(program);
|
||||
const auto emit = [&](ValueOpcode opcode, std::initializer_list<Value> args) {
|
||||
return Value(&block.AppendNewInst(opcode, args));
|
||||
};
|
||||
const auto user = emit(ValueOpcode::GetUserData, {Value(ScalarReg(0))});
|
||||
const auto uniform = emit(ValueOpcode::INotEqual32, {user, Value(0u)});
|
||||
const auto lane = emit(ValueOpcode::GetBuiltin,
|
||||
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
|
||||
auto varying = emit(ValueOpcode::INotEqual32, {lane, Value(0u)});
|
||||
// Both failed and partially uniform subgraphs share their operands exponentially.
|
||||
for (uint32_t i = 0; i < depth; ++i)
|
||||
varying = emit(ValueOpcode::LogicalOr, {varying, varying});
|
||||
auto condition = emit(operation, {varying, uniform});
|
||||
for (uint32_t i = 0; i < depth; ++i)
|
||||
condition = emit(ValueOpcode::LogicalOr, {condition, condition});
|
||||
condition = emit(ValueOpcode::LogicalNot, {condition});
|
||||
condition = emit(ValueOpcode::ConditionRef, {condition});
|
||||
for (uint32_t i = 0; i < 3; ++i) AddValueBlock(program);
|
||||
for (uint32_t i = 0; i < 2; ++i) {
|
||||
program.blocks[i]->condition = condition;
|
||||
program.blocks[i]->terminator.kind = TerminatorKind::ConditionalBranch;
|
||||
program.blocks[i]->terminator.true_block = program.blocks[i == 0 ? 1 : 3];
|
||||
program.blocks[i]->terminator.false_block = program.blocks[2];
|
||||
}
|
||||
auto plan = ExtractResourcePlan(program);
|
||||
Check(plan.value_storage.size() <= 2u * depth + 12u &&
|
||||
plan.control_flow.size() == 4 &&
|
||||
plan.control_flow[0].condition == plan.control_flow[1].condition &&
|
||||
!plan.capture_specialization_reads,
|
||||
"shared predicates expanded into trees or retained varying dependencies");
|
||||
for (const uint32_t flag : {0u, 1u}) {
|
||||
const std::array<uint32_t, 1> user_data{flag};
|
||||
SrtWalker walker(plan, {.user_data = user_data});
|
||||
uint32_t result = 99;
|
||||
const bool conjunction = operation == ValueOpcode::LogicalAnd;
|
||||
const bool known = conjunction ? flag == 0 : flag != 0;
|
||||
Check(walker.Evaluate(plan.control_flow[0].condition, result) == known &&
|
||||
(!known || result == uint32_t{conjunction}),
|
||||
"shared predicate changed its uniform factor or cached runtime values");
|
||||
std::vector<uint32_t> flat;
|
||||
Check(walker.RefreshFlatBuffer(flat) &&
|
||||
plan.visited_blocks == (known
|
||||
? (conjunction ? std::vector<uint8_t>{1, 1, 0, 1}
|
||||
: std::vector<uint8_t>{1, 0, 1, 0})
|
||||
: std::vector<uint8_t>{1, 1, 1, 1}),
|
||||
"shared predicate skipped an unknown branch or followed a rejected branch");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TestNestedExecPredicatesReuseReads() {
|
||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||
using Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind;
|
||||
constexpr uint32_t depth = 12;
|
||||
Program program;
|
||||
program.stage = Libs::Graphics::ShaderType::Compute;
|
||||
program.srt_plan_complete = program.resource_tracking_complete = true;
|
||||
for (uint32_t i = 0; i < 3; ++i) AddValueBlock(program);
|
||||
auto &entry = *program.blocks[0];
|
||||
auto &use = *program.blocks[1];
|
||||
auto &exit = *program.blocks[2];
|
||||
const auto emit = [&](ValueOpcode opcode, std::initializer_list<Value> args) {
|
||||
return Value(&entry.AppendNewInst(opcode, args));
|
||||
};
|
||||
const auto lane = emit(ValueOpcode::GetBuiltin,
|
||||
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
|
||||
program.memory_info.push_back({.kind = ResourceKind::ScalarAddress, .planning_only = true});
|
||||
const auto address = emit(ValueOpcode::GetAddressResource, {Value(0x1000u), Value(0u)});
|
||||
const auto count = emit(ValueOpcode::LoadAddressU32,
|
||||
{address, Value(0u), Value(0u), Value(true)});
|
||||
count.Instruction()->SetFlags(MemoryFlags{.index = 0});
|
||||
auto condition = emit(ValueOpcode::LogicalNot,
|
||||
{emit(ValueOpcode::ULessThanEqual32, {count, lane})});
|
||||
// A compact shared DAG must not become a tree of fresh EXEC evaluations.
|
||||
for (uint32_t i = 0; i < depth; ++i) {
|
||||
const auto mask = emit(ValueOpcode::INotEqual32, {lane, Value(i)});
|
||||
condition = emit(ValueOpcode::LogicalAnd,
|
||||
{mask, emit(ValueOpcode::LogicalOr, {condition, condition})});
|
||||
}
|
||||
entry.condition = condition;
|
||||
entry.terminator.kind = TerminatorKind::ConditionalBranch;
|
||||
entry.terminator.true_block = &use;
|
||||
entry.terminator.false_block = &exit;
|
||||
entry.AddBranch(&use);
|
||||
entry.AddBranch(&exit);
|
||||
use.terminator.kind = TerminatorKind::Branch;
|
||||
use.terminator.true_block = &exit;
|
||||
use.AddBranch(&exit);
|
||||
DescriptorSource source;
|
||||
source.dwords = {Value(0x8000u), Value(1u << 16u), Value(16u), Value(3u << 28u)};
|
||||
source.dword_count = 4;
|
||||
program.descriptor_sources.push_back(source);
|
||||
program.info.buffers.push_back({.source = 0, .read = true});
|
||||
program.memory_info.push_back({.kind = ResourceKind::Buffer, .resource = 0});
|
||||
const auto buffer = Value(&use.AppendNewInst(ValueOpcode::GetBufferResource,
|
||||
{source.dwords[0], source.dwords[1], source.dwords[2], source.dwords[3]}));
|
||||
use.AppendNewInst(ValueOpcode::LoadBufferU32,
|
||||
{buffer, Value(0u), Value(0u), Value(0u), Value(true)})
|
||||
.SetFlags(MemoryFlags{.index = 1});
|
||||
auto plan = ExtractResourcePlan(program);
|
||||
struct Reads { uint32_t value = 0; bool clean = true; uint32_t strict = 0, ordinary = 0; } reads;
|
||||
const SrtRuntime runtime{
|
||||
.read_memory = +[](void *data, uint64_t, std::span<uint32_t>) {
|
||||
++static_cast<Reads *>(data)->ordinary;
|
||||
return false;
|
||||
},
|
||||
.userdata = &reads,
|
||||
.read_specialization_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
|
||||
auto &reads = *static_cast<Reads *>(data);
|
||||
++reads.strict;
|
||||
Check(reads.strict <= depth + 1,
|
||||
"shared EXEC predicates repeated scalar reads beyond their distinct masks");
|
||||
if (!reads.clean || address != 0x1000u || words.size() != 1) return false;
|
||||
words[0] = reads.value;
|
||||
return true;
|
||||
}};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
for (const auto state : {Reads{64u, true}, Reads{0u, true}, Reads{0u, false}, Reads{64u, true}}) {
|
||||
reads = state;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"nested EXEC predicates rejected a valid resource snapshot");
|
||||
const bool inactive = reads.clean && reads.value == 0;
|
||||
Check(snapshot.buffers[0].dwords[0] == (inactive ? 0u : 0x8000u),
|
||||
"nested EXEC predicates lost an unknown branch or retained stale runtime facts");
|
||||
Check(reads.strict > 0 && reads.ordinary == 0 &&
|
||||
snapshot.specialization_reads.size() <= depth + 1,
|
||||
"shared EXEC predicates repeated scalar reads beyond their distinct masks");
|
||||
}
|
||||
}
|
||||
|
||||
void TestFiniteImageRefreshReusesScalarReads() {
|
||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||
Program program;
|
||||
|
|
@ -735,6 +874,156 @@ void TestFiniteImageRefreshReusesScalarReads() {
|
|||
"finite image refresh grew reusable resource storage after warmup");
|
||||
}
|
||||
|
||||
void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask) {
|
||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||
using Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind;
|
||||
Program program;
|
||||
program.stage = Libs::Graphics::ShaderType::Compute;
|
||||
program.srt_plan_complete = program.resource_tracking_complete = true;
|
||||
for (uint32_t i = 0; i < 6; ++i) AddValueBlock(program);
|
||||
auto &entry = *program.blocks[0];
|
||||
auto &loop = *program.blocks[1];
|
||||
auto &body = *program.blocks[2];
|
||||
auto &merge = *program.blocks[3];
|
||||
auto &use = *program.blocks[4];
|
||||
auto &exit = *program.blocks[5];
|
||||
const auto emit = [](Block &block, ValueOpcode opcode, std::initializer_list<Value> args) {
|
||||
return Value(&block.AppendNewInst(opcode, args));
|
||||
};
|
||||
const auto branch = [](Block &block, Block &target, Value condition = {}, Block *other = nullptr) {
|
||||
block.terminator.kind = other ? TerminatorKind::ConditionalBranch : TerminatorKind::Branch;
|
||||
block.terminator.true_block = ⌖
|
||||
block.terminator.false_block = other;
|
||||
block.condition = condition;
|
||||
block.AddBranch(&target);
|
||||
if (other) block.AddBranch(other);
|
||||
};
|
||||
const auto lane = emit(entry, ValueOpcode::GetBuiltin,
|
||||
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
|
||||
const auto active = emit(entry, ValueOpcode::INotEqual32, {lane, Value(0u)});
|
||||
const auto initial_mask = independent_mask
|
||||
? emit(entry, ValueOpcode::IEqual32, {lane, Value(0u)}) : active;
|
||||
const auto initial = emit(entry, ValueOpcode::SelectU32,
|
||||
{initial_mask, Value(UINT32_MAX), Value(0u)});
|
||||
program.memory_info.push_back({.kind = ResourceKind::ScalarAddress, .planning_only = true});
|
||||
const auto address = emit(entry, ValueOpcode::GetAddressResource, {Value(0x1000u), Value(0u)});
|
||||
const auto count = emit(entry, ValueOpcode::LoadAddressU32,
|
||||
{address, Value(0u), Value(0u), Value(true)});
|
||||
count.Instruction()->SetFlags(MemoryFlags{.index = 0});
|
||||
const auto rejected = emit(entry, ValueOpcode::ULessThanEqual32, {count, lane});
|
||||
const auto accepted = emit(entry, ValueOpcode::LogicalNot, {rejected});
|
||||
branch(entry, loop, emit(entry, ValueOpcode::LogicalAnd, {active, accepted}), &merge);
|
||||
|
||||
// A nonempty traversal contains a varying loop-carried value. Its cycle must
|
||||
// remain unknown; only the proven empty traversal can select the bypass arm.
|
||||
auto &carried = loop.AppendNewInst(ValueOpcode::Phi);
|
||||
carried.SetFlags(Type::U32);
|
||||
const auto next = emit(body, ValueOpcode::IAdd32, {Value(&carried), Value(1u)});
|
||||
carried.AddPhiOperand(&entry, lane);
|
||||
carried.AddPhiOperand(&body, next);
|
||||
branch(loop, body, emit(loop, ValueOpcode::INotEqual32, {Value(&carried), Value(UINT32_MAX)}), &merge);
|
||||
loop.terminator.loop_header = true;
|
||||
loop.terminator.continue_block = &body;
|
||||
loop.terminator.merge_block = &merge;
|
||||
branch(body, loop);
|
||||
auto &selected = merge.AppendNewInst(ValueOpcode::Phi);
|
||||
selected.SetFlags(Type::U32);
|
||||
selected.AddPhiOperand(&entry, initial);
|
||||
selected.AddPhiOperand(&loop, Value(&carried));
|
||||
const auto valid = emit(merge, ValueOpcode::INotEqual32, {Value(&selected), Value(UINT32_MAX)});
|
||||
branch(merge, use, emit(merge, ValueOpcode::LogicalAnd, {active, valid}), &exit);
|
||||
branch(use, exit);
|
||||
DescriptorSource source;
|
||||
source.dwords = {Value(0x8000u), Value(1u << 16u), Value(16u), Value(3u << 28u)};
|
||||
source.dword_count = 4;
|
||||
program.descriptor_sources.push_back(source);
|
||||
program.info.buffers.push_back({.source = 0, .read = true});
|
||||
program.memory_info.push_back({.kind = ResourceKind::Buffer, .resource = 0});
|
||||
const auto buffer = emit(use, ValueOpcode::GetBufferResource,
|
||||
{source.dwords[0], source.dwords[1], source.dwords[2], source.dwords[3]});
|
||||
emit(use, ValueOpcode::LoadBufferU32, {buffer, Value(0u), Value(0u), Value(0u), Value(true)})
|
||||
.Instruction()->SetFlags(MemoryFlags{.index = 1});
|
||||
auto plan = ExtractResourcePlan(program);
|
||||
struct Reads { uint32_t count = 0; bool clean = true; } reads;
|
||||
const SrtRuntime runtime{
|
||||
.userdata = &reads,
|
||||
.read_specialization_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
|
||||
const auto &reads = *static_cast<Reads *>(data);
|
||||
if (!reads.clean || address != 0x1000u || words.size() != 1) return false;
|
||||
words[0] = reads.count;
|
||||
return true;
|
||||
}};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
for (const auto state : {Reads{0u, true}, Reads{4u, true}, Reads{0u, false}, Reads{0u, true}}) {
|
||||
reads = state;
|
||||
uint32_t condition = 99;
|
||||
Check(!SrtWalker(plan, runtime).Evaluate(plan.control_flow[3].condition, condition),
|
||||
"direct merge evaluation used missing or previous runtime reachability");
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"partial traversal proof rejected a valid resource snapshot");
|
||||
const bool inactive = !independent_mask && reads.clean && reads.count == 0u;
|
||||
Check(snapshot.buffers[0].dwords[0] == (inactive ? 0u : 0x8000u),
|
||||
"empty traversal kept its descriptor or an uncertain traversal lost it");
|
||||
Check(specialization.buffers[0].zero_stride_oob == inactive,
|
||||
"inactive traversal did not expose dead vector reads to specialization");
|
||||
if (inactive) {
|
||||
Check(!snapshot.specialization_reads.empty(), "empty traversal lost its clean scalar read");
|
||||
for (const auto &read : snapshot.specialization_reads)
|
||||
Check(read == std::pair<uint64_t, uint64_t>{0x1000u, 4u},
|
||||
"empty traversal proof captured an unrelated memory range");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TestCyclicControlPredicatesRemainUnknown() {
|
||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||
using Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind;
|
||||
Program program;
|
||||
program.stage = Libs::Graphics::ShaderType::Compute;
|
||||
program.srt_plan_complete = program.resource_tracking_complete = true;
|
||||
for (uint32_t i = 0; i < 4; ++i) AddValueBlock(program);
|
||||
auto &entry = *program.blocks[0];
|
||||
auto &left = *program.blocks[1];
|
||||
auto &right = *program.blocks[2];
|
||||
auto &exit = *program.blocks[3];
|
||||
const auto lane = Value(&entry.AppendNewInst(ValueOpcode::GetBuiltin,
|
||||
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)}));
|
||||
const auto u = Value(&entry.AppendNewInst(ValueOpcode::INotEqual32, {lane, Value(0u)}));
|
||||
const auto v = Value(&entry.AppendNewInst(ValueOpcode::INotEqual32, {lane, Value(1u)}));
|
||||
auto &p = left.AppendNewInst(ValueOpcode::Phi);
|
||||
auto &q = right.AppendNewInst(ValueOpcode::Phi);
|
||||
p.SetFlags(Type::U1);
|
||||
q.SetFlags(Type::U1);
|
||||
const auto a = Value(&left.AppendNewInst(ValueOpcode::LogicalAnd, {u, Value(&p)}));
|
||||
const auto b = Value(&right.AppendNewInst(ValueOpcode::LogicalAnd, {v, Value(&q)}));
|
||||
p.AddPhiOperand(&entry, Value(true));
|
||||
p.AddPhiOperand(&right, b);
|
||||
q.AddPhiOperand(&entry, Value(true));
|
||||
q.AddPhiOperand(&left, a);
|
||||
const auto branch = [](Block &block, Value condition, Block &yes, Block &no) {
|
||||
block.condition = condition;
|
||||
block.terminator.kind = TerminatorKind::ConditionalBranch;
|
||||
block.terminator.true_block = &yes;
|
||||
block.terminator.false_block = &no;
|
||||
block.AddBranch(&yes);
|
||||
block.AddBranch(&no);
|
||||
};
|
||||
// Two loop entries have no single header whose Phi can be discarded up front.
|
||||
branch(entry, u, left, right);
|
||||
branch(left, a, right, exit);
|
||||
branch(right, b, left, exit);
|
||||
auto plan = ExtractResourcePlan(program);
|
||||
SrtWalker walker(plan, {});
|
||||
uint32_t value = 99;
|
||||
Check(!walker.Evaluate(plan.control_flow[1].condition, value) &&
|
||||
!walker.Evaluate(plan.control_flow[2].condition, value),
|
||||
"cyclic varying control acquired an unsupported uniform value");
|
||||
std::vector<uint32_t> flat;
|
||||
Check(walker.RefreshFlatBuffer(flat) && plan.visited_blocks == std::vector<uint8_t>({1, 1, 1, 1}),
|
||||
"cyclic control lost a possible branch during reachability");
|
||||
}
|
||||
|
||||
void TestMixedSamplerVariantsShareRuntimeDescriptor() {
|
||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||
auto program = MixedSamplerProgram();
|
||||
|
|
@ -766,6 +1055,63 @@ void TestMixedSamplerVariantsShareRuntimeDescriptor() {
|
|||
"sampler variants retained stale or duplicated descriptors after refresh");
|
||||
}
|
||||
|
||||
void TestIndirectImageSamplerClasses(bool mixed) {
|
||||
using namespace Libs::Graphics::ShaderRecompiler::IR;
|
||||
using Libs::Graphics::Prospero::BufferFormat;
|
||||
using Libs::Graphics::Prospero::TextureNumericClass;
|
||||
auto program = MixedSamplerProgram();
|
||||
auto &first = program.descriptor_sources[0];
|
||||
first.dwords[0] = Value(0x100u);
|
||||
first.dwords[1] = Value(static_cast<uint32_t>(BufferFormat::k8UNorm) << 20u);
|
||||
first.dwords[3] = program.descriptor_sources[1].dwords[3];
|
||||
program.descriptor_sources[1].dwords[1] = Value(static_cast<uint32_t>(
|
||||
mixed ? BufferFormat::k8UInt : BufferFormat::k8UNorm) << 20u);
|
||||
DescriptorSource root;
|
||||
root.dword_count = 8;
|
||||
root.dwords.fill(Value(0u));
|
||||
root.indirect_descriptor.emplace().sources = {0, 1, 0};
|
||||
program.info.images.resize(1);
|
||||
program.info.images[0].source = static_cast<uint32_t>(program.descriptor_sources.size());
|
||||
program.descriptor_sources.push_back(root);
|
||||
program.info.samplers.resize(1);
|
||||
program.info.sampled_pairs.resize(1);
|
||||
auto plan = ExtractResourcePlan(program);
|
||||
std::array<uint32_t, 1> user_data{0x11111111u};
|
||||
uint32_t reads = 0;
|
||||
const SrtRuntime runtime{.user_data = user_data, .userdata = &reads,
|
||||
.read_specialization_memory = RejectSpecializationRead};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) && reads == 0 &&
|
||||
snapshot.images.size() == 2 && snapshot.images[0].dwords[0] == 0x100u &&
|
||||
snapshot.images[1].dwords[0] == 1u &&
|
||||
specialization.images[0].numeric_class == TextureNumericClass::Float &&
|
||||
specialization.images[1].numeric_class ==
|
||||
(mixed ? TextureNumericClass::Uint : TextureNumericClass::Float),
|
||||
"indirect image materialization rejected or changed a typed candidate");
|
||||
const auto offset = specialization.images[0].indirect_mapping_offset;
|
||||
Check(snapshot.flattened_srt[offset] == 3u &&
|
||||
snapshot.flattened_srt[offset + 2u] == 0u &&
|
||||
snapshot.flattened_srt[offset + 4u] == 1u &&
|
||||
snapshot.flattened_srt[offset + 6u] == 0u,
|
||||
"mixed image classes changed selector ordinals or duplicated candidates");
|
||||
ApplyResourceSpecialization(program, specialization);
|
||||
Check(program.info.images[0].indirect_resources == std::vector<uint32_t>({0, 1}) &&
|
||||
snapshot.samplers.size() == 1 && program.info.samplers.size() == (mixed ? 2u : 1u),
|
||||
"indirect images lost candidates or created unnecessary sampler variants");
|
||||
user_data[0] = 0x33333333u;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"mixed image sampler refresh failed");
|
||||
for (uint32_t index = 0; index < program.info.samplers.size(); ++index) {
|
||||
const auto &sampler = program.info.samplers[index];
|
||||
Check(sampler.snapshot_index == 0u && sampler.source == plan.info.samplers[0].source &&
|
||||
sampler.force_point_filtering == (index != 0u) &&
|
||||
sampler.integer_border == (index != 0u) &&
|
||||
snapshot.samplers[sampler.snapshot_index].dwords[0] == user_data[0],
|
||||
"typed sampler variants lost filtering, border class, or shared live descriptor");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace Common {
|
||||
|
|
@ -793,8 +1139,14 @@ int main() {
|
|||
for (const bool memory_condition : {false, true})
|
||||
TestWrittenDescriptorPredicateReads(memory_condition);
|
||||
TestFailedMaterializationRejectsStage();
|
||||
TestSharedControlPredicates();
|
||||
TestNestedExecPredicatesReuseReads();
|
||||
TestFiniteImageRefreshReusesScalarReads();
|
||||
for (const bool independent_mask : {false, true})
|
||||
TestEmptyTraversalSkipsUnmappedDescriptor(independent_mask);
|
||||
TestCyclicControlPredicatesRemainUnknown();
|
||||
TestMixedSamplerVariantsShareRuntimeDescriptor();
|
||||
for (const bool mixed : {false, true}) TestIndirectImageSamplerClasses(mixed);
|
||||
std::puts("ResourceMaterializationTests: all cases passed");
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -550,29 +550,51 @@ void TestConstantBufferBounds() {
|
|||
struct Case {
|
||||
uint32_t offset;
|
||||
uint32_t immediate;
|
||||
uint32_t stride;
|
||||
uint32_t records;
|
||||
bool valid;
|
||||
uint32_t expected;
|
||||
uint32_t reads;
|
||||
};
|
||||
for (const auto &test : {Case{12u, 0u, true, 0xa5a5a5a5u},
|
||||
Case{3u, 1u, true, 0x12345678u},
|
||||
Case{0xfffffffcu, 4u, false, 0u},
|
||||
Case{16u, 0u, false, 0u}}) {
|
||||
struct MemoryProbe {
|
||||
TestMemory memory{{{0x3000u, 0x12345678u}, {0x300cu, 0xa5a5a5a5u}}};
|
||||
uint32_t attempts = 0;
|
||||
};
|
||||
const auto read_memory = [](void *userdata, uint64_t address,
|
||||
std::span<uint32_t> values) {
|
||||
auto &probe = *static_cast<MemoryProbe *>(userdata);
|
||||
++probe.attempts;
|
||||
return ReadMemory(&probe.memory, address, values);
|
||||
};
|
||||
// PS5 scalar-buffer loads return zero per out-of-range DWORD; an unreadable
|
||||
// in-range address remains a failed memory read.
|
||||
for (const auto &test : {Case{12u, 0u, 0u, 16u, true, 0xa5a5a5a5u, 1u},
|
||||
Case{3u, 1u, 0u, 16u, true, 0x12345678u, 1u},
|
||||
Case{0xfffffffcu, 4u, 0u, 16u, true, 0u, 0u},
|
||||
Case{16u, 0u, 0u, 16u, true, 0u, 0u},
|
||||
Case{12u, 0u, 0u, 14u, true, 0u, 0u},
|
||||
Case{0u, 0u, 0u, 0u, true, 0u, 0u},
|
||||
Case{8u, 0u, 0u, 16u, false, 0u, 1u},
|
||||
Case{12u, 0u, 48u, 32768u, true, 0xa5a5a5a5u, 1u},
|
||||
Case{0x6339c0u, 16u, 48u, 32768u, true, 0u, 0u}}) {
|
||||
Fixture fixture;
|
||||
const auto memory = fixture.AddMemory(ResourceKind::ScalarBuffer, test.immediate);
|
||||
const auto buffer =
|
||||
fixture.Emit(ValueOpcode::GetBufferResource,
|
||||
{Value(0x3000u), Value(0u), Value(16u), Value(0u)});
|
||||
{Value(0x3000u), Value(test.stride << 16u),
|
||||
Value(test.records), Value(0u)});
|
||||
const auto read = fixture.EmitMemory(ValueOpcode::ReadConstBuffer,
|
||||
{buffer, Value(test.offset)}, memory);
|
||||
LoadBuffer(fixture, {read, Value(0u), Value(16u), Value(0u)});
|
||||
fixture.Plan();
|
||||
|
||||
TestMemory memory_image{{{0x3000u, 0x12345678u}, {0x300cu, 0xa5a5a5a5u}}};
|
||||
SrtRuntime runtime{.read_memory = ReadMemory, .userdata = &memory_image};
|
||||
MemoryProbe memory_image;
|
||||
SrtRuntime runtime{.read_memory = read_memory, .userdata = &memory_image};
|
||||
std::vector<uint32_t> flat;
|
||||
Check(SrtWalker(ExtractResourcePlan(fixture.program), runtime).RefreshFlatBuffer(flat) == test.valid,
|
||||
"constant-buffer walk misaligned or wrapped its offset components");
|
||||
Check(test.valid ? flat == std::vector<uint32_t>{test.expected} : memory_image.reads == 0,
|
||||
Check((!test.valid || flat == std::vector<uint32_t>{test.expected}) &&
|
||||
memory_image.attempts == test.reads,
|
||||
"constant-buffer walk read the wrong word or accessed an out-of-bounds address");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,376 @@
|
|||
#include "libs/libVdecsw.cpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
std::vector<Loader::SymbolResolve> registrations;
|
||||
}
|
||||
namespace Loader {
|
||||
void SymbolDatabase::Add(const SymbolResolve& symbol, uint64_t, const std::string&) {
|
||||
registrations.push_back(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
using namespace Libs::Vdecsw;
|
||||
|
||||
#define CHECK(condition) do { if (!(condition)) { \
|
||||
std::fprintf(stderr, "VdecswTests:%d: %s\n", __LINE__, #condition); std::abort(); \
|
||||
} } while (false)
|
||||
|
||||
// Six 16x16 YUV420 frames with Y=40,60,80,100,120,140, encoded by libx264
|
||||
// (QP18, bframes=2, b-adapt=0, keyint=12, aud=1; SEI removed). Coding order
|
||||
// is I0,P3,B1,B2,P5,B4. Both delayed output and packet metadata are observable.
|
||||
constexpr uint8_t Avc[] = {
|
||||
0x00,0x00,0x00,0x01,0x09,0x10,0x00,0x00,0x00,0x01,0x67,0x64,0x00,0x0a,0xac,0xd9,
|
||||
0x5e,0x84,0x00,0x00,0x03,0x00,0x04,0x00,0x00,0x03,0x00,0x30,0x3c,0x48,0x96,0x58,
|
||||
0x00,0x00,0x00,0x01,0x68,0xeb,0xe0,0x8c,0xb2,0x2c,0x00,0x00,0x01,0x65,0x88,0x84,
|
||||
0x00,0xeb,0xfe,0xf5,0x27,0xf8,0x14,0xd0,0x8c,0x2d,0x00,0x00,0x00,0x01,0x09,0x30,
|
||||
0x00,0x00,0x01,0x41,0x9a,0x23,0x6c,0x7f,0xa0,0xc3,0x2f,0x81,0xec,0xfb,0x1c,0x00,
|
||||
0x00,0x00,0x01,0x09,0x50,0x00,0x00,0x01,0x41,0x9e,0x41,0x78,0xaf,0xd6,0x81,0x00,
|
||||
0x00,0x00,0x01,0x09,0x50,0x00,0x00,0x01,0x01,0x9e,0x62,0x6a,0x49,0xff,0xd8,0x80,
|
||||
0x00,0x00,0x00,0x01,0x09,0x30,0x00,0x00,0x01,0x41,0x9a,0x65,0x49,0xa8,0x41,0x68,
|
||||
0x99,0x4c,0x14,0xff,0xa0,0xc3,0x2f,0x81,0xdd,0x0c,0xc1,0x00,0x00,0x00,0x01,0x09,
|
||||
0x50,0x00,0x00,0x01,0x01,0x9e,0x84,0x6a,0x49,0xff,0xd8,0x81,
|
||||
};
|
||||
constexpr std::array<size_t, 7> PacketOffsets{0, 58, 79, 95, 112, 139, sizeof(Avc)};
|
||||
constexpr std::array<uint32_t, 6> PresentationOrder{0, 3, 1, 2, 5, 4};
|
||||
// One independent, immediately displayable IDR (same pixels, QP18, no B frames).
|
||||
constexpr uint8_t Intra[] = {
|
||||
0x00,0x00,0x00,0x01,0x09,0x10,0x00,0x00,0x00,0x01,0x67,0x64,0x10,0x0a,0xac,0xbb,
|
||||
0xd0,0x80,0x00,0x00,0x03,0x00,0x80,0x00,0x00,0x03,0x01,0x02,0x00,0x00,0x00,0x01,
|
||||
0x68,0xee,0x02,0x32,0xc8,0xb0,0x00,0x00,0x01,0x65,0x88,0x84,0x3a,0xfe,0xf5,0x27,
|
||||
0xf8,0x14,0xd0,0x8c,0x2d,
|
||||
};
|
||||
|
||||
InputData Input(uint32_t packet) {
|
||||
return {sizeof(InputData), const_cast<uint8_t*>(Avc + PacketOffsets[packet]),
|
||||
PacketOffsets[packet + 1] - PacketOffsets[packet],
|
||||
PresentationOrder[packet] * 1000u, packet * 1000u,
|
||||
0x12340000u + PresentationOrder[packet]};
|
||||
}
|
||||
|
||||
struct GuardedMemory {
|
||||
uint8_t* allocation;
|
||||
uint8_t* data;
|
||||
size_t size;
|
||||
explicit GuardedMemory(size_t bytes): size(bytes) {
|
||||
allocation = static_cast<uint8_t*>(std::aligned_alloc(256, (bytes + 767u) & ~size_t{255}));
|
||||
CHECK(allocation != nullptr);
|
||||
data = allocation + 256;
|
||||
std::memset(allocation, 0xa7, bytes + 512);
|
||||
}
|
||||
~GuardedMemory() { Check(); std::free(allocation); }
|
||||
void Check() const {
|
||||
for (size_t i = 0; i < 256; ++i)
|
||||
CHECK(allocation[i] == 0xa7 && data[size + i] == 0xa7);
|
||||
}
|
||||
};
|
||||
|
||||
ConfigInfo Config() {
|
||||
ConfigInfo config{};
|
||||
config.this_size = sizeof(config);
|
||||
config.resource_type = config.codec_type = 1;
|
||||
config.profile = 100;
|
||||
config.max_level = 10;
|
||||
config.max_width = config.max_height = 16;
|
||||
config.max_dpb_frames = 4;
|
||||
config.extra_dpb_frames = 1;
|
||||
config.input_queue_depth = 3;
|
||||
config.cpu_thread_priority = -1;
|
||||
config.optimize_progressive = true;
|
||||
return config;
|
||||
}
|
||||
|
||||
struct Session {
|
||||
GuardedMemory queue_memory{sizeof(ComputeQueue)};
|
||||
GuardedMemory state_memory{sizeof(State)};
|
||||
ConfigInfo config = Config();
|
||||
MemoryInfo memory{sizeof(MemoryInfo)};
|
||||
ComputeQueue* queue = nullptr;
|
||||
State* decoder = nullptr;
|
||||
|
||||
explicit Session(bool disable_sync = false) {
|
||||
ComputeMemoryInfo compute{sizeof(compute), 0, queue_memory.data};
|
||||
CHECK(QueryComputeMemoryInfo(&compute) == 0);
|
||||
CHECK(compute.cpu_gpu_memory_size == queue_memory.size);
|
||||
CHECK(compute.cpu_gpu_memory == queue_memory.data);
|
||||
ComputeConfigInfo queue_config{sizeof(queue_config), 3, 3, true, 0, 0};
|
||||
CHECK(AllocateComputeQueue(&queue_config, &compute, &queue) == 0);
|
||||
config.compute_queue = queue;
|
||||
config.disable_sync_input = disable_sync;
|
||||
CHECK(QueryDecoderMemoryInfo(&config, &memory) == 0);
|
||||
CHECK(memory.cpu_memory_size == state_memory.size);
|
||||
CHECK(memory.gpu_memory_size == 0 && memory.cpu_gpu_memory_size == 0);
|
||||
CHECK(memory.max_frame_buffer_size == 10240 && memory.frame_buffer_alignment == 256);
|
||||
memory.cpu_memory = state_memory.data;
|
||||
CHECK(CreateDecoder(&config, &memory, &decoder) == 0);
|
||||
CHECK(ReleaseComputeQueue(queue) == Fail(Error::ComputeQueue));
|
||||
}
|
||||
~Session() {
|
||||
CHECK(DeleteDecoder(decoder) == 0);
|
||||
CHECK(DeleteDecoder(decoder) == Fail(Error::DecoderInstance));
|
||||
CHECK(ReleaseComputeQueue(queue) == 0);
|
||||
CHECK(ReleaseComputeQueue(queue) == Fail(Error::ComputeQueue));
|
||||
}
|
||||
void Submit(uint32_t packet) {
|
||||
auto input = Input(packet);
|
||||
CHECK(SetDecodeInput(decoder, &input) == 0);
|
||||
InputResult result{sizeof(result)};
|
||||
CHECK(TrySyncDecodeInput(decoder, &result) == 0);
|
||||
CHECK(result.decoded_au == input.au_data && result.reserved0 == 0);
|
||||
}
|
||||
};
|
||||
|
||||
void TestRegistrationAndErrors() {
|
||||
Loader::SymbolDatabase symbols;
|
||||
InitVdecsw_1(&symbols);
|
||||
g_print_name = false;
|
||||
constexpr std::array nids{"0moTubWCsTM", "hIgrg5h4V6s", "A+2M7EivuOU", "+L5ArV1tPGA",
|
||||
"ecUtPX+dBYk", "fX-zOOefbbs", "aqMiF0AgUYI", "veb-YBrOqo0", "kMBw37oH8nI",
|
||||
"rgtMCOpyBSc", "ihNT-uuEAr4", "5Y6nZqIZvBg", "l4sQYy5wPkc"};
|
||||
CHECK(registrations.size() == nids.size());
|
||||
for (size_t i = 0; i < nids.size(); ++i) {
|
||||
const auto& symbol = registrations[i];
|
||||
CHECK(symbol.name == nids[i] && symbol.library == "Vdecsw" && symbol.module == "Vdecsw");
|
||||
CHECK(symbol.library_version == 1 && symbol.module_version_major == 1 && symbol.module_version_minor == 1);
|
||||
}
|
||||
CHECK(uint32_t(Fail(Error::StructSize)) == 0x81510101u);
|
||||
CHECK(QueryComputeMemoryInfo(nullptr) == Fail(Error::ArgumentPointer));
|
||||
ComputeMemoryInfo compute{};
|
||||
CHECK(QueryComputeMemoryInfo(&compute) == Fail(Error::StructSize));
|
||||
auto config = Config();
|
||||
MemoryInfo memory{sizeof(memory)};
|
||||
config.max_width = 3840; config.max_height = 2160; config.max_level = 52;
|
||||
CHECK(QueryDecoderMemoryInfo(&config, &memory) == 0 && memory.max_frame_buffer_size == 12445696u);
|
||||
config.this_size = 72;
|
||||
CHECK(QueryDecoderMemoryInfo(&config, &memory) == Fail(Error::StructSize));
|
||||
Session session;
|
||||
GuardedMemory other_queue(sizeof(ComputeQueue));
|
||||
ComputeMemoryInfo compute_duplicate{sizeof(compute_duplicate), other_queue.size, other_queue.data};
|
||||
ComputeConfigInfo same_queue{sizeof(same_queue), 3, 3, false, 0, 0};
|
||||
ComputeQueue* duplicate = nullptr;
|
||||
CHECK(AllocateComputeQueue(&same_queue, &compute_duplicate, &duplicate) == Fail(Error::ComputeQueue));
|
||||
CHECK(duplicate == nullptr);
|
||||
compute_duplicate.cpu_gpu_memory = session.queue_memory.data;
|
||||
same_queue.queue_id = 2;
|
||||
CHECK(AllocateComputeQueue(&same_queue, &compute_duplicate, &duplicate) == Fail(Error::ComputeQueue));
|
||||
State* duplicate_decoder = nullptr;
|
||||
CHECK(CreateDecoder(&session.config, &session.memory, &duplicate_decoder) == Fail(Error::MemoryPointer));
|
||||
CHECK(duplicate_decoder == nullptr);
|
||||
CHECK(ResetDecoder(nullptr) == Fail(Error::DecoderInstance));
|
||||
InputResult completion{sizeof(completion)};
|
||||
CHECK(TrySyncDecodeInput(session.decoder, &completion) == Fail(Error::InputQueueEmpty));
|
||||
OutputInfo output{sizeof(output)};
|
||||
CHECK(TrySyncDecodeOutput(session.decoder, &output) == Fail(Error::OutputBufferEmpty));
|
||||
GuardedMemory pixels(session.memory.max_frame_buffer_size);
|
||||
FrameBuffer frame{sizeof(frame), pixels.data + 1, pixels.size};
|
||||
CHECK(SetDecodeOutput(session.decoder, &frame) == Fail(Error::FrameBufferAlignment));
|
||||
frame.data = pixels.data; frame.size--;
|
||||
CHECK(SetDecodeOutput(session.decoder, &frame) == Fail(Error::FrameBufferSize));
|
||||
frame.size++;
|
||||
CHECK(SetDecodeOutput(session.decoder, &frame) == 0);
|
||||
CHECK(SetDecodeOutput(session.decoder, &frame) == Fail(Error::OutputBufferFull));
|
||||
CHECK(TrySyncDecodeOutput(session.decoder, &output) == Fail(Error::DecodePending));
|
||||
CHECK(ResetDecoder(session.decoder) == 0);
|
||||
CHECK(TrySyncDecodeOutput(session.decoder, &output) == Fail(Error::OutputBufferEmpty));
|
||||
}
|
||||
|
||||
void CheckPicture(const OutputInfo& output, uint32_t frame, GuardedMemory& pixels) {
|
||||
CHECK(output.valid && output.picture_count == 1 && !output.error_frame && !output.discarded_frame);
|
||||
CHECK(output.width == 16 && output.height == 16 && output.pitch == 256 && output.pitch_in_bytes == 256);
|
||||
CHECK(output.frame_buffer == pixels.data && output.frame_buffer_size == pixels.size);
|
||||
for (uint32_t y = 0; y < 16; ++y)
|
||||
for (uint32_t x = 0; x < 16; ++x) CHECK(pixels.data[y * 256 + x] == 40 + frame * 20);
|
||||
Decoder::AvcPictureInfo picture{sizeof(picture)};
|
||||
CHECK(GetAvcPictureInfo(&output, &picture, nullptr) == 0);
|
||||
CHECK(picture.is_valid && picture.pts_data == frame * 1000u);
|
||||
CHECK(picture.attached_data == 0x12340000u + frame);
|
||||
CHECK(picture.profile_idc == 100 && picture.level_idc == 10);
|
||||
CHECK(picture.pic_width_in_mbs_minus1 == 0 && picture.pic_height_in_map_units_minus1 == 0);
|
||||
pixels.Check();
|
||||
}
|
||||
|
||||
void TestBFramesAndFinalization() {
|
||||
Session session;
|
||||
GuardedMemory pixels(session.memory.max_frame_buffer_size);
|
||||
FrameBuffer buffer{sizeof(buffer), pixels.data, pixels.size};
|
||||
CHECK(SetDecodeOutput(session.decoder, &buffer) == 0);
|
||||
uint32_t frames = 0;
|
||||
bool registered = true;
|
||||
OutputInfo output{sizeof(output)};
|
||||
const auto receive = [&] {
|
||||
if (!registered) { CHECK(SetDecodeOutput(session.decoder, &buffer) == 0); registered = true; }
|
||||
const auto result = TrySyncDecodeOutput(session.decoder, &output);
|
||||
if (result == Fail(Error::DecodePending)) return false;
|
||||
CHECK(result == 0);
|
||||
CheckPicture(output, frames++, pixels);
|
||||
registered = false;
|
||||
return true;
|
||||
};
|
||||
CHECK(!receive()); // Output may be supplied before the first AU.
|
||||
for (uint32_t packet = 0; packet < 6; ++packet) {
|
||||
session.Submit(packet);
|
||||
while (receive()) CHECK(!output.last_frame);
|
||||
}
|
||||
CHECK(frames < 6); // B pictures remain in the decoder until end of sequence.
|
||||
CHECK(FinalizeDecodeSequence(session.decoder) == 0);
|
||||
while (frames < 6) { CHECK(receive()); CHECK(output.last_frame == (frames == 6)); }
|
||||
CHECK(FinalizeDecodeSequence(session.decoder) == 0);
|
||||
auto input = Input(0);
|
||||
CHECK(SetDecodeInput(session.decoder, &input) == Fail(Error::ApiFail));
|
||||
CHECK(ResetDecoder(session.decoder) == 0);
|
||||
Decoder::AvcPictureInfo picture{sizeof(picture)};
|
||||
CHECK(GetAvcPictureInfo(&output, &picture, nullptr) == Fail(Error::OutputInfo));
|
||||
CHECK(TrySyncDecodeOutput(session.decoder, &output) == Fail(Error::OutputBufferEmpty));
|
||||
InputResult completion{sizeof(completion)};
|
||||
CHECK(TrySyncDecodeInput(session.decoder, &completion) == Fail(Error::InputQueueEmpty));
|
||||
session.Submit(0); // Reset accepts a new sequence.
|
||||
}
|
||||
|
||||
void TestImmediateIntra() {
|
||||
Session session;
|
||||
InputData input{sizeof(input), const_cast<uint8_t*>(Intra), sizeof(Intra), 0, 0, 0x12340000};
|
||||
CHECK(SetDecodeInput(session.decoder, &input) == 0);
|
||||
InputResult completion{sizeof(completion)};
|
||||
CHECK(TrySyncDecodeInput(session.decoder, &completion) == 0);
|
||||
CHECK(completion.output_frame_count == 1 && completion.decoded_au == Intra);
|
||||
GuardedMemory pixels(session.memory.max_frame_buffer_size);
|
||||
FrameBuffer buffer{sizeof(buffer), pixels.data, pixels.size};
|
||||
CHECK(SetDecodeOutput(session.decoder, &buffer) == 0);
|
||||
OutputInfo output{sizeof(output)};
|
||||
CHECK(TrySyncDecodeOutput(session.decoder, &output) == 0);
|
||||
CheckPicture(output, 0, pixels);
|
||||
CHECK(!output.last_frame); // Output is available without another AU or finalization.
|
||||
}
|
||||
|
||||
void TestCompletionOrderAndBackpressure() {
|
||||
Session session;
|
||||
for (uint32_t packet = 0; packet < 3; ++packet) {
|
||||
auto input = Input(packet);
|
||||
CHECK(SetDecodeInput(session.decoder, &input) == 0);
|
||||
}
|
||||
auto retry = Input(3);
|
||||
CHECK(SetDecodeInput(session.decoder, &retry) == Fail(Error::InputQueueFull));
|
||||
for (uint32_t packet = 0; packet < 3; ++packet) {
|
||||
InputResult output{sizeof(output)};
|
||||
CHECK(TrySyncDecodeInput(session.decoder, &output) == 0);
|
||||
CHECK(output.decoded_au == Input(packet).au_data);
|
||||
}
|
||||
session.Submit(3); // A rejected AU was not consumed.
|
||||
CHECK(ResetDecoder(session.decoder) == 0);
|
||||
}
|
||||
|
||||
Decoder::Input BackendInput(uint32_t packet) {
|
||||
const auto input = Input(packet);
|
||||
return {input.au_data, input.au_size, input.pts, input.dts, input.attached_data};
|
||||
}
|
||||
|
||||
void CheckBackendPicture(const Decoder::Output& output, uint32_t frame, GuardedMemory& pixels) {
|
||||
CHECK(output.valid && output.buffer == pixels.data && output.width == 16 && output.height == 16);
|
||||
for (uint32_t y = 0; y < 16; ++y)
|
||||
for (uint32_t x = 0; x < 16; ++x) CHECK(pixels.data[y * 256 + x] == 40 + frame * 20);
|
||||
Decoder::PictureInfo picture{};
|
||||
CHECK(Decoder::GetPictureInfo(pixels.data, &picture));
|
||||
CHECK(picture.pts == frame * 1000u && picture.attached_data == 0x12340000u + frame);
|
||||
pixels.Check();
|
||||
}
|
||||
|
||||
void TestFailedCopyRetention() {
|
||||
auto* decoder = Decoder::Create({1, 16, 16, 8});
|
||||
CHECK(decoder != nullptr);
|
||||
for (uint32_t packet = 0; packet < 6; ++packet) {
|
||||
uint32_t produced = 0;
|
||||
CHECK(Decoder::Submit(decoder, BackendInput(packet), &produced) == Decoder::Result::Ok);
|
||||
}
|
||||
CHECK(Decoder::Finalize(decoder) == Decoder::Result::Ok);
|
||||
GuardedMemory pixels(10240);
|
||||
Decoder::Output output{};
|
||||
CHECK(Decoder::Receive(decoder, {pixels.data, 1}, &output) == Decoder::Result::FrameBufferSize);
|
||||
CHECK(!output.valid);
|
||||
for (uint32_t frame = 0; frame < 6; ++frame) {
|
||||
CHECK(Decoder::Receive(decoder, {pixels.data, pixels.size}, &output) == Decoder::Result::Ok);
|
||||
CheckBackendPicture(output, frame, pixels);
|
||||
CHECK(output.last_frame == (frame == 5));
|
||||
}
|
||||
CHECK(Decoder::Receive(decoder, {pixels.data, pixels.size}, &output) == Decoder::Result::NoFrame);
|
||||
Decoder::PictureInfo picture{};
|
||||
Decoder::Destroy(decoder);
|
||||
CHECK(!Decoder::GetPictureInfo(pixels.data, &picture));
|
||||
}
|
||||
|
||||
void TestBoundedQueueFinalization() {
|
||||
auto* decoder = Decoder::Create({1, 16, 16, 2});
|
||||
CHECK(decoder != nullptr);
|
||||
GuardedMemory pixels(10240);
|
||||
Decoder::Output output{};
|
||||
uint32_t frames = 0;
|
||||
uint32_t rejected = 0;
|
||||
for (uint32_t packet = 0; packet < 6;) {
|
||||
uint32_t produced = 0;
|
||||
const auto result = Decoder::Submit(decoder, BackendInput(packet), &produced);
|
||||
if (result == Decoder::Result::InputFull) {
|
||||
CHECK(produced == 0);
|
||||
++rejected;
|
||||
CHECK(Decoder::Receive(decoder, {pixels.data, pixels.size}, &output) == Decoder::Result::Ok);
|
||||
CheckBackendPicture(output, frames++, pixels);
|
||||
CHECK(!output.last_frame);
|
||||
} else { CHECK(result == Decoder::Result::Ok); ++packet; }
|
||||
}
|
||||
CHECK(rejected > 0 && frames < 6);
|
||||
// The retained queue is full, and FFmpeg still holds delayed B pictures.
|
||||
CHECK(Decoder::Finalize(decoder) == Decoder::Result::Ok);
|
||||
while (frames < 6) {
|
||||
CHECK(Decoder::Receive(decoder, {pixels.data, pixels.size}, &output) == Decoder::Result::Ok);
|
||||
CheckBackendPicture(output, frames++, pixels);
|
||||
CHECK(output.last_frame == (frames == 6));
|
||||
}
|
||||
CHECK(Decoder::Receive(decoder, {pixels.data, pixels.size}, &output) == Decoder::Result::NoFrame);
|
||||
Decoder::Destroy(decoder);
|
||||
}
|
||||
|
||||
void TestCombinedDecode() {
|
||||
auto* decoder = Decoder::Create({1, 16, 16});
|
||||
CHECK(decoder != nullptr);
|
||||
GuardedMemory pixels(10240);
|
||||
Decoder::Output output{};
|
||||
uint32_t frames = 0;
|
||||
for (uint32_t packet = 0; packet < 6; ++packet) {
|
||||
CHECK(Decoder::Decode(decoder, BackendInput(packet), {pixels.data, pixels.size}, &output) == Decoder::Result::Ok);
|
||||
if (output.valid) CheckBackendPicture(output, frames++, pixels);
|
||||
CHECK(Decoder::Flush(decoder, {pixels.data, pixels.size}, &output) == Decoder::Result::Ok);
|
||||
CHECK(!output.valid); // Legacy Flush is receive-only; the next AU remains acceptable.
|
||||
}
|
||||
CHECK(frames == 4);
|
||||
CHECK(Decoder::Finalize(decoder) == Decoder::Result::Ok);
|
||||
while (frames < 6) {
|
||||
CHECK(Decoder::Flush(decoder, {pixels.data, pixels.size}, &output) == Decoder::Result::Ok);
|
||||
CheckBackendPicture(output, frames++, pixels);
|
||||
}
|
||||
Decoder::Destroy(decoder);
|
||||
}
|
||||
|
||||
void TestDisabledInputSync() {
|
||||
Session session(true);
|
||||
auto input = Input(0);
|
||||
CHECK(SetDecodeInput(session.decoder, &input) == 0);
|
||||
InputResult completion{sizeof(completion)};
|
||||
CHECK(TrySyncDecodeInput(session.decoder, &completion) == Fail(Error::ApiFail));
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
TestRegistrationAndErrors();
|
||||
TestBFramesAndFinalization();
|
||||
TestImmediateIntra();
|
||||
TestCompletionOrderAndBackpressure();
|
||||
TestFailedCopyRetention();
|
||||
TestBoundedQueueFinalization();
|
||||
TestCombinedDecode();
|
||||
TestDisabledInputSync();
|
||||
std::puts("Vdecsw tests passed");
|
||||
}
|
||||
|
|
@ -109,6 +109,18 @@ struct TestFailure {};
|
|||
|
||||
int g_failed_tests = 0;
|
||||
|
||||
struct GpuRangeEvent {
|
||||
bool mapped;
|
||||
uint64_t address;
|
||||
size_t size;
|
||||
bool operator==(const GpuRangeEvent&) const = default;
|
||||
};
|
||||
std::vector<GpuRangeEvent> g_gpu_range_events;
|
||||
|
||||
void ObserveGpuRange(bool mapped, uint64_t address, size_t size) {
|
||||
g_gpu_range_events.push_back({mapped, address, size});
|
||||
}
|
||||
|
||||
[[noreturn]] void Fail(const char* test, const std::string& message) {
|
||||
std::fflush(stdout);
|
||||
std::fprintf(stderr, "VirtualMemoryAllocationTests: %s failed: %s\n", test, message.c_str());
|
||||
|
|
@ -619,7 +631,8 @@ void TestConcurrentBackingReads() {
|
|||
constexpr uint64_t original = 0x123456789abcdef0ull;
|
||||
for (const auto& readers: {std::pair {&TryReadBacking, &TryReadBacking},
|
||||
std::pair {&TryReadBacking, &TryReadSparseBacking},
|
||||
std::pair {&TryReadSparseBacking, &TryReadSparseBacking}}) {
|
||||
std::pair {&TryReadSparseBacking, &TryReadSparseBacking},
|
||||
std::pair {&CopySparseMemory, &CopySparseMemory}}) {
|
||||
const auto base = MapNamedFlexible(test, SceKernelPageSize, SceKernelProtCpuRw,
|
||||
"concurrent_backing_reads");
|
||||
std::memcpy(reinterpret_cast<void*>(base), &original, sizeof(original));
|
||||
|
|
@ -654,7 +667,7 @@ void TestBackingReadExcludesWritesAndUnmap() {
|
|||
const char* test = "BackingReadExcludesWritesAndUnmap";
|
||||
constexpr uint64_t original = 0x123456789abcdef0ull;
|
||||
constexpr uint64_t replacement = 0xfedcba9876543210ull;
|
||||
for (const auto read: {TryReadBacking, TryReadSparseBacking}) {
|
||||
for (const auto read: {TryReadBacking, TryReadSparseBacking, CopySparseMemory}) {
|
||||
for (const bool unmap: {false, true}) {
|
||||
const auto base = MapNamedFlexible(test, SceKernelPageSize, SceKernelProtCpuRw,
|
||||
"backing_read_exclusion");
|
||||
|
|
@ -1152,6 +1165,54 @@ void TestRuntimeMemoryOwnerLifecycle() {
|
|||
std::printf("[host] %-48s ok\n", test);
|
||||
}
|
||||
|
||||
void TestCopySparseMemoryMappings() {
|
||||
using namespace Libs::LibKernel::Memory;
|
||||
const char* test = "CopySparseMemoryMappings";
|
||||
constexpr uint64_t size = SceKernelPageSize * 5;
|
||||
const auto base = AllocateRuntimeMemory(0, size, Common::VirtualMemory::Mode::ReadWrite,
|
||||
"sparse_copy_private");
|
||||
Check(test, base != 0, "private allocation failed");
|
||||
for (const auto page : {0u, 2u, 4u}) {
|
||||
Check(test, FreeGuestMemory(base + page * SceKernelPageSize, SceKernelPageSize),
|
||||
"private page release failed");
|
||||
}
|
||||
void* shared = reinterpret_cast<void*>(base);
|
||||
CheckOk(test, KernelMapNamedFlexibleMemory(&shared, SceKernelPageSize, SceKernelProtCpuRw,
|
||||
SceKernelMapFixed, "sparse_copy_shared"),
|
||||
"map shared first page");
|
||||
constexpr std::array<uint8_t, 5> values {0x3c, 0xa7, 0, 0x5d, 0};
|
||||
for (const auto page : {0u, 1u, 3u}) {
|
||||
std::memset(reinterpret_cast<void*>(base + page * SceKernelPageSize), values[page],
|
||||
SceKernelPageSize);
|
||||
}
|
||||
CheckOk(test, KernelMprotect(shared, SceKernelPageSize, 0), "protect shared first page");
|
||||
constexpr uint64_t prefix = 29, suffix = 17, copied = size - prefix - suffix;
|
||||
std::vector<uint8_t> bytes(copied + 2, 0x55);
|
||||
Check(test, !TryReadSparseBacking(base + prefix, bytes.data() + 1, copied) &&
|
||||
std::all_of(bytes.begin(), bytes.end(), [](uint8_t value) { return value == 0x55; }),
|
||||
"strict backing read accepted private memory or changed its destination");
|
||||
Check(test, CopySparseMemory(base + prefix, bytes.data() + 1, copied),
|
||||
"sparse copy rejected mixed shared, private and unbacked pages");
|
||||
Check(test, bytes.front() == 0x55 && bytes.back() == 0x55,
|
||||
"sparse copy overwrote destination canaries");
|
||||
for (uint64_t i = 0; i < copied; ++i) {
|
||||
Check(test, bytes[i + 1] == values[(prefix + i) / SceKernelPageSize],
|
||||
"sparse copy misplaced mapped data or an unbacked gap");
|
||||
}
|
||||
uint64_t canary = UINT64_MAX;
|
||||
Check(test, !CopySparseMemory(0x10000, &canary, sizeof(canary)) &&
|
||||
!CopySparseMemory(UINT64_MAX - 3, &canary, sizeof(canary)) &&
|
||||
!CopySparseMemory(base, &canary, 0) &&
|
||||
!CopySparseMemory(base, nullptr, sizeof(canary)) && canary == UINT64_MAX,
|
||||
"invalid sparse copy accepted its range or changed its destination");
|
||||
CheckOk(test, KernelMunmap(base, SceKernelPageSize), "unmap shared first page");
|
||||
for (const auto page : {1u, 3u}) {
|
||||
Check(test, FreeGuestMemory(base + page * SceKernelPageSize, SceKernelPageSize),
|
||||
"private page cleanup failed");
|
||||
}
|
||||
std::printf("[host] %-48s ok\n", test);
|
||||
}
|
||||
|
||||
void TestFlexibleMapQueryAndWholeMunmap() {
|
||||
const char* test = "FlexibleMapQueryAndWholeMunmap";
|
||||
const auto baseline = AvailableFlexibleMemory(test);
|
||||
|
|
@ -2669,6 +2730,14 @@ void TestAutomaticMemoryReuseAndAliases() {
|
|||
constexpr uint64_t base = kExtendedMemoryBase + 0x20000000;
|
||||
const auto end = static_cast<int64_t>(KernelGetDirectMemorySize());
|
||||
int64_t physical = -1;
|
||||
CheckOk(test, UnmapAmmMemory(base, block), "AMM unmap empty range");
|
||||
Check(test, KernelMunmap(base, block) == Libs::LibKernel::KERNEL_ERROR_EACCES,
|
||||
"kernel unmap unexpectedly accepted an empty range");
|
||||
for (const auto [address, size] : {std::pair{base + 1, page}, {base, page - 1},
|
||||
{kExtendedMemoryBase - page, page},
|
||||
{kExtendedMemoryBase + kExtendedMemorySize - page, page * 2}})
|
||||
Check(test, UnmapAmmMemory(address, size) == Libs::LibKernel::KERNEL_ERROR_EINVAL,
|
||||
"AMM unmap accepted an unaligned or out-of-domain range");
|
||||
CheckOk(test, AllocateDirectMemory(0, end, block, block, 0, &physical, true),
|
||||
"allocate automatic backing");
|
||||
CheckOk(test, MapAutomaticMemory(base, block, SceKernelMtypeC, SceKernelProtCpuRw),
|
||||
|
|
@ -2691,7 +2760,18 @@ void TestAutomaticMemoryReuseAndAliases() {
|
|||
MapAutomaticMemory(base + block * 4, page, SceKernelMtypeC, SceKernelProtCpuRw) ==
|
||||
Libs::LibKernel::KERNEL_ERROR_EAGAIN,
|
||||
"restored mappings remained available to the automatic allocator");
|
||||
CheckOk(test, KernelMunmap(base, block), "KernelMunmap(original)");
|
||||
TestFailPhysicalMemoryUnmapAfter(0);
|
||||
g_gpu_range_events.clear();
|
||||
TestSetGpuRangeCallback(ObserveGpuRange);
|
||||
const int unmap_result = UnmapAmmMemory(base - page, block + page * 2);
|
||||
TestSetGpuRangeCallback(nullptr);
|
||||
CheckFailed(test, unmap_result, "AMM unmap owner failure");
|
||||
Check(test, g_gpu_range_events == std::vector<GpuRangeEvent>{{false, base, block}, {true, base, block}},
|
||||
"failed AMM unmap did not restore exactly the surviving GPU range");
|
||||
Check(test, *reinterpret_cast<uint64_t*>(base + page * 3) == 0x0123456789abcdefull,
|
||||
"failed AMM unmap did not restore the original backing");
|
||||
CheckOk(test, UnmapAmmMemory(base - page, block + page * 2),
|
||||
"AMM unmap original with leading and trailing holes");
|
||||
Check(test,
|
||||
MapAutomaticMemory(base, block, SceKernelMtypeC, SceKernelProtCpuRw) ==
|
||||
Libs::LibKernel::KERNEL_ERROR_EAGAIN,
|
||||
|
|
@ -2702,20 +2782,22 @@ void TestAutomaticMemoryReuseAndAliases() {
|
|||
Query(test, base).offset == static_cast<uint64_t>(physical) &&
|
||||
Query(test, base + page).offset == static_cast<uint64_t>(physical + page * 4),
|
||||
"automatic allocation did not skip the union of overlapping aliases");
|
||||
CheckOk(test, KernelMunmap(reinterpret_cast<uint64_t>(first_alias), page * 3),
|
||||
"KernelMunmap(first alias)");
|
||||
CheckOk(test, UnmapAmmMemory(reinterpret_cast<uint64_t>(first_alias), page * 3),
|
||||
"AMM unmap first alias");
|
||||
CheckOk(test, MapAutomaticMemory(base + block * 4, page, SceKernelMtypeC, SceKernelProtCpuRw),
|
||||
"MapAutomaticMemory(partial alias release)");
|
||||
Check(test, Query(test, base + block * 4).offset == static_cast<uint64_t>(physical + page),
|
||||
"partial alias release did not reclaim its unreferenced page");
|
||||
CheckOk(test, KernelMunmap(reinterpret_cast<uint64_t>(second_alias), page),
|
||||
"KernelMunmap(partial second alias)");
|
||||
CheckOk(test, UnmapAmmMemory(reinterpret_cast<uint64_t>(first_alias), block + page),
|
||||
"AMM unmap holes and partial second alias");
|
||||
CheckOk(test, MapAutomaticMemory(base + block * 5, page, SceKernelMtypeC, SceKernelProtCpuRw),
|
||||
"MapAutomaticMemory(partial unmap)");
|
||||
Check(test, Query(test, base + block * 5).offset == static_cast<uint64_t>(physical + page * 2),
|
||||
"partial unmap did not reclaim the matching physical page");
|
||||
Check(test, *reinterpret_cast<uint64_t*>(base + block * 3 + page) == 0x0123456789abcdefull,
|
||||
"automatic reuse changed a surviving alias");
|
||||
CheckOk(test, UnmapAmmMemory(base + block * 4, block + page), "AMM unmap multiple sparse mappings");
|
||||
CheckOk(test, UnmapAmmMemory(base + block * 4, block + page), "AMM repeat unmap after final release");
|
||||
CheckOk(test, KernelCheckedReleaseDirectMemory(physical, block),
|
||||
"KernelCheckedReleaseDirectMemory(donation)");
|
||||
ExpectUnmapped(test, base);
|
||||
|
|
@ -2777,6 +2859,17 @@ void TestAutomaticMemoryFragmentedMapRollback() {
|
|||
"automatic allocation beyond donated capacity");
|
||||
CheckOk(test, MapAutomaticMemory(base, block * 2, SceKernelMtypeC, SceKernelProtCpuRw),
|
||||
"reuse both donations after final alias removal");
|
||||
TestFailPhysicalMemoryUnmapAfter(1);
|
||||
g_gpu_range_events.clear();
|
||||
TestSetGpuRangeCallback(ObserveGpuRange);
|
||||
const int unmap_result = KernelMunmap(base, block * 2);
|
||||
TestSetGpuRangeCallback(nullptr);
|
||||
CheckFailed(test, unmap_result, "unmap second backing failure");
|
||||
ExpectUnmapped(test, base);
|
||||
Check(test, Query(test, base + block).offset == static_cast<uint64_t>(second) &&
|
||||
g_gpu_range_events == std::vector<GpuRangeEvent>{
|
||||
{false, base, block}, {false, base + block, block}, {true, base + block, block}},
|
||||
"failed strict unmap restored a freed prefix or lost the surviving GPU range");
|
||||
CheckOk(test, KernelCheckedReleaseDirectMemory(first, block), "release first donation");
|
||||
CheckOk(test, KernelCheckedReleaseDirectMemory(gap, block), "release ordinary allocation");
|
||||
CheckOk(test, KernelCheckedReleaseDirectMemory(second, block), "release second donation");
|
||||
|
|
@ -4420,6 +4513,7 @@ int main(int argc, char** argv) {
|
|||
RunTest(TestConcurrentBackingReads);
|
||||
RunTest(TestBackingReadExcludesWritesAndUnmap);
|
||||
RunTest(TestSparseBackingReadPreservesResidency);
|
||||
RunTest(TestCopySparseMemoryMappings);
|
||||
RunTest(TestSparseReadDuringDirectCommit);
|
||||
RunTest(TestDirectMapQueryOffsetAndPartialMunmap);
|
||||
RunTest(TestFlexibleMemoryUsesSharedBacking);
|
||||
|
|
@ -4473,6 +4567,7 @@ int main(int argc, char** argv) {
|
|||
RunTest(TestProsperoArgumentAndInfoSizeContracts);
|
||||
RunTest(TestGuestAddressSpaceOwnsReservationsBeforeBacking);
|
||||
RunTest(TestSparseBackingReadPreservesResidency);
|
||||
RunTest(TestCopySparseMemoryMappings);
|
||||
RunTest(TestSparseReadDuringDirectCommit);
|
||||
RunTest(TestConcurrentBackingReads);
|
||||
RunTest(TestBackingReadExcludesWritesAndUnmap);
|
||||
|
|
|
|||
|
|
@ -1316,7 +1316,7 @@ void TestShaderFeatureCollection() {
|
|||
{Value(0u), Value(0u), Value(0u)});
|
||||
block->AppendNewInst(ValueOpcode::ImageGatherRaw,
|
||||
{Value(0u), Value(0u), Value(0u), Value(0u),
|
||||
Value(0u), Value(0u), Value(0u)});
|
||||
Value(0u), Value(0u), Value(0u), Value(true)});
|
||||
auto &shared = block->AppendNewInst(ValueOpcode::LoadSharedU32,
|
||||
{Value(0u), Value(true)});
|
||||
shared.SetFlags(MemoryFlags{.index = 0});
|
||||
|
|
@ -7913,6 +7913,29 @@ void TestNewShaderRecompilerDsAddtidTranslation() {
|
|||
CheckSpirvBinaryValidates(result.spirv);
|
||||
}
|
||||
|
||||
void TestDsReturningAtomic64Decoder() {
|
||||
using namespace ShaderRecompiler::Decoder;
|
||||
// CS23e96bf5029f62fd: native DATA is the comparison and DATA2 the replacement.
|
||||
const uint32_t max_words[] = {0xd9a00008u, 0x00000400u};
|
||||
const uint32_t cmp_words[] = {0xd9c00010u, 0x0202003fu};
|
||||
Instruction decoded;
|
||||
DecodeInstruction(max_words, 0u, decoded);
|
||||
Check(decoded.opcode == Opcode::DS_MAX_RTN_U64 &&
|
||||
decoded.word_count == 2u && decoded.data_dwords == 2u &&
|
||||
decoded.data_bits == 32u && decoded.src_count == 2u &&
|
||||
decoded.src0.reg == 0u && decoded.src1.reg == 4u &&
|
||||
decoded.dst.reg == 0u && decoded.offset == 8u && !decoded.gds,
|
||||
"captured DS_MAX_RTN_U64 lost its QWORD width, byte offset, or aliases");
|
||||
DecodeInstruction(cmp_words, 0u, decoded);
|
||||
Check(decoded.opcode == Opcode::DS_CMPST_RTN_B64 &&
|
||||
decoded.word_count == 2u && decoded.data_dwords == 2u &&
|
||||
decoded.data_bits == 32u && decoded.src_count == 3u &&
|
||||
decoded.src0.reg == 63u && decoded.src1.reg == 0u &&
|
||||
decoded.src2.reg == 2u && decoded.dst.reg == 2u &&
|
||||
decoded.offset == 16u && !decoded.gds,
|
||||
"captured DS_CMPST_RTN_B64 lost its comparison/replacement pairs or aliases");
|
||||
}
|
||||
|
||||
void TestDsFloatMinMaxDecoder() {
|
||||
using namespace ShaderRecompiler::Decoder;
|
||||
for (bool max_value : {false, true}) {
|
||||
|
|
@ -12390,7 +12413,7 @@ void TestValuePhiValidation() {
|
|||
#endif
|
||||
}
|
||||
|
||||
void TestWave32MaskProjection() {
|
||||
void TestLaneMaskProjection() {
|
||||
using namespace ShaderRecompiler::IR;
|
||||
for (const uint32_t wave_size : {32u, 64u}) {
|
||||
Program program;
|
||||
|
|
@ -12429,6 +12452,19 @@ void TestWave32MaskProjection() {
|
|||
: U32(Value(3u));
|
||||
return ir.INotEqual(ir.BitwiseAnd(ir.ShiftRightLogical(word, bit), U32(Value(1u))), zero);
|
||||
};
|
||||
const auto low_half = e.ULessThan(U32(e.Emit(ValueOpcode::LaneId)), U32(Value(32u)));
|
||||
const auto mask_bit = [&](uint32_t low, uint32_t high) {
|
||||
const auto word = wave_size == 64u ? e.Select(low_half, U32(Value(low)), U32(Value(high)))
|
||||
: U32(Value(low));
|
||||
return thread_bit(e, word);
|
||||
};
|
||||
const auto full = mask_bit(UINT32_MAX, UINT32_MAX);
|
||||
const auto empty = mask_bit(0u, 0u);
|
||||
const auto low_only = mask_bit(UINT32_MAX, 0u);
|
||||
const auto high_only = mask_bit(0u, UINT32_MAX);
|
||||
const auto partial = mask_bit(0x80000000u, 0x80000000u);
|
||||
const auto full_write = e.Select(full, U32(Value(42u)), raw);
|
||||
const auto empty_write = e.Select(empty, U32(Value(42u)), raw);
|
||||
const auto active_mask = ballot(e, active);
|
||||
const auto direct = thread_bit(e, active_mask);
|
||||
const auto arbitrary = thread_bit(e, raw);
|
||||
|
|
@ -12471,7 +12507,14 @@ void TestWave32MaskProjection() {
|
|||
program.blocks[1]->condition = remaining_predicate;
|
||||
|
||||
ConstantPropagationPass(program.blocks, wave_size);
|
||||
Check(full.Resolve() == Value(true) && empty.Resolve() == Value(false) &&
|
||||
full_write.Resolve() == Value(42u) && empty_write.Resolve() == raw.Resolve(),
|
||||
"constant EXEC mask retained lane extraction or register predication");
|
||||
Check(partial.Resolve().TryInstruction() != nullptr,
|
||||
"partial mask was incorrectly folded to a constant predicate");
|
||||
if (wave_size == 32u) {
|
||||
Check(low_only.Resolve() == Value(true) && high_only.Resolve() == Value(false),
|
||||
"wave32 constant mask incorrectly consumed the high half");
|
||||
Check(direct.Resolve() == active.Resolve(), "wave32 ballot round trip lost its predicate");
|
||||
Check(EquivalentValue(program, combined, combined_predicate),
|
||||
"wave32 OR mask projection changed a lane predicate");
|
||||
|
|
@ -12491,6 +12534,9 @@ void TestWave32MaskProjection() {
|
|||
projected->PhiBlock(1) == loop,
|
||||
"mask projection lost loop Phi type or predecessor order");
|
||||
} else {
|
||||
Check(low_only.Resolve().TryInstruction() != nullptr &&
|
||||
high_only.Resolve().TryInstruction() != nullptr,
|
||||
"wave64 mixed halves were incorrectly folded to a constant predicate");
|
||||
Check(direct.Resolve().TryInstruction()->GetOpcode() == ValueOpcode::INotEqual32 &&
|
||||
live.Resolve().TryInstruction()->GetOpcode() == ValueOpcode::INotEqual32 &&
|
||||
combined.Resolve().TryInstruction()->GetOpcode() == ValueOpcode::INotEqual32 &&
|
||||
|
|
@ -12511,6 +12557,86 @@ void TestWave32MaskProjection() {
|
|||
}
|
||||
}
|
||||
|
||||
void TestLateConstantPropagation() {
|
||||
using namespace ShaderRecompiler::IR;
|
||||
for (const uint32_t wave_size : {32u, 64u}) {
|
||||
for (const bool reverse : {false, true}) {
|
||||
Program program;
|
||||
program.stage = ShaderType::Compute;
|
||||
program.wave_size = wave_size;
|
||||
for (uint32_t id = 0; id < 4; ++id) {
|
||||
program.block_storage.push_back(std::make_unique<Block>());
|
||||
program.blocks.push_back(program.block_storage.back().get());
|
||||
program.blocks.back()->id = id;
|
||||
}
|
||||
auto *entry = program.blocks[0];
|
||||
auto *header = program.blocks[1];
|
||||
auto *latch = program.blocks[2];
|
||||
auto *exit = program.blocks[3];
|
||||
entry->AddBranch(header);
|
||||
header->AddBranch(latch);
|
||||
header->AddBranch(exit);
|
||||
latch->AddBranch(header);
|
||||
entry->terminator.kind = ShaderRecompiler::CFG::TerminatorKind::Branch;
|
||||
entry->terminator.true_block = header;
|
||||
header->terminator.kind = ShaderRecompiler::CFG::TerminatorKind::ConditionalBranch;
|
||||
header->terminator.true_block = latch;
|
||||
header->terminator.false_block = exit;
|
||||
latch->terminator.kind = ShaderRecompiler::CFG::TerminatorKind::Branch;
|
||||
latch->terminator.true_block = header;
|
||||
IREmitter e(entry);
|
||||
const auto raw = e.GetUserData(static_cast<ScalarReg>(0));
|
||||
const auto active = e.INotEqual(raw, U32(Value(0u)));
|
||||
header->condition = active;
|
||||
auto &predicate = header->AppendNewInst(ValueOpcode::Phi);
|
||||
predicate.SetFlags(Type::U1);
|
||||
auto &mask = header->AppendNewInst(ValueOpcode::Phi);
|
||||
mask.SetFlags(Type::U32);
|
||||
const auto thread_bit = [](IREmitter &ir, U32 word) {
|
||||
const auto lane = ir.BitwiseAnd(U32(ir.Emit(ValueOpcode::LaneId)), U32(Value(31u)));
|
||||
return ir.INotEqual(ir.BitwiseAnd(ir.ShiftRightLogical(word, lane), U32(Value(1u))),
|
||||
U32(Value(0u)));
|
||||
};
|
||||
IREmitter body(header);
|
||||
const auto live = thread_bit(body, U32(Value(&mask)));
|
||||
const auto selected = body.Select(live, U32(Value(42u)), raw);
|
||||
const auto projected = body.Emit(ValueOpcode::SelectU1,
|
||||
{Value(&predicate), live, Value(false)});
|
||||
const auto absorbed = body.LogicalAnd(active,
|
||||
body.LogicalOr(body.LogicalNot(active), U1(Value(&predicate))));
|
||||
IREmitter tail(latch);
|
||||
const auto full = thread_bit(tail, U32(Value(UINT32_MAX)));
|
||||
const auto full_word = tail.Select(full, U32(Value(UINT32_MAX)), U32(Value(0u)));
|
||||
predicate.AddPhiOperand(entry, Value(true));
|
||||
predicate.AddPhiOperand(latch, full);
|
||||
mask.AddPhiOperand(entry, Value(UINT32_MAX));
|
||||
mask.AddPhiOperand(latch, full_word);
|
||||
exit->condition = projected;
|
||||
if (reverse) std::ranges::reverse(program.blocks);
|
||||
|
||||
ConstantPropagationPass(program.blocks, wave_size);
|
||||
if (reverse) std::ranges::reverse(program.blocks);
|
||||
Check(Value(&predicate).Resolve() == Value(true) &&
|
||||
Value(&mask).Resolve() == Value(UINT32_MAX) && live.Resolve() == Value(true) &&
|
||||
selected.Resolve() == Value(42u) && projected.Resolve() == Value(true) &&
|
||||
absorbed.Resolve() == active.Resolve(),
|
||||
"late constants did not reach Phi, nested mask patterns or new logical nodes");
|
||||
ResolveControlFlowIdentities(program);
|
||||
Check(exit->condition == Value(true), "late constant lost its control-flow use");
|
||||
RemoveIdentities(program.blocks);
|
||||
ValidateProgram(program, true);
|
||||
}
|
||||
}
|
||||
|
||||
Block cycle;
|
||||
auto &phi = cycle.AppendNewInst(ValueOpcode::Phi);
|
||||
phi.SetFlags(Type::U32);
|
||||
phi.AddPhiOperand(&cycle, Value(&phi));
|
||||
ConstantPropagationPass({&cycle});
|
||||
Check(phi.GetOpcode() == ValueOpcode::Phi && phi.Arg(0).TryInstruction() == &phi,
|
||||
"an ungrounded Phi cycle acquired a constant");
|
||||
}
|
||||
|
||||
void TestU64ShiftConstantPropagation() {
|
||||
using namespace ShaderRecompiler::IR;
|
||||
|
||||
|
|
@ -14232,8 +14358,15 @@ void TestBoundedScalarMaterialImageKeys() {
|
|||
Check(!MaterializeResources(plan, runtime, snapshot, specialization) && memory.keys == 0,
|
||||
"scalar material byte range escaped V# bounds");
|
||||
memory = {.mixed = true};
|
||||
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"selected material fields bypassed image compatibility");
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 4 && specialization.images.size() == 4,
|
||||
"selected material fields rejected mixed native numeric classes");
|
||||
for (uint32_t i = 0; i < snapshot.images.size(); ++i) {
|
||||
Check(specialization.images[i].numeric_class ==
|
||||
(snapshot.images[i].dwords[0] == 5588u
|
||||
? Prospero::TextureNumericClass::Uint : Prospero::TextureNumericClass::Float),
|
||||
"selected material fields lost a candidate's native numeric class");
|
||||
}
|
||||
memory = {};
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"native scalar material image selection did not recover after rejected candidates");
|
||||
|
|
@ -14468,7 +14601,8 @@ void TestSrtWalkerRealSBufferTranslation() {
|
|||
|
||||
user_data[10] = 3 * sizeof(uint32_t);
|
||||
const auto bounds_walked = ShaderRecompiler::IR::SrtWalker(plan, runtime).RefreshFlatBuffer(flat);
|
||||
Check(!bounds_walked, "real S_BUFFER_LOAD walk ignored descriptor bounds");
|
||||
Check(bounds_walked && flat == std::vector<uint32_t>{table[0], table[1], table[2], 0u},
|
||||
"real S_BUFFER_LOAD walk did not zero only the out-of-bounds DWORD");
|
||||
CheckFlattenedReadSlots(
|
||||
ir, 4, "real S_BUFFER_LOAD patch used the wrong flat offsets");
|
||||
|
||||
|
|
@ -15605,6 +15739,7 @@ int main() {
|
|||
// here.
|
||||
TestScalarAshrI64Decoder();
|
||||
TestImageAtomicWidthDecoder();
|
||||
TestDsReturningAtomic64Decoder();
|
||||
TestDsFloatMinMaxDecoder();
|
||||
TestNewShaderDecoderArchitecture();
|
||||
TestImageAddressOperands();
|
||||
|
|
@ -15698,7 +15833,8 @@ int main() {
|
|||
TestFinalSsaRejectsRegisterStatePseudos();
|
||||
#endif
|
||||
TestValuePhiValidation();
|
||||
TestWave32MaskProjection();
|
||||
TestLaneMaskProjection();
|
||||
TestLateConstantPropagation();
|
||||
TestU64ShiftConstantPropagation();
|
||||
TestU64RegisterPairRoundTrip();
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
|
|
|
|||
Loading…
Reference in New Issue