Compare commits

...

16 Commits

Author SHA1 Message Date
nmzik df5b9b92be minor cleanup 2026-10-12 15:06:38 +02:00
Devran Cosmo Uenal 5b0ef07742 audio: keep NGS2 sampler voices within max_waveform_blocks (#1450)
audio: preserve NGS2 streaming buffer ownership
2026-10-12 14:22:06 +02:00
Ettore Sartori 5ce9a52936 Fix the splash screen (#1378)
Fix swapchain acquire semaphore reuse

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-12 13:40:59 +02:00
nmzik dad900c61d Fix BC alias writes and GC 2026-10-12 04:51:02 +02:00
nmzik 5b730d4df0 simplify guest gpu 2026-10-12 02:52:49 +02:00
nmzik e699a35f20 netctl: add opt-in host IPv4 connectivity and launcher toggle 2026-10-12 01:20:43 +02:00
nmzik 41beda4367 ampr: fix APR completion loss in 768-byte read batches
Co-authored-by: Chai T <ct@talnikar.in>
2026-10-12 01:20:43 +02:00
Chai T 048fe9f413 kernel: export sceKernelFtruncate through existing native handler 2026-10-12 01:20:43 +02:00
nmzik e51de54ee4 add hdr display support 2026-10-11 20:29:32 +02:00
nmzik 984cfcebd1 shader: emit native texel offsets using sample execution context
Co-authored-by: netwarex <mail@pety.me>
2026-10-11 18:53:01 +02:00
nmzik df0412afb5 shader: preserve shared predicates during resource plan extraction
Co-authored-by: netwarex <mail@pety.me>
2026-10-11 18:53:01 +02:00
nmzik f28c0fccb2 texture: preserve guest DCC metadata for comparison depth aliases
Co-authored-by: netwarex <mail@pety.me>
2026-10-11 18:53:01 +02:00
netwarex 4c87dbebb2 shader: select indirect texture descriptors for image gathers 2026-10-11 18:53:01 +02:00
nmzik 16a7fe689a fix windows compilation (2) (#1411)
* fix windows compialtion (2)

* loader: guard LoadExec close_range path with __linux__ for macOS

---------

Co-authored-by: ReverendOzmund <catmilker@proton.me>
2026-10-11 11:11:02 +02:00
thenotsoquietninja44-collab 6f0cddd440 implement LoadExec (windows) 2026-10-11 08:24:33 +02:00
nmzik 4084a008ea fix windows compilation 2026-10-11 07:53:59 +02:00
42 changed files with 1806 additions and 716 deletions
+14 -10
View File
@@ -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
+8
View File
@@ -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;
}
+6 -1
View File
@@ -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();
+15 -19
View File
@@ -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;
+40 -62
View File
@@ -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();
}
+84 -4
View File
@@ -36,7 +36,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.
@@ -631,6 +633,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 +756,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 +1116,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
@@ -1166,6 +1230,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 +1249,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 +1269,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) {
@@ -1923,6 +2000,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);
+10
View File
@@ -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
@@ -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,
@@ -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) {
@@ -622,8 +624,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,
+1 -7
View File
@@ -39,13 +39,7 @@ enum class CommandBufferDebugOp : uint32_t {
DispatchDirect,
DrawIndex,
DrawIndexAuto,
EopWrite,
EopInterrupt,
EopWriteBack,
EopFlip,
EopWriteBackFlip,
EopOnlyFlip,
DispatchIndirect,
DispatchIndirect = 9,
Unknown,
};
-166
View File
@@ -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;
-36
View File
@@ -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
+8
View File
@@ -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) {
+3 -1
View File
@@ -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;
}
+33 -31
View File
@@ -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
+79 -20
View File
@@ -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 = &image;
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;
@@ -685,6 +685,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);
@@ -323,25 +323,25 @@ 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};
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 +350,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;
}
@@ -794,21 +789,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));
@@ -829,32 +831,38 @@ 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 EmitGather = [&](uint32_t resource, uint32_t array_index) {
const auto& candidate = state.program.info.images[resource];
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);
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);
const auto sample = EmitImageAccess(ctx, inst, result_type, EmitGather);
ctx.Define(inst, ResultVector(ctx, UnpackImageGather(ctx, mem, sample),
result_numeric_class, false, mem, true));
return;
@@ -878,7 +886,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
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;
@@ -908,9 +916,20 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
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()) {
ctx.Fail(inst, "has a nonconstant 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& candidate = state.program.info.images[resource];
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,
@@ -920,7 +939,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
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);
@@ -687,7 +687,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 +715,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),
@@ -317,8 +317,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)
@@ -989,23 +989,29 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
// Keep only that Boolean structure, never the varying shader dependency graph.
Value unknown;
bool capture_control_reads = false;
std::unordered_map<const Inst*, Value> predicates;
std::function<Value(Value)> ClonePredicate = [&](Value value) -> Value {
value = value.Resolve();
if (value.IsEmpty()) return {};
const auto* inst = value.TryInstruction();
if (inst == nullptr)
return ValidateRuntimeValue(program, value, RuntimeValueType::Integer) ? value : Value {};
// Preserve shared EXEC predicates, including subgraphs with no uniform factor.
const auto [found, inserted] = predicates.try_emplace(inst);
if (!inserted) return found->second;
auto& result = found->second;
if (ValidateRuntimeValue(program, value, RuntimeValueType::Integer)) {
capture_control_reads |=
!ValidateRuntimeValue(program, value, RuntimeValueType::ImmutableInteger);
return Clone(value);
return result = 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;
if (operand.IsEmpty() || op == ValueOpcode::ConditionRef) return result = operand;
auto& node = plan.value_storage.emplace_back(op);
node.SetArg(0, operand);
return Value(&node);
return result = Value(&node);
}
if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr) return {};
auto left = ClonePredicate(inst->Arg(0));
@@ -1019,7 +1025,7 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
auto& node = plan.value_storage.emplace_back(op);
node.SetArg(0, left);
node.SetArg(1, right);
return Value(&node);
return result = Value(&node);
};
plan.control_flow = ResourceControlFlow(program, ClonePredicate);
plan.uniform_fill = AnalyzeUniformFill(program);
@@ -1406,6 +1412,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);
}
}
@@ -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">
+4
View File
@@ -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;
+4
View File
@@ -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();
+4
View File
@@ -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";
}
+18 -15
View File
@@ -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;
}
@@ -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);
+1
View File
@@ -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);
+86 -57
View File
@@ -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;
+161 -124
View File
@@ -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));
+85 -18
View File
@@ -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;
+2
View File
@@ -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
View File
@@ -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());
+144 -2
View File
@@ -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();
+388 -19
View File
@@ -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), &param.header) == OK,
"sampler queue failed");
return Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&voice), &param.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();
+59
View File
@@ -640,6 +640,64 @@ 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() == depth + 5 &&
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 TestFiniteImageRefreshReusesScalarReads() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
Program program;
@@ -793,6 +851,7 @@ int main() {
for (const bool memory_condition : {false, true})
TestWrittenDescriptorPredicateReads(memory_condition);
TestFailedMaterializationRejectsStage();
TestSharedControlPredicates();
TestFiniteImageRefreshReusesScalarReads();
TestMixedSamplerVariantsShareRuntimeDescriptor();
std::puts("ResourceMaterializationTests: all cases passed");
+22 -19
View File
@@ -270,7 +270,7 @@ MakeIndirectImageFixture(bool malformed, uint32_t table_stride = 32, uint32_t ta
sample.kind = ResourceKind::Image;
sample.image_dimension = Decoder::ImageDimension::Dim2D;
const auto sampled = fixture->Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture->ImageAddress()},
{image, sampler, fixture->ImageAddress(), Value(true)},
fixture->AddMemory(sample, 0x10f0));
const auto sampled_x =
fixture->Emit(ValueOpcode::CompositeExtractU32x4, {sampled, Value(0u)});
@@ -486,7 +486,8 @@ void TestWaterfallImageTable() {
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
const auto value = fixture->Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture->ImageAddress()}, fixture->AddMemory(memory, 0x92cu), block);
{image, sampler, fixture->ImageAddress(), block == entry ? active : guard},
fixture->AddMemory(memory, 0x92cu), block);
fixture->Emit(ValueOpcode::ReferenceU32,
{fixture->Emit(ValueOpcode::CompositeExtractU32x4, {value, Value(0u)}, 0, block)}, 0, block);
};
@@ -641,7 +642,7 @@ void TestGuardedDirectImageTable() {
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x128));
fixture.PlanAndTrack();
const auto source = fixture.program.info.images[0].source;
@@ -989,7 +990,7 @@ void TestBoundedComputeImageLoop() {
sample.kind = ResourceKind::Image;
sample.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture.ImageAddress()},
{image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(sample, 0x29c));
fixture.PlanAndTrack();
const auto source = fixture.program.info.images[0].source;
@@ -1325,7 +1326,9 @@ void TestUniformizedMaterialImageKeys() {
MemoryInfo image_memory;
image_memory.kind = ResourceKind::Image;
image_memory.image_dimension = Decoder::ImageDimension::Dim2D;
const auto sampled = fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, address},
const auto sampled = fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, address,
variant == Variant::EmptyFirstEntry ? material_guard : sample_guard},
fixture.AddMemory(image_memory, 0x1aecu), sample);
if (first_lane) {
const auto component = fixture.Emit(
@@ -1477,7 +1480,7 @@ void TestImageDescriptorFields() {
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
memory.image_r128 = r128;
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x80));
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
@@ -1571,7 +1574,7 @@ void TestUniformScalarBufferImage() {
sample.kind = ResourceKind::Image;
sample.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture.ImageAddress()}, fixture.AddMemory(sample, 0x228));
{image, sampler, fixture.ImageAddress(), Value(true)}, fixture.AddMemory(sample, 0x228));
fixture.PlanAndTrack();
const auto plan = ExtractResourcePlan(fixture.program);
@@ -1866,7 +1869,7 @@ void TestImagesSamplersAndAliases() {
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
memory.image_sample_flags = sample_flags;
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, image_address},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, image_address, Value(true)},
fixture.AddMemory(memory, pc));
return std::pair{image, sampler};
};
@@ -1938,14 +1941,14 @@ void TestSampleAdjustSamplerScratch() {
memory.image_dimension = Decoder::ImageDimension::Dim2D;
memory.image_sample_flags = Decoder::ImageSampleFlagAdjust;
fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture.ImageAddress()},
{image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x1ec));
// PPSA24156's zero border payload leaves the reserved scratch shift as
// the entire DWORD after the frontend folds its OR with zero.
const auto shift_sampler = fixture.Sampler(
{Value(0x36u), Value(0xfff000u), Value(0x02500000u), scratch}, 0x200);
fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, shift_sampler, fixture.ImageAddress()},
{image, shift_sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x200));
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
@@ -1994,7 +1997,7 @@ void TestSampleAdjustSamplerScratch() {
rejected_memory.image_dimension = Decoder::ImageDimension::Dim2D;
rejected_memory.image_sample_flags = flags;
rejected.Emit(ValueOpcode::ImageSampleRaw,
{rejected_image, rejected_sampler, rejected.ImageAddress()},
{rejected_image, rejected_sampler, rejected.ImageAddress(), Value(true)},
rejected.AddMemory(rejected_memory, 0x200));
CheckFatal([&] { rejected.PlanAndTrack(); },
@@ -2112,7 +2115,7 @@ void TestGatherLodSamplerValidation() {
fixture.Emit(ValueOpcode::ImageGatherRaw,
{fixture.Image(image_words), fixture.Sampler(sampler_words),
fixture.ImageAddress(), image_words[1], image_words[3],
sampler_words[1], sampler_words[2]},
sampler_words[1], sampler_words[2], Value(true)},
fixture.AddMemory(memory, i * 4));
}
fixture.PlanAndTrack();
@@ -2514,7 +2517,7 @@ ResourcePlan ConditionalSamplerPlan(bool diamond, bool reverse, bool reverse_phi
sample.kind = ResourceKind::Image;
sample.image_dimension = Decoder::ImageDimension::Dim2D;
const auto result =
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, address},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, address, Value(true)},
fixture.AddMemory(sample, 0x4d4));
fixture.Emit(ValueOpcode::ReferenceU32,
{fixture.Emit(ValueOpcode::CompositeExtractU32x4,
@@ -2619,7 +2622,7 @@ void TestFiniteImagePhiCycle() {
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 4));
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
@@ -2685,12 +2688,12 @@ void TestFiniteInlineSampler() {
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
if (format == F::k32Float)
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, previous, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, previous, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x440u));
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x1dd0u));
if (format == F::k32Float)
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, direct, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, direct, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x1dd8u));
}
fixture.PlanAndTrack();
@@ -2764,7 +2767,7 @@ void TestFiniteInlineSampler() {
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
memory.image_sample_flags = flags;
adjusted.Emit(ValueOpcode::ImageSampleRaw, {image, shared, adjusted.ImageAddress()},
adjusted.Emit(ValueOpcode::ImageSampleRaw, {image, shared, adjusted.ImageAddress(), Value(true)},
adjusted.AddMemory(memory, 0x1dd0u));
}
CheckFatal([&] { adjusted.PlanAndTrack(); }, "not a valid runtime value",
@@ -2817,7 +2820,7 @@ void TestFiniteImageBitScanSentinel() {
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 4));
if (!nonzero) {
CheckFatal([&] { fixture.PlanAndTrack(); }, "not a valid runtime value",
+327 -26
View File
@@ -159,6 +159,8 @@ struct BufferCacheTestAccess {
static_assert(std::same_as<decltype(BufferCache::m_slot_buffers),
Common::SlotVector<Buffer>>);
static uint64_t GcTick(const BufferCache &cache) { return cache.m_gc_tick; }
static void SetGarbageCollectionThresholds(BufferCache &cache,
uint64_t trigger,
uint64_t critical) {
@@ -224,6 +226,8 @@ struct TextureCacheTestAccess {
static_assert(TextureCache::ImagePageTable::kAddressSpaceBits == 44);
static_assert(TextureCache::ImagePageTable::kFirstLevelBits == 14);
static uint64_t GcTick(const TextureCache &cache) { return cache.m_gc_tick; }
static std::unique_lock<TrackingSpinLock> Lock(TextureCache &cache) {
return std::unique_lock(cache.m_lock);
}
@@ -2614,6 +2618,31 @@ public:
packet[4] = value;
};
const auto cache_ticks = [&] {
std::pair<uint64_t, uint64_t> ticks;
gpu.SendCommandSync([&] {
ticks = {TextureCacheTestAccess::GcTick(context.GetTextureCache()),
BufferCacheTestAccess::GcTick(context.GetBufferCache())};
});
return ticks;
};
const auto initial_cache_ticks = cache_ticks();
uint32_t graphics_value = 0;
uint32_t async_value = 0;
std::array<uint32_t, 5> graphics_list{}, compute_list{};
write(graphics_list.data(), &graphics_value, 17);
write(compute_list.data(), &async_value, 29);
// A frame can contain many lists; completing them must not age either cache.
for (uint32_t i = 0; i < 32; ++i) {
gpu.Submit(graphics_list, {});
gpu.SubmitCompute(0x20, compute_list);
}
finish();
Require(name, "command-list cache lifetime",
graphics_value == 17 && async_value == 29 &&
cache_ticks() == initial_cache_ticks,
"individual graphics or compute lists aged the caches within a frame");
gpu.SendCommandSync([&] {
setup.BufferInit();
SubmitInfo blocked;
@@ -2648,6 +2677,10 @@ public:
parser.join();
Require(name, "marker insertion", parser_nonblocking,
"inserting a suspend point blocked the GPU command thread");
Require(name, "frame cache maintenance",
cache_ticks() == std::pair{initial_cache_ticks.first + 1,
initial_cache_ticks.second + 1},
"a suspend point did not age both caches exactly once");
std::binary_semaphore second_entered{0};
std::binary_semaphore second_returned{0};
@@ -2685,6 +2718,10 @@ public:
"or the host command lane");
Require(name, "frame count", gpu.GetFrameNum() == 2,
"suspend-point insertion did not advance the frame counter once");
Require(name, "async cache lifetime",
cache_ticks() == std::pair{initial_cache_ticks.first + 2,
initial_cache_ticks.second + 2},
"asynchronous work aged the caches between frame boundaries");
// A marker must stay behind a stalled graphics stream, then clear its
// predicate before the next stream. Async compute releases that stream.
@@ -6741,6 +6778,99 @@ public:
"to the image");
texture_cache.MarkGpuWritten(mirror_refresh);
// Unity copies BC mip regions and packed mip tails through integer images.
// The CPU allocation remains white; later array sampling must see GPU writes.
for (uint32_t format_index = 0; format_index < 3; ++format_index) {
constexpr std::array formats{vk::Format::eBc4UnormBlock,
vk::Format::eBc5UnormBlock,
vk::Format::eBc7UnormBlock};
constexpr std::array guest_formats{Prospero::BufferFormat::kBc4UNorm,
Prospero::BufferFormat::kBc5UNorm,
Prospero::BufferFormat::kBc7UNorm};
constexpr uint32_t width = 128, levels = 8, layers = 3;
const uint32_t block_bytes = format_index == 0 ? 8 : 16;
const uint64_t address = base + 0x1900000 + format_index * 0x40000;
auto array = MakeLinearDesc(address, 0, formats[format_index],
guest_formats[format_index], Prospero::ImageType::kColor2D,
{width, width, 1}, layers, block_bytes, 1);
array.info.resources.levels = levels;
array.info.tile_mode = Prospero::TileMode::kStandard4KB;
array.info.UpdateSize();
array.view_info.level_count = levels;
std::memset(memory + address - base, 0xff, array.info.data.size);
auto value_for = [&](uint32_t layer, uint64_t offset) {
return 0x7c000000u | (format_index << 16) | (layer << 8) |
static_cast<uint32_t>(offset / 4096);
};
auto write_raw = [&](uint32_t layer, uint32_t level, uint32_t value) {
const auto &mip = array.info.mip_layout[level];
auto raw = MakeLinearDesc(
address + layer * array.info.tiled_slice_stride + mip.offset, 0,
block_bytes == 8 ? vk::Format::eR32G32Uint
: vk::Format::eR32G32B32A32Uint,
block_bytes == 8 ? Prospero::BufferFormat::k32_32UInt
: Prospero::BufferFormat::k32_32_32_32UInt,
Prospero::ImageType::kColor2D, {mip.pitch, mip.height, 1},
1, block_bytes, 1);
raw.type = BindingType::Storage;
raw.info.tile_mode = array.info.tile_mode;
raw.info.UpdateSize();
raw.view_info.usage = vk::ImageUsageFlagBits::eStorage;
const auto producer = texture_cache.FindImage(raw);
(void)texture_cache.FindTexture(producer, raw);
vk::ClearValue clear{};
std::fill_n(clear.color.uint32.data(), 4, value);
TextureCacheTestAccess::ClearImage(texture_cache, scheduler.Current(),
producer, {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, clear);
};
for (const uint32_t layer : {0u, 2u}) {
uint64_t previous_offset = UINT64_MAX;
for (uint32_t level = 0; level < levels; ++level) {
const auto offset = array.info.mip_layout[level].offset;
if (offset == previous_offset) continue;
previous_offset = offset;
write_raw(layer, level, value_for(layer, offset));
}
}
std::vector<vk::BufferImageCopy> probes;
std::vector<uint32_t> expected;
for (uint32_t layer = 0; layer < layers; ++layer) {
for (uint32_t level = 0; level < levels; ++level) {
vk::BufferImageCopy copy{};
copy.bufferOffset = expected.size() * sizeof(uint32_t);
copy.imageSubresource = {vk::ImageAspectFlagBits::eColor, level, layer, 1};
copy.imageExtent = {std::min(width >> level, 4u),
std::min(width >> level, 4u), 1};
probes.push_back(copy);
expected.insert(expected.end(), block_bytes / sizeof(uint32_t),
layer == 1 ? UINT32_MAX
: value_for(layer, array.info.mip_layout[level].offset));
}
}
auto verify_array = [&] {
const auto consumer = texture_cache.FindImage(array);
(void)texture_cache.FindTexture(consumer, array);
auto readback = CreateHostBuffer(name, expected.size() * sizeof(uint32_t),
vk::BufferUsageFlagBits::eTransferDst, std::vector<uint32_t>(expected.size()));
texture_cache.GetImage(consumer).Download(probes, readback.buffer, 0, readback.size);
HostReadBarrier(readback.buffer, readback.size, vk::PipelineStageFlagBits::eTransfer,
vk::AccessFlagBits::eTransferWrite);
scheduler.Finish();
Require(name, "raw image producers populate BC array",
ReadBuffer(name, readback, expected.size()) == expected,
"BC array lost GPU mip/tail writes or overwrote an untouched layer");
DestroyBuffer(&readback);
};
verify_array();
constexpr uint32_t replacement = 0x13579bdfu;
write_raw(2, array.info.first_tail_level, replacement);
for (uint32_t level = array.info.first_tail_level; level < levels; ++level) {
const auto word = (2 * levels + level) * block_bytes / sizeof(uint32_t);
std::fill_n(expected.begin() + word, block_bytes / sizeof(uint32_t), replacement);
}
verify_array();
}
// A write through a separate buffer must not replace untouched image
// texels with stale backing bytes. Exact-base writes still refresh it.
constexpr uint64_t buffer_alias_offset = 0x1a00000;
@@ -11083,6 +11213,78 @@ public:
context.ShutdownGpu();
LibKernel::Memory::InstallGpuResources(nullptr);
}
{
// PPSA01593 samples a DCC R32 color surface through a comparison sampler.
constexpr uint64_t comparison_dcc = base + 0x10000;
std::memset(mapped, 0, 0x10000);
std::memset(reinterpret_cast<void *>(comparison_dcc), 0xff, 0x1000);
RenderContext context(m_runtime_context);
context.InitializeGpu(nullptr);
LibKernel::Memory::InstallGpuResources(&context);
auto &scheduler = context.GetCommandScheduler();
HW::Context registers{};
HW::UserConfig user_config{};
HW::Shader shaders{};
scheduler.Begin(registers, user_config, shaders);
context.MapMemory(base, allocation_size);
auto &cache = context.GetTextureCache();
auto &executor = context.GetRenderExecutor();
const ShaderTextureResource descriptor{{static_cast<uint32_t>(base >> 8u),
0xc1600000u, 0x000fc00fu, 0x91b00204u, 0, 0x00700000u,
0x002b0000u | (static_cast<uint32_t>(comparison_dcc >> 8u) << 24u),
static_cast<uint32_t>(comparison_dcc >> 16u)}};
ShaderRecompiler::IR::DescriptorValue value{};
value.dword_count = 8;
std::copy_n(descriptor.fields, 8, value.dwords.begin());
ShaderRecompiler::IR::ImageResource resource{};
resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Storage;
resource.numeric_class = Prospero::TextureNumericClass::Float;
resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D;
resource.written = true;
const auto color = RenderExecutorTestAccess::ResolveTexture(executor, resource, value);
(void)cache.FindTexture(color.image_id, color.desc);
vk::ClearValue paint{};
paint.color.float32 = std::array{0.375f, 0.f, 0.f, 0.f};
TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), color.image_id,
{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, paint);
resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Sampled;
resource.written = false;
resource.read = true;
resource.depth_compare = true;
auto comparison = RenderExecutorTestAccess::ResolveTexture(executor, resource, value);
(void)cache.FindTexture(comparison.image_id, comparison.desc);
const auto &depth = cache.GetImage(comparison.image_id);
Require(name, "expanded DCC comparison alias",
comparison.image_id != color.image_id &&
depth.info.pixel_format == vk::Format::eD32Sfloat &&
depth.info.guest_format == Prospero::BufferFormat::k32Float &&
depth.info.tile_mode == Prospero::TileMode::kRenderTarget &&
depth.info.extent == vk::Extent3D{64, 64, 1} &&
depth.info.metadata.kind == ImageMetadataKind::Dcc &&
depth.info.metadata.range == GuestRange{comparison_dcc, 0x1000} &&
ReadCachedTexel(name, context, comparison.image_id) ==
std::vector<u32>{std::bit_cast<u32>(0.375f)},
"comparison promotion lost native color data or guest DCC layout");
for (const u32 code : {0x40404040u, 0x80808080u}) {
WriteMetadata(context, comparison_dcc, 0x1000, code);
comparison = RenderExecutorTestAccess::ResolveTexture(executor, resource, value);
(void)cache.FindTexture(comparison.image_id, comparison.desc);
Require(name, "DCC clear on a comparison backing",
ReadCachedTexel(name, context, comparison.image_id, {63, 63, 0}) ==
std::vector<u32>{code == 0x40404040u ? 0u : 0x3f800000u},
"DCC clear used the native depth format or color aspect");
std::array<uint8_t, 0x1000> metadata{};
Require(name, "comparison clear expands metadata",
LibKernel::Memory::TryReadBacking(comparison_dcc, metadata.data(),
metadata.size()) &&
std::ranges::all_of(metadata, [](uint8_t byte) { return byte == 0xff; }),
"comparison clear retained the native DCC clear key");
}
context.UnmapMemory(base, allocation_size);
scheduler.Finish();
context.ShutdownGpu();
LibKernel::Memory::InstallGpuResources(nullptr);
}
Require(name, "unmap",
Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0,
"captured scene mapping release failed");
@@ -34888,12 +35090,11 @@ TestCase ImageSampleOpcodeAliasUsesNormalCoords() {
return test;
}
TestCase ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu() {
template <u32 wave_size> TestCase ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovU32(&code, 20,
1); // Non-constant +1 X offset is not a SPIR-V ConstOffset.
AppendVMovU32(&code, 20, 1); // +1 X remains a full dword with A16 coordinates.
AppendVMovLiteral(&code, 21, 0x36003900u); // x=0.625, y=0.375 packed as f16.
AppendVMovU32(&code, 22, 0);
code.push_back(EncodeMimg0(0x30, 0xf));
@@ -34904,18 +35105,63 @@ TestCase ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu() {
AppendEnd(&code);
auto image = MakeRgbaImage(4, 4);
SetRgbaPixel(&image, 4, 2, 1, 0x3f800000u, 0x40000000u, 0x40400000u,
SetRgbaPixel(&image, 4, 2, 1, 0x3f000000u, 0, 0, 0);
SetRgbaPixel(&image, 4, 3, 1, 0x3f800000u, 0x40000000u, 0x40400000u,
0x40800000u);
TestCase test;
test.name = "ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu";
test.name = wave_size == 32 ? "ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu32"
: "ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu64";
test.code = code;
test.compute_info.wave_size = wave_size;
test.has_compute_info = true;
test.expected = {0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u};
test.opcodes = {O::V_MOV_B32, O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD,
O::S_ENDPGM};
test.sampled_image_rgba = image;
test.required_spirv = {"UnpackHalf2x16"};
test.forbidden_spirv = {"OpBitFieldSExtract"};
test.required_spirv = {"UnpackHalf2x16", "Lod|ConstOffset"};
test.forbidden_spirv = {"OpBitFieldSExtract", "OpImageQuerySizeLod", "OpFDiv"};
return test;
}
template <u32 wave_size> TestCase ImageSampleOffsetsReadFourNeighbors() {
using O = ShaderOpcode;
TestCase test;
test.name = wave_size == 32 ? "ImageSampleOffsetsReadFourNeighbors32"
: "ImageSampleOffsetsReadFourNeighbors64";
test.compute_info.wave_size = wave_size;
test.has_compute_info = true;
constexpr std::array offsets{0x01u, 0x3fu, 0x3f00u, 0x0100u};
constexpr std::array opcodes{0x37u, 0x37u, 0x34u, 0x32u};
for (u32 sample = 0; sample < offsets.size(); ++sample) {
AppendVMovLiteral(&test.code, 20, offsets[sample]);
u32 coordinate = 21;
if (opcodes[sample] == 0x32u) {
for (float gradient : {0.25f, 0.f, 0.f, 0.25f}) {
AppendVMovLiteral(&test.code, coordinate++, std::bit_cast<u32>(gradient));
}
}
// All instructions address the center of texel (2, 1) before applying the offset.
AppendVMovLiteral(&test.code, coordinate, std::bit_cast<u32>(0.625f));
AppendVMovLiteral(&test.code, coordinate + 1, std::bit_cast<u32>(0.375f));
if (opcodes[sample] == 0x34u) AppendVMovU32(&test.code, coordinate + 2, 0);
test.code.push_back(EncodeMimg0(opcodes[sample], 0x1));
test.code.push_back(EncodeMimg1(sample, 20));
AppendStoreVgpr(&test.code, sample, sample);
}
AppendEnd(&test.code);
test.sampled_image_rgba = MakeRgbaImage(4, 4);
SetRgbaPixel(&test.sampled_image_rgba, 4, 2, 1, std::bit_cast<u32>(16.f), 0, 0, 0);
constexpr std::array<std::array<u32, 2>, 4> neighbors{{{3, 1}, {1, 1}, {2, 0}, {2, 2}}};
for (u32 sample = 0; sample < neighbors.size(); ++sample) {
const auto value = std::bit_cast<u32>(float(sample + 1));
SetRgbaPixel(&test.sampled_image_rgba, 4, neighbors[sample][0], neighbors[sample][1],
value, 0, 0, 0);
test.expected.push_back(value);
}
test.opcodes = {O::V_MOV_B32, O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.required_spirv = {"Lod|ConstOffset", "Grad|ConstOffset"};
test.forbidden_spirv = {"OpImageQuerySizeLod", "OpFDiv", "OpBitFieldSExtract", "OpSwitch"};
return test;
}
@@ -34986,7 +35232,8 @@ TestCase ImageGatherCompareOpcodes() {
O::V_MOV_B32, O::IMAGE_GATHER4_C, O::IMAGE_GATHER4_C_LZ,
O::IMAGE_GATHER4_C_O, O::IMAGE_GATHER4_C_LZ_O, O::BUFFER_STORE_DWORD,
O::S_ENDPGM};
test.required_spirv = {"OpImageDrefGather", "OpBitFieldSExtract"};
test.required_spirv = {"OpImageDrefGather", "ConstOffset"};
test.forbidden_spirv = {"OpBitFieldSExtract"};
test.compile_only = true;
return test;
}
@@ -35538,7 +35785,7 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
std::memcpy(&memory_flag_bits, &memory_flags, sizeof(memory_flags));
auto &sample = block->AppendNewInst(
ValueOpcode::ImageSampleRaw,
{Value(&image), Value(&sampler), Value(&address)}, memory_flag_bits);
{Value(&image), Value(&sampler), Value(&address), Value(true)}, memory_flag_bits);
auto &sample_x = block->AppendNewInst(ValueOpcode::CompositeExtractU32x4,
{Value(&sample), Value(0u)});
block->AppendNewInst(ValueOpcode::ReferenceU32, {Value(&sample_x)});
@@ -35749,29 +35996,63 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
resource == 4u ? vk::ImageViewType::e2DArray : vk::ImageViewType::e2D, layers));
}
const auto native_sampler = vulkan.CreateSampler(test.name);
for (const bool fetch : {false, true}) {
enum class Access { Sample, Gather, GatherHomogeneous, Fetch };
auto *access = &sample;
for (const auto mode : {Access::Sample, Access::Gather,
Access::GatherHomogeneous, Access::Fetch}) {
const bool fetch = mode == Access::Fetch;
const bool gather = mode == Access::Gather || mode == Access::GatherHomogeneous;
const bool homogeneous = mode == Access::GatherHomogeneous;
if (mode == Access::Gather) {
test.name = "IndirectImageGatherContiguousRuns";
const auto position = std::ranges::find_if(*block, [&](const Inst &inst) {
return &inst == access;
});
auto &read = *block->PrependNewInst(
position, ValueOpcode::ImageGatherRaw,
{Value(&image), Value(&sampler), Value(&address), Value(0u), Value(0u),
Value(0u), Value(0u), Value(true)}, memory_flag_bits);
access->ReplaceUsesWith(Value(&read), false);
access = &read;
compiled.program.info.images[4].cube = false;
auto &gather_memory = compiled.program.memory_info[0];
gather_memory.dmask = 1;
gather_memory.image_sample_flags = ShaderRecompiler::Decoder::ImageSampleFlagLevelZero |
ShaderRecompiler::Decoder::ImageSampleFlagOffset;
// GATHER4_LZ_O places its packed signed offset before the coordinates.
address.SetArg(0, Value(0x3fu));
address.SetArg(1, Value(std::bit_cast<u32>(0.75f)));
address.SetArg(2, Value(std::bit_cast<u32>(0.25f)));
address.SetArg(3, Value(0u));
}
auto &selected_resources = compiled.program.info.images[0].indirect_resources;
selected_resources = homogeneous ? std::vector<u32>{0u, 2u, 3u, 5u, 6u}
: root.indirect_resources;
if (homogeneous) test.name = "IndirectImageGatherHomogeneous";
compiled.resources.flattened_srt[6] = homogeneous ? 4u : ordinals[5];
if (fetch) {
test.name = "IndirectImageFetchContiguousRuns";
const auto position = std::ranges::find_if(*block, [&](const Inst &inst) {
return &inst == &sample;
return &inst == access;
});
auto &read = *block->PrependNewInst(
position, ValueOpcode::ImageRead,
{Value(&image), Value(&address), Value(true)}, memory_flag_bits);
sample.ReplaceUsesWith(Value(&read), false);
access->ReplaceUsesWith(Value(&read), false);
sampler.Invalidate();
compiled.program.info.samplers.clear();
compiled.program.info.sampled_pairs.clear();
compiled.program.info.images[4].cube = false;
compiled.program.memory_info[0].dmask = 0xf;
compiled.program.memory_info[0].image_sample_flags = 0;
compiled.program.memory_info[0].image_has_mip = true;
for (u32 component = 0; component < 4u; ++component)
address.SetArg(component, Value(0u));
// Native DIM=2DArray places LOD after the layer, even for a 2D candidate.
address.SetArg(3, Value(1u));
compiled.program.shader_info_complete = false;
CollectShaderInfo(compiled.program, {.compute = &compute});
}
compiled.program.shader_info_complete = false;
CollectShaderInfo(compiled.program, {.compute = &compute});
for (const u32 wave_size : {32u, 64u}) {
compute.wave_size = wave_size;
compute.host_subgroup_size = vulkan.SubgroupSize();
@@ -35782,31 +36063,38 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
compiled.packed_user_data.resize(compiled.program.bindings.ShaderDataDwords());
ValidateSpirv(test.name, compiled.spirv);
const auto halves = wave_size > compute.host_subgroup_size ? 2u : 1u;
const auto image_opcode = fetch ? "OpImageFetch" : gather ? "OpImageGather"
: "OpImageSampleExplicitLod";
Require(test.name, "native array image runs", tools.Disassemble(compiled.spirv, &text) &&
CountText(text, fetch ? "OpImageFetch" : "OpImageSampleExplicitLod") == 3u * halves,
CountText(text, image_opcode) == (homogeneous ? 1u : 3u) * halves,
"mixed runs expanded into per-image accesses");
if (fetch) {
if (homogeneous) {
Require(test.name, "homogeneous gather array", text.find("OpSwitch") == std::string::npos,
"homogeneous gather emitted a descriptor switch");
}
if (fetch || gather) {
std::vector<u32> decorated;
std::vector<u32> fetched_images;
std::vector<u32> image_operands;
for (size_t offset = 5; offset < compiled.spirv.size();) {
const auto words = std::span<const u32>(compiled.spirv).subspan(
offset, compiled.spirv[offset] >> 16u);
const auto opcode = static_cast<spv::Op>(words[0] & 0xffffu);
if (opcode == spv::OpDecorate && words[2] == spv::DecorationNonUniform)
decorated.push_back(words[1]);
if (opcode == spv::OpImageFetch) fetched_images.push_back(words[3]);
if (opcode == (fetch ? spv::OpImageFetch : spv::OpImageGather))
image_operands.push_back(words[3]);
offset += words.size();
}
Require(test.name, "nonuniform fetch operands",
std::ranges::count_if(fetched_images, [&](u32 operand) {
Require(test.name, "nonuniform image operands",
std::ranges::count_if(image_operands, [&](u32 operand) {
return std::ranges::find(decorated, operand) != decorated.end();
}) == 2u * halves,
"a dynamically selected fetch image lacks its nonuniform decoration");
}) == (homogeneous ? 1u : 2u) * halves,
"a dynamically selected image operand lacks its nonuniform decoration");
}
test.initial.assign(wave_size, 0xdeadbeefu);
test.expected.assign(wave_size, 0u);
for (u32 lane = 0; lane < ordinals.size(); ++lane) {
const auto resource = root.indirect_resources[ordinals[lane]];
const auto resource = selected_resources[compiled.resources.flattened_srt[1u + lane]];
test.expected[lane] = std::bit_cast<u32>(float(resource + (fetch && resource != 0u ? 10u : 0u)));
}
auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size());
@@ -35814,7 +36102,7 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
fetch ? vk::Sampler{} : native_sampler, textures);
const auto actual = vulkan.ReadBuffer(test.name, buffer, test.expected.size());
vulkan.DestroyBuffer(&buffer);
CompareWords(test, "root, native slots, cube and mapping bounds", test.expected, actual);
CompareWords(test, "root, native slots, array runs and mapping bounds", test.expected, actual);
}
std::printf("[compute] %-32s ok\n", test.name);
}
@@ -37659,7 +37947,10 @@ std::vector<TestCase> MakeCases() {
AddCase(ImageD16GatherPacksHalfPairs);
AddCase(ImageSampleA16SamplerCoordsOnGpu);
AddCase(ImageSampleOpcodeAliasUsesNormalCoords);
AddCase(ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu);
AddCase(ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu<32>);
AddCase(ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu<64>);
AddCase(ImageSampleOffsetsReadFourNeighbors<32>);
AddCase(ImageSampleOffsetsReadFourNeighbors<64>);
AddCase(ImageSampleA16CompareBiasRdna2AddressOrder);
AddCase(ImageGatherCompareOpcodes);
AddCase(ImageStoreVariants);
@@ -40070,7 +40361,7 @@ void CheckImageSamplerSpecialization() {
sizeof(signed_memory_flags));
mixed_sampler_block->AppendNewInst(
ValueOpcode::ImageSampleRaw,
{Value(&image_handle), Value(&sampler_handle), Value(&image_address)},
{Value(&image_handle), Value(&sampler_handle), Value(&image_address), Value(true)},
signed_memory_flag_bits);
ShaderRecompiler::IR::DescriptorValue native_image_descriptor{};
@@ -43327,6 +43618,16 @@ int main(int argc, char **argv) {
RunCase(&vulkan, FlatVirtualAddressRebasesGuestAllocation());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--image-sample-offset-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, ImageSampleOffsetsReadFourNeighbors<32>());
RunCase(&vulkan, ImageSampleOffsetsReadFourNeighbors<64>());
RunCase(&vulkan, ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu<32>());
RunCase(&vulkan, ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu<64>());
RunCase(&vulkan, ImageSampleAndGather());
RunCase(nullptr, ImageGatherCompareOpcodes());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--image-gather-lod-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, ImageGatherExplicitLod());
+1 -1
View File
@@ -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});