Compare commits

...

30 Commits

Author SHA1 Message Date
Felipe 80932f8da1 Remove leftover update workspace folders on launcher startup (#1782)
* Remove leftover update workspace folders on launcher startup

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Skip recently active update workspaces during cleanup

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Keep update workspaces for an hour before cleanup so Windows downloads are not removed

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-21 20:43:30 +02:00
Artyom Zabroda f0e55803ba Add CMakeUserPresets.json to .gitignore (#1766)
Add CMakeUserPresets.json to the .gitignore
2026-10-21 20:41:03 +02:00
Felipe ca6a7b88ce launcher: add Enable all / Disable all to the cheats dialog (#1773)
launcher: add Enable all / Disable all buttons to the cheats dialog

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-21 20:34:59 +02:00
nmzik 13b291f998 gpu: honour DB_ALPHA_TO_MASK as alpha-to-coverage and keep its depth writes behind the shader 2026-10-21 19:17:50 +02:00
nmzik 2944d126b3 audio: implement sceAudioOutGetSystemState 2026-10-21 19:17:50 +02:00
nmzik 7b7294aa03 gpu: drop attachments whose sample count differs from the raster when a draw mixes MSAA and single-sample targets 2026-10-21 19:17:50 +02:00
nmzik 4644c52927 graphics: initialize BDA tables before first buffer registration
Co-authored-by: Leandro Bras <shadowgaming.7@gmail.com>
2026-10-21 05:48:13 +02:00
nmzik 4bb4be50b9 agc: remove count-limited logging and register trace 2026-10-21 05:48:13 +02:00
nmzik be9e13915d log: format LOGF output out of line so small guest fiber stacks do not reserve it 2026-10-21 05:48:13 +02:00
nmzik 68d0ba5a81 shader: bound scalar-read overlap checks by the bytes fixed-offset buffer writes reach 2026-10-21 05:48:13 +02:00
nmzik 70668fbd98 gpu: drop global barriers for PM4 flush events and releases, ordering buffer-writing draws like dispatches 2026-10-21 02:02:25 +02:00
nmzik ea1b032c59 shader: drop the unread F32Class bits and the per-pass HS reflection counter reset 2026-10-20 23:54:32 +02:00
nmzik 3c4124dafa shader: emit F32 min/max as exact compare-selects, MED3 with one max and saturate as NClamp 2026-10-20 23:36:26 +02:00
nmzik 8cc5f5364a shader: share SMEM load results across emulated wave64 halves 2026-10-20 22:32:48 +02:00
nmzik 1ce663b5a9 shader: lower HS neighbour LDS reads, control-point output read-back, looped HS reflection and integer partitioning
Co-authored-by: NeySlim <4509984+NeySlim@users.noreply.github.com>
2026-10-20 22:18:00 +02:00
nmzik 66f488a17f main: take boolean emulator options as value-less flags 2026-10-20 21:34:45 +02:00
shadowbeat070 7b7f9a70c6 kernel: answer ESRCH for a thread the detached-thread reaper has joined 2026-10-20 15:54:14 +02:00
nmzik 6ad393775f memory: write GPU publishes to backed pages and skip reserved holes like sparse reads 2026-10-20 15:53:23 +02:00
nmzik f492027be2 shader: select pointer-table images through indexed x3 material keys in readlane waterfalls 2026-10-20 15:21:19 +02:00
nmzik 8843291506 gpu: accept any T# perf_mod on storage textures through a shared canonical encoding check 2026-10-20 15:21:19 +02:00
nmzik 63ca9c0b80 shader: decode PS5 image_gather4_c_a and image_gather4_c_o_a 2026-10-20 15:21:19 +02:00
Mr. Big Britches 0caf4a6aa0 audio: route NGS2 custom mastering voices to the render output 2026-10-20 15:21:19 +02:00
Mr. Big Britches c715891242 shader: memoize the tessellation layout per LS/HS pair and VGT configuration 2026-10-20 15:21:19 +02:00
nmzik f3e01aaf1e tests: cover tessellation patch constants, predicated mul24 addresses, and forward-branch joins 2026-10-20 15:21:19 +02:00
Mr. Big Britches 0957f088ad shader: reflect HS patch constants and lower TES patch inputs through predicated addresses 2026-10-20 15:21:19 +02:00
Mr. Big Britches 4a2961cdf9 shader: reflect tessellation strides across forward branches, DS write widths, and packed patch masks 2026-10-20 15:21:19 +02:00
Mr. Big Britches f0e462a8a2 shader: allocate split-wave dispatcher spill IDs in program order 2026-10-20 15:21:19 +02:00
Mr. Big Britches feed075a9f gpu: implement PA_CL_UCP user clip planes through draw push constants 2026-10-20 15:21:19 +02:00
Mr. Big Britches 18e6747dbf audio: allow looping waveform blocks and exit-loop on NGS2 custom samplers 2026-10-20 15:21:19 +02:00
Mr. Big Britches 6b6274fa5a audio: route NGS2 custom rack buffers and run USER_FX side-chain modules 2026-10-20 15:21:19 +02:00
52 changed files with 1366 additions and 880 deletions
+1
View File
@@ -6,3 +6,4 @@ _Build/vscode-clang/
_Build/
cmake-build-debug/
_PipelineCache/
CMakeUserPresets.json
+1 -1
View File
@@ -27,7 +27,7 @@
"args": [
"--game", "D:/gamesps5/PPSA07429",
"--printf-direction", "File",
"--graphics-debug-dump", "True"
"--graphics-debug-dump"
],
"stopAtEntry": false,
"cwd": "${workspaceFolder}\\_Build\\vscode-clang",
+2
View File
@@ -655,6 +655,8 @@ if(BUILD_TESTING)
add_test(NAME save_data_dialog COMMAND $<TARGET_FILE:save_data_dialog_tests>)
add_test(NAME http_uri_parse COMMAND $<TARGET_FILE:http_uri_parse_tests>)
add_test(NAME shader_recompiler_compute COMMAND $<TARGET_FILE:shader_recompiler_compute_tests>)
add_test(NAME shader_recompiler_tessellation
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --tessellation-only)
add_test(NAME shader_recompiler_alignbyte
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --alignbyte-only)
add_test(NAME graphics_pipeline_rasterization
+8 -2
View File
@@ -51,20 +51,26 @@ inline constexpr auto BrightWhite = fmt::fg(fmt::terminal_color::bright_white)
} // namespace Color
// Out of line, so callers on small guest stacks (fibers) do not reserve the formatting frame.
template <typename... Args>
[[gnu::noinline]] void Printf(fmt::text_style style, const char* format, const Args&... args) {
Write(style, fmt::sprintf(format, args...));
}
} // namespace Log
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define LOGF(...) \
do { \
if (!::Log::IsSilent()) { \
::Log::Write(::fmt::sprintf(__VA_ARGS__)); \
::Log::Printf({}, __VA_ARGS__); \
} \
} while (false)
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define LOGF_COLOR(style, ...) \
do { \
if (!::Log::IsSilent()) { \
::Log::Write((style), ::fmt::sprintf(__VA_ARGS__)); \
::Log::Printf((style), __VA_ARGS__); \
} \
} while (false)
@@ -104,7 +104,6 @@ public:
uint32_t value);
void PrepareCpuFlip(uint64_t request_id);
void SynchronizeGpu();
void EmitGlobalBarrier();
void TriggerEopEventAtEndOfPipe(uint32_t interrupt_context_id);
void DispatchDirect(uint32_t thread_group_x, uint32_t thread_group_y, uint32_t thread_group_z,
uint32_t mode);
@@ -73,7 +73,6 @@ constexpr auto MakeContextDispatchTable() {
g_hw_ctx_func[Pm4::PA_SC_BINNER_CNTL_0] = HwCtxSetPaScExtendedControl;
g_hw_ctx_func[Pm4::PA_SC_BINNER_CNTL_1] = HwCtxSetPaScExtendedControl;
g_hw_ctx_func[Pm4::PA_SC_CONSERVATIVE_RASTERIZATION_CNTL] = HwCtxSetPaScExtendedControl;
g_hw_ctx_func[Pm4::DB_ALPHA_TO_MASK] = HwCtxSetAlphaToMask;
g_hw_ctx_func[Pm4::VGT_DRAW_PAYLOAD_CNTL] = HwCtxSetDrawPayloadControl;
g_hw_ctx_func[Pm4::VGT_PRIMITIVEID_RESET] = HwCtxSetPrimitiveIdReset;
g_hw_ctx_func[Pm4::PA_CL_OBJPRIM_ID_CNTL] = HwCtxSetObjprimIdControl;
@@ -79,7 +79,6 @@ uint32_t HwCtxSetAaMask(CommandProcessor&, uint32_t, uint32_t, const uint32_t*,
uint32_t HwCtxSetVtxControl(CommandProcessor&, uint32_t, uint32_t, const uint32_t*, uint32_t);
uint32_t HwCtxSetPaScExtendedControl(CommandProcessor&, uint32_t, uint32_t, const uint32_t*,
uint32_t);
uint32_t HwCtxSetAlphaToMask(CommandProcessor&, uint32_t, uint32_t, const uint32_t*, uint32_t);
uint32_t HwCtxSetDrawPayloadControl(CommandProcessor&, uint32_t, uint32_t, const uint32_t*,
uint32_t);
uint32_t HwCtxSetPrimitiveIdReset(CommandProcessor&, uint32_t, uint32_t, const uint32_t*, uint32_t);
@@ -71,12 +71,6 @@ constexpr uint32_t NormalizeRegisterOffset(uint32_t raw_offset) {
return raw_offset & ~RegisterSelectorMask;
}
bool ReleaseMemGcrNeedsBarrier(uint32_t eop_event_type, uint32_t gcr_cntl) {
return eop_event_type != 0x28u ||
(gcr_cntl & (GcrGl2MetadataInvalidate | GcrGl0VectorInvalidate | GcrGl1Invalidate |
GcrGl2Invalidate | GcrGl2Writeback)) != 0;
}
uint32_t ReleaseMemCacheActionFromGcr(uint32_t gcr_cntl) {
return ((gcr_cntl & GcrGl2Writeback) != 0 ? 0x38u : 0x00u);
}
@@ -289,12 +283,6 @@ KYTY_HW_CTX_PARSER(HwCtxSetCentroidPriority) {
static void HwCtxIgnoreAaMaskRegister([[maybe_unused]] uint32_t cmd_offset,
[[maybe_unused]] uint32_t value) {}
static void HwCtxIgnoreAlphaToMaskRegister([[maybe_unused]] uint32_t value) {}
static void HwCtxIgnoreDisabledUserClipPlane(CommandProcessor& cp, uint32_t value) {
EXIT_NOT_IMPLEMENTED(value != 0 || cp.GetCtx().GetClipControl().user_clip_planes != 0);
}
static void HwCtxIgnorePrimitiveIdReset([[maybe_unused]] uint32_t value) {}
static void HwCtxIgnoreVtxControl([[maybe_unused]] uint32_t value) {}
@@ -439,16 +427,6 @@ KYTY_HW_CTX_PARSER(HwCtxSetPointState) {
return num_values;
}
KYTY_HW_CTX_PARSER(HwCtxSetAlphaToMask) {
auto num_values = KYTY_PM4_LEN(cmd_id) - 2u;
for (uint32_t i = 0; i < num_values; i++) {
HwCtxIgnoreAlphaToMaskRegister(buffer[i]);
}
return num_values;
}
KYTY_HW_CTX_PARSER(HwCtxSetDrawPayloadControl) {
cp.GetCtx().SetDrawPayloadControl(buffer[0]);
return KYTY_PM4_LEN(cmd_id) - 2u;
@@ -2263,30 +2241,14 @@ KYTY_CP_OP_PARSER(CpOpReleaseMem) {
}
};
if (data_sel == 0 || interrupt_selector == 4) {
if (eop_event_type != 0x28 || gcr_cntl != 0) {
cp.EmitGlobalBarrier();
}
// Cache actions only order GPU work, which the renderer's resource barriers already order.
if (data_sel == 0 || interrupt_selector == 4 ||
(release_dst == ReleaseMemDstMemory && dst_gpu_addr == nullptr)) {
trigger_interrupt();
return 7;
}
if (release_dst == ReleaseMemDstMemory && dst_gpu_addr == nullptr) {
if (eop_event_type != 0x28 || gcr_cntl != 0) {
cp.EmitGlobalBarrier();
}
trigger_interrupt();
return 7;
}
if (ReleaseMemGcrNeedsBarrier(eop_event_type, gcr_cntl)) {
cp.EmitGlobalBarrier();
}
auto cache_action = ReleaseMemCacheActionFromGcr(gcr_cntl);
cache_policy = 0;
@@ -2899,7 +2861,7 @@ void GraphicsInitJmpTablesCxIndirect() {
for (auto cmd_offset = Pm4::PA_CL_UCP_0_X; cmd_offset <= Pm4::PA_CL_UCP_5_W; cmd_offset++) {
g_hw_ctx_indirect_func[cmd_offset] = [](KYTY_HW_CTX_INDIRECT_ARGS) {
HwCtxIgnoreDisabledUserClipPlane(cp, value);
cp.GetCtx().SetUserClipPlane(cmd_offset - Pm4::PA_CL_UCP_0_X, value);
};
}
@@ -3491,7 +3453,8 @@ void GraphicsInitJmpTablesCxIndirect() {
g_hw_ctx_indirect_func[Pm4::PA_SC_CONSERVATIVE_RASTERIZATION_CNTL] =
[](KYTY_HW_CTX_INDIRECT_ARGS) { HwCtxIgnorePaScExtendedControl(cmd_offset, value); };
g_hw_ctx_indirect_func[Pm4::DB_ALPHA_TO_MASK] = [](KYTY_HW_CTX_INDIRECT_ARGS) {
HwCtxIgnoreAlphaToMaskRegister(value);
cp.GetCtx().SetAlphaToMaskEnable(
KYTY_PM4_GET(value, DB_ALPHA_TO_MASK, ALPHA_TO_MASK_ENABLE) != 0);
};
g_hw_ctx_indirect_func[Pm4::CB_COLOR_CONTROL] = [](KYTY_HW_CTX_INDIRECT_ARGS) {
cp.GetCtx().SetColorControl(DecodeColorControl(value));
+5 -27
View File
@@ -607,7 +607,6 @@ bool GuestGpu::Process(Submission& submission) {
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(); });
cp.BufferFlush();
@@ -1222,22 +1221,6 @@ void CommandProcessor::WriteAtEndOfPipe64(uint32_t cache_policy, uint32_t event_
interrupt_context_id);
}
void CommandProcessor::EmitGlobalBarrier() {
Common::LockGuard lock(m_renderer.GetMutex());
vk::MemoryBarrier2 barrier {};
barrier.srcStageMask = vk::PipelineStageFlagBits2::eAllCommands;
barrier.srcAccessMask = vk::AccessFlagBits2::eMemoryWrite;
barrier.dstStageMask = vk::PipelineStageFlagBits2::eAllCommands;
barrier.dstAccessMask = vk::AccessFlagBits2::eMemoryRead | vk::AccessFlagBits2::eMemoryWrite;
vk::DependencyInfo dependency {};
dependency.memoryBarrierCount = 1;
dependency.pMemoryBarriers = &barrier;
GetScheduler().EndRendering();
CurrentBuffer().Handle().pipelineBarrier2(dependency);
}
void CommandProcessor::TriggerEopEventAtEndOfPipe(uint32_t interrupt_context_id) {
(void)CurrentBuffer().Handle();
GetScheduler().DeferPriorityOperation(
@@ -1257,21 +1240,17 @@ void CommandProcessor::TriggerEvent(uint32_t event_type, uint32_t event_index,
}
const auto valid_cache_event_index = event_index == 0x00000000 || event_index == 0x00000007;
// Partial flushes and cache writebacks only order GPU work, which the renderer's resource
// barriers already order.
switch (event_type) {
// CsPartialFlush, GsPartialFlush, PsPartialFlush.
case 0x00000007:
case 0x0000000f:
case 0x00000010: EmitGlobalBarrier(); break;
// CbDbDataWritebackInvalidate, CbDataWritebackInvalidate.
case 0x00000010: break;
// CbDbDataWritebackInvalidate, CbDataWritebackInvalidate, DbDataWritebackInvalidate,
// DbMetadataWritebackInvalidate, CbMetadataWritebackInvalidate.
case 0x00000016:
case 0x00000031:
if (!valid_cache_event_index) {
EXIT("unknown event type: 0x%08" PRIx32 ", 0x%08" PRIx32 "\n", event_type,
event_index);
}
EmitGlobalBarrier();
break;
// DbDataWritebackInvalidate, DbMetadataWritebackInvalidate, CbMetadataWritebackInvalidate.
case 0x0000002a:
case 0x0000002c:
case 0x0000002e:
@@ -1279,7 +1258,6 @@ void CommandProcessor::TriggerEvent(uint32_t event_type, uint32_t event_index,
EXIT("unknown event type: 0x%08" PRIx32 ", 0x%08" PRIx32 "\n", event_type,
event_index);
}
EmitGlobalBarrier();
break;
case 0x0000000d:
case 0x0000000e:
+16
View File
@@ -5,6 +5,8 @@
#include "common/common.h"
#include "graphics/guest_gpu/gpu_defs.h"
#include <array>
namespace Libs::Graphics::HW {
struct ColorBase {
@@ -574,6 +576,12 @@ struct ShaderRegisters {
uint32_t m_paScShaderControl = 0;
DepthShaderControl db_shader_control;
bool alpha_to_mask_enable = false;
// DB_SHADER_CONTROL.ALPHA_TO_MASK_DISABLE overrides DB_ALPHA_TO_MASK per shader.
[[nodiscard]] bool AlphaToMask() const {
return alpha_to_mask_enable && !db_shader_control.alpha_to_mask_disable;
}
[[nodiscard]] uint32_t GetExportCount() const {
return 1u + ((m_spiVsOutConfig >> 1u) & 0x1Fu);
@@ -824,6 +832,11 @@ public:
void SetBlendColor(const BlendColor& color) { m_blend_color = color; }
[[nodiscard]] const ClipControl& GetClipControl() const { return m_clip_control; }
void SetClipControl(const ClipControl& control) { m_clip_control = control; }
// PA_CL_UCP_0_X..PA_CL_UCP_5_W as raw float bits, four per plane.
[[nodiscard]] const std::array<uint32_t, 24>& GetUserClipPlanes() const {
return m_user_clip_planes;
}
void SetUserClipPlane(uint32_t index, uint32_t value) { m_user_clip_planes[index] = value; }
[[nodiscard]] const RenderControl& GetRenderControl() const { return m_render_control; }
void SetRenderControl(const RenderControl& control) { m_render_control = control; }
[[nodiscard]] const DepthRenderOverride& GetDepthRenderOverride() const {
@@ -908,6 +921,7 @@ public:
void SetDepthShaderControl(const DepthShaderControl& value) {
m_sh_regs.db_shader_control = value;
}
void SetAlphaToMaskEnable(bool value) { m_sh_regs.alpha_to_mask_enable = value; }
void SetScShaderControl(uint32_t value) { m_sh_regs.m_paScShaderControl = value; }
@@ -924,6 +938,8 @@ private:
ColorControl m_color_control;
ScanModeControl m_scan_mode_control;
std::array<uint32_t, 24> m_user_clip_planes {};
AaSampleControl m_aa_sample_control;
AaConfig m_aa_config;
+2
View File
@@ -549,6 +549,8 @@ constexpr uint32_t VGT_SHADER_STAGES_EN = 0x2D5;
constexpr uint32_t VGT_LS_HS_CONFIG = 0x2D6;
constexpr uint32_t VGT_TF_PARAM = 0x2DB;
constexpr uint32_t DB_ALPHA_TO_MASK = 0x2DC;
constexpr uint32_t DB_ALPHA_TO_MASK_ALPHA_TO_MASK_ENABLE_SHIFT = 0;
constexpr uint32_t DB_ALPHA_TO_MASK_ALPHA_TO_MASK_ENABLE_MASK = 0x1;
constexpr uint32_t PA_SU_POLY_OFFSET_DB_FMT_CNTL = 0x2DE;
constexpr uint32_t PA_SU_POLY_OFFSET_DB_FMT_CNTL_NEG_NUM_DB_BITS_SHIFT = 0;
constexpr uint32_t PA_SU_POLY_OFFSET_DB_FMT_CNTL_NEG_NUM_DB_BITS_MASK = 0xFF;
+4
View File
@@ -256,6 +256,10 @@ BufferCache::BufferCache(GraphicContext& graphics, CommandScheduler& scheduler,
m_gds_buffer.Flush(0, m_gds_buffer.Size());
SetVulkanObjectNameF(m_graphics.device, m_bda_pagetable_buffer.Handle(),
"BDA Page Table Buffer");
m_scheduler.BeginCommand();
for (auto* buffer: {&m_bda_pagetable_buffer, m_fault_manager.GetFaultBuffer()}) {
buffer->Fill(0, buffer->Size(), 0);
}
const auto null_id =
m_slot_buffers.insert(m_graphics, m_scheduler, MemoryUsage::DeviceLocal, 0, AllFlags, 16);
EXIT_IF(null_id != NULL_BUFFER_ID);
@@ -327,7 +327,7 @@ void CommandScheduler::CheckActive() const {
}
CommandBuffer& CommandScheduler::Current() {
CheckActive();
EXIT_IF(m_command.IsInvalid());
return m_command;
}
+7 -1
View File
@@ -104,8 +104,14 @@ void CommandBuffer::BeginRendering(const RenderState& state) const {
rendering.pDepthAttachment = depth_stencil.has_depth ? &depth : nullptr;
rendering.pStencilAttachment = depth_stencil.has_stencil ? &stencil : nullptr;
Handle().beginRendering(rendering);
// The open pass holds the cleared contents, so a later clear begins a new pass.
m_render_state = state;
m_rendering = true;
for (auto& color: m_render_state.color_attachments) {
color.is_clear = false;
}
m_render_state.depth_stencil_attachment.depth_clear = false;
m_render_state.depth_stencil_attachment.stencil_clear = false;
m_rendering = true;
}
void CommandBuffer::EndRendering() const {
+3 -4
View File
@@ -373,10 +373,9 @@ static void ClipPrint(const char* func, const HW::ClipControl& c) {
static void ClipCheck(const HW::ClipControl& c) {
// dx_linear_attr_clip_enable preserves linear (noperspective) attributes at clip-generated
// vertices, which Vulkan provides as part of clipping and interpolation.
EXIT_NOT_IMPLEMENTED(c.user_clip_planes != 0 || c.user_clip_plane_mode != 0 ||
c.vertex_kill_any || c.user_clip_plane_negate_y ||
c.user_clip_plane_cull_only || c.cull_on_clipping_error_disable ||
c.force_viewport_index_from_vs_enable);
EXIT_NOT_IMPLEMENTED(c.user_clip_plane_mode != 0 || c.vertex_kill_any ||
c.user_clip_plane_negate_y || c.user_clip_plane_cull_only ||
c.cull_on_clipping_error_disable || c.force_viewport_index_from_vs_enable);
}
static void RcPrint(const char* func, const HW::RenderControl& c) {
@@ -143,7 +143,8 @@ NativeStorageBuffer(RenderContext& context, const PreparedBindings::BufferSource
return result;
}
bool IsSupportedDepthTextureEncoding(const ShaderTextureResource& descriptor, bool r128) {
// Reserved and unsupported T# fields must be zero; perf_mod only scales sampler state.
static bool IsCanonicalTextureEncoding(const ShaderTextureResource& descriptor, bool r128) {
constexpr uint32_t field1_reserved_mask = 0x200fff00u;
constexpr uint32_t field2_reserved_mask = 0xf0003000u;
const uint32_t field3_expected = descriptor.DstSelXYZW() |
@@ -154,17 +155,25 @@ bool IsSupportedDepthTextureEncoding(const ShaderTextureResource& descriptor, bo
const uint32_t field4_expected = descriptor.Depth() | (descriptor.BaseArray5() << 16u);
const uint32_t field5_expected = (static_cast<uint32_t>(descriptor.PerfMod5()) << 20u) |
(static_cast<uint32_t>(descriptor.MaxMip()) << 4u);
const bool common = (descriptor.fields[1] & field1_reserved_mask) == 0 &&
(descriptor.fields[2] & field2_reserved_mask) == 0 &&
descriptor.fields[3] == field3_expected;
if (r128) {
return common && descriptor.fields[4] == 0 && descriptor.fields[5] == 0 &&
descriptor.fields[6] == 0 && descriptor.fields[7] == 0;
if ((descriptor.fields[1] & field1_reserved_mask) != 0 ||
(descriptor.fields[2] & field2_reserved_mask) != 0 || descriptor.fields[3] != field3_expected) {
return false;
}
const bool full = common && descriptor.fields[4] == field4_expected &&
descriptor.fields[5] == field5_expected;
if (!full ||
(descriptor.MsaaDepth() && !IsMultisampledTexture(descriptor.Type()))) {
if (r128) {
return descriptor.fields[4] == 0 && descriptor.fields[5] == 0 && descriptor.fields[6] == 0 &&
descriptor.fields[7] == 0;
}
return descriptor.fields[4] == field4_expected && descriptor.fields[5] == field5_expected;
}
bool IsSupportedDepthTextureEncoding(const ShaderTextureResource& descriptor, bool r128) {
if (!IsCanonicalTextureEncoding(descriptor, r128)) {
return false;
}
if (r128) {
return true;
}
if (descriptor.MsaaDepth() && !IsMultisampledTexture(descriptor.Type())) {
return false;
}
const auto metadata_control = descriptor.fields[6] & 0x00ffffffu;
@@ -281,36 +290,12 @@ static bool IsSupportedStorageTextureDescriptor(const ShaderRecompiler::IR::Imag
!descriptor.MsaaDepth();
}
static bool IsSupportedStorageTextureEncoding(const ShaderRecompiler::IR::ImageResource& resource,
const ShaderTextureResource& descriptor) {
constexpr uint32_t field1_reserved_mask = 0x200fff00u;
constexpr uint32_t field2_reserved_mask = 0xf0003000u;
constexpr uint32_t field5_expected = 0x00700000u;
constexpr uint32_t field5_max_mip_mask = 0x000000f0u;
const uint32_t expected_field3 = descriptor.DstSelXYZW() |
(static_cast<uint32_t>(descriptor.BaseLevel()) << 12u) |
(static_cast<uint32_t>(descriptor.LastLevel()) << 16u) |
(static_cast<uint32_t>(descriptor.TileMode()) << 20u) |
(static_cast<uint32_t>(descriptor.Type()) << 28u);
const uint32_t expected_field4 =
descriptor.Depth() | (static_cast<uint32_t>(descriptor.BaseArray5()) << 16u);
const bool common = (descriptor.fields[1] & field1_reserved_mask) == 0 &&
(descriptor.fields[2] & field2_reserved_mask) == 0 &&
descriptor.fields[3] == expected_field3;
if (resource.r128) {
return common && descriptor.fields[4] == 0 && descriptor.fields[5] == 0 &&
descriptor.fields[6] == 0 && descriptor.fields[7] == 0;
}
return common && descriptor.fields[4] == expected_field4 &&
(descriptor.fields[5] & ~field5_max_mip_mask) == field5_expected;
}
void ValidateStorageTexture(const ShaderRecompiler::IR::ImageResource& resource,
const ShaderTextureResource& descriptor, uint64_t size) {
const auto format = descriptor.Format();
const bool resource_ok = IsSupportedStorageImageResource(resource);
const bool descriptor_ok = IsSupportedStorageTextureDescriptor(resource, descriptor);
const bool encoding_ok = IsSupportedStorageTextureEncoding(resource, descriptor);
const bool encoding_ok = IsCanonicalTextureEncoding(descriptor, resource.r128);
const bool uint_resource = resource.numeric_class == Prospero::TextureNumericClass::Uint;
const bool raw_sint_storage = uint_resource &&
ShaderRecompiler::IR::RawSignedStorageFormat(resource, format) != Prospero::BufferFormat::kInvalid;
@@ -956,8 +941,12 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
for (const auto& resource: program.info.buffers) {
if (!resource.written || resource.descriptor_index == UINT32_MAX) continue;
const auto& written = writer->buffer_sources[resource.descriptor_index];
if (written.size != 0 && ImageRangeOverlaps(address, size,
written.address, written.size)) {
// ADD_TID and swizzle move fixed-offset writes past write_extent.
const bool linear = (resource.packed_stride & ((1u << 14u) | (1u << 20u))) == 0u;
const auto written_size =
linear ? std::min<uint64_t>(written.size, resource.write_extent) : written.size;
if (written_size != 0 && ImageRangeOverlaps(address, size,
written.address, written_size)) {
EXIT("scalar resource reads overlap a shader buffer write\n");
}
}
@@ -780,10 +780,12 @@ PipelineCache::GraphicsPrograms PipelineCache::GetGraphicsPrograms(
}
}
}
if (context.GetClipControl().clip_disable) {
auto& last_stage = vertex_info[tess_active ? 2u : 0u];
const auto& clip_control = context.GetClipControl();
if (clip_control.clip_disable) {
const auto& viewport = context.GetScreenViewport().viewports[0];
const auto& limits = m_graphics.GetPhysicalDeviceProperties().limits;
auto& clip = vertex_info[tess_active ? 2u : 0u].clip_space;
auto& clip = last_stage.clip_space;
clip.scale[0] = viewport.xscale;
clip.scale[1] = viewport.yscale;
clip.offset[0] = viewport.xoffset;
@@ -793,9 +795,16 @@ PipelineCache::GraphicsPrograms PipelineCache::GetGraphicsPrograms(
clip.half_extent[1] =
static_cast<float>(std::min(limits.maxViewportDimensions[1], 16384u)) * 0.5f;
clip.enabled = true;
} else if (clip_control.user_clip_planes != 0) {
// CLIP_DISABLE also disables user planes. How they combine with shader clip and cull
// distances is unverified; draw push constants only reach VS/mesh and PS, and the
// generated RECTLIST stages do not forward clip distances.
EXIT_NOT_IMPLEMENTED((last_stage.pa_cl_vs_out_cntl & 0xffffu) != 0 || tess_active ||
Prospero::IsRectList(user_config.GetPrimType()));
last_stage.user_clip_planes = clip_control.user_clip_planes;
}
uint32_t push_data_cursor = ShaderRecompiler::IR::PushData::GraphicsDrawDwords(
mesh_active, object_primitive_id);
mesh_active, object_primitive_id, last_stage.user_clip_planes);
GraphicsPrograms result;
if (pixel_active) {
result.pixel = m_program_cache->Get(pixel_params, pixel_info, push_data_cursor);
@@ -962,6 +971,7 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
if (static_params.sample_shading_enable && !m_graphics.sample_rate_shading_enabled) {
EXIT("Pipeline: sample-rate shading is required but unsupported by the host\n");
}
static_params.alpha_to_coverage_enable = ps_active && ctx.GetShaderRegisters().AlphaToMask();
const bool rect_list = Prospero::IsRectList(command.GetUserConfig().GetPrimType());
static_params.cull_back = !rect_list && mc.cull_back;
static_params.cull_front = !rect_list && mc.cull_front;
@@ -39,6 +39,7 @@ struct PipelineStaticParameters {
bool primitive_restart_enable = false;
uint32_t samples = 1;
bool sample_shading_enable = false;
bool alpha_to_coverage_enable = false;
uint32_t color_mask[RENDER_COLOR_ATTACHMENTS_MAX] = {};
bool cull_front = false;
bool cull_back = false;
@@ -62,7 +63,7 @@ struct PipelineStaticParameters {
static_assert(std::is_trivially_copyable_v<PipelineStaticParameters>);
static_assert(std::is_standard_layout_v<PipelineStaticParameters>);
static_assert(alignof(PipelineStaticParameters) == 1);
static_assert(sizeof(PipelineStaticParameters) == 116);
static_assert(sizeof(PipelineStaticParameters) == 117);
struct PipelineRenderingState {
std::array<vk::Format, RENDER_COLOR_ATTACHMENTS_MAX> color_formats {};
@@ -92,7 +92,8 @@ bool HasShaderBufferWrites(const ShaderStageRuntime& runtime) {
const auto& program = *runtime.program;
const auto& resources = *runtime.resources;
EXIT_IF(resources.buffers.size() != program.info.buffers.size());
bool has_writes = false;
// Address writes reach memory outside the bound buffer descriptors.
bool has_writes = program.has_address_writes;
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
if (!program.info.buffers[i].written) {
continue;
@@ -422,6 +422,8 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
multisampling.sampleShadingEnable = static_params.sample_shading_enable ? VK_TRUE : VK_FALSE;
multisampling.rasterizationSamples = vulkan_sample_count(static_params.samples);
multisampling.minSampleShading = 1.0f;
multisampling.alphaToCoverageEnable =
static_params.alpha_to_coverage_enable ? VK_TRUE : VK_FALSE;
vk::PipelineColorBlendAttachmentState color_blend_attachment[RENDER_COLOR_ATTACHMENTS_MAX] = {};
for (uint32_t i = 0; i < rendering.color_count; i++) {
+51 -16
View File
@@ -436,7 +436,7 @@ static bool DrawHasValidVertexShader(const HW::Shader& sh_ctx) {
static bool PixelShaderHasDepthOrCoverageSideEffects(const HW::ShaderRegisters& sh_regs) {
const auto& db = sh_regs.db_shader_control;
return db.shader_kill_enable || db.shader_z_export_enable || db.shader_mask_export_enable ||
db.shader_dual_export_enable || db.shader_execute_on_noop;
db.shader_dual_export_enable || db.shader_execute_on_noop || sh_regs.AlphaToMask();
}
struct DrawRenderState {
@@ -935,6 +935,29 @@ bool RenderExecutor::PrepareDrawRenderState(CommandBuffer& buffer, const DrawCal
LogDrawPhase(draw.Name(), "ResolveRenderDepthTarget");
}
ResolveRenderDepthTarget(buffer, state.depth_info);
// TODO(PPSA09243): Vulkan passes take one sample count. Where PS5 attachments mix counts,
// keep those matching the raster count instead of resolving multisampled shadows.
const auto colors = std::span {state.color_info, state.color_count};
const auto pass_samples = state.depth_info.image_id ? state.depth_info.desc.info.samples
: state.color_info[0].desc.info.samples;
if (std::ranges::any_of(colors, [&](const RenderColorInfo& color) {
return color.desc.info.samples != pass_samples;
})) {
const auto raster =
render_sample_count(buffer.GetRegisters().GetAaConfig().msaa_num_samples);
const auto dropped = std::ranges::remove_if(colors, [&](const RenderColorInfo& color) {
return color.desc.info.samples != raster;
});
state.color_count -= static_cast<uint32_t>(dropped.size());
if (state.depth_info.image_id && state.depth_info.desc.info.samples != raster) {
state.depth_info = {};
}
static std::atomic_bool logged = false;
if (!logged.exchange(true, std::memory_order_relaxed)) {
Log::WriteToConsoleAndLog(
"Warning: dropping attachments whose sample count differs from the raster.\n");
}
}
if (state.color_count == 0 && !state.depth_info.image_id && !state.ps_active) {
LogFramebufferSkip(draw.Name(), state.color_info[0], state.depth_info, buffer,
@@ -1193,13 +1216,14 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
if (!mesh_active) {
CommitIndexBuffer(vk_buffer, index_binding);
}
const auto draw_dwords = ShaderRecompiler::IR::PushData::GraphicsDrawDwords(
mesh_active, state.ps_input_info.object_primitive_id);
using ShaderRecompiler::IR::PushData;
const auto draw_stages =
vk::ShaderStageFlagBits::eFragment |
(mesh_active ? vk::ShaderStageFlagBits::eMeshEXT : vk::ShaderStageFlagBits::eVertex);
const auto draw_dwords =
PushData::GraphicsDrawDwords(mesh_active, state.ps_input_info.object_primitive_id);
if (draw_dwords != 0) {
std::array<uint32_t, ShaderRecompiler::IR::PushData::MeshDrawDwordCount> draw_data {};
const auto draw_stages = vk::ShaderStageFlagBits::eFragment |
(mesh_active ? vk::ShaderStageFlagBits::eMeshEXT
: vk::ShaderStageFlagBits::eVertex);
std::array<uint32_t, PushData::MeshDrawDwordCount> draw_data {};
if (mesh_active) {
draw_data = {
draw.index_count,
@@ -1213,6 +1237,12 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
vk_buffer.pushConstants(pipeline.pipeline_layout, draw_stages, 0,
draw_dwords * sizeof(uint32_t), draw_data.data());
}
if (const auto planes = vertex_stages.back().user_clip_planes; planes != 0) {
vk_buffer.pushConstants(pipeline.pipeline_layout, draw_stages,
PushData::UserClipPlaneDword * sizeof(uint32_t),
std::bit_width(planes) * 4u * sizeof(uint32_t),
buffer.GetRegisters().GetUserClipPlanes().data());
}
auto& dynamic_state = m_context.GetCommandScheduler().GetDynamicState();
UpdateGraphicsDynamicState(buffer, dynamic_state, vertex_stages.back(), state.depth_info,
@@ -1220,6 +1250,20 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
dynamic_state.SetAttachmentFeedbackLoopAspects(feedback_aspects);
dynamic_state.Commit(m_context.GetGraphics(), vk_buffer);
vk::PipelineStageFlags shader_write_stages = {};
for (const auto& stage: vertex_stages) {
if (HasShaderBufferWrites(stage.stage)) {
shader_write_stages |= ShaderPipelineStages(NativeShaderStage(stage.logical_stage));
}
}
if (state.ps_active && HasShaderBufferWrites(state.ps_input_info.stage)) {
shader_write_stages |= vk::PipelineStageFlagBits::eFragmentShader;
}
if (shader_write_stages) {
// As for dispatches, buffer writes wait for earlier accesses to the memory.
m_context.GetCommandScheduler().EndRendering();
ShaderWriteHazardBarrier(vk_buffer, shader_write_stages);
}
LogDrawPhase(draw.Name(), "BeginRendering");
if (!draw.IsIndexed()) {
SetDrawDebugPhase(buffer, submit_id, draw, 0x400u);
@@ -1240,15 +1284,6 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
if (!draw.IsIndexed()) {
SetDrawDebugPhase(buffer, submit_id, draw, 0x600u);
}
vk::PipelineStageFlags shader_write_stages = {};
for (const auto& stage: vertex_stages) {
if (HasShaderBufferWrites(stage.stage)) {
shader_write_stages |= ShaderPipelineStages(NativeShaderStage(stage.logical_stage));
}
}
if (state.ps_active && HasShaderBufferWrites(state.ps_input_info.stage)) {
shader_write_stages |= vk::PipelineStageFlagBits::eFragmentShader;
}
if (shader_write_stages) {
m_context.GetCommandScheduler().EndRendering();
ShaderWriteBarrier(vk_buffer, shader_write_stages);
+177 -97
View File
@@ -21,15 +21,21 @@ struct TessellationAddress {
Kind kind = Kind::Unknown;
uint32_t coefficient = 0;
uint32_t constant = 0;
bool operator==(const TessellationAddress&) const = default;
};
uint32_t ReflectTessellationStride(const Decoder::Program& program, bool local,
uint32_t control_points, uint32_t input_stride) {
// Reflects the LS output stride, or the HS output stride and patch constant range.
void ReflectTessellationLayout(const Decoder::Program& program, bool local,
ShaderTessellationInputInfo& info) {
using namespace Decoder;
using Address = TessellationAddress;
using Kind = Address::Kind;
std::array<Address, IR::NumVectorRegs> registers {};
registers[local ? 3u : 1u] =
using Address = TessellationAddress;
using Kind = Address::Kind;
using Registers = std::array<Address, IR::NumVectorRegs>;
const auto control_points = local ? info.input_control_points : info.output_control_points;
Registers entry {}, registers;
entry[local ? 3u : 1u] =
local ? Address {Kind::Affine, 1u, 0u} : Address {Kind::PackedControlPoint};
const auto constant = [](uint32_t value) { return Address {Kind::Affine, 0u, value}; };
const auto read = [&](const Operand& operand) -> Address {
@@ -79,78 +85,122 @@ uint32_t ReflectTessellationStride(const Decoder::Program& program, bool local,
? value
: Address {};
};
uint32_t stride = 0;
for (const auto& inst: program.instructions) {
// Stage exits preserve the active path's definitions. An internal join
// would require merging register definitions.
EXIT_NOT_IMPLEMENTED(IsDirectBranch(inst.opcode) &&
inst.branch_target != program.instructions.back().pc);
const bool local_store =
inst.opcode == Opcode::DS_WRITE_B32 || inst.opcode == Opcode::DS_WRITE2_B32;
const bool buffer_store = inst.opcode == Opcode::BUFFER_STORE_DWORD ||
inst.opcode == Opcode::BUFFER_STORE_DWORDX2 ||
inst.opcode == Opcode::BUFFER_STORE_DWORDX3 ||
inst.opcode == Opcode::BUFFER_STORE_DWORDX4;
const bool control_store =
!local && buffer_store && inst.src2.kind == OperandKind::Sgpr && inst.src2.reg == 2u;
const bool control_read =
!local && (inst.opcode == Opcode::DS_READ_B32 || inst.opcode == Opcode::DS_READ2_B32);
if ((local && local_store) || control_store || control_read) {
const auto address = read(inst.src0);
if (address.kind != Kind::Affine) {
EXIT("%s tessellation address is not affine at pc 0x%08x\n", local ? "LS" : "HS",
inst.pc);
}
if (address.coefficient != 0u) {
const auto expected = control_read ? input_stride : stride;
EXIT_NOT_IMPLEMENTED((address.coefficient & 3u) != 0u ||
(expected != 0u && expected != address.coefficient));
if (!control_read) stride = address.coefficient;
}
// A branch joins its target with the fallthrough path. Definitions that differ between
// the paths are unknown there. Loop headers repeat the walk until back edges are stable.
std::unordered_map<uint32_t, Registers> joins, loops;
const auto merge = [](Registers& into, const Registers& from) {
bool changed = false;
for (uint32_t index = 0; index < into.size(); index++) {
if (into[index] == from[index]) continue;
changed |= into[index] != Address {};
into[index] = {};
}
// Store vdata is a source, despite occupying the decoder's dst field.
if (local_store || buffer_store || inst.dst.kind != OperandKind::Vgpr) continue;
const auto lhs = read(inst.src0);
const auto rhs = read(inst.src1);
const auto third = read(inst.src2);
Address value;
switch (inst.opcode) {
case Opcode::V_BFE_U32:
if (lhs.kind == Kind::PackedControlPoint && rhs.kind == Kind::Affine &&
rhs.coefficient == 0u && third.kind == Kind::Affine &&
third.coefficient == 0u) {
if (rhs.constant == 8u && third.constant == 5u) value = {Kind::Affine, 1u, 0u};
if (rhs.constant == 0u && third.constant == 8u) value = constant(0u);
return changed;
};
uint32_t stride = 0, patch_begin = UINT32_MAX, patch_end = 0;
for (bool repeat = true; repeat;) {
repeat = false;
registers = entry;
for (const auto& inst: program.instructions) {
if (const auto join = joins.find(inst.pc); join != joins.end()) {
merge(registers, join->second);
joins.erase(join);
}
if (const auto loop = loops.find(inst.pc); loop != loops.end())
merge(registers, loop->second);
if (IsDirectBranch(inst.opcode)) {
const bool back = inst.branch_target <= inst.pc;
const auto [join, inserted] =
(back ? loops : joins).try_emplace(inst.branch_target, registers);
const bool changed = !inserted && merge(join->second, registers);
repeat |= back && (inserted || changed);
}
const bool local_store = IsDsWriteOpcode(inst.opcode);
const bool buffer_store = inst.opcode == Opcode::BUFFER_STORE_DWORD ||
inst.opcode == Opcode::BUFFER_STORE_DWORDX2 ||
inst.opcode == Opcode::BUFFER_STORE_DWORDX3 ||
inst.opcode == Opcode::BUFFER_STORE_DWORDX4;
const bool control_store = !local && buffer_store &&
inst.src2.kind == OperandKind::Sgpr && inst.src2.reg == 2u;
if ((local && local_store) || control_store) {
const auto address = read(inst.src0);
if (address.kind != Kind::Affine) {
EXIT("%s tessellation address is not affine at pc 0x%08x\n",
local ? "LS" : "HS", inst.pc);
}
break;
case Opcode::V_MUL_U32_U24: value = multiply(low24(lhs), low24(rhs)); break;
case Opcode::V_MAD_U32_U24: value = add(multiply(low24(lhs), low24(rhs)), third); break;
case Opcode::V_LSHL_ADD_U32:
if (rhs.kind == Kind::Affine && rhs.coefficient == 0u && rhs.constant < 32u)
value = add(multiply(lhs, constant(1u << rhs.constant)), third);
break;
case Opcode::V_LSHLREV_B32:
if (lhs.kind == Kind::Affine && lhs.coefficient == 0u && lhs.constant < 32u)
value = multiply(rhs, constant(1u << lhs.constant));
break;
case Opcode::V_SUB_NC_U32:
if (lhs.kind == Kind::Affine && rhs.kind == Kind::Affine &&
lhs.coefficient >= rhs.coefficient && lhs.constant >= rhs.constant)
value = affine(lhs.coefficient - rhs.coefficient, lhs.constant - rhs.constant);
break;
default: break;
if (address.coefficient != 0u) {
EXIT_NOT_IMPLEMENTED((address.coefficient & 3u) != 0u ||
(stride != 0u && stride != address.coefficient));
stride = address.coefficient;
} else if (control_store) {
// Control-point-independent HS outputs are patch constants.
const auto begin = address.constant + inst.offset;
patch_begin = std::min(patch_begin, begin);
patch_end = std::max(patch_end, begin + 4u * inst.data_dwords);
}
}
// Store vdata is a source, despite occupying the decoder's dst field.
if (local_store || buffer_store || inst.dst.kind != OperandKind::Vgpr) continue;
const auto lhs = read(inst.src0);
const auto rhs = read(inst.src1);
const auto third = read(inst.src2);
Address value;
switch (inst.opcode) {
case Opcode::V_BFE_U32:
if (lhs.kind == Kind::PackedControlPoint && rhs.kind == Kind::Affine &&
rhs.coefficient == 0u && third.kind == Kind::Affine &&
third.coefficient == 0u) {
if (rhs.constant == 8u && third.constant == 5u)
value = {Kind::Affine, 1u, 0u};
if (rhs.constant == 0u && third.constant == 8u) value = constant(0u);
}
break;
case Opcode::V_AND_B32:
if ((lhs.kind == Kind::PackedControlPoint && rhs == constant(0xffu)) ||
(rhs.kind == Kind::PackedControlPoint && lhs == constant(0xffu)))
value = constant(0u);
break;
case Opcode::V_ADD_NC_U32: value = add(lhs, rhs); break;
case Opcode::V_MUL_LO_U32: value = multiply(lhs, rhs); break;
case Opcode::V_MUL_U32_U24: value = multiply(low24(lhs), low24(rhs)); break;
case Opcode::V_MAD_U32_U24:
value = add(multiply(low24(lhs), low24(rhs)), third);
break;
case Opcode::V_LSHL_ADD_U32:
if (rhs.kind == Kind::Affine && rhs.coefficient == 0u && rhs.constant < 32u)
value = add(multiply(lhs, constant(1u << rhs.constant)), third);
break;
case Opcode::V_LSHLREV_B32:
if (lhs.kind == Kind::Affine && lhs.coefficient == 0u && lhs.constant < 32u)
value = multiply(rhs, constant(1u << lhs.constant));
break;
case Opcode::V_SUB_NC_U32:
if (lhs.kind == Kind::Affine && rhs.kind == Kind::Affine &&
lhs.coefficient >= rhs.coefficient && lhs.constant >= rhs.constant)
value =
affine(lhs.coefficient - rhs.coefficient, lhs.constant - rhs.constant);
break;
default: break;
}
if (inst.dst.sdwa_sel != 6u || inst.dst.op_sel || inst.dst.omod || inst.dst.clamp ||
inst.dst.dpp)
value = {};
for (uint32_t index = 0;
index < std::max(inst.data_dwords, 1u) && inst.dst.reg + index < registers.size();
index++) {
registers[inst.dst.reg + index] = {};
}
registers.at(inst.dst.reg) = value;
}
if (inst.dst.sdwa_sel != 6u || inst.dst.op_sel || inst.dst.omod || inst.dst.clamp ||
inst.dst.dpp)
value = {};
for (uint32_t index = 0;
index < std::max(inst.data_dwords, 1u) && inst.dst.reg + index < registers.size(); index++) {
registers[inst.dst.reg + index] = {};
}
registers.at(inst.dst.reg) = value;
}
EXIT_NOT_IMPLEMENTED(stride == 0u);
return stride;
EXIT_NOT_IMPLEMENTED(!joins.empty() || stride == 0u);
(local ? info.ls_stride : info.hs_stride) = stride;
if (patch_end != 0u) {
EXIT_NOT_IMPLEMENTED(patch_begin < stride * control_points);
info.patch_base = patch_begin;
info.patch_size = patch_end - patch_begin;
}
}
const IR::Inst* TessellationBufferBase(const IR::Inst& inst) {
@@ -161,6 +211,15 @@ const IR::Inst* TessellationBufferBase(const IR::Inst& inst) {
: nullptr;
}
// Whether an access predicate, possibly a conjunction of EXEC masks, implies a condition.
bool Implies(IR::Value predicate, IR::Value condition) {
predicate = predicate.Resolve();
if (predicate == condition.Resolve()) return true;
const auto* inst = predicate.TryInstruction();
return inst != nullptr && inst->GetOpcode() == IR::ValueOpcode::LogicalAnd &&
(Implies(inst->Arg(0), condition) || Implies(inst->Arg(1), condition));
}
IR::Value ActiveAddress(IR::Block& block, IR::Block::iterator before, IR::Value value,
IR::Value predicate, std::unordered_map<IR::Inst*, IR::Value>& resolved) {
using namespace IR;
@@ -169,18 +228,28 @@ IR::Value ActiveAddress(IR::Block& block, IR::Block::iterator before, IR::Value
if (source == nullptr) return value;
if (const auto found = resolved.find(source); found != resolved.end()) return found->second;
if (source->GetOpcode() == ValueOpcode::SelectU32) {
return source->Arg(0).Resolve() == predicate.Resolve()
return Implies(predicate, source->Arg(0))
? ActiveAddress(block, before, source->Arg(1), predicate, resolved)
: value;
}
if (source->GetOpcode() != ValueOpcode::IAdd32 && source->GetOpcode() != ValueOpcode::ISub32)
// Pure address arithmetic that stays behind EXEC selects; mul24 lowers to IMul32 of BFEs.
const auto opcode = source->GetOpcode();
if (opcode != ValueOpcode::IAdd32 && opcode != ValueOpcode::ISub32 &&
opcode != ValueOpcode::IMul32 && opcode != ValueOpcode::BitFieldUExtract)
return value;
const auto lhs = ActiveAddress(block, before, source->Arg(0), predicate, resolved);
const auto rhs = ActiveAddress(block, before, source->Arg(1), predicate, resolved);
if (lhs != source->Arg(0).Resolve() || rhs != source->Arg(1).Resolve()) {
auto copy = block.PrependNewInst(before, source->GetOpcode(), {lhs, rhs},
source->Flags<uint64_t>());
value = Value(&*copy);
std::array<Value, 3> args;
bool changed = false;
for (size_t index = 0; index < source->NumArgs(); index++) {
args[index] = ActiveAddress(block, before, source->Arg(index), predicate, resolved);
changed |= args[index] != source->Arg(index).Resolve();
}
if (changed) {
const auto flags = source->Flags<uint64_t>();
const auto copy =
source->NumArgs() == 2u
? block.PrependNewInst(before, opcode, {args[0], args[1]}, flags)
: block.PrependNewInst(before, opcode, {args[0], args[1], args[2]}, flags);
value = Value(&*copy);
}
resolved.emplace(source, value);
return value;
@@ -193,11 +262,12 @@ void AnalyzeTessellationPrograms(std::span<const uint32_t> local, std::span<cons
const auto local_program = Decoder::DecodeFrontProgram(local);
Decoder::Program control_program;
Decoder::DecodeProgram(control, control_program);
info.ls_stride = ReflectTessellationStride(local_program, true, info.input_control_points, 0u);
info.hs_stride = ReflectTessellationStride(control_program, false, info.output_control_points,
info.ls_stride);
LOGF("Tessellation interface: input_cp=%u output_cp=%u ls_stride=%u hs_stride=%u\n",
info.input_control_points, info.output_control_points, info.ls_stride, info.hs_stride);
ReflectTessellationLayout(local_program, true, info);
ReflectTessellationLayout(control_program, false, info);
LOGF("Tessellation interface: input_cp=%u output_cp=%u ls_stride=%u hs_stride=%u "
"patch_base=0x%x patch_size=%u\n",
info.input_control_points, info.output_control_points, info.ls_stride, info.hs_stride,
info.patch_base, info.patch_size);
}
void LowerTessellationMemory(IR::Program& program, const CompileOptions& options) {
@@ -218,7 +288,7 @@ void LowerTessellationMemory(IR::Program& program, const CompileOptions& options
}
}
ConstantPropagationPass(program.blocks, program.wave_size);
uint32_t reads = 0, writes = 0, factors = 0;
uint32_t reads = 0, writes = 0, factors = 0, patches = 0;
for (auto* block: program.blocks) {
for (auto it = block->begin(); it != block->end(); ++it) {
auto& inst = *it;
@@ -261,15 +331,22 @@ void LowerTessellationMemory(IR::Program& program, const CompileOptions& options
kind = TessellationAttribute::Factor;
factors += components;
} else {
kind = write ? TessellationAttribute::ControlOutput
: TessellationAttribute::EvaluationInput;
EXIT_NOT_IMPLEMENTED(write
? options.stage != ShaderType::TessellationControl
: options.stage != ShaderType::TessellationEvaluation);
// After its barrier, the HS may read back outputs of other control points.
const bool output = options.stage == ShaderType::TessellationControl;
EXIT_NOT_IMPLEMENTED(
!output && (write || options.stage != ShaderType::TessellationEvaluation));
kind = output ? TessellationAttribute::ControlOutput
: TessellationAttribute::EvaluationInput;
if (address.IsImmediate() &&
address.U32() >= tess.hs_stride * tess.output_control_points) {
EXIT_NOT_IMPLEMENTED(!write);
kind = TessellationAttribute::PatchOutput;
const auto begin = address.U32() + memory.offset;
EXIT_NOT_IMPLEMENTED(begin < tess.patch_base ||
begin + 4u * components >
tess.patch_base + tess.patch_size);
kind = output ? TessellationAttribute::PatchOutput
: TessellationAttribute::PatchInput;
// HS read-backs must not hide misplaced patch stores from the check below.
if (write || !output) patches += components;
}
}
} else {
@@ -320,11 +397,14 @@ void LowerTessellationMemory(IR::Program& program, const CompileOptions& options
ConstantPropagationPass(program.blocks, program.wave_size);
RemoveIdentities(program);
EliminateDeadCode(program.blocks);
LOGF("%s tessellation lowering: reads=%u writes=%u factors=%u\n",
// Patch constant stores whose EXEC selects were not stripped would land in control points.
EXIT_NOT_IMPLEMENTED(options.stage == ShaderType::TessellationControl &&
tess.patch_size != 0u && patches == 0u);
LOGF("%s tessellation lowering: reads=%u writes=%u factors=%u patches=%u\n",
options.stage == ShaderType::Local ? "LS"
: options.stage == ShaderType::TessellationControl ? "HS"
: "TES",
reads, writes, factors);
reads, writes, factors, patches);
}
} // namespace Libs::Graphics::ShaderRecompiler
@@ -115,7 +115,7 @@ uint32_t EmitF32ToU32(EmitterState& state, uint32_t src, bool signed_value) {
} else {
state.builder.AddFunction(spv::OpConvertFToU, TypeU32(state), converted_raw, trunc);
}
const auto nan = EmitClassifyF32(state, src).nan;
const auto nan = Unary(state, spv::OpIsNan, TypeBool(state), src);
if (signed_value) {
const auto below = Binary(state, spv::OpFOrdLessThanEqual, TypeBool(state), src,
ConstantF32(state, 0xcf000000u));
@@ -242,11 +242,9 @@ uint32_t EmitPackFloat2x16Rtz(EmitterState& state, uint32_t arg0, uint32_t arg1)
}
uint32_t EmitFPSaturate32(EmitterState& state, uint32_t arg0) {
// The clamp modifier runs in DX10 clamp mode: NaN becomes 0. FClamp leaves NaN undefined.
const auto clamped = EmitExt(state, TypeF32(state), GLSLstd450FClamp,
{arg0, ConstantF32(state, 0), ConstantF32(state, 0x3f800000u)});
return Select(state, TypeF32(state), EmitClassifyF32(state, arg0).nan, ConstantF32(state, 0),
clamped);
// The clamp modifier runs in DX10 clamp mode: NaN becomes 0, as NClamp's NMax step does.
return EmitExt(state, TypeF32(state), GLSLstd450NClamp,
{arg0, ConstantF32(state, 0), ConstantF32(state, 0x3f800000u)});
}
uint32_t EmitSMulHi(EmitterState& state, uint32_t arg0, uint32_t arg1) {
@@ -17,16 +17,9 @@ uint32_t EmitCompareU32Constant(EmitterState& state, spv::Op opcode, uint32_t va
}
F32Class EmitClassifyF32Bits(EmitterState& state, uint32_t bits) {
F32Class cls;
cls.bits = bits;
const auto abs_bits = EmitAndConstant(state, cls.bits, 0x7fffffffu);
cls.nan = EmitCompareU32Constant(state, spv::OpUGreaterThan, abs_bits, 0x7f800000u);
cls.zero = EmitCompareU32Constant(state, spv::OpIEqual, abs_bits, 0);
return cls;
}
F32Class EmitClassifyF32(EmitterState& state, uint32_t value) {
return EmitClassifyF32Bits(state, EmitBitcastF32ToU32(state, value));
const auto abs_bits = EmitAndConstant(state, bits, 0x7fffffffu);
return {EmitCompareU32Constant(state, spv::OpUGreaterThan, abs_bits, 0x7f800000u),
EmitCompareU32Constant(state, spv::OpIEqual, abs_bits, 0)};
}
uint32_t EmitClassMaskBitMatch(EmitterState& state, uint32_t mask, uint32_t bit,
@@ -100,51 +93,53 @@ uint32_t EmitClassMaskF16(EmitterState& state, uint32_t bits, uint32_t mask) {
return EmitClassMaskBits(state, bits, mask, true);
}
static uint32_t EmitMinMaxF32(EmitterState& state, uint32_t lhs, uint32_t rhs,
const F32Class& lhs_class, const F32Class& rhs_class,
bool max_value) {
const auto numeric_cond = state.builder.AllocateId();
state.builder.AddFunction(max_value ? spv::OpFOrdGreaterThanEqual : spv::OpFOrdLessThan,
TypeBool(state), numeric_cond, lhs, rhs);
const auto ordered_bits =
EmitSelectValueU32(state, numeric_cond, lhs_class.bits, rhs_class.bits);
const auto both_zero = EmitLogicalAndBool(state, lhs_class.zero, rhs_class.zero);
const auto zero_bits = max_value ? EmitAndU32(state, lhs_class.bits, rhs_class.bits)
: EmitOrU32(state, lhs_class.bits, rhs_class.bits);
const auto numeric_bits = EmitSelectValueU32(state, both_zero, zero_bits, ordered_bits);
const auto rhs_nan_bits =
EmitSelectValueU32(state, rhs_class.nan, lhs_class.bits, numeric_bits);
const auto result_bits = EmitSelectValueU32(state, lhs_class.nan, rhs_class.bits, rhs_nan_bits);
return EmitBitcastU32ToF32(state, result_bits);
static uint32_t EmitMinMaxF32(EmitterState& state, uint32_t lhs, uint32_t rhs, uint32_t lhs_bits,
uint32_t rhs_bits, bool max_value) {
// RDNA2 V_MIN/V_MAX_F32 return the other source for a NaN source (S1 when both are NaN)
// and order -0 below +0. An unordered compare keeps S0 for a NaN S1 and, unlike NMin/NMax,
// always returns the exact bits of one source.
const auto either = EmitOrU32(state, lhs_bits, rhs_bits);
const auto both_zero = EmitCompareU32Constant(state, spv::OpIEqual,
EmitAndConstant(state, either, 0x7fffffffu), 0);
const auto zero =
EmitBitcastU32ToF32(state, max_value ? EmitAndU32(state, lhs_bits, rhs_bits) : either);
const auto keep_lhs =
Binary(state, max_value ? spv::OpFUnordGreaterThanEqual : spv::OpFUnordLessThan,
TypeBool(state), lhs, rhs);
const auto ordered = Select(state, TypeF32(state), keep_lhs, lhs, rhs);
const auto numeric = Select(state, TypeF32(state), both_zero, zero, ordered);
return Select(state, TypeF32(state), Unary(state, spv::OpIsNan, TypeBool(state), lhs), rhs,
numeric);
}
uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) {
const auto lhs_class = EmitClassifyF32(state, lhs);
const auto rhs_class = EmitClassifyF32(state, rhs);
return EmitMinMaxF32(state, lhs, rhs, lhs_class, rhs_class, max_value);
return EmitMinMaxF32(state, lhs, rhs, EmitBitcastF32ToU32(state, lhs),
EmitBitcastF32ToU32(state, rhs), max_value);
}
uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
const auto a_class = EmitClassifyF32(state, a);
const auto b_class = EmitClassifyF32(state, b);
const auto c_class = EmitClassifyF32(state, c);
const auto min_ab = EmitMinMaxF32(state, a, b, a_class, b_class, false);
const auto min_ab_class = EmitClassifyF32(state, min_ab);
const auto min3 = EmitMinMaxF32(state, min_ab, c, min_ab_class, c_class, false);
const auto max_ab = EmitMinMaxF32(state, a, b, a_class, b_class, true);
const auto max_ab_class = EmitClassifyF32(state, max_ab);
const auto max3 = EmitMinMaxF32(state, max_ab, c, max_ab_class, c_class, true);
const auto max_bc = EmitMinMaxF32(state, b, c, b_class, c_class, true);
const auto max_ac = EmitMinMaxF32(state, a, c, a_class, c_class, true);
// RDNA2 compares numerically against MAX3, including ties between signed zeros.
const auto a_bits = EmitBitcastF32ToU32(state, a);
const auto b_bits = EmitBitcastF32ToU32(state, b);
const auto c_bits = EmitBitcastF32ToU32(state, c);
const auto min_ab = EmitMinMaxF32(state, a, b, a_bits, b_bits, false);
const auto min3 =
EmitMinMaxF32(state, min_ab, c, EmitBitcastF32ToU32(state, min_ab), c_bits, false);
const auto max_ab = EmitMinMaxF32(state, a, b, a_bits, b_bits, true);
const auto max3 =
EmitMinMaxF32(state, max_ab, c, EmitBitcastF32ToU32(state, max_ab), c_bits, true);
// RDNA2 compares numerically against MAX3, including ties between signed zeros, then takes
// V_MAX_F32 of the other two sources in their original order.
const auto a_is_max = Binary(state, spv::OpFOrdEqual, TypeBool(state), max3, a);
const auto b_is_max = Binary(state, spv::OpFOrdEqual, TypeBool(state), max3, b);
const auto median = Select(state, TypeF32(state), a_is_max, max_bc,
Select(state, TypeF32(state), b_is_max, max_ac, max_ab));
const auto nan_ab = EmitLogicalOrBool(state, a_class.nan, b_class.nan);
const auto any_nan = EmitLogicalOrBool(state, nan_ab, c_class.nan);
const auto x = Select(state, TypeF32(state), a_is_max, b, a);
const auto y =
Select(state, TypeF32(state), EmitLogicalOrBool(state, a_is_max, b_is_max), c, b);
const auto median = EmitMinMaxF32Value(state, x, y, true);
const auto is_nan = [&](uint32_t value) {
return Unary(state, spv::OpIsNan, TypeBool(state), value);
};
const auto any_nan =
EmitLogicalOrBool(state, EmitLogicalOrBool(state, is_nan(a), is_nan(b)), is_nan(c));
return Select(state, TypeF32(state), any_nan, min3, median);
}
@@ -349,6 +349,38 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo
return vector;
}
uint32_t DistancePointer(EmitterState& state, IR::StageOutputKind kind, uint32_t index) {
if (state.program.stage == ShaderType::Mesh) {
return MeshOutputPointer(state, kind, index);
}
const auto variable = kind == IR::StageOutputKind::ClipDistance ? state.clip_distance_variable
: state.cull_distance_variable;
const auto pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain,
TypePointer(state, spv::StorageClassOutput, TypeF32(state)), pointer,
variable, ConstantU32(state, index));
return pointer;
}
// The clipper keeps the half space where dot(plane, position) >= 0 for each UCP_ENA plane.
void EmitUserClipDistances(EmitterState& state, uint32_t position) {
uint32_t index = 0;
for (auto mask = state.user_clip_planes; mask != 0; mask &= mask - 1u) {
const auto dword = IR::PushData::UserClipPlaneDword + 4u * std::countr_zero(mask);
uint32_t components[4] {};
for (uint32_t i = 0; i < 4; i++) {
components[i] = Unary(state, spv::OpBitcast, TypeF32(state),
EmitPushDataLoad(state, ConstantU32(state, dword + i)));
}
const auto plane = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 4), plane,
components[0], components[1], components[2], components[3]);
state.builder.AddFunction(
spv::OpStore, DistancePointer(state, IR::StageOutputKind::ClipDistance, index++),
Binary(state, spv::OpDot, TypeF32(state), plane, position));
}
}
void EmitAuxPositionExport(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo& exp) {
auto& state = ctx.state;
for (uint32_t component = 0; component < 4; component++) {
@@ -389,23 +421,13 @@ void EmitAuxPositionExport(ValueEmitContext& ctx, uint32_t data, const IR::Expor
state.builder.AddFunction(spv::OpStore, state.point_size_variable, f32);
continue;
}
auto StoreDistance = [&](IR::StageOutputKind kind, uint32_t variable, uint32_t index) {
if (index == UINT32_MAX) {
return;
auto StoreDistance = [&](IR::StageOutputKind kind, uint32_t index) {
if (index != UINT32_MAX) {
state.builder.AddFunction(spv::OpStore, DistancePointer(state, kind, index), f32);
}
uint32_t pointer;
if (state.program.stage == ShaderType::Mesh) {
pointer = MeshOutputPointer(state, kind, index);
} else {
pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain,
TypePointer(state, spv::StorageClassOutput, TypeF32(state)),
pointer, variable, ConstantU32(state, index));
}
state.builder.AddFunction(spv::OpStore, pointer, f32);
};
StoreDistance(IR::StageOutputKind::ClipDistance, state.clip_distance_variable, output.clip_distance);
StoreDistance(IR::StageOutputKind::CullDistance, state.cull_distance_variable, output.cull_distance);
StoreDistance(IR::StageOutputKind::ClipDistance, output.clip_distance);
StoreDistance(IR::StageOutputKind::CullDistance, output.cull_distance);
}
}
@@ -596,6 +618,9 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
state.input_info.vertex->clip_space.enabled) {
value = ConvertPositionToClipSpace(state, value);
}
if (exp.kind == IR::ExportTargetKind::Position && state.user_clip_planes != 0) {
EmitUserClipDistances(state, value);
}
if (state.program.stage == ShaderType::Mesh) {
const auto kind = exp.kind == IR::ExportTargetKind::Position
? IR::StageOutputKind::Position
@@ -608,7 +633,6 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto equal = state.builder.AllocateId();
const auto invalid = state.builder.AllocateId();
const auto distance = state.builder.AllocateId();
const auto distance_pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpFOrdEqual, TypeBoolVector(state, 4), equal,
value, zero);
state.builder.AddFunction(spv::OpAll, TypeBool(state), invalid, equal);
@@ -617,11 +641,10 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
state.builder.AddFunction(spv::OpSelect, TypeF32(state), distance, invalid,
ConstantF32Value(state, -1.0f),
ConstantF32Value(state, 0.0f));
state.builder.AddFunction(
spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeF32(state)),
distance_pointer, state.clip_distance_variable,
ConstantU32(state, state.invalid_position_clip_distance));
state.builder.AddFunction(spv::OpStore, distance_pointer, distance);
state.builder.AddFunction(spv::OpStore,
DistancePointer(state, IR::StageOutputKind::ClipDistance,
state.invalid_position_clip_distance),
distance);
static std::atomic_bool logged = false;
if (!logged.exchange(true, std::memory_order_relaxed)) {
Log::WriteToConsoleAndLog(
@@ -104,9 +104,8 @@ struct EmitterState {
std::array<uint32_t, 3> u32_vector_types {};
std::array<uint32_t, 3> i32_vector_types {};
std::array<uint32_t, 3> f32_vector_types {};
std::array<uint32_t, 6> tess_variables {};
std::array<uint32_t, IR::NumTessellationAttributes> tess_variables {};
uint32_t tess_inner_variable = 0;
uint32_t tess_patch_base = 0;
uint32_t lane_count = 1;
uint32_t lane_half = 0;
@@ -148,6 +147,7 @@ struct EmitterState {
uint32_t point_size_variable = 0;
uint32_t clip_distance_variable = 0;
uint32_t invalid_position_clip_distance = UINT32_MAX;
uint32_t user_clip_planes = 0;
uint32_t cull_distance_variable = 0;
uint32_t layer_variable = 0;
uint32_t viewport_index_variable = 0;
@@ -285,7 +285,6 @@ struct ImageSampleLayout {
};
struct F32Class {
uint32_t bits = 0;
uint32_t nan = 0;
uint32_t zero = 0;
};
@@ -510,8 +509,6 @@ inline constexpr auto EmitLogicalNotBool = EmitNative<spv::OpLogicalNot, IR::Typ
F32Class EmitClassifyF32Bits(EmitterState& state, uint32_t bits);
F32Class EmitClassifyF32(EmitterState& state, uint32_t value);
uint32_t EmitClassMaskBitMatch(EmitterState& state, uint32_t mask, uint32_t bit,
uint32_t class_match);
@@ -244,7 +244,7 @@ void DefineDescriptors(EmitterState& state) {
counts[static_cast<size_t>(Kind::ShaderData)] =
bindings.ShaderDataDwords() != 0 && !bindings.UsesPushData();
if (state.program.bindings.UsesPushData() || state.program.stage == ShaderType::Mesh ||
state.object_primitive_id_variable != 0) {
state.object_primitive_id_variable != 0 || state.user_clip_planes != 0) {
const auto type = PushConstantBlockType(state);
state.push_constant_variable = state.builder.DefineGlobalVariable(
TypePointer(state, spv::StorageClassPushConstant, type), spv::StorageClassPushConstant);
@@ -600,14 +600,24 @@ void DefineOutputs(EmitterState& state) {
cull_distance_count = std::max(cull_distance_count, output.index + 1);
}
}
const bool position = std::ranges::any_of(state.outputs, [](const OutputBinding& output) {
return output.kind == IR::StageOutputKind::Position;
});
if (position && state.input_info.vertex->user_clip_planes != 0) {
// PipelineCache rejects UCP_ENA with shader clip or cull distances.
EXIT_IF(clip_distance_count + cull_distance_count != 0);
state.user_clip_planes = state.input_info.vertex->user_clip_planes;
for (auto mask = state.user_clip_planes; mask != 0; mask &= mask - 1u) {
state.outputs.push_back(
{{IR::StageOutputKind::ClipDistance, clip_distance_count++, 0, "gl_ClipDistance"}});
}
}
if (state.program.stage == ShaderType::Mesh) {
DefineMeshOutputs(state, clip_distance_count, cull_distance_count);
return;
}
if (state.program.stage == ShaderType::Vertex && clip_distance_count + cull_distance_count < 8u &&
std::ranges::any_of(state.outputs, [](const OutputBinding& output) {
return output.kind == IR::StageOutputKind::Position;
})) {
if (state.program.stage == ShaderType::Vertex &&
clip_distance_count + cull_distance_count < 8u && position) {
// Reserve one plane for the enabled PA_CL_CLIP_CNTL clipping-error cull.
state.invalid_position_clip_distance = clip_distance_count++;
state.outputs.push_back({{IR::StageOutputKind::ClipDistance,
@@ -233,14 +233,17 @@ void Invoke(Return (*emit)(Context&, Args...), ValueEmitContext& ctx, const IR::
}
void EmitDirectInstruction(ValueEmitContext& ctx, const IR::Inst& inst) {
if (ctx.half != 0 && (inst.GetOpcode() == IR::ValueOpcode::Ballot ||
inst.GetOpcode() == IR::ValueOpcode::ReadFirstLane ||
inst.GetOpcode() == IR::ValueOpcode::SubgroupUMinU32)) {
// These operations combine both emulated halves into one whole-wave result.
using Op = IR::ValueOpcode;
const auto op = inst.GetOpcode();
if (ctx.half != 0 &&
(op == Op::Ballot || op == Op::ReadFirstLane || op == Op::SubgroupUMinU32 ||
op == Op::ReadConstBuffer ||
(op == Op::LoadAddressU32 && ctx.Memory(inst).kind == IR::ResourceKind::ScalarAddress))) {
// Whole-wave results and SMEM loads (SGPR operands, no EXEC) are identical in both halves.
ctx.Define(inst, ctx.other_half->Result(inst));
return;
}
switch (inst.GetOpcode()) {
switch (op) {
#define VALUE_OPCODE(name, ...) \
case IR::ValueOpcode::name: return Invoke(Emit##name, ctx, inst);
#include "graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc"
@@ -623,6 +626,12 @@ void EmitProgram(EmitterState& state) {
}
if (state.program.dispatcher_fallback) {
auto& dispatch = dispatcher.emplace();
// Allocate both halves in program order so the SPIR-V is identical across runs.
const auto add_spill = [&](const IR::Inst* inst) {
for (uint32_t half = 0; half < state.lane_count; half++) {
dispatch.spills[half].emplace(inst, state.builder.AllocateId());
}
};
for (const auto* block: program.blocks) {
for (const auto& inst: *block) {
if (inst.GetOpcode() != IR::ValueOpcode::Phi) {
@@ -632,7 +641,7 @@ void EmitProgram(EmitterState& state) {
ctx.Fail(inst, "cannot be stored by the dispatcher");
break;
}
dispatch.spills[0].emplace(&inst, state.builder.AllocateId());
add_spill(&inst);
}
}
const auto mark_cross_block = [&](IR::Value value, const IR::Block* consumer) {
@@ -647,7 +656,7 @@ void EmitProgram(EmitterState& state) {
return;
}
if (!dispatch.spills[0].contains(definition)) {
dispatch.spills[0].emplace(definition, state.builder.AllocateId());
add_spill(definition);
}
};
for (const auto* block: program.blocks) {
@@ -669,12 +678,7 @@ void EmitProgram(EmitterState& state) {
dispatch.continue_label = state.builder.AllocateId();
dispatch.merge_label = state.builder.AllocateId();
ctx.dispatcher_spills = &dispatch.spills[0];
if (state.lane_count == 2) {
for (const auto& [inst, id]: dispatch.spills[0]) {
dispatch.spills[1].emplace(inst, state.builder.AllocateId());
}
high.dispatcher_spills = &dispatch.spills[1];
}
high.dispatcher_spills = &dispatch.spills[1];
}
DefineGetBdaPointer(state);
DefineIndirectBufferLoads(ctx);
@@ -5,6 +5,17 @@
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
namespace {
bool IsTessellationInput(IR::TessellationAttribute kind) {
using Attribute = IR::TessellationAttribute;
return kind == Attribute::ControlInput || kind == Attribute::EvaluationInput ||
kind == Attribute::PatchInput;
}
bool IsTessellationPatch(IR::TessellationAttribute kind) {
using Attribute = IR::TessellationAttribute;
return kind == Attribute::PatchOutput || kind == Attribute::PatchInput;
}
uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
auto& state = ctx.state;
using Attribute = IR::TessellationAttribute;
@@ -12,8 +23,9 @@ uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto& tess = state.input_info.vertex->tess;
const auto variable = state.tess_variables.at(static_cast<uint32_t>(kind));
EXIT_IF(variable == 0);
const auto input = kind == Attribute::ControlInput || kind == Attribute::EvaluationInput;
const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput;
const bool patch = IsTessellationPatch(kind);
const auto storage =
IsTessellationInput(kind) ? spv::StorageClassInput : spv::StorageClassOutput;
const auto pointer = state.builder.AllocateId();
if (kind == Attribute::Factor) {
EXIT_NOT_IMPLEMENTED(!inst.Arg(1).IsImmediate());
@@ -26,15 +38,13 @@ uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
return pointer;
}
auto address = ctx.Arg(inst, 1);
if (kind == Attribute::PatchOutput) {
address =
EmitBinaryU32(state, spv::OpISub, address, ConstantU32(state, state.tess_patch_base));
if (patch) {
address = EmitBinaryU32(state, spv::OpISub, address, ConstantU32(state, tess.patch_base));
}
const bool local = kind == Attribute::LocalOutput || kind == Attribute::ControlInput;
const auto stride = local ? tess.ls_stride : tess.hs_stride;
const auto offset = kind == Attribute::PatchOutput ? address
: EmitBinaryU32(state, spv::OpUMod, address,
ConstantU32(state, stride));
const auto offset =
patch ? address : EmitBinaryU32(state, spv::OpUMod, address, ConstantU32(state, stride));
const auto attribute =
EmitBinaryU32(state, spv::OpShiftRightLogical, offset, ConstantU32(state, 4));
const auto component =
@@ -42,12 +52,14 @@ uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
EmitBinaryU32(state, spv::OpShiftRightLogical, offset, ConstantU32(state, 2)),
ConstantU32(state, 3));
const auto type = TypePointer(state, storage, TypeU32(state));
if (kind == Attribute::LocalOutput || kind == Attribute::PatchOutput) {
if (kind == Attribute::LocalOutput || patch) {
state.builder.AddFunction(spv::OpAccessChain, type, pointer, variable, attribute,
component);
} else {
// An HS writes only its own control point but may read any other.
const auto vertex =
kind == Attribute::ControlOutput
kind == Attribute::ControlOutput &&
inst.GetOpcode() == IR::ValueOpcode::SetTessellationAttribute
? EmitLaneId(state)
: EmitBinaryU32(state, spv::OpUDiv, address, ConstantU32(state, stride));
state.builder.AddFunction(spv::OpAccessChain, type, pointer, variable, vertex, attribute,
@@ -60,20 +72,12 @@ uint32_t TessellationPointer(ValueEmitContext& ctx, const IR::Inst& inst) {
void DefineTessellationInterfaces(EmitterState& state) {
using Attribute = IR::TessellationAttribute;
std::array<bool, 6> used {};
uint32_t patch_begin = UINT32_MAX, patch_end = 0;
std::array<bool, IR::NumTessellationAttributes> used {};
for (const auto* block: state.program.blocks) {
for (const auto& inst: *block) {
if (inst.GetOpcode() != IR::ValueOpcode::GetTessellationAttribute &&
inst.GetOpcode() != IR::ValueOpcode::SetTessellationAttribute)
continue;
const auto kind = inst.Arg(0).U32();
used.at(kind) = true;
if (kind == static_cast<uint32_t>(Attribute::PatchOutput)) {
EXIT_NOT_IMPLEMENTED(!inst.Arg(1).IsImmediate());
patch_begin = std::min(patch_begin, inst.Arg(1).U32());
patch_end = std::max(patch_end, inst.Arg(1).U32() + 4u);
}
if (inst.GetOpcode() == IR::ValueOpcode::GetTessellationAttribute ||
inst.GetOpcode() == IR::ValueOpcode::SetTessellationAttribute)
used.at(inst.Arg(0).U32()) = true;
}
}
if (std::ranges::none_of(used, [](bool value) { return value; })) return;
@@ -83,15 +87,16 @@ void DefineTessellationInterfaces(EmitterState& state) {
};
for (uint32_t index = 0; index < used.size(); index++) {
if (!used[index]) continue;
const auto kind = static_cast<Attribute>(index);
const bool input = kind == Attribute::ControlInput || kind == Attribute::EvaluationInput;
const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput;
uint32_t type;
const auto kind = static_cast<Attribute>(index);
const bool patch = IsTessellationPatch(kind);
const auto storage =
IsTessellationInput(kind) ? spv::StorageClassInput : spv::StorageClassOutput;
uint32_t type;
if (kind == Attribute::Factor) {
type = array(TypeF32(state), 4u);
} else if (kind == Attribute::PatchOutput) {
state.tess_patch_base = patch_begin;
type = array(TypeU32Vector(state, 4), (patch_end - patch_begin + 15u) / 16u);
} else if (patch) {
// HS and TES share the reflected patch layout so their interfaces match.
type = array(TypeU32Vector(state, 4), (tess.patch_size + 15u) / 16u);
} else {
const bool local = kind == Attribute::LocalOutput || kind == Attribute::ControlInput;
const auto stride = local ? tess.ls_stride : tess.hs_stride;
@@ -112,11 +117,10 @@ void DefineTessellationInterfaces(EmitterState& state) {
state.builder.AddAnnotation(spv::OpDecorate, state.tess_inner_variable,
spv::DecorationPatch);
} else {
state.builder.AddAnnotation(
spv::OpDecorate, variable, spv::DecorationLocation,
kind == Attribute::PatchOutput ? (tess.hs_stride + 15u) / 16u : 0u);
state.builder.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation,
patch ? (tess.hs_stride + 15u) / 16u : 0u);
}
if (kind == Attribute::Factor || kind == Attribute::PatchOutput) {
if (kind == Attribute::Factor || patch) {
state.builder.AddAnnotation(spv::OpDecorate, variable, spv::DecorationPatch);
}
}
@@ -124,7 +128,9 @@ void DefineTessellationInterfaces(EmitterState& state) {
void DefineTessellationExecutionModes(EmitterState& state) {
const auto& tess = state.input_info.vertex->tess;
EXIT_NOT_IMPLEMENTED(tess.domain != 1u || tess.partitioning != 2u ||
// CxVgtTfParam: triangles, integer or fractional-odd partitioning, clockwise output.
EXIT_NOT_IMPLEMENTED(tess.domain != 1u ||
(tess.partitioning != 0u && tess.partitioning != 2u) ||
tess.output_topology != 2u);
state.builder.RequireCapability(spv::CapabilityTessellation);
if (state.program.stage == ShaderType::TessellationControl) {
@@ -132,7 +138,9 @@ void DefineTessellationExecutionModes(EmitterState& state) {
tess.output_control_points);
} else {
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeTriangles);
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeSpacingFractionalOdd);
state.builder.AddExecutionMode(
state.main_func, tess.partitioning == 0u ? spv::ExecutionModeSpacingEqual
: spv::ExecutionModeSpacingFractionalOdd);
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeVertexOrderCw);
}
}
@@ -195,6 +195,9 @@ constexpr MimgGatherInfo MIMG_GATHER_OPCODE_LIST[] = {
{0x5fu, Opcode::IMAGE_GATHER4_C_LZ_O,
ImageSampleFlagCompare | ImageSampleFlagLevelZero | ImageSampleFlagOffset},
{0x61u, Opcode::IMAGE_GATHER4H, ImageSampleFlagGatherHorizontal},
{0xc8u, Opcode::IMAGE_GATHER4_C, ImageSampleFlagCompare | ImageSampleFlagAdjust},
{0xd8u, Opcode::IMAGE_GATHER4_C_O,
ImageSampleFlagCompare | ImageSampleFlagOffset | ImageSampleFlagAdjust},
};
constexpr MimgAtomicInfo MIMG_ATOMIC_OPCODE_LIST[] = {
@@ -166,6 +166,8 @@ void ApplyMemoryInfo(Instruction& inst, const MemoryOpcodeInfo* info) {
inst.formatted = info->formatted;
}
} // namespace
bool IsDsWriteOpcode(Opcode opcode) {
switch (opcode) {
case Opcode::DS_WRITE_B8:
@@ -184,6 +186,8 @@ bool IsDsWriteOpcode(Opcode opcode) {
}
}
namespace {
bool IsDsAtomicOpcode(Opcode opcode) {
switch (opcode) {
case Opcode::DS_ADD_U32:
@@ -882,6 +882,7 @@ Program DecodeFrontProgram(std::span<const uint32_t> front);
void DecodeProgram(std::span<const uint32_t> code, Program& program);
bool IsConditionalBranch(Opcode opcode);
bool IsDirectBranch(Opcode opcode);
bool IsDsWriteOpcode(Opcode opcode);
void DecodeScalarSource(uint32_t code, uint32_t pc, Operand& operand);
void DecodeScalarDestination(uint32_t code, uint32_t pc, Operand& operand);
+12 -1
View File
@@ -125,6 +125,7 @@ struct BufferResource {
uint32_t descriptor_index = UINT32_MAX;
uint32_t first_use_pc = 0;
uint32_t max_byte_extent = 0;
uint32_t write_extent = 0; // UINT32_MAX once a write is runtime-addressed or formatted
uint32_t packed_stride = 0;
Prospero::BufferFormat descriptor_format = Prospero::BufferFormat::kInvalid;
uint32_t descriptor_swizzle = DstSel(4, 5, 6, 7);
@@ -220,8 +221,11 @@ enum class TessellationAttribute {
ControlOutput,
EvaluationInput,
PatchOutput,
PatchInput,
Factor
};
constexpr uint32_t NumTessellationAttributes =
static_cast<uint32_t>(TessellationAttribute::Factor) + 1u;
enum class StageInputKind {
VertexIndex,
@@ -356,7 +360,14 @@ struct PushData {
static constexpr uint32_t NoStart = UINT32_MAX;
std::array<uint32_t, DwordCount> dwords {};
[[nodiscard]] static constexpr uint32_t GraphicsDrawDwords(bool mesh, bool object_id) {
// User clip planes 0..N follow the mesh draw parameters or object ID, four dwords each.
static constexpr uint32_t UserClipPlaneDword = MeshDrawDwordCount;
[[nodiscard]] static constexpr uint32_t GraphicsDrawDwords(bool mesh, bool object_id,
uint32_t user_clip_planes = 0) {
if (user_clip_planes != 0) {
return UserClipPlaneDword + 4u * std::bit_width(user_clip_planes);
}
return mesh ? MeshDrawDwordCount : static_cast<uint32_t>(object_id);
}
@@ -1559,12 +1559,17 @@ private:
const Inst* IndexedMaterialRead(const Inst* read, Value active,
DescriptorSource::IndirectDescriptor::SelectorRead& selector) const {
if (read == nullptr) return nullptr;
uint32_t lane = 0;
if (read != nullptr && read->GetOpcode() == ValueOpcode::CompositeExtractU32x2) {
if (!ImmediateU32(read->Arg(1), lane) || lane >= 2u) return nullptr;
const auto op = read->GetOpcode();
if (op == ValueOpcode::CompositeExtractU32x2 || op == ValueOpcode::CompositeExtractU32x3) {
const bool x3 = op == ValueOpcode::CompositeExtractU32x3;
if (!ImmediateU32(read->Arg(1), lane) || lane >= (x3 ? 3u : 2u)) return nullptr;
read = read->Arg(0).Resolve().TryInstruction();
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32x2) return nullptr;
} else if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32) {
if (read == nullptr ||
read->GetOpcode() != (x3 ? ValueOpcode::LoadBufferU32x3 : ValueOpcode::LoadBufferU32x2))
return nullptr;
} else if (op != ValueOpcode::LoadBufferU32) {
return nullptr;
}
const auto& memory = m_program.memory_info[read->Flags<MemoryFlags>().index];
@@ -1586,8 +1591,9 @@ private:
const auto* read = SelectorReadValue(selected->Arg(1), active, selector);
if (indexed) {
read = IndexedMaterialRead(read, active, selector);
// This optional domain is checked against the dispatch's writable buffers.
// Other stages and unbounded/image writes need the original heap domain.
// Bounded heaps fall back when the material aliases a written buffer; pointer
// tables rely on the renderer's specialization-read alias check. Other stages
// and unbounded/image writes keep the original domain.
if (read == nullptr || m_program.stage != ShaderType::Compute ||
std::ranges::any_of(m_program.blocks, [](const Block* block) {
return std::ranges::any_of(*block, [](const Inst& inst) {
@@ -3062,6 +3068,7 @@ private:
}
if (indirect.key_count.IsEmpty() &&
!MatchUniformizedMaterialKey(key, handle, indirect, material_source, false) &&
!MatchUniformizedMaterialKey(key, handle, indirect, material_source, true) &&
!MatchReversedImageRange(key, indirect)) {
const auto maximum = SelectorMaximum(key);
if (maximum >= MaxIndirectDescriptorProbes) return false;
@@ -3742,10 +3749,10 @@ private:
}
}
uint32_t AddBuffer(uint32_t source, const MemoryInfo& memory, ValueOpcode op, uint32_t pc) {
uint32_t AddBuffer(uint32_t source, const MemoryInfo& memory, const Inst& inst, uint32_t pc) {
for (uint32_t i = 0; i < m_info.buffers.size(); i++) {
if (m_info.buffers[i].source == source) {
Merge(m_info.buffers[i], memory, op, pc);
Merge(m_info.buffers[i], memory, inst, pc);
return i;
}
}
@@ -3755,13 +3762,14 @@ private:
BufferResource resource;
resource.source = source;
resource.first_use_pc = pc;
Merge(resource, memory, op, pc);
Merge(resource, memory, inst, pc);
m_info.buffers.push_back(resource);
return static_cast<uint32_t>(m_info.buffers.size() - 1);
}
static void Merge(BufferResource& resource, const MemoryInfo& memory, ValueOpcode op,
static void Merge(BufferResource& resource, const MemoryInfo& memory, const Inst& inst,
uint32_t pc) {
const auto op = inst.GetOpcode();
const auto access = BufferAccessOf(op);
const bool atomic = access == BufferAccess::Atomic;
const bool write = access == BufferAccess::Write || atomic;
@@ -3769,6 +3777,13 @@ private:
resource.max_byte_extent = std::max(resource.max_byte_extent, ByteExtent(memory));
resource.read = resource.read || !write || atomic;
resource.written = resource.written || write;
if (write) {
// Formatted stores write the whole format; otherwise zero index, VGPR and SGPR offsets
// leave only the immediate offset.
const bool fixed = !memory.formatted && inst.Arg(1).Resolve() == Value(0u) &&
inst.Arg(2).Resolve() == Value(0u) && inst.Arg(3).Resolve() == Value(0u);
resource.write_extent = std::max(resource.write_extent, fixed ? ByteExtent(memory) : UINT32_MAX);
}
resource.atomic = resource.atomic || atomic;
resource.formatted = resource.formatted || memory.formatted;
resource.scalar = resource.scalar || op == ValueOpcode::ReadConstBuffer ||
@@ -3938,7 +3953,7 @@ private:
m_info.uses_dma = true;
return;
}
resource = AddBuffer(source, memory, op, flags.pc);
resource = AddBuffer(source, memory, inst, flags.pc);
if (resource == UINT32_MAX) {
Fail(flags.pc, "buffer resource limit exceeded");
}
+19 -3
View File
@@ -47,6 +47,8 @@ struct ShaderMapEntry {
static std::unique_ptr<std::unordered_map<uint64_t, ShaderMapEntry>> g_shader_map;
static std::mutex g_shader_map_mutex;
static std::unordered_map<uint64_t, ShaderTessellationInputInfo> g_tess_layouts;
static std::mutex g_tess_layouts_mutex;
void ShaderInit() {
EXIT_IF(g_shader_map != nullptr);
@@ -353,10 +355,11 @@ static void ShaderGetStaticInputInfoPS(
ps_info.ps_pixel_kill_enable = sh.db_shader_control.shader_kill_enable;
ps_info.ps_depth_export_enable = sh.db_shader_control.shader_z_export_enable;
ps_info.ps_sample_mask_export_enable = sh.db_shader_control.shader_mask_export_enable;
// Alpha-to-mask coverage must gate depth writes, so it rules out early fragment tests.
ps_info.ps_early_z =
(sh.db_shader_control.shader_z_behavior == 1 && !sh.db_shader_control.shader_kill_enable &&
!sh.db_shader_control.shader_z_export_enable &&
!sh.db_shader_control.shader_mask_export_enable);
!sh.db_shader_control.shader_mask_export_enable && !sh.AlphaToMask());
ps_info.ps_execute_on_noop = sh.db_shader_control.shader_execute_on_noop;
for (uint32_t i = 0; i < ps_info.input_num; i++) {
@@ -414,6 +417,7 @@ void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector<uint32_t
key.push_back(info.scratch_size_dwords);
key.push_back(info.lds_size_dwords);
key.push_back(info.pa_cl_vs_out_cntl);
key.push_back(info.user_clip_planes);
key.push_back(static_cast<uint32_t>(info.clip_space.enabled));
if (info.clip_space.enabled) {
for (const float value: info.clip_space.scale) {
@@ -440,7 +444,8 @@ void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector<uint32_t
if (info.tess.input_control_points != 0) {
const auto& tess = info.tess;
key.insert(key.end(), {tess.output_control_points, tess.ls_stride, tess.hs_stride,
tess.domain, tess.partitioning, tess.output_topology});
tess.patch_base, tess.patch_size, tess.domain, tess.partitioning,
tess.output_topology});
}
for (int i = 0; i < info.resources_num; i++) {
@@ -650,7 +655,18 @@ PrepareTessellationPrograms(const HW::VertexShaderInfo& regs, const HW::Context&
};
EXIT_IF(tess.input_control_points == 0 || tess.input_control_points > 32 ||
tess.output_control_points == 0 || tess.output_control_points > 32);
ShaderRecompiler::AnalyzeTessellationPrograms(params[0].code, params[1].code, tess);
// Reflection decodes both programs; the key also covers the VGT fields cached in tess.
const uint64_t key[] = {local_hash, control_hash,
uint64_t {sh.m_vgtLsHsConfig & 0xfff00u} << 32u | sh.m_vgtTfParam};
{
std::scoped_lock lock(g_tess_layouts_mutex);
auto [entry, inserted] = g_tess_layouts.try_emplace(XXH3_64bits(key, sizeof(key)), tess);
if (inserted) {
ShaderRecompiler::AnalyzeTessellationPrograms(params[0].code, params[1].code,
entry->second);
}
tess = entry->second;
}
for (auto& stage: input_info) {
stage.tess = tess;
}
+4
View File
@@ -130,6 +130,8 @@ struct ShaderTessellationInputInfo {
uint32_t output_control_points = 0;
uint32_t ls_stride = 0;
uint32_t hs_stride = 0;
uint32_t patch_base = 0;
uint32_t patch_size = 0;
uint32_t domain = 0;
uint32_t partitioning = 0;
uint32_t output_topology = 0;
@@ -152,6 +154,8 @@ struct ShaderVertexInputInfo {
// LDS_SIZE of the GS stage a non-NGG vertex shader runs on; usually zero.
uint32_t lds_size_dwords = 0;
uint32_t pa_cl_vs_out_cntl = 0;
// PA_CL_CLIP_CNTL.UCP_ENA; set only on the last pre-rasterization stage.
uint32_t user_clip_planes = 0;
ShaderClipSpaceTransform clip_space;
ShaderMeshInputInfo mesh;
ShaderTessellationInputInfo tess;
+51 -47
View File
@@ -28,9 +28,9 @@ static bool CoalescePlaceholders(uint64_t vaddr, uint64_t size) {
}
#endif
template <typename Regions, typename Source, typename Gap>
static void CopySparseRange(const Regions& regions, uint64_t vaddr, void* data, uint64_t size,
Source&& source, Gap&& gap) {
template <typename Regions, typename Region, typename Gap>
static void ForEachSparseRange(const Regions& regions, uint64_t vaddr, uint64_t size,
Region&& region, Gap&& gap) {
const auto end = vaddr + size;
auto mapping = regions.lower_bound(vaddr);
if (mapping != regions.begin()) {
@@ -40,23 +40,34 @@ static void CopySparseRange(const Regions& regions, uint64_t vaddr, void* data,
}
}
auto* destination = static_cast<uint8_t*>(data);
auto cursor = vaddr;
auto cursor = vaddr;
for (; mapping != regions.end() && mapping->second.vaddr < end; ++mapping) {
const auto& entry = mapping->second;
const auto start = std::max(vaddr, entry.vaddr);
const auto bytes = std::min(end, entry.vaddr + entry.size) - start;
if (cursor < start) {
gap(cursor, destination + (cursor - vaddr), start - cursor);
gap(cursor, start - cursor);
}
std::memcpy(destination + (start - vaddr), source(entry, start), bytes);
region(entry, start, bytes);
cursor = start + bytes;
}
if (cursor < end) {
gap(cursor, destination + (cursor - vaddr), end - cursor);
gap(cursor, end - cursor);
}
}
template <typename Regions, typename Source, typename Gap>
static void CopySparseRange(const Regions& regions, uint64_t vaddr, void* data, uint64_t size,
Source&& source, Gap&& gap) {
auto* destination = static_cast<uint8_t*>(data);
ForEachSparseRange(
regions, vaddr, size,
[&](const auto& entry, uint64_t start, uint64_t bytes) {
std::memcpy(destination + (start - vaddr), source(entry, start), bytes);
},
[&](uint64_t start, uint64_t bytes) { gap(start, destination + (start - vaddr), bytes); });
}
class GuestBackingStore {
public:
enum class FailureReason {
@@ -200,36 +211,16 @@ public:
return false;
}
// Skip unbacked gaps, mirroring sparse reads that zero-fill them.
std::lock_guard lock(m_mutex);
const auto end = vaddr + size;
auto first = m_maps.upper_bound(vaddr);
if (first == m_maps.begin()) {
return false;
}
--first;
auto current = vaddr;
for (auto mapping = first; current < end; ++mapping) {
if (mapping == m_maps.end() || current < mapping->second.vaddr ||
current >= mapping->second.vaddr + mapping->second.size) {
return false;
}
const auto& entry = mapping->second;
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
if (!RangeOk(entry.backing_offset + current - entry.vaddr, bytes)) {
return false;
}
current += bytes;
}
const auto* source = static_cast<const uint8_t*>(data);
current = vaddr;
for (auto mapping = first; current < end; ++mapping) {
const auto& entry = mapping->second;
const auto bytes = std::min(end, entry.vaddr + entry.size) - current;
const auto offset = entry.backing_offset + current - entry.vaddr;
std::memcpy(m_backing_base + offset, source + (current - vaddr), bytes);
current += bytes;
}
const auto* source = static_cast<const uint8_t*>(data);
ForEachSparseRange(
m_maps, vaddr, size,
[&](const auto& entry, uint64_t start, uint64_t bytes) {
std::memcpy(m_backing_base + (entry.backing_offset + start - entry.vaddr),
source + (start - vaddr), bytes);
},
[](uint64_t, uint64_t) {});
return true;
}
@@ -775,8 +766,14 @@ public:
});
}
// Writes the backed bytes of a range that holds no private mappings.
bool TryWriteBacking(uint64_t vaddr, const void* data, uint64_t size) {
return m_backing->TryWriteBacking(vaddr, data, size);
if (!Owns(vaddr, size)) {
return false;
}
std::shared_lock lock(m_mutex);
return !OverlapsPrivateUnlocked(vaddr, size) &&
m_backing->TryWriteBacking(vaddr, data, size);
}
bool TryReadBacking(uint64_t vaddr, void* data, uint64_t size) {
@@ -790,15 +787,8 @@ public:
}
std::shared_lock lock(m_mutex);
if constexpr (!copy_private) {
auto mapping = m_mapped.upper_bound(vaddr);
if (mapping != m_mapped.begin()) {
--mapping;
}
for (; mapping != m_mapped.end() && mapping->second.vaddr < vaddr + size; ++mapping) {
const auto& region = mapping->second;
if (region.type == MappingType::Private && region.vaddr + region.size > vaddr) {
return false;
}
if (OverlapsPrivateUnlocked(vaddr, size)) {
return false;
}
}
return m_backing->TryReadSparseBacking(
@@ -1111,6 +1101,20 @@ private:
return true;
}
[[nodiscard]] bool OverlapsPrivateUnlocked(uint64_t vaddr, uint64_t size) const {
auto mapping = m_mapped.upper_bound(vaddr);
if (mapping != m_mapped.begin()) {
--mapping;
}
for (; mapping != m_mapped.end() && mapping->second.vaddr < vaddr + size; ++mapping) {
const auto& region = mapping->second;
if (region.type == MappingType::Private && region.vaddr + region.size > vaddr) {
return true;
}
}
return false;
}
bool AddMappedUnlocked(uint64_t vaddr, uint64_t size, MappingType type) {
if (size == 0 || UINT64_MAX - vaddr < size) {
return false;
+12 -4
View File
@@ -3389,6 +3389,11 @@ int KYTY_SYSV_ABI PthreadCancel(Pthread thread) {
return KERNEL_ERROR_EINVAL;
}
// A joined thread's native handle is gone; the guest may still hold its handle.
if (thread->free) {
return KERNEL_ERROR_ESRCH;
}
int result = pthread_cancel(thread->p);
LOGF("\tthread cancel: %s, %d\n", thread->name.c_str(), result);
@@ -3492,10 +3497,8 @@ int KYTY_SYSV_ABI PthreadGetprio(Pthread thread, int* prio) {
EXIT_NOT_IMPLEMENTED(prio == nullptr);
sched_param native_param {};
int native_policy = 0;
if (pthread_getschedparam(thread->p, &native_policy, &native_param) != 0) {
return KERNEL_ERROR_EINVAL;
if (thread->free) {
return KERNEL_ERROR_ESRCH;
}
*prio = thread->attr->guest_priority;
@@ -3510,6 +3513,11 @@ int KYTY_SYSV_ABI PthreadSetprio(Pthread thread, int prio) {
return KERNEL_ERROR_ESRCH;
}
// A joined thread's native handle is gone; the guest may still hold its handle.
if (thread->free) {
return KERNEL_ERROR_ESRCH;
}
sched_param param {};
int pol = 0;
+21 -36
View File
@@ -244,8 +244,10 @@ void MainDialogPrivate::FindInterpreter() {
}
}
static QString BoolArg(bool value) {
return value ? QStringLiteral("true") : QStringLiteral("false");
static void AddFlag(QStringList& args, bool enabled, const char* name) {
if (enabled) {
args << name;
}
}
static QStringList CreateEmulatorArgs(const Configuration& info) {
@@ -273,54 +275,37 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
if (info.gpu_index >= 0) {
args << "--gpu" << QString::number(info.gpu_index);
}
if (info.fullscreen_enabled) {
args << "--fullscreen";
}
if (info.hdr_enabled) {
args << "--hdr";
}
if (info.hide_cursor_enabled) {
args << "--hide-cursor";
}
args << "--readback-linear-images" << BoolArg(info.readback_linear_images);
args << "--sync-raw-image-buffers" << BoolArg(info.sync_raw_image_buffers);
args << "--trophy-notifications" << BoolArg(info.trophy_enabled);
AddFlag(args, info.fullscreen_enabled, "--fullscreen");
AddFlag(args, info.hdr_enabled, "--hdr");
AddFlag(args, info.hide_cursor_enabled, "--hide-cursor");
AddFlag(args, info.readback_linear_images, "--readback-linear-images");
AddFlag(args, info.sync_raw_image_buffers, "--sync-raw-image-buffers");
AddFlag(args, !info.trophy_enabled, "--no-trophy-notifications");
args << "--trophy-duration" << QString::number(info.trophy_duration_seconds);
args << "--perf-overlay" << BoolArg(info.perf_overlay_enabled);
args << "--skip-notice-screen" << BoolArg(info.skip_notice_screen);
args << "--network-connected" << BoolArg(info.network_connected);
if (info.tessellation_enabled) {
args << "--tessellation";
}
AddFlag(args, info.perf_overlay_enabled, "--perf-overlay");
AddFlag(args, info.skip_notice_screen, "--skip-notice-screen");
AddFlag(args, info.network_connected, "--network-connected");
AddFlag(args, info.tessellation_enabled, "--tessellation");
args << "--vblank-frequency" << QString::number(info.vblank_frequency);
args << "--console-language" << QString::number(info.console_language);
args << "--vulkan-validation" << BoolArg(info.vulkan_validation_enabled);
args << "--shader-validation" << BoolArg(info.shader_validation_enabled);
AddFlag(args, info.vulkan_validation_enabled, "--vulkan-validation");
AddFlag(args, info.shader_validation_enabled, "--shader-validation");
args << "--shader-optimization-type" << EnumToText(info.shader_optimization_type);
args << "--shader-log-direction" << EnumToText(info.shader_log_direction);
args << "--shader-log-folder" << info.shader_log_folder;
args << "--command-buffer-dump" << BoolArg(info.command_buffer_dump_enabled);
AddFlag(args, info.command_buffer_dump_enabled, "--command-buffer-dump");
args << "--command-buffer-dump-folder" << info.command_buffer_dump_folder;
args << "--printf-direction" << EnumToText(info.printf_direction);
args << "--printf-output-file" << info.printf_output_file;
if (info.profiler_enabled) {
args << "--profile";
}
args << "--spirv-debug-printf" << "false";
if (info.amd_cpu_enabled) {
args << "--amd-cpu";
}
AddFlag(args, info.profiler_enabled, "--profile");
AddFlag(args, info.amd_cpu_enabled, "--amd-cpu");
#if defined(_WIN32)
if (info.red_zone_protection_enabled) {
args << "--redzone";
}
AddFlag(args, info.red_zone_protection_enabled, "--redzone");
#endif
for (const auto& binding: info.host_input_mapping) {
args << "--keymap" << binding;
}
if (info.renderdoc_enabled) {
args << "--rd";
}
AddFlag(args, info.renderdoc_enabled, "--rd");
QString game = info.basedir;
if (!info.elf.isEmpty() && !GameContent::IsArchive(info.basedir)) {
+10
View File
@@ -105,7 +105,17 @@ PatchesDialog::PatchesDialog(const Configuration& game, QWidget* parent)
auto* local_buttons = new QDialogButtonBox(local);
auto* reload = local_buttons->addButton(tr("Reload"), QDialogButtonBox::ActionRole);
m_apply = local_buttons->addButton(tr("Apply selection"), QDialogButtonBox::ActionRole);
auto* enable_all = local_buttons->addButton(tr("Enable all"), QDialogButtonBox::ActionRole);
auto* disable_all = local_buttons->addButton(tr("Disable all"), QDialogButtonBox::ActionRole);
local_layout->addWidget(local_buttons);
auto set_all = [this](Qt::CheckState state) {
for (int i = 0; i < m_patches->count(); ++i)
{
m_patches->item(i)->setCheckState(state);
}
};
connect(enable_all, &QPushButton::clicked, this, [set_all] { set_all(Qt::Checked); });
connect(disable_all, &QPushButton::clicked, this, [set_all] { set_all(Qt::Unchecked); });
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
layout->addWidget(buttons);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close);
+22 -1
View File
@@ -4,6 +4,7 @@
#include <QAbstractButton>
#include <QApplication>
#include <QDateTime>
#include <QDebug>
#include <QDesktopServices>
#include <QDir>
@@ -96,7 +97,27 @@ bool UpdateChecker::IsSupported() {
void UpdateChecker::ShowPreviousResult(QWidget* parent) {
const auto directory = InstallDirectory();
QFile result(QDir(directory).filePath(QStringLiteral(".kyty-update-result")));
if (!directory.isEmpty()) {
// The helper cannot delete its own executable on Windows, so its workspace is
// removed by the next launcher start.
const QDir install(directory);
for (const auto& name:
install.entryList({QStringLiteral(".kyty-update-??????")},
QDir::Dirs | QDir::Hidden | QDir::NoSymLinks | QDir::NoDotAndDotDot)) {
QDir workspace(install.filePath(name));
QDateTime newest = QFileInfo(workspace.path()).lastModified();
for (const auto& entry: workspace.entryInfoList(QDir::Files | QDir::Dirs | QDir::Hidden |
QDir::NoDotAndDotDot)) {
newest = qMax(newest, entry.lastModified());
}
// A workspace touched recently may belong to a running download or helper. Windows
// lists a file still being downloaded with a stale time, so allow a generous margin.
if (newest.secsTo(QDateTime::currentDateTime()) > 60 * 60) {
workspace.removeRecursively();
}
}
}
QFile result(QDir(directory).filePath(QStringLiteral(".kyty-update-result")));
if (directory.isEmpty() || !result.open(QIODevice::ReadOnly)) {
return;
}
-85
View File
@@ -1439,14 +1439,6 @@ int KYTY_SYSV_ABI AgcCreateInterpolantMapping2(ShaderRegister* regs, const Shade
int KYTY_SYSV_ABI AgcGetDataPacketPayloadAddress(uint32_t** addr, uint32_t* cmd, int type) {
PRINT_NAME();
static std::atomic<uint32_t> log_count {0};
if (log_count.fetch_add(1) < 64) {
LOGF("\t addr = 0x%016" PRIx64 "\n"
"\t cmd = 0x%016" PRIx64 "\n"
"\t type = %d\n",
reinterpret_cast<uint64_t>(addr), reinterpret_cast<uint64_t>(cmd), type);
}
EXIT_NOT_IMPLEMENTED(addr == nullptr);
if (type != 0) {
@@ -1463,20 +1455,10 @@ int KYTY_SYSV_ABI AgcGetDataPacketPayloadAddress(uint32_t** addr, uint32_t* cmd,
int KYTY_SYSV_ABI AgcGetDataPacketPayloadRange(MemoryRange* range, uint32_t* cmd, int type) {
PRINT_NAME();
static std::atomic<uint32_t> log_count {0};
const bool should_log = (log_count.fetch_add(1) < 64);
if (should_log) {
LOGF("\t range = 0x%016" PRIx64 "\n"
"\t cmd = 0x%016" PRIx64 "\n"
"\t type = %d\n",
reinterpret_cast<uint64_t>(range), reinterpret_cast<uint64_t>(cmd), type);
}
EXIT_NOT_IMPLEMENTED(range == nullptr);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
const auto cmd_id = cmd[0];
const auto len = KYTY_PM4_LEN(cmd_id);
if (type != 0) {
range->m_base = cmd + 2;
@@ -1491,13 +1473,6 @@ int KYTY_SYSV_ABI AgcGetDataPacketPayloadRange(MemoryRange* range, uint32_t* cmd
}
}
if (should_log) {
LOGF("\t cmd_id = 0x%08" PRIx32 ", len = %" PRIu32 "\n"
"\t base = 0x%016" PRIx64 "\n"
"\t size = 0x%016" PRIx64 "\n",
cmd_id, len, reinterpret_cast<uint64_t>(range->m_base), range->m_size);
}
return OK;
}
@@ -1746,15 +1721,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbDispatch(CommandBuffer* buf, uint32_t thread_group_
uint32_t modifier) {
PRINT_NAME();
static std::atomic<uint32_t> log_count {0};
if (log_count.fetch_add(1) < 64) {
LOGF("\t thread_group_x = %" PRIu32 "\n"
"\t thread_group_y = %" PRIu32 "\n"
"\t thread_group_z = %" PRIu32 "\n"
"\t modifier = 0x%08" PRIx32 "\n",
thread_group_x, thread_group_y, thread_group_z, modifier);
}
if (buf == nullptr) {
return nullptr;
}
@@ -1862,16 +1828,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbSetShRegisterRangeDirect(CommandBuffer* buf, uint32
const uint32_t* values, uint32_t num_values) {
PRINT_NAME();
static std::atomic<uint32_t> log_count {0};
if (log_count.fetch_add(1) < 64) {
LOGF("\t buf = 0x%016" PRIx64 "\n"
"\t offset = %" PRIx32 "\n"
"\t values = 0x%016" PRIx64 "\n"
"\t num_values = %" PRIu32 "\n",
reinterpret_cast<uint64_t>(buf), offset, reinterpret_cast<uint64_t>(values),
num_values);
}
EXIT_NOT_IMPLEMENTED(buf == nullptr);
auto* cmd = buf->AllocateDW(num_values + 2);
@@ -1902,12 +1858,6 @@ uint32_t KYTY_SYSV_ABI AgcCbSetShRegisterRangeDirectGetSize(uint32_t num_values)
uint32_t* KYTY_SYSV_ABI AgcCbSetShRegistersDirect(CommandBuffer* buf,
const volatile ShaderRegister* regs,
uint32_t num_regs) {
PRINT_NAME();
AgcTrace("\t regs = 0x%016" PRIx64 "\n"
"\t num_regs = %" PRIu32 "\n",
reinterpret_cast<uint64_t>(regs), num_regs);
if (num_regs == 0) {
return nullptr;
}
@@ -2047,23 +1997,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbReleaseMem(CommandBuffer* buf, uint8_t action, uint
uint8_t interrupt, uint32_t interrupt_ctx_id) {
PRINT_NAME();
static std::atomic<uint32_t> log_count {0};
if (log_count.fetch_add(1) < 64) {
LOGF("\t action = 0x%02" PRIx8 "\n"
"\t gcr_cntl = 0x%04" PRIx16 "\n"
"\t dst = %" PRIu8 "\n"
"\t cache_policy = 0x%02" PRIx8 "\n"
"\t address = 0x%016" PRIx64 "\n"
"\t data_sel = 0x%02" PRIx8 "\n"
"\t data = 0x%016" PRIx64 "\n"
"\t gds_offset = %" PRIu16 "\n"
"\t gds_size = %" PRIu16 "\n"
"\t interrupt = 0x%02" PRIx8 "\n"
"\t interrupt_ctx_id = %" PRIu32 "\n",
action, gcr_cntl, dst, cache_policy, reinterpret_cast<uint64_t>(address), data_sel,
data, gds_offset, gds_size, interrupt, interrupt_ctx_id);
}
EXIT_NOT_IMPLEMENTED(buf == nullptr);
EXIT_NOT_IMPLEMENTED(dst > 1);
EXIT_NOT_IMPLEMENTED(data_sel != 0 && data_sel != 1 && data_sel != 2 && data_sel != 3 &&
@@ -2950,13 +2883,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbEventWrite(CommandBuffer* buf, uint8_t event_type,
const volatile void* address) {
PRINT_NAME();
static std::atomic<uint32_t> log_count {0};
if (log_count.fetch_add(1) < 64) {
LOGF("\t event_type = 0x%02" PRIx8 "\n"
"\t address = 0x%016" PRIx64 "\n",
event_type, reinterpret_cast<uint64_t>(address));
}
EXIT_NOT_IMPLEMENTED(buf == nullptr);
EXIT_NOT_IMPLEMENTED(event_type > 0x3fu);
@@ -3011,17 +2937,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbAcquireMem(CommandBuffer* buf, uint8_t engine, uin
uint64_t size_bytes, uint32_t poll_cycles) {
PRINT_NAME();
static std::atomic<uint32_t> log_count {0};
if (log_count.fetch_add(1) < 64) {
LOGF("\t engine = 0x%02" PRIx8 "\n"
"\t cb_db_op = 0x%08" PRIx32 "\n"
"\t gcr_cntl = 0x%08" PRIx32 "\n"
"\t base = 0x%016" PRIx64 "\n"
"\t size_bytes = 0x%016" PRIx64 "\n"
"\t poll_cycles = %" PRIu32 "\n",
engine, cb_db_op, gcr_cntl, reinterpret_cast<uint64_t>(base), size_bytes, poll_cycles);
}
EXIT_NOT_IMPLEMENTED(buf == nullptr);
auto* cmd = buf->AllocateDW(8);
+19
View File
@@ -841,6 +841,12 @@ struct AudioOutPortState {
uint64_t reserved2[2];
};
struct AudioOutSystemState {
float loudness;
uint8_t reserved8[4];
uint64_t reserved64[3];
};
int KYTY_SYSV_ABI AudioOutInit() {
PRINT_NAME();
@@ -952,6 +958,19 @@ int KYTY_SYSV_ABI AudioOutGetPortState(int handle, AudioOutPortState* state) {
return OK;
}
int KYTY_SYSV_ABI AudioOutGetSystemState(AudioOutSystemState* state) {
PRINT_NAME();
if (state == nullptr) {
return AUDIO_OUT_ERROR_INVALID_POINTER;
}
// Loudness is undefined until measurement starts; no measurement is emulated.
*state = {};
return OK;
}
int KYTY_SYSV_ABI AudioOutSetVolume(int handle, uint32_t flag, int* vol) {
PRINT_NAME();
+2
View File
@@ -18,6 +18,7 @@ namespace AudioOut {
struct AudioOutOutputParam;
struct AudioOutPortState;
struct AudioOutSystemState;
int KYTY_SYSV_ABI AudioOutInit();
int KYTY_SYSV_ABI AudioOutOpen(int user_id, int type, int index, uint32_t len, uint32_t freq,
@@ -27,6 +28,7 @@ int KYTY_SYSV_ABI AudioOutOutputs(AudioOutOutputParam* param, uint32_t num);
int KYTY_SYSV_ABI AudioOutOutput(int handle, const void* ptr);
int KYTY_SYSV_ABI AudioOutClose(int handle);
int KYTY_SYSV_ABI AudioOutGetPortState(int handle, AudioOutPortState* state);
int KYTY_SYSV_ABI AudioOutGetSystemState(AudioOutSystemState* state);
} // namespace AudioOut
+1
View File
@@ -131,6 +131,7 @@ LIB_DEFINE(InitAudio_1_AudioOut) {
LIB_FUNC("QOQtbeDqsT4", AudioOut::AudioOutOutput);
LIB_FUNC("s1--uE9mBFw", AudioOut::AudioOutClose);
LIB_FUNC("GrQ9s4IrNaQ", AudioOut::AudioOutGetPortState);
LIB_FUNC("R5hemoKKID8", AudioOut::AudioOutGetSystemState);
}
} // namespace LibAudioOut
+162 -75
View File
@@ -344,7 +344,16 @@ enum class Ngs2RackType {
CustomSampler,
};
struct Ngs2UserFxContext {
struct Ngs2UserFxProcessContext {
float** channels;
std::array<uintptr_t, 3> user_data;
uint32_t flags;
uint32_t num_channels;
uint32_t grain_samples;
uint32_t sample_rate;
};
struct Ngs2UserFx2Context {
void* common;
void* param;
void* work;
@@ -354,7 +363,7 @@ struct Ngs2UserFxContext {
uint64_t reserved[4] {};
};
struct Ngs2UserFxProcessContext {
struct Ngs2UserFx2ProcessContext {
float** channels;
void* common;
const void* param;
@@ -370,18 +379,27 @@ struct Ngs2UserFxProcessContext {
uint64_t reserved2[4] {};
};
struct Ngs2UserFxOption {
struct Ngs2UserFx2Option {
Ngs2CustomModuleOption header;
int KYTY_SYSV_ABI (*setup)(Ngs2UserFxContext*);
int KYTY_SYSV_ABI (*cleanup)(Ngs2UserFxContext*);
int KYTY_SYSV_ABI (*setup)(Ngs2UserFx2Context*);
int KYTY_SYSV_ABI (*cleanup)(Ngs2UserFx2Context*);
uintptr_t control;
int KYTY_SYSV_ABI (*process)(Ngs2UserFxProcessContext*);
int KYTY_SYSV_ABI (*process)(Ngs2UserFx2ProcessContext*);
size_t common_size;
size_t param_size;
size_t work_size;
uintptr_t user_data;
};
static bool Ngs2RackIsCustom(Ngs2RackType type) {
switch (type) {
case Ngs2RackType::CustomSubmixer:
case Ngs2RackType::CustomMastering:
case Ngs2RackType::CustomSampler: return true;
default: return false;
}
}
struct Ngs2RackInternal {
Ngs2Internal* ngs = nullptr;
Ngs2RackInternal* next = nullptr;
@@ -390,7 +408,16 @@ struct Ngs2RackInternal {
Ngs2ContextBufferInfo buffer_info;
Ngs2BufferAllocator allocator;
std::array<std::vector<uint8_t>, 24> common;
std::array<Ngs2UserFxOption, 24> fx {};
std::array<Ngs2UserFx2Option, 24> fx {};
[[nodiscard]] uint32_t NumBuffers() const {
return Ngs2RackIsCustom(type) ? option.custom_sampler.custom_rack_option.num_buffers : 1;
}
[[nodiscard]] uint32_t PortBufferId(size_t port) const {
const auto& custom = option.custom_sampler.custom_rack_option;
return Ngs2RackIsCustom(type) ? custom.port[port].source_buffer_id : 0;
}
};
enum class Ngs2VoicePlayState : uint32_t { Empty = 0, Playing = 3, Paused = 5, Stopped = 0xb };
@@ -519,10 +546,12 @@ struct Ngs2VoiceInternal {
uint32_t repeated_count = 0;
};
struct Module {
std::vector<uint8_t> param, work, state;
uint32_t flags = 1;
bool control_warned = false;
bool render_warned = false;
std::vector<uint8_t> param, work, state;
void KYTY_SYSV_ABI (*handler)(Ngs2UserFxProcessContext*) = nullptr;
std::array<uintptr_t, 3> user_data {};
uint32_t flags = 1;
bool control_warned = false;
bool render_warned = false;
};
Ngs2VoicePlayState state = Ngs2VoicePlayState::Empty;
uint32_t state_flags = 0;
@@ -572,6 +601,15 @@ struct Ngs2VoiceInternal {
channels * sizeof(int16_t));
}
// Custom racks keep num_buffers consecutive channel-major buffers; other racks keep one.
void ClearSamples(uint32_t grain) {
samples.assign(size_t(grain) * channels * rack->NumBuffers(), 0.0f);
}
float* Buffer(uint32_t id, uint32_t grain) {
return samples.data() + size_t(id) * grain * channels;
}
void ResetSetupState() {
SetEvent(4);
std::ranges::fill(ports, Port {});
@@ -665,7 +703,6 @@ struct Ngs2VoiceInternal {
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) >
@@ -839,9 +876,10 @@ static Ngs2RackInternal* g_racks_list = nullptr;
static std::atomic_uint32_t g_next_ngs_uid = 1;
static Common::Mutex g_racks_mutex;
static_assert(sizeof(Ngs2UserFxContext) == 72);
static_assert(sizeof(Ngs2UserFxProcessContext) == 104);
static_assert(sizeof(Ngs2UserFxOption) == 72);
static_assert(sizeof(Ngs2UserFxProcessContext) == 48);
static_assert(sizeof(Ngs2UserFx2Context) == 72);
static_assert(sizeof(Ngs2UserFx2ProcessContext) == 104);
static_assert(sizeof(Ngs2UserFx2Option) == 72);
static_assert(sizeof(Ngs2SystemOption) == 144);
static_assert(sizeof(Ngs2SystemInfo) == 184);
static_assert(sizeof(Ngs2RackOption) == 176);
@@ -900,15 +938,6 @@ static Ngs2Internal* Ngs2FindSystem(uintptr_t system_handle) {
return nullptr;
}
static bool Ngs2RackIsCustom(Ngs2RackType type) {
switch (type) {
case Ngs2RackType::CustomSubmixer:
case Ngs2RackType::CustomMastering:
case Ngs2RackType::CustomSampler: return true;
default: return false;
}
}
int KYTY_SYSV_ABI Ngs2SystemQueryBufferSize(const Ngs2SystemOption* option,
Ngs2ContextBufferInfo* buffer_info) {
PRINT_NAME();
@@ -1188,14 +1217,23 @@ static void Ngs2InitModules(Ngs2RackInternal& rack) {
return;
}
const auto& custom = rack.option.custom_sampler.custom_rack_option;
EXIT_NOT_IMPLEMENTED(custom.num_buffers != 1);
EXIT_NOT_IMPLEMENTED(custom.num_buffers == 0 || custom.num_buffers > 4 ||
rack.option.common.max_ports > 16);
for (uint32_t p = 0; p < rack.option.common.max_ports; ++p) {
EXIT_NOT_IMPLEMENTED(custom.port[p].source_buffer_id >= custom.num_buffers);
}
auto* voices = reinterpret_cast<Ngs2VoiceInternal*>(&rack + 1);
for (uint32_t m = 0; m < custom.num_modules; ++m) {
if (custom.module[m].module_id != 0x1f) {
const auto& info = custom.module[m];
// Modules process their source buffer in place, so skipped modules pass audio through.
EXIT_NOT_IMPLEMENTED(info.source_buffer_id != info.dest_buffer_id ||
std::max(info.source_buffer_id, info.extra_buffer_id) >=
custom.num_buffers);
if (info.module_id != 0x1f) {
continue;
}
auto& fx = rack.fx[m];
fx = *reinterpret_cast<const Ngs2UserFxOption*>(custom.module[m].option);
fx = *reinterpret_cast<const Ngs2UserFx2Option*>(info.option);
rack.common[m].resize(fx.common_size);
for (uint32_t i = 0; i < rack.option.common.max_voices; ++i) {
auto& module = voices[i].modules[m];
@@ -1203,7 +1241,7 @@ static void Ngs2InitModules(Ngs2RackInternal& rack) {
module.work.resize(fx.work_size);
module.state.resize(custom.module[m].state_size);
if (fx.setup != nullptr) {
Ngs2UserFxContext context {
Ngs2UserFx2Context context {
rack.common[m].data(), module.param.data(), module.work.data(),
fx.user_data, rack.option.common.max_voices, i};
EXIT_NOT_IMPLEMENTED(fx.setup(&context) != OK);
@@ -1430,12 +1468,12 @@ int KYTY_SYSV_ABI Ngs2RackDestroy(uintptr_t rack_handle, Ngs2ContextBufferInfo*
const auto& fx = rack->fx[m];
if (fx.cleanup != nullptr) {
auto& module = voices[i].modules[m];
Ngs2UserFxContext context {rack->common[m].data(),
module.param.data(),
module.work.data(),
fx.user_data,
rack->option.common.max_voices,
i};
Ngs2UserFx2Context context {rack->common[m].data(),
module.param.data(),
module.work.data(),
fx.user_data,
rack->option.common.max_voices,
i};
EXIT_NOT_IMPLEMENTED(fx.cleanup(&context) != OK);
}
}
@@ -1729,7 +1767,7 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
}
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);
voice.ClearSamples(grain);
output = 0;
}
voice.has_samples = output != 0;
@@ -1747,7 +1785,7 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
}
EXIT_NOT_IMPLEMENTED(voice.rendering);
voice.rendering = true;
voice.samples.assign(grain * voice.channels, 0.0f);
voice.ClearSamples(grain);
voice.has_samples = false;
if (voice.state == Ngs2VoicePlayState::Playing) {
if (voice.rack->type == Ngs2RackType::Sampler ||
@@ -1770,7 +1808,8 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
}
}
for (auto* source: voices) {
for (const auto& port: source->ports) {
for (size_t p = 0; p < source->ports.size(); ++p) {
const auto& port = source->ports[p];
if (port.dest != &voice || port.volume == 0.0f) {
continue;
}
@@ -1778,7 +1817,10 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
if (!source->has_samples) {
continue;
}
EXIT_NOT_IMPLEMENTED(port.input != 0);
// Input N of a custom rack voice feeds its buffer N.
EXIT_NOT_IMPLEMENTED(port.input >= voice.rack->NumBuffers());
const auto* input = source->Buffer(source->rack->PortBufferId(p), grain);
auto* output = voice.Buffer(port.input, grain);
const auto* matrix = port.matrix < 0 ? nullptr : &source->matrices.at(port.matrix);
const size_t output_channels =
matrix == nullptr
@@ -1791,8 +1833,7 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
? (src == dst ? 1.0f : 0.0f)
: (*matrix)[dst * source->channels + src]);
for (uint32_t i = 0; i < grain; ++i) {
voice.samples[dst * grain + i] +=
source->samples[src * grain + i] * level;
output[dst * grain + i] += input[src * grain + i] * level;
}
}
}
@@ -1801,36 +1842,50 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
}
if (voice.has_samples) {
const auto& custom = voice.rack->option.custom_sampler.custom_rack_option;
const auto rate = voice.rack->ngs->option.sample_rate;
for (size_t m = 0; m < voice.modules.size(); ++m) {
if (custom.module[m].module_id != 0x1f) {
if (!voice.modules[m].render_warned) {
const auto& info = custom.module[m];
auto& module = voice.modules[m];
if (info.module_id != 0x19 && info.module_id != 0x1f) {
if (!module.render_warned) {
Log::WriteToConsoleAndLog(fmt::sprintf(
"warning: NGS2 custom module 0x%02x at index %zu is ignored during render\n",
custom.module[m].module_id, m));
voice.modules[m].render_warned = true;
info.module_id, m));
module.render_warned = true;
}
continue;
}
EXIT_NOT_IMPLEMENTED(custom.module[m].source_buffer_id != 0 ||
custom.module[m].dest_buffer_id != 0);
auto& module = voice.modules[m];
const auto& fx = voice.rack->fx[m];
std::vector<float*> channels(voice.channels);
for (uint32_t c = 0; c < voice.channels; ++c) {
channels[c] = voice.samples.data() + c * grain;
if (info.module_id == 0x19 && module.handler == nullptr) {
continue;
}
// Handlers receive the source channels followed by the extra buffer's channels.
std::vector<float*> channels(2 * size_t(voice.channels));
auto* source = voice.Buffer(info.source_buffer_id, grain);
auto* extra = voice.Buffer(info.extra_buffer_id, grain);
for (uint32_t c = 0; c < voice.channels; ++c) {
channels[c] = source + c * grain;
channels[voice.channels + c] = extra + c * grain;
}
if (info.module_id == 0x19) {
Ngs2UserFxProcessContext context {channels.data(), module.user_data,
module.flags, voice.channels,
grain, rate};
module.handler(&context);
} else {
const auto& fx = voice.rack->fx[m];
Ngs2UserFx2ProcessContext context {channels.data(),
voice.rack->common[m].data(),
module.param.data(),
module.work.data(),
module.state.data(),
fx.user_data,
module.flags,
voice.channels,
voice.channels,
grain,
rate};
EXIT_NOT_IMPLEMENTED(fx.process(&context) != OK);
}
Ngs2UserFxProcessContext context {channels.data(),
voice.rack->common[m].data(),
module.param.data(),
module.work.data(),
module.state.data(),
fx.user_data,
module.flags,
voice.channels,
voice.channels,
grain,
voice.rack->ngs->option.sample_rate};
EXIT_NOT_IMPLEMENTED(fx.process(&context) != OK);
module.flags = 0;
}
}
@@ -1867,7 +1922,9 @@ int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuff
const auto grain = ngs->option.num_grain_samples;
for (auto* voice: voices) {
Ngs2RenderVoice(*voice, voices, grain);
if (voice->rack->type != Ngs2RackType::Mastering || !voice->has_samples) {
const bool mastering = voice->rack->type == Ngs2RackType::Mastering ||
voice->rack->type == Ngs2RackType::CustomMastering;
if (!mastering || !voice->has_samples) {
continue;
}
EXIT_NOT_IMPLEMENTED(voice->output_id >= num_buffer_info);
@@ -2387,24 +2444,39 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
EXIT_NOT_IMPLEMENTED(!Ngs2RackIsCustom(voice->rack->type));
const auto index = param->id & 0x1fu;
EXIT_NOT_IMPLEMENTED(index >= voice->modules.size());
if ((param->id & 0xffffffe0u) != 0x40001f00u ||
voice->rack->option.custom_sampler.custom_rack_option.module[index].module_id !=
0x1f) {
if (!voice->modules[index].control_warned) {
const auto module_id =
voice->rack->option.custom_sampler.custom_rack_option.module[index].module_id;
auto& module = voice->modules[index];
if ((param->id & 0xffffffe0u) != (0x40000000u | module_id << 8u) ||
(module_id != 0x19 && module_id != 0x1f)) {
if (!module.control_warned) {
Log::WriteToConsoleAndLog(fmt::sprintf(
"warning: NGS2 custom control 0x%08" PRIx32 " is ignored\n",
param->id));
voice->modules[index].control_warned = true;
module.control_warned = true;
}
break;
}
struct FxParam {
if (module_id == 0x19) {
struct FxParam {
Ngs2VoiceParamHeader header;
decltype(module.handler) handler;
std::array<uintptr_t, 3> user_data;
};
EXIT_NOT_IMPLEMENTED(param->size != sizeof(FxParam));
const auto& fx = *reinterpret_cast<const FxParam*>(param);
// Enabling the handler requests a reset; later updates mark user data dirty.
module.flags |= module.handler == nullptr ? 1u : 2u;
module.handler = fx.handler;
module.user_data = fx.user_data;
break;
}
struct Fx2Param {
Ngs2VoiceParamHeader header;
const void* data;
size_t size;
};
const auto& fx = *reinterpret_cast<const FxParam*>(param);
auto& module = voice->modules[index];
const auto& fx = *reinterpret_cast<const Fx2Param*>(param);
EXIT_NOT_IMPLEMENTED(voice->rack->fx[index].control != 0 ||
fx.size != module.param.size());
std::memcpy(module.param.data(), fx.data, fx.size);
@@ -2439,7 +2511,6 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
break;
}
case 4: {
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Sampler);
for (auto& block: voice->blocks) {
if (block.info.num_repeats != 0) {
block.info.num_repeats = 0;
@@ -2483,9 +2554,25 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
voice->channels = channels[0];
break;
}
case 0x4003:
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::CustomMastering);
case 0x4003: {
// SceNgs2CustomMasteringVoiceSetupParam {numInputChannels, flags} or
// SceNgs2CustomMasteringVoiceOutputParam {outputId, reserved}.
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::CustomMastering ||
param->size != sizeof(Ngs2VoiceParamHeader) + 8u);
const auto* values = reinterpret_cast<const uint32_t*>(param + 1);
switch (param->id & 0xffffu) {
case 0:
EXIT_NOT_IMPLEMENTED(values[0] == 0 ||
values[0] > voice->rack->option.common.max_voice_channels ||
values[1] != 0);
voice->channels = values[0];
break;
case 1: voice->output_id = values[0]; break;
default:
EXIT("unsupported custom mastering control: 0x%08" PRIx32 "\n", param->id);
}
break;
}
default: EXIT("unknown rack_id: 0x%" PRIx32 "\n", rack_id);
}
+51 -147
View File
@@ -9,6 +9,7 @@
#include "emulator.h"
#include "kytyGitVersion.h"
#include <algorithm>
#include <charconv>
#include <cstdio>
#include <filesystem>
@@ -71,35 +72,30 @@ static void PrintUsage() {
::printf(" --amd-cpu Apply AMD CPU instruction patches.\n");
::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"
::printf(" --vulkan-validation Enable Vulkan validation.\n");
::printf(" --network-connected Use the host local network.\n");
::printf(" --gpu-assisted-validation Bounds-check shader accesses on the GPU.\n"
" Implies --vulkan-validation; very slow.\n");
::printf(" --shader-validation <true|false> Enable shader validation.\n");
::printf(" --shader-validation Enable shader validation.\n");
::printf(" --tessellation Draw tessellation patches; skipped by default.\n");
::printf(" --shader-optimization-type <value> None, Size, or Performance.\n");
::printf(" --shader-log-direction <value> Silent, Console, or File.\n");
::printf(" --shader-log-folder <path> Shader log output folder.\n");
::printf(" --command-buffer-dump <true|false> Enable command buffer dumps.\n");
::printf(" --command-buffer-dump Enable command buffer dumps.\n");
::printf(" --command-buffer-dump-folder <path> Command buffer dump folder.\n");
::printf(" --graphics-debug-dump <true|false> Enable graphics debug dumps.\n");
::printf(" --graphics-debug-dump Enable graphics debug dumps.\n");
::printf(" --printf-direction <value> Silent, Console, or File.\n");
::printf(" --printf-output-file <path> Guest printf output file.\n");
::printf(" --profile Enable the Tracy profiler.\n");
::printf(" --spirv-debug-printf <true|false> Enable SPIR-V debug printf.\n");
::printf(
" --readback-linear-images <true|false> Read back writable linear images on submit.\n");
::printf(
" --sync-raw-image-buffers <true|false> Synchronize raw reads of GPU images. Default: false.\n");
::printf(
" --trophy-notifications <true|false> Show trophy unlock toasts and play their sound.\n");
::printf(" --spirv-debug-printf Enable SPIR-V debug printf.\n");
::printf(" --readback-linear-images Read back writable linear images on submit.\n");
::printf(" --sync-raw-image-buffers Synchronize raw reads of GPU images.\n");
::printf(" --no-trophy-notifications Disable trophy unlock toasts and their sound.\n");
::printf(
" --trophy-duration <seconds> Trophy toast duration, 5-10. Default: 6.\n");
::printf(" --perf-overlay <true|false> Show the performance panel (F2 or Cmd+P).\n");
::printf(" --perf-overlay Show the performance panel (F2 or Cmd+P).\n");
::printf(" --playgo-hack Use the supplied PlayGo stub fallback.\n");
::printf(
" --skip-notice-screen <true|false> Skip startup logos and notices in supported games.\n"
" Default: false.\n");
::printf(" --skip-notice-screen Skip startup logos and notices in supported games.\n");
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
::printf(" --redzone Protect the guest SysV red zone.\n");
#endif
@@ -117,22 +113,6 @@ static bool NextArg(int argc, char* argv[], int& index, std::string& out) {
return true;
}
static bool ParseBool(const std::string& value, bool& out) {
if (Common::EqualNoCase(value, "true") || value == "1" || Common::EqualNoCase(value, "yes") ||
Common::EqualNoCase(value, "on")) {
out = true;
return true;
}
if (Common::EqualNoCase(value, "false") || value == "0" || Common::EqualNoCase(value, "no") ||
Common::EqualNoCase(value, "off")) {
out = false;
return true;
}
return false;
}
template <typename E>
static bool ParseEnum(const std::string& value, E& out) {
auto enum_value = magic_enum::enum_cast<E>(value.c_str());
@@ -200,6 +180,42 @@ static bool ParseUserId(const std::string& value, int32_t& out) {
return true;
}
using Config::ConfigOptions;
struct Flag {
std::string_view name;
bool ConfigOptions::* field;
bool value = true;
};
static constexpr Flag FLAGS[] = {
{"--rd", &ConfigOptions::renderdoc_enabled},
{"--fullscreen", &ConfigOptions::fullscreen_enabled},
{"--hdr", &ConfigOptions::hdr_enabled},
{"--hide-cursor", &ConfigOptions::hide_cursor_enabled},
{"--no-splash", &ConfigOptions::splash_enabled, false},
{"--vr", &ConfigOptions::vr_enabled},
{"--amd-cpu", &ConfigOptions::amd_cpu_enabled},
{"--network-connected", &ConfigOptions::network_connected},
{"--vulkan-validation", &ConfigOptions::vulkan_validation_enabled},
{"--gpu-assisted-validation", &ConfigOptions::gpu_assisted_validation_enabled},
{"--shader-validation", &ConfigOptions::shader_validation_enabled},
{"--tessellation", &ConfigOptions::tessellation_enabled},
{"--command-buffer-dump", &ConfigOptions::command_buffer_dump_enabled},
{"--graphics-debug-dump", &ConfigOptions::graphics_debug_dump_enabled},
{"--profile", &ConfigOptions::profiler_enabled},
{"--spirv-debug-printf", &ConfigOptions::spirv_debug_printf_enabled},
{"--readback-linear-images", &ConfigOptions::readback_linear_images},
{"--sync-raw-image-buffers", &ConfigOptions::sync_raw_image_buffers},
{"--no-trophy-notifications", &ConfigOptions::trophy_enabled, false},
{"--perf-overlay", &ConfigOptions::perf_overlay_enabled},
{"--playgo-hack", &ConfigOptions::playgo_hack_enabled},
{"--skip-notice-screen", &ConfigOptions::skip_notice_screen},
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
{"--redzone", &ConfigOptions::red_zone_protection_enabled},
#endif
};
static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_help) {
show_help = false;
@@ -212,63 +228,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
continue;
}
if (arg == "--rd") {
options.config.renderdoc_enabled = true;
if (const auto* flag = std::ranges::find(FLAGS, arg, &Flag::name); flag != std::end(FLAGS)) {
options.config.*flag->field = flag->value;
continue;
}
if (arg == "--fullscreen") {
options.config.fullscreen_enabled = true;
continue;
}
if (arg == "--hdr") {
options.config.hdr_enabled = true;
continue;
}
if (arg == "--hide-cursor") {
options.config.hide_cursor_enabled = true;
continue;
}
if (arg == "--no-splash") {
options.config.splash_enabled = false;
continue;
}
if (arg == "--vr") {
options.config.vr_enabled = true;
continue;
}
if (arg == "--amd-cpu") {
options.config.amd_cpu_enabled = true;
continue;
}
if (arg == "--playgo-hack") {
options.config.playgo_hack_enabled = true;
continue;
}
if (arg == "--tessellation") {
options.config.tessellation_enabled = true;
continue;
}
if (arg == "--profile") {
options.config.profiler_enabled = true;
continue;
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
if (arg == "--redzone") {
options.config.red_zone_protection_enabled = true;
continue;
}
#endif
if (!arg.starts_with("--")) {
::printf("game input must be provided with --game\n");
return false;
@@ -411,26 +375,6 @@ 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());
return false;
}
} else if (arg == "--gpu-assisted-validation") {
if (!ParseBool(value, options.config.gpu_assisted_validation_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--shader-validation") {
if (!ParseBool(value, options.config.shader_validation_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--shader-optimization-type") {
if (!ParseEnum(value, options.config.shader_optimization_type)) {
::printf("invalid shader optimization type: %s\n", value.c_str());
@@ -443,18 +387,8 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
}
} else if (arg == "--shader-log-folder") {
options.config.shader_log_folder = Common::PathFromUtf8(value);
} else if (arg == "--command-buffer-dump") {
if (!ParseBool(value, options.config.command_buffer_dump_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--command-buffer-dump-folder") {
options.config.command_buffer_dump_folder = Common::PathFromUtf8(value);
} else if (arg == "--graphics-debug-dump") {
if (!ParseBool(value, options.config.graphics_debug_dump_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--printf-direction") {
if (!ParseEnum(value, options.config.printf_direction)) {
::printf("invalid printf direction: %s\n", value.c_str());
@@ -462,36 +396,6 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
}
} else if (arg == "--printf-output-file") {
options.config.printf_output_file = Common::PathFromUtf8(value);
} else if (arg == "--spirv-debug-printf") {
if (!ParseBool(value, options.config.spirv_debug_printf_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--readback-linear-images") {
if (!ParseBool(value, options.config.readback_linear_images)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--sync-raw-image-buffers") {
if (!ParseBool(value, options.config.sync_raw_image_buffers)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--trophy-notifications") {
if (!ParseBool(value, options.config.trophy_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--skip-notice-screen") {
if (!ParseBool(value, options.config.skip_notice_screen)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--perf-overlay") {
if (!ParseBool(value, options.config.perf_overlay_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--keymap") {
const auto split = value.find('=');
if (split == std::string::npos || split == 0 || split + 1 == value.size()) {
+57 -6
View File
@@ -47,6 +47,7 @@ struct Fixture {
} else {
rack.option.custom_sampler.max_waveform_blocks = capacity;
rack.option.custom_sampler.custom_rack_option.state_size = sizeof(Ngs2CustomSamplerVoiceState);
rack.option.custom_sampler.custom_rack_option.num_buffers = 1;
}
}
@@ -703,6 +704,53 @@ void TestSubmixerSetupAndRouting() {
}
}
uint32_t g_side_chain_flags = 0;
void KYTY_SYSV_ABI MultiplyBySideChain(Ngs2UserFxProcessContext* context) {
Check(context->num_channels == 1 && context->grain_samples == 1 &&
context->user_data[0] == 7,
"user FX context did not describe the custom voice");
g_side_chain_flags = context->flags;
context->channels[0][0] *= context->channels[1][0];
}
void TestCustomSideChainUserFx() {
Fixture main(48000), side(48000);
Ngs2RackInternal custom_rack {};
auto& custom = custom_rack.option.custom_submixer.custom_rack_option;
custom_rack.ngs = &main.system;
custom_rack.type = Ngs2RackType::CustomSubmixer;
custom.num_buffers = 2;
custom.num_modules = 1;
custom.module[0] = {nullptr, 0x19, 0, 1, 0};
Ngs2VoiceInternal voice;
voice.rack = &custom_rack;
voice.ports.resize(1);
voice.modules.resize(1);
struct {
Ngs2VoiceParamHeader header {24, 0, 0x40020000};
uint32_t inputs = 1, outputs = 1, flags = 0, reserved = 0;
} setup;
struct {
Ngs2VoiceParamHeader header;
void KYTY_SYSV_ABI (*handler)(Ngs2UserFxProcessContext*);
uintptr_t user_data[3];
} fx {{40, 0, 0x40001900}, MultiplyBySideChain, {7, 0, 0}};
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&voice), &setup.header) == OK &&
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&voice), &fx.header) == OK,
"custom submixer user FX setup failed");
voice.state = Ngs2VoicePlayState::Playing;
main.voice.ports.push_back({&voice, 0, 1.0f, -1});
side.voice.ports.push_back({&voice, 1, 1.0f, -1});
const int16_t main_pcm = 8192, side_pcm = 16384;
main.Queue(&main_pcm, 1, 0);
side.Queue(&side_pcm, 1, 0);
Ngs2RenderVoice(voice, {&main.voice, &side.voice, &voice}, 1);
Check(voice.has_samples && voice.samples[0] == 0.125f && voice.samples[1] == 0.5f &&
g_side_chain_flags == 1,
"user FX did not receive the side-chain buffer after the source channels");
}
void TestPcm16RenderOutput() {
Ngs2Internal system;
system.option = Ngs2DefaultSystemOption();
@@ -1145,9 +1193,9 @@ void KYTY_SYSV_ABI RecordLoopCallback(const Ngs2VoiceCallbackInfo* info) {
}
}
void TestLoopCallbacksAndExit() {
void TestLoopCallbacksAndExit(Ngs2RackType type) {
const int16_t pcm[] = {8192, 16384, 24576};
Fixture f(48000);
Fixture f(48000, 1, type);
LoopCallbacks log;
f.voice.callback = reinterpret_cast<uintptr_t>(&RecordLoopCallback);
f.voice.callback_data = reinterpret_cast<uintptr_t>(&log);
@@ -1158,7 +1206,7 @@ void TestLoopCallbacksAndExit() {
"loop and completion callbacks lost the completed repeat count");
Check(f.voice.blocks.empty(), "finite loop did not complete");
Fixture active(48000);
Fixture active(48000, 1, type);
log.events.clear();
log.pause = true;
active.voice.callback = reinterpret_cast<uintptr_t>(&RecordLoopCallback);
@@ -1170,7 +1218,8 @@ void TestLoopCallbacksAndExit() {
Check(log.events.size() == 1 && active.voice.decoded_samples == 2 &&
active.voice.blocks.front().cursor == 0 && active.voice.samples[2] == 0,
"loop callback pause consumed another iteration");
Ngs2VoiceParamHeader exit_loop {sizeof(Ngs2VoiceParamHeader), 0, 0x10000004};
Ngs2VoiceParamHeader exit_loop {sizeof(Ngs2VoiceParamHeader), 0,
type == Ngs2RackType::Sampler ? 0x10000004u : 0x40010004u};
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&active.voice), &exit_loop);
active.voice.SetEvent(32);
active.Render(5);
@@ -1178,7 +1227,7 @@ void TestLoopCallbacksAndExit() {
active.voice.blocks.empty() && log.events.size() == 1,
"exiting infinite loop lost its final iteration or ignored callback flags");
Fixture pending(48000);
Fixture pending(48000, 1, type);
pending.Queue(pcm, 1, 1);
pending.Queue(pcm + 1, 1, 1, UINT32_MAX);
pending.Queue(pcm + 2, 1, 0);
@@ -1608,13 +1657,15 @@ int main() {
TestAtrac9Discrete();
TestPcm16RenderOutput();
TestSubmixerSetupAndRouting();
TestCustomSideChainUserFx();
TestWaveformBlockGeometry();
TestAtrac9ResampleAfterStarvation();
TestAtrac9ResampleAcrossFrames();
TestResamplingPhaseAfterStarvation();
TestSamplerReuseResetsRouting();
TestDeferredSamplerSetup();
TestLoopCallbacksAndExit();
TestLoopCallbacksAndExit(Ngs2RackType::Sampler);
TestLoopCallbacksAndExit(Ngs2RackType::CustomSampler);
TestAtrac9Looping();
TestAtrac9WaveformJoining();
TestWaveformReadAddress();
+99
View File
@@ -3421,6 +3421,104 @@ void TestIndexedMaterialImageKeys() {
}
}
void TestPointerTableIndexedMaterialKeys() {
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
// A waterfall scalarizes one lane's indexed x3 key; matching lanes sample the
// descriptor at the user-data pointer plus key * 32.
Fixture f;
auto *entry = f.block, *body = f.AddBlock(), *done = f.AddBlock();
entry->AddBranch(body);
entry->AddBranch(done);
body->AddBranch(done);
const auto lane = f.Emit(ValueOpcode::GetBuiltin,
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
const auto active = f.Emit(ValueOpcode::INotEqual32, {lane, Value(UINT32_MAX)});
const auto material = f.Buffer({f.UserData(0), f.UserData(1), f.UserData(2), f.UserData(3)});
MemoryInfo request;
request.kind = ResourceKind::Buffer;
request.idxen = true;
const auto loaded = f.Emit(ValueOpcode::LoadBufferU32x3,
{material, lane, Value(0u), Value(0u), active}, f.AddMemory(request, 0x28));
const auto selected = f.Emit(ValueOpcode::SelectU32,
{active, f.Emit(ValueOpcode::CompositeExtractU32x3, {loaded, Value(2u)}), Value(0u)});
const auto key = f.Emit(ValueOpcode::ReadLane, {selected, f.UserData(6)});
const auto guard = f.Emit(ValueOpcode::LogicalAnd,
{active, f.Emit(ValueOpcode::IEqual32, {key, selected})});
entry->condition = f.Emit(ValueOpcode::ConditionRef, {guard}, CFG::BranchCondition::ExecNonZero);
entry->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch,
.true_block = body, .false_block = done};
body->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = done};
done->terminator.kind = CFG::TerminatorKind::Return;
f.block = body;
const auto table = f.Address(f.UserData(4), f.UserData(5));
const auto offset = f.Emit(ValueOpcode::ShiftLeftLogical32, {key, Value(5u)});
std::array<Value, 8> words;
for (uint32_t word = 0; word < words.size(); ++word) {
MemoryInfo scalar;
scalar.kind = ResourceKind::ScalarAddress;
scalar.offset = word * 4u;
words[word] = f.Emit(ValueOpcode::LoadAddressU32, {table, offset, Value(0u), Value(true)},
f.AddMemory(scalar, 0x60));
}
MemoryInfo sample;
sample.kind = ResourceKind::Image;
sample.image_dimension = Decoder::ImageDimension::Dim2D;
f.Emit(ValueOpcode::ImageSampleRaw,
{f.Image(words, 0x6c), f.Sampler({Value(0u), Value(0u), Value(0u), Value(0u)}),
f.ImageAddress(), guard},
f.AddMemory(sample, 0x6c));
f.PlanAndTrack();
const auto plan = ExtractResourcePlan(f.program);
Check(plan.info.images.size() == 1u, "pointer-table waterfall image was not tracked");
const auto &indirect = *plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor;
Check(indirect.selector && indirect.selector->stride == 0u && indirect.selector->offset == 8u &&
!indirect.selector->global && !indirect.table_scalar && indirect.table_stride == 32u &&
plan.descriptor_sources[indirect.table_source].dword_count == 2u,
"pointer-table waterfall lost its indexed x3 material selector");
constexpr uint32_t base = 0x1000u, heap = 0x2000u, records = 6u;
struct Memory {
std::vector<uint32_t> words = std::vector<uint32_t>((heap - base + 16u * 32u) / 4u);
uint32_t descriptors = 0;
} memory;
const auto put = [&](uint32_t key) {
auto *texture = memory.words.data() + (heap - base + key * 32u) / 4u;
texture[0] = 0x100u + key;
texture[1] = static_cast<uint32_t>(Libs::Graphics::Prospero::BufferFormat::k16UNorm) << 20u;
texture[3] = (9u << 28u) | Libs::Graphics::DstSel(4, 5, 6, 7);
};
for (const uint32_t key : {0u, 5u, 9u}) put(key);
for (uint32_t row = 0; row < records; ++row)
memory.words[(row * 12u + 8u) / 4u] = row % 2u == 0u ? 9u : 5u;
const std::array<uint32_t, 7> user_data{base, 12u << 16u, records, 0x5204u, heap, 0u, 0u};
const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &m = *static_cast<Memory *>(data);
if (address < base || address - base + words.size_bytes() > m.words.size() * 4u) return false;
if (address >= heap) {
const auto key = (address - heap) / 32u;
if (words.size() != 8u || (key != 0u && key != 5u && key != 9u)) return false;
++m.descriptors;
}
std::copy_n(m.words.begin() + (address - base) / 4u, words.size(), words.begin());
return true;
};
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = read};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) && memory.descriptors == 3u &&
snapshot.images.size() == 3u,
"pointer-table selector did not read exactly the zero and material-key descriptors");
Check(std::ranges::find(snapshot.specialization_reads, std::pair<uint64_t, uint64_t>{base + 8u, 64u}) !=
snapshot.specialization_reads.end(),
"pointer-table selector rows escaped writable-alias tracking");
const auto mapping = specialization.images[0].indirect_mapping_offset;
Check(snapshot.images[0].dwords[0] == 0x100u && specialization.images[0].indirect_search_iterations == 2u &&
snapshot.flattened_srt[mapping] == 3u && snapshot.flattened_srt[mapping + 3u] == 5u &&
snapshot.flattened_srt[mapping + 5u] == 9u,
"pointer-table selector lost sorted raw keys or the OOB-zero root descriptor");
}
void TestImageDescriptorFields() {
constexpr std::array<std::pair<uint32_t, uint32_t>, 5> reserved{
{{1u, 0x20000000u}, {2u, 0x70003000u}, {4u, 0xe000e000u},
@@ -8701,6 +8799,7 @@ int main() {
Run("bounded compute image loop", TestBoundedComputeImageLoop);
Run("uniformized material image keys", TestUniformizedMaterialImageKeys);
Run("indexed material image keys", TestIndexedMaterialImageKeys);
Run("pointer-table indexed material keys", TestPointerTableIndexedMaterialKeys);
Run("image descriptor fields", TestImageDescriptorFields);
Run("draw-uniform scalar image", TestUniformScalarBufferImage);
Run("compact scalar image table", TestCompactScalarImageTable);
+265 -85
View File
@@ -84,6 +84,7 @@
#include <cstring>
#include <initializer_list>
#include <limits>
#include <map>
#include <memory>
#include <mutex>
#include <numeric>
@@ -27913,8 +27914,8 @@ TestCase VectorMed3F32NanUsesMin3Path(u32 wave) {
test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_MED3_F32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.ir_counts = {{"FPMedTri32", std::size(cases)}};
// Measured 17,593/36,607 words; keep classification growth below a single MED3.
test.max_spirv_words = wave == 32u ? 17600u : 36620u;
// Measured 16,140/33,705 words; keep min/max expansion growth below a single MED3.
test.max_spirv_words = wave == 32u ? 16150u : 33720u;
return test;
}
@@ -43692,6 +43693,58 @@ void CheckPs5GameExampleImageClearRuntimeShape() {
std::printf("[host] %-32s ok\n", "Ps5GameExampleImageClear");
}
struct TessellationStage {
// Immediate byte addresses of the lowered accesses, per attribute kind.
std::map<ShaderRecompiler::IR::TessellationAttribute, std::set<u32>> addresses;
std::string assembly;
};
TessellationStage CompileTessellationStage(const char *name, const std::vector<u32> &code,
ShaderType stage,
const ShaderTessellationInputInfo &tess) {
using namespace ShaderRecompiler;
ShaderVertexInputInfo vertex;
vertex.logical_stage = stage;
vertex.tess = tess;
CompileOptions options;
options.stage = stage;
options.input_info.vertex = &vertex;
auto translated = TranslateProgram(code, options);
TessellationStage result;
for (const auto *block : translated.program.blocks) {
for (const auto &inst : *block) {
if ((inst.GetOpcode() == IR::ValueOpcode::GetTessellationAttribute ||
inst.GetOpcode() == IR::ValueOpcode::SetTessellationAttribute) &&
inst.Arg(1).IsImmediate()) {
result.addresses[static_cast<IR::TessellationAttribute>(inst.Arg(0).U32())].insert(
inst.Arg(1).U32());
}
}
}
auto compiled = CompileProgram(std::move(translated), options, {});
ValidateSpirv(name, compiled.spirv);
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2);
Require(name, "disassembly", tools.Disassemble(compiled.spirv, &result.assembly),
"failed to disassemble tessellation shader");
return result;
}
// Whether a Patch uvec4[vectors] variable of a storage class sits at a Location.
bool HasPatchInterface(const std::string &assembly, const std::string &storage, u32 vectors,
u32 location) {
const auto variable = " = OpVariable %_ptr_" + storage + "__arr_v4uint_uint_" +
std::to_string(vectors) + " " + storage + "\n";
const auto end = assembly.find(variable);
if (end == std::string::npos) {
return false;
}
const auto begin = assembly.rfind('%', end);
const auto id = assembly.substr(begin, end - begin);
return assembly.find("OpDecorate " + id + " Patch\n") != std::string::npos &&
assembly.find("OpDecorate " + id + " Location " + std::to_string(location) + "\n") !=
std::string::npos;
}
void CheckTessellationProgram(const char *name, u32 ls_stride, u32 hs_stride) {
using namespace ShaderRecompiler;
const bool multiplied_stride = hs_stride == 112;
@@ -43700,8 +43753,7 @@ void CheckTessellationProgram(const char *name, u32 ls_stride, u32 hs_stride) {
const u32 relative_patch = multiplied_stride ? 28 : 32;
const u32 output_address = multiplied_stride ? 31 : 35;
const u32 input_address = multiplied_stride ? 25 : 29;
std::array<std::vector<u32>, 3> code;
auto &local = code[0];
std::vector<u32> local, control, evaluation;
// The fused LS addresses one control point using its v3 ordinal.
local.push_back(EncodeVop2(0x0b, local_address, 255u, 3));
local.push_back(ls_stride);
@@ -43710,7 +43762,6 @@ void CheckTessellationProgram(const char *name, u32 ls_stride, u32 hs_stride) {
local.push_back(EncodeSop1(0x20, 0, 6));
local.push_back(0xffffffffu); // Shader footer is not executable after LS handoff.
auto &control = code[1];
AppendVop3(&control, 0x365, 0, 193u, InlineU32(0));
AppendVop3(&control, 0x366, 0, 193u, Vgpr(0));
control.push_back(EncodeSopp(0x0a));
@@ -43769,7 +43820,6 @@ void CheckTessellationProgram(const char *name, u32 ls_stride, u32 hs_stride) {
}
AppendEnd(&control);
auto &evaluation = code[2];
AppendVop3(&evaluation, 0x169, 24, 255u, Vgpr(7));
evaluation.push_back(3 * hs_stride);
evaluation.push_back(EncodeMubuf0(0x0d, 28));
@@ -43790,91 +43840,221 @@ void CheckTessellationProgram(const char *name, u32 ls_stride, u32 hs_stride) {
tess.ls_stride == ls_stride && tess.hs_stride == hs_stride,
"captured LS and HS address arithmetic must produce distinct strides");
constexpr std::array stages{ShaderType::Local, ShaderType::TessellationControl,
ShaderType::TessellationEvaluation};
for (u32 stage = 0; stage < stages.size(); stage++) {
ShaderVertexInputInfo vertex;
vertex.logical_stage = stages[stage];
vertex.tess = tess;
CompileOptions options;
options.stage = stages[stage];
options.input_info.vertex = &vertex;
auto translated = TranslateProgram(code[stage], options);
std::set<u32> local_offsets, evaluation_offsets, factor_offsets, patch_offsets;
for (const auto *block : translated.program.blocks) {
for (const auto &inst : *block) {
if (inst.GetOpcode() != IR::ValueOpcode::GetTessellationAttribute &&
inst.GetOpcode() != IR::ValueOpcode::SetTessellationAttribute) {
continue;
}
if (!inst.Arg(1).IsImmediate()) {
continue;
}
switch (static_cast<IR::TessellationAttribute>(inst.Arg(0).U32())) {
case IR::TessellationAttribute::LocalOutput:
local_offsets.insert(inst.Arg(1).U32());
break;
case IR::TessellationAttribute::EvaluationInput:
evaluation_offsets.insert(inst.Arg(1).U32());
break;
case IR::TessellationAttribute::Factor:
factor_offsets.insert(inst.Arg(1).U32());
break;
case IR::TessellationAttribute::PatchOutput:
patch_offsets.insert(inst.Arg(1).U32());
break;
default:
break;
}
}
}
Require(name, "separate DS offsets", stage != 0 || local_offsets == std::set<u32>{4, 12},
"DS_WRITE2 must preserve both independently encoded offsets");
Require(name, "offchip component addressing",
stage != 2 ||
evaluation_offsets == std::set<u32>{28, 2 * hs_stride + 28},
"TES control point and component offsets must survive lowering");
Require(name, "triangle factor layout",
stage != 1 || factor_offsets == std::set<u32>{0, 4, 8, 12},
"the four packed guest factors must retain their order");
Require(name, "predicated patch addresses",
stage != 1 || patch_offsets == std::set<u32>{0x7fc0, 0x7fd0, 0x7fe0, 0x7ff0},
"CP0 patch stores must use the address assigned on their active predicate");
auto result = CompileProgram(std::move(translated), options, {});
ValidateSpirv(name, result.spirv);
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2);
std::string assembly;
Require(name, "disassembly", tools.Disassemble(result.spirv, &assembly),
"failed to disassemble tessellation shader");
Require(name, "native local inputs",
stage != 0 || (assembly.find("VertexIndex") != std::string::npos &&
assembly.find("InstanceIndex") != std::string::npos),
"LS v2/v5 must read native vertex/instance indices");
Require(name, "control point lane",
stage != 1 || (assembly.find("InvocationId") != std::string::npos &&
assembly.find("SubgroupLocalInvocationId") == std::string::npos),
"HS lane masks must use control point invocation IDs");
Require(name, "control point barrier",
stage != 1 || (assembly.find("OpMemoryModel Logical GLSL450") != std::string::npos &&
assembly.find("OpControlBarrier %uint_2 %uint_4 %uint_0") != std::string::npos),
"HS barriers must match GLSL450 control point synchronization semantics");
Require(name, "factor builtins",
stage != 1 || (assembly.find("TessLevelOuter") != std::string::npos &&
assembly.find("TessLevelInner") != std::string::npos),
"triangle factors require outer[4] and inner[2] Vulkan builtins");
Require(name, "native domain and winding",
stage != 2 || (assembly.find("TessCoord") != std::string::npos &&
assembly.find("Triangles") != std::string::npos &&
assembly.find("SpacingFractionalOdd") != std::string::npos &&
assembly.find("VertexOrderCw") != std::string::npos),
"TES must preserve guest triangle domain, partitioning, and winding");
}
Require(name, "patch constant range", tess.patch_base == 0x7fc0u && tess.patch_size == 52u,
"CP0 patch stores must reflect the HS patch constant range");
using Attribute = IR::TessellationAttribute;
auto ls = CompileTessellationStage(name, local, ShaderType::Local, tess);
auto hs = CompileTessellationStage(name, control, ShaderType::TessellationControl, tess);
auto ds = CompileTessellationStage(name, evaluation, ShaderType::TessellationEvaluation, tess);
Require(name, "separate DS offsets", ls.addresses[Attribute::LocalOutput] == std::set<u32>{4, 12},
"DS_WRITE2 must preserve both independently encoded offsets");
Require(name, "offchip component addressing",
ds.addresses[Attribute::EvaluationInput] == std::set<u32>{28, 2 * hs_stride + 28},
"TES control point and component offsets must survive lowering");
Require(name, "triangle factor layout",
hs.addresses[Attribute::Factor] == std::set<u32>{0, 4, 8, 12},
"the four packed guest factors must retain their order");
Require(name, "predicated patch addresses",
hs.addresses[Attribute::PatchOutput] == std::set<u32>{0x7fc0, 0x7fd0, 0x7fe0, 0x7ff0},
"CP0 patch stores must use the address assigned on their active predicate");
Require(name, "native local inputs",
ls.assembly.find("VertexIndex") != std::string::npos &&
ls.assembly.find("InstanceIndex") != std::string::npos,
"LS v2/v5 must read native vertex/instance indices");
Require(name, "control point lane",
hs.assembly.find("InvocationId") != std::string::npos &&
hs.assembly.find("SubgroupLocalInvocationId") == std::string::npos,
"HS lane masks must use control point invocation IDs");
Require(name, "control point barrier",
hs.assembly.find("OpMemoryModel Logical GLSL450") != std::string::npos &&
hs.assembly.find("OpControlBarrier %uint_2 %uint_4 %uint_0") != std::string::npos,
"HS barriers must match GLSL450 control point synchronization semantics");
Require(name, "factor builtins",
hs.assembly.find("TessLevelOuter") != std::string::npos &&
hs.assembly.find("TessLevelInner") != std::string::npos,
"triangle factors require outer[4] and inner[2] Vulkan builtins");
Require(name, "native domain and winding",
ds.assembly.find("TessCoord") != std::string::npos &&
ds.assembly.find("Triangles") != std::string::npos &&
ds.assembly.find("SpacingFractionalOdd") != std::string::npos &&
ds.assembly.find("VertexOrderCw") != std::string::npos,
"TES must preserve guest triangle domain, partitioning, and winding");
std::printf("[host] %-32s ok\n", name);
}
// PPSA25468: CP0 writes patch constants below the 32 KiB offchip ring end
// through mul24 addresses that reuse data VGPRs under nested EXEC masks, and
// the TES reads them back. As in God of War Ragnarok, the HS reads its next LS
// record, loops on a v_cmpx header and CP0 reads back another CP's outputs.
void CheckTessellationPatchConstants() {
using namespace ShaderRecompiler;
using Attribute = IR::TessellationAttribute;
constexpr const char *name = "TessellationPatchConstants";
constexpr u32 ls_stride = 48, hs_stride = 80, patch_stride = 0xf0;
std::vector<u32> local;
// One 128-bit DS store writes the LS record.
local.push_back(EncodeVop2(0x0b, 20, 255u, 3)); // v_mul_u32_u24 v20, ls_stride, v3
local.push_back(ls_stride);
local.push_back(EncodeDs0(0xdf, 16)); // ds_write_b128 v20, v[2:5] offset:16
local.push_back(EncodeDs1(0, 2, 20));
local.push_back(EncodeSop1(0x20, 0, 6));
const auto make_control = [&](bool divergent, bool carried) {
std::vector<u32> control;
AppendVop3(&control, 0x365, 0, 193u, InlineU32(0));
AppendVop3(&control, 0x366, 0, 193u, Vgpr(0));
control.push_back(EncodeVopc(0xd1, 249u, 3));
control.push_back(EncodeVopcSdwa(0, 0, 0, 6, 1, 0, 0, 0, 0, 0, 0, 0, 1));
const auto skip_control = control.size();
control.push_back(0u); // s_cbranch_execz
AppendVop3(&control, 0x148, 16, Vgpr(1), InlineU32(8), InlineU32(5));
control.push_back(EncodeVop2(0x1b, 50, 255u, 1)); // v_and_b32 v50, 0xff, v1
control.push_back(0xffu);
control.push_back(EncodeVop2(0x0b, 0, 255u, 16)); // v_mul_u32_u24 v0, hs_stride, v16
control.push_back(hs_stride);
control.push_back(EncodeVopc(0xc4, InlineU32(2), 16)); // v_cmp_gt_u32 vcc, 2, v16
control.push_back(EncodeVop2(0x25, 1, InlineU32(1), 16)); // v_add_nc_u32 v1, 1, v16
control.push_back(EncodeVop2(0x01, 1, InlineU32(0), 1)); // v_cndmask_b32 v1, 0, v1, vcc
AppendVop3(&control, 0x169, 1, Vgpr(1), 255u); // v_mul_lo_u32 v1, v1, ls_stride
control.push_back(ls_stride);
AppendVop3(&control, 0x143, 17, 255u, Vgpr(50), Vgpr(0));
control.push_back(3 * hs_stride);
AppendVop3(&control, 0x143, 14, 255u, Vgpr(50), Vgpr(1));
control.push_back(3 * ls_stride);
// ds_read2_b32 v[10:11], v14 offset0:4 offset1:5
control.push_back(EncodeDs0(0x37, (5u << 8u) | 4u));
control.push_back(EncodeDs1(10, 0, 14));
control.push_back(EncodeMubuf0(0x1d, 16));
control.push_back(EncodeMubuf1(10, 2, 17, 2));
// v17 stays affine at the forward branch's join unless the paths diverge.
control.push_back(EncodeVopc(0x01, InlineU32(0), 10)); // v_cmp_lt_f32 vcc, 0, v10
const auto skip_scale = control.size();
control.push_back(0u); // s_cbranch_vccz
control.push_back(EncodeVop1(0x01, 12, 242u)); // v_mov_b32 v12, 1.0
if (divergent) {
control.push_back(EncodeVop2(0x25, 17, InlineU32(4), 17)); // v_add_nc_u32 v17, 4, v17
}
control[skip_scale] = EncodeSopp(0x06, control.size() - skip_scale - 1u);
control.push_back(EncodeSop1(0x04, 6, 126)); // s_mov_b64 s[6:7], exec
control.push_back(EncodeSMovB32(5, InlineU32(0))); // s_mov_b32 s5, 0
const auto loop = control.size();
control.push_back(EncodeVopc(0xd1, 5, 16)); // v_cmpx_lt_u32 s5, v16
const auto exit_loop = control.size();
control.push_back(0u); // s_cbranch_execz
control.push_back(EncodeMubuf0(0x1c, 24));
control.push_back(EncodeMubuf1(12, 2, 17, 2));
// Only the back edge shows a loop-carried v17 at the store above.
if (carried) {
control.push_back(EncodeVop2(0x25, 17, InlineU32(4), 17)); // v_add_nc_u32 v17, 4, v17
}
control.push_back(EncodeVop2(0x25, 13, InlineU32(1), 13)); // v_add_nc_u32 v13, 1, v13
control.push_back(EncodeSop2(0x00, 5, 5, InlineU32(1))); // s_add_u32 s5, s5, 1
control.push_back(EncodeSopp(0x02, loop - control.size() - 1u)); // s_branch
control[exit_loop] = EncodeSopp(0x08, control.size() - exit_loop - 1u);
control.push_back(EncodeSop1(0x04, 126, 6)); // s_mov_b64 exec, s[6:7]
control.push_back(EncodeSopp(0x0a)); // s_barrier
// CP0 reuses data VGPR v10 for the ring address 0x8000 - (patch + 1) * 0xf0.
control.push_back(EncodeVopc(0xd4, InlineU32(1), 16)); // v_cmpx_gt_u32 1, v16
const auto skip_patch = control.size();
control.push_back(0u); // s_cbranch_execz
control.push_back(EncodeVop2(0x0b, 11, 255u, 50)); // v_mul_u32_u24 v11, 3 * hs_stride, v50
control.push_back(3 * hs_stride);
// buffer_load_dwordx2 v[11:12], v11, s[8:11], s2 offen offset:hs_stride + 16 (CP1)
control.push_back(EncodeMubuf0(0x0d, hs_stride + 16));
control.push_back(EncodeMubuf1(11, 2, 11, 2));
control.push_back(EncodeVop2(0x25, 10, InlineU32(1), 50)); // v_add_nc_u32 v10, 1, v50
control.push_back(EncodeVop2(0x0b, 10, 255u, 10)); // v_mul_u32_u24 v10, 0xf0, v10
control.push_back(patch_stride);
control.push_back(EncodeVop2(0x26, 10, 255u, 10)); // v_sub_nc_u32 v10, 0x8000, v10
control.push_back(0x8000u);
control.push_back(EncodeVopc(0x01, InlineU32(0), 11)); // v_cmp_lt_f32 vcc, 0, v11
control.push_back(EncodeSop1(0x24, 0, 106)); // s_and_saveexec_b64 s[0:1], vcc
const auto skip_nested = control.size();
control.push_back(0u); // s_cbranch_execz
control.push_back(EncodeMubuf0(0x1c, 0)); // buffer_store_dword v11, v10, s[8:11], s2 offen
control.push_back(EncodeMubuf1(11, 2, 10, 2));
control[skip_nested] = EncodeSopp(0x08, control.size() - skip_nested - 1u);
control.push_back(EncodeSop1(0x04, 126, 0)); // s_mov_b64 exec, s[0:1]
// buffer_store_dwordx3 v[11:13], v10, s[8:11], s2 offen offset:16
control.push_back(EncodeMubuf0(0x1f, 16));
control.push_back(EncodeMubuf1(11, 2, 10, 2));
control[skip_control] = EncodeSopp(0x08, control.size() - skip_control - 1u);
control[skip_patch] = EncodeSopp(0x08, control.size() - skip_patch - 1u);
AppendEnd(&control);
return control;
};
const auto control = make_control(false, false);
std::vector<u32> evaluation;
AppendVop3(&evaluation, 0x169, 24, 255u, Vgpr(7));
evaluation.push_back(3 * hs_stride);
evaluation.push_back(EncodeVop2(0x25, 2, InlineU32(1), 7));
evaluation.push_back(EncodeVop2(0x0b, 2, 255u, 2));
evaluation.push_back(patch_stride);
evaluation.push_back(EncodeVop2(0x26, 2, 255u, 2));
evaluation.push_back(0x8000u);
evaluation.push_back(EncodeMubuf0(0x0d, hs_stride + 16)); // CP1 dwords 4-5
evaluation.push_back(EncodeMubuf1(10, 2, 24, 4));
evaluation.push_back(EncodeMubuf0(0x0d, 16)); // patch constant dwords 4-5
evaluation.push_back(EncodeMubuf1(12, 2, 2, 4));
evaluation.push_back(EncodeVop2(0x03, 10, Vgpr(12), 10)); // v_add_f32 v10, v12, v10
evaluation.push_back(EncodeVop2(0x03, 11, Vgpr(13), 11));
evaluation.push_back(EncodeExp0(0x0c, 0xf));
evaluation.push_back(EncodeExp1(5, 6, 10, 11));
AppendEnd(&evaluation);
ShaderTessellationInputInfo tess{.input_control_points = 3,
.output_control_points = 3,
.domain = 1,
.partitioning = 0,
.output_topology = 2};
AnalyzeTessellationPrograms(local, control, tess);
Require(name, "reflected interface",
tess.ls_stride == ls_stride && tess.hs_stride == hs_stride &&
tess.patch_base == 0x7f10u && tess.patch_size == 28u,
"b128 LS records, selected LS reads, joined and looping HS paths and CP0 patch "
"stores must reflect the layout");
auto ls = CompileTessellationStage(name, local, ShaderType::Local, tess);
auto hs = CompileTessellationStage(name, control, ShaderType::TessellationControl, tess);
auto ds = CompileTessellationStage(name, evaluation, ShaderType::TessellationEvaluation, tess);
Require(name, "128-bit LS record",
ls.addresses[Attribute::LocalOutput] == std::set<u32>{16, 20, 24, 28},
"DS_WRITE_B128 must lower to four consecutive LS outputs");
Require(name, "predicated patch outputs",
hs.addresses[Attribute::PatchOutput] == std::set<u32>{0x7f10, 0x7f20, 0x7f24, 0x7f28},
"CP0 mul24 addresses under nested EXEC masks must reach patch constants");
Require(name, "control point read-back",
hs.addresses[Attribute::ControlOutput] == std::set<u32>{hs_stride + 16, hs_stride + 20} &&
hs.assembly.find("OpUDiv %uint %uint_96 %uint_80") != std::string::npos,
"CP0 must read CP1's outputs by address instead of its own invocation's");
Require(name, "patch inputs",
ds.addresses[Attribute::PatchInput] == std::set<u32>{0x7f20, 0x7f24} &&
ds.addresses[Attribute::EvaluationInput] ==
std::set<u32>{hs_stride + 16, hs_stride + 20},
"TES patch constant reads must not alias control point inputs");
Require(name, "matching patch interface",
HasPatchInterface(hs.assembly, "Output", 2, 5) &&
HasPatchInterface(ds.assembly, "Input", 2, 5),
"HS and TES patch variables must share their Location and type");
Require(name, "integer partitioning", ds.assembly.find("SpacingEqual") != std::string::npos,
"integer partitioning must map to equal spacing");
std::printf("[host] %-32s ok\n", name);
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
for (const bool carried : {false, true}) {
ExpectFatal(carried ? "TessellationLoopCarriedAddress" : "TessellationDivergentJoin", [&] {
ShaderTessellationInputInfo info{.input_control_points = 3, .output_control_points = 3};
AnalyzeTessellationPrograms(local, make_control(!carried, carried), info);
});
}
#endif
}
void CheckTessellationPrograms() {
CheckTessellationProgram("TessellationShiftedStride", 124, 128);
CheckTessellationProgram("TessellationMultipliedStride", 108, 112);
CheckTessellationPatchConstants();
}
void CheckEmbeddedFetchVertexOffset() {
+12 -9
View File
@@ -1596,20 +1596,23 @@ void TestDirectMapQueryOffsetAndPartialMunmap() {
ExpectUnmapped(test, base + SceKernelPageSize);
constexpr uint64_t transaction_sentinel = 0x5452414e53414354ull; // "TRANSACT"
constexpr uint64_t rejected_write = 0x4e4f504152544941ull; // "NOPARTIA"
constexpr uint64_t sparse_write = 0x4e4f504152544941ull; // "NOPARTIA"
constexpr uint64_t backed_prefix_mask = 0xffffffffull;
const auto crossing_address = base + SceKernelPageSize - sizeof(uint32_t);
std::memcpy(reinterpret_cast<void*>(alias_base + SceKernelPageSize - sizeof(uint32_t)),
&transaction_sentinel, sizeof(transaction_sentinel));
Check(test,
!Libs::LibKernel::Memory::TryWriteBacking(crossing_address, &rejected_write,
sizeof(rejected_write)),
"TryWriteBacking should reject a range crossing an unmapped span");
uint64_t backing_after_rejected_write = 0;
std::memcpy(&backing_after_rejected_write,
Libs::LibKernel::Memory::TryWriteBacking(crossing_address, &sparse_write,
sizeof(sparse_write)),
"TryWriteBacking should write a range crossing an unmapped span");
uint64_t backing_after_sparse_write = 0;
std::memcpy(&backing_after_sparse_write,
reinterpret_cast<const void*>(alias_base + SceKernelPageSize - sizeof(uint32_t)),
sizeof(backing_after_rejected_write));
Check(test, backing_after_rejected_write == transaction_sentinel,
"failed backing writes must not modify a validated prefix");
sizeof(backing_after_sparse_write));
Check(test,
backing_after_sparse_write == ((sparse_write & backed_prefix_mask) |
(transaction_sentinel & ~backed_prefix_mask)),
"sparse backing writes must fill the backed prefix and skip the unmapped span");
uint64_t rejected_read = transaction_sentinel;
Check(test,
!Libs::LibKernel::Memory::TryReadBacking(crossing_address, &rejected_read,
+16
View File
@@ -6214,6 +6214,10 @@ void TestNewShaderRecompilerImageGatherVariants() {
EncodeMimg1(76, 0, 1, 20), // image_gather4_c_o
EncodeMimg0(0x5f, 0x1),
EncodeMimg1(80, 0, 1, 24), // image_gather4_c_lz_o
EncodeMimg0(0xc8, 0x1),
EncodeMimg1(84, 0, 1, 28), // image_gather4_c_a
EncodeMimg0(0xd8, 0x1),
EncodeMimg1(88, 0, 1, 32), // image_gather4_c_o_a
0xbf810000u,
};
@@ -6249,6 +6253,10 @@ void TestNewShaderRecompilerImageGatherVariants() {
Check((result.decoded_dump.find("sample_flags=compare|offset|level_zero addr_components=4") != std::string::npos),
"IMAGE_GATHER4_C_LZ_O did not expose compare+offset+level-zero sample "
"metadata");
Check((result.decoded_dump.find("sample_flags=compare|adjust addr_components=3") != std::string::npos),
"PS5 IMAGE_GATHER4_C_A did not expose compare+adjust sample metadata");
Check((result.decoded_dump.find("sample_flags=compare|offset|adjust addr_components=4") != std::string::npos),
"PS5 IMAGE_GATHER4_C_O_A did not expose compare+offset+adjust sample metadata");
Check((result.ir_dump.find("ImageGather4 v60") != std::string::npos),
"IMAGE_GATHER4_LZ did not lower to shared IR ImageGather4");
Check((result.ir_dump.find("ImageGather4 v64") != std::string::npos),
@@ -6261,6 +6269,10 @@ void TestNewShaderRecompilerImageGatherVariants() {
"IMAGE_GATHER4_C_O did not lower to shared IR ImageGather4");
Check((result.ir_dump.find("ImageGather4 v80") != std::string::npos),
"IMAGE_GATHER4_C_LZ_O did not lower to shared IR ImageGather4");
Check((result.ir_dump.find("ImageGather4 v84") != std::string::npos),
"IMAGE_GATHER4_C_A did not lower to shared IR ImageGather4");
Check((result.ir_dump.find("ImageGather4 v88") != std::string::npos),
"IMAGE_GATHER4_C_O_A did not lower to shared IR ImageGather4");
Check((result.ir_dump.find("data_dwords=4") != std::string::npos),
"IMAGE_GATHER4 did not preserve four-component result metadata");
Check((result.ir_dump.find("image_flags=0x30") != std::string::npos),
@@ -6273,6 +6285,10 @@ void TestNewShaderRecompilerImageGatherVariants() {
"IMAGE_GATHER4_C_O flags did not survive into IR memory metadata");
Check((result.ir_dump.find("image_flags=0x38") != std::string::npos),
"IMAGE_GATHER4_C_LZ_O flags did not survive into IR memory metadata");
Check((result.ir_dump.find("image_flags=0x408") != std::string::npos),
"IMAGE_GATHER4_C_A flags did not survive into IR memory metadata");
Check((result.ir_dump.find("image_flags=0x418") != std::string::npos),
"IMAGE_GATHER4_C_O_A flags did not survive into IR memory metadata");
Check(SpirvContainsCapability(result.spirv, 25),
"SPIR-V binary does not request ImageGatherExtended");
Check(SpirvContainsOpcode(result.spirv, 96),