mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-28 16:23:05 +00:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d2422abbe | ||
|
|
b929bbe3e0 | ||
|
|
ae1e92621d | ||
|
|
5a491abbda | ||
|
|
afbb322b32 | ||
|
|
5601861d67 | ||
|
|
cd06601022 | ||
|
|
c23de62184 | ||
|
|
9caec9f2c8 | ||
|
|
cdb64bfd4f | ||
|
|
e5dcfdb87b | ||
|
|
984010227f | ||
|
|
57c97ebd86 |
No files matched your search
@@ -876,64 +876,15 @@ void CommandProcessor::DrawIndexOffset(uint32_t index_offset, uint32_t index_cou
|
||||
}
|
||||
|
||||
void CommandProcessor::DrawIndirect(uint32_t data_offset, uint32_t draw_initiator, bool indexed) {
|
||||
EXIT_NOT_IMPLEMENTED((draw_initiator & ~0x20u) != 2u);
|
||||
EXIT_NOT_IMPLEMENTED(m_draw_indirect_args_base_addr == 0);
|
||||
|
||||
const auto* args_addr =
|
||||
reinterpret_cast<const void*>(m_draw_indirect_args_base_addr + data_offset);
|
||||
|
||||
if (!indexed) {
|
||||
DrawIndirectArgs args {};
|
||||
std::memcpy(&args, args_addr, sizeof(args));
|
||||
m_num_instances = args.instance_count;
|
||||
DrawIndexAuto({.vertex_count = args.vertex_count_per_instance,
|
||||
.instance_count = args.instance_count,
|
||||
.first_vertex = args.start_vertex_location,
|
||||
.first_instance = args.start_instance_location,
|
||||
.offset_source = DrawOffsetSource::IndirectArgs});
|
||||
return;
|
||||
}
|
||||
|
||||
DrawIndexedIndirectArgs args {};
|
||||
std::memcpy(&args, args_addr, sizeof(args));
|
||||
|
||||
uint64_t index_size = 0;
|
||||
switch (m_index_type_and_size) {
|
||||
case 0: index_size = 2; break;
|
||||
case 1: index_size = 4; break;
|
||||
case 2: index_size = 1; break;
|
||||
default: EXIT("unknown index_type_and_size: %u\n", m_index_type_and_size);
|
||||
}
|
||||
|
||||
auto* index_addr = reinterpret_cast<const void*>(
|
||||
m_index_base_addr + static_cast<uint64_t>(args.start_index_location) * index_size);
|
||||
|
||||
const uint32_t index_count =
|
||||
(m_index_buffer_size != 0 ? std::min(args.index_count_per_instance, m_index_buffer_size)
|
||||
: args.index_count_per_instance);
|
||||
if (GraphicsRunDebugDumpEnabled() && index_count != args.index_count_per_instance) {
|
||||
static std::atomic<uint32_t> log_count {0};
|
||||
if (log_count.fetch_add(1, std::memory_order_relaxed) < 64) {
|
||||
LOGF("\t DrawIndexIndirect: clamped index_count from %" PRIu32 " to %" PRIu32
|
||||
" using INDEX_BUFFER_SIZE\n",
|
||||
args.index_count_per_instance, index_count);
|
||||
}
|
||||
}
|
||||
|
||||
m_num_instances = args.instance_count;
|
||||
DrawIndex({.index_count = index_count,
|
||||
.index_addr = index_addr,
|
||||
.instance_count = args.instance_count,
|
||||
.base_vertex = static_cast<int32_t>(args.base_vertex_location),
|
||||
.first_instance = args.start_instance_location,
|
||||
.offset_source = DrawOffsetSource::IndirectArgs});
|
||||
const auto args_size = indexed ? sizeof(DrawIndexedIndirectArgs) : sizeof(DrawIndirectArgs);
|
||||
DrawIndirectMulti(data_offset, 1, nullptr, args_size, draw_initiator, indexed);
|
||||
}
|
||||
|
||||
void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_count_or_count,
|
||||
const volatile uint32_t* count_addr,
|
||||
uint32_t stride_in_bytes, uint32_t draw_initiator,
|
||||
bool indexed) {
|
||||
EXIT_NOT_IMPLEMENTED((draw_initiator & ~0x20u) != 2u);
|
||||
EXIT_NOT_IMPLEMENTED((draw_initiator & ~0x20u) != (indexed ? 0u : 2u));
|
||||
EXIT_NOT_IMPLEMENTED(m_draw_indirect_args_base_addr == 0);
|
||||
|
||||
uint32_t draw_count = max_count_or_count;
|
||||
|
||||
@@ -315,8 +315,6 @@ void RenderExecutor::ResolveRenderDepthTarget(CommandBuffer& buffer, RenderDepth
|
||||
r.depth_compare_op = static_cast<vk::CompareOp>(dc.zfunc);
|
||||
|
||||
r.depth_bounds_test_enable = dc.depth_bounds_enable;
|
||||
r.depth_min_bounds = hw.GetDepthBoundsMin();
|
||||
r.depth_max_bounds = hw.GetDepthBoundsMax();
|
||||
|
||||
r.stencil_clear_enable =
|
||||
has_stencil && rc.stencil_clear_enable && !z.depth_view.stencil_write_disable;
|
||||
|
||||
@@ -29,8 +29,6 @@ struct RenderDepthInfo {
|
||||
bool depth_write_enable = false;
|
||||
vk::CompareOp depth_compare_op = vk::CompareOp::eNever;
|
||||
bool depth_bounds_test_enable = false;
|
||||
float depth_min_bounds = 0.0f;
|
||||
float depth_max_bounds = 0.0f;
|
||||
bool stencil_clear_enable = false;
|
||||
uint8_t stencil_clear_value = 0;
|
||||
bool stencil_test_enable = false;
|
||||
|
||||
@@ -390,7 +390,28 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
|
||||
const auto encoded = static_cast<float>(value & 0xffu) / 255.0f;
|
||||
return encoded <= 0.04045f ? encoded / 12.92f : std::pow((encoded + 0.055f) / 1.055f, 2.4f);
|
||||
};
|
||||
const auto float16 = [](uint32_t value) {
|
||||
const auto sign = (value & 0x8000u) << 16u;
|
||||
const auto exponent = (value >> 10u) & 0x1fu;
|
||||
const auto mantissa = value & 0x3ffu;
|
||||
if (exponent == 0) {
|
||||
return std::copysign(static_cast<float>(mantissa) * 0x1p-24f,
|
||||
std::bit_cast<float>(sign));
|
||||
}
|
||||
return std::bit_cast<float>(sign | ((exponent == 31 ? 255u : exponent + 112u) << 23u) |
|
||||
(mantissa << 13u));
|
||||
};
|
||||
switch (format) {
|
||||
case vk::Format::eR16G16Sfloat:
|
||||
next.float32[1] = float16(packed >> 16u);
|
||||
[[fallthrough]];
|
||||
case vk::Format::eR16Sfloat: next.float32[0] = float16(packed); break;
|
||||
case vk::Format::eR16G16Unorm:
|
||||
next.float32[1] = static_cast<float>(packed >> 16u) / 65535.0f;
|
||||
[[fallthrough]];
|
||||
case vk::Format::eR16Unorm:
|
||||
next.float32[0] = static_cast<float>(packed & 0xffffu) / 65535.0f;
|
||||
break;
|
||||
// A single-plane float target carries its clear as raw float bits, the same encoding the
|
||||
// depth decoder below uses. Without this the clear is discarded and the target keeps stale
|
||||
// contents.
|
||||
@@ -443,6 +464,10 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
|
||||
// Keep this separate: the first register word alone cannot describe a 64-bit color.
|
||||
[[nodiscard]] inline bool DecodeColorDwordFill(vk::Format format, uint32_t packed,
|
||||
vk::ClearColorValue& clear) {
|
||||
if ((format == vk::Format::eR16Sfloat || format == vk::Format::eR16Unorm) &&
|
||||
(packed & 0xffffu) != (packed >> 16u)) {
|
||||
return false;
|
||||
}
|
||||
if (format == vk::Format::eR16G16B16A16Sfloat) {
|
||||
if (packed != 0) {
|
||||
return false;
|
||||
|
||||
@@ -807,30 +807,52 @@ void RenderExecutor::PrepareBindings(const ShaderStageRuntime& runtime,
|
||||
}
|
||||
}
|
||||
|
||||
void RenderExecutor::FindBuffers(PreparedBindings& prepared) {
|
||||
void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
|
||||
KYTY_PROFILER_FUNCTION();
|
||||
EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime);
|
||||
const auto& program = *prepared.runtime->program;
|
||||
const auto& snapshot = *prepared.runtime->resources;
|
||||
auto& cache = m_context.GetBufferCache();
|
||||
|
||||
prepared.buffer_sources.clear();
|
||||
const auto& layout = program.bindings;
|
||||
if (layout.memory_offset_count == 0) {
|
||||
return;
|
||||
}
|
||||
const auto& resources = layout.descriptors.front().resources;
|
||||
prepared.buffer_sources.reserve(resources.size());
|
||||
for (const auto resource: resources) {
|
||||
auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(snapshot.buffers[resource]);
|
||||
const auto address = descriptor.Base48();
|
||||
const auto requested_size = descriptor.GetSize();
|
||||
if (address == 0 || requested_size == 0) {
|
||||
prepared.buffer_sources.push_back({});
|
||||
auto& cache = m_context.GetBufferCache();
|
||||
for (auto* stage: stages) {
|
||||
auto& prepared = *stage;
|
||||
EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime);
|
||||
const auto& program = *prepared.runtime->program;
|
||||
const auto& snapshot = *prepared.runtime->resources;
|
||||
prepared.buffer_sources.clear();
|
||||
const auto& layout = program.bindings;
|
||||
if (layout.memory_offset_count == 0) {
|
||||
continue;
|
||||
}
|
||||
const auto size = Libs::LibKernel::Memory::ClampRangeSize(address, requested_size);
|
||||
prepared.buffer_sources.push_back({address, size, cache.FindBuffer(address, size)});
|
||||
const auto& resources = layout.descriptors.front().resources;
|
||||
prepared.buffer_sources.reserve(resources.size());
|
||||
for (const auto resource: resources) {
|
||||
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(snapshot.buffers[resource]);
|
||||
const auto address = descriptor.Base48();
|
||||
auto size = descriptor.GetSize();
|
||||
if (address == 0 || size == 0) {
|
||||
prepared.buffer_sources.push_back({});
|
||||
continue;
|
||||
}
|
||||
if (descriptor.NumRecords() == UINT32_MAX) {
|
||||
if (program.info.buffers[resource].written) {
|
||||
// Sentinel write ranges end before tables captured by any bound stage.
|
||||
for (const auto* reader: stages) {
|
||||
for (const auto [read_address, read_size]: reader->runtime->resources->specialization_reads) {
|
||||
if (read_size == 0) continue;
|
||||
if (read_address > address) {
|
||||
size = std::min(size, read_address - address);
|
||||
} else if (address - read_address < read_size) {
|
||||
EXIT("scalar resource reads overlap a shader buffer write\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const auto& graphics = m_context.GetGraphics();
|
||||
const auto limit = uint64_t {graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange} -
|
||||
(graphics.StorageMinAlignment() - 1);
|
||||
size = std::min({size, uint64_t {256} * 1024 * 1024, limit});
|
||||
}
|
||||
}
|
||||
size = Libs::LibKernel::Memory::ClampRangeSize(address, size);
|
||||
prepared.buffer_sources.push_back({address, size, cache.FindBuffer(address, size)});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -919,8 +941,8 @@ void RenderExecutor::RebindImages(PreparedBindings& prepared) {
|
||||
void RenderExecutor::PrepareGraphicsBindings(std::span<PreparedBindings* const> stages,
|
||||
std::span<RenderColorInfo> colors) {
|
||||
bool uses_dma = false;
|
||||
FindBuffers(stages);
|
||||
for (auto* stage: stages) {
|
||||
FindBuffers(*stage);
|
||||
uses_dma |= stage->runtime->program->info.uses_dma;
|
||||
}
|
||||
if (uses_dma) {
|
||||
|
||||
@@ -831,9 +831,6 @@ 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.depth_bounds_test_enable = depth.depth_bounds_test_enable;
|
||||
static_params.depth_min_bounds = depth.depth_min_bounds;
|
||||
static_params.depth_max_bounds = depth.depth_max_bounds;
|
||||
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;
|
||||
|
||||
@@ -38,9 +38,6 @@ struct PipelineStaticParameters {
|
||||
bool primitive_restart_enable = false;
|
||||
uint32_t samples = 1;
|
||||
bool sample_shading_enable = false;
|
||||
bool depth_bounds_test_enable = false;
|
||||
float depth_min_bounds = 0.0f;
|
||||
float depth_max_bounds = 0.0f;
|
||||
uint32_t color_mask[RENDER_COLOR_ATTACHMENTS_MAX] = {};
|
||||
bool cull_front = false;
|
||||
bool cull_back = false;
|
||||
@@ -64,7 +61,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) == 125);
|
||||
static_assert(sizeof(PipelineStaticParameters) == 116);
|
||||
|
||||
struct PipelineRenderingState {
|
||||
std::array<vk::Format, RENDER_COLOR_ATTACHMENTS_MAX> color_formats {};
|
||||
|
||||
@@ -471,14 +471,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
EXIT_NOT_IMPLEMENTED(pipeline.pipeline_layout == nullptr);
|
||||
|
||||
vk::PipelineDepthStencilStateCreateInfo depth_stencil_info {};
|
||||
depth_stencil_info.depthBoundsTestEnable =
|
||||
#if defined(__APPLE__)
|
||||
VK_FALSE; // MoltenVK lacks the depthBounds feature; depth-bounds testing is disabled
|
||||
#else
|
||||
(static_params.depth_bounds_test_enable ? VK_TRUE : VK_FALSE);
|
||||
#endif
|
||||
depth_stencil_info.minDepthBounds = static_params.depth_min_bounds;
|
||||
depth_stencil_info.maxDepthBounds = static_params.depth_max_bounds;
|
||||
|
||||
std::vector<vk::DynamicState> dynamic_states {
|
||||
vk::DynamicState::eViewportWithCount,
|
||||
@@ -497,6 +489,8 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
vk::DynamicState::eBlendConstants,
|
||||
};
|
||||
#if !defined(__APPLE__)
|
||||
dynamic_states.push_back(vk::DynamicState::eDepthBoundsTestEnable);
|
||||
dynamic_states.push_back(vk::DynamicState::eDepthBounds);
|
||||
if (rendering.color_count != 0) {
|
||||
dynamic_states.push_back(vk::DynamicState::eColorWriteEnableEXT);
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ public:
|
||||
uint32_t mode);
|
||||
|
||||
void PrepareBindings(const ShaderStageRuntime& runtime, PreparedBindings& prepared);
|
||||
void FindBuffers(PreparedBindings& bindings);
|
||||
void FindBuffers(std::span<PreparedBindings* const> stages);
|
||||
void RebindBuffers(PreparedBindings& bindings);
|
||||
void RebindImages(PreparedBindings& bindings);
|
||||
void CommitBindings(CommandBuffer& buffer, vk::PipelineBindPoint pipeline_bind_point,
|
||||
|
||||
@@ -342,7 +342,8 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
std::copy(std::begin(input_info.dispatch_threads_num), std::end(input_info.dispatch_threads_num),
|
||||
bindings.shader_data.begin() + program.bindings.dispatch_thread_dword);
|
||||
}
|
||||
FindBuffers(bindings);
|
||||
PreparedBindings* descriptor_stage = &bindings;
|
||||
FindBuffers(std::span {&descriptor_stage, 1u});
|
||||
if (program.info.uses_dma) {
|
||||
m_context.PrepareBda();
|
||||
}
|
||||
@@ -351,7 +352,6 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
RebindBuffers(bindings);
|
||||
|
||||
auto vk_buffer = buffer.Handle();
|
||||
PreparedBindings* descriptor_stage = &bindings;
|
||||
CommitBindings(buffer, vk::PipelineBindPoint::eCompute, pipeline,
|
||||
std::span {&descriptor_stage, 1u});
|
||||
bool has_storage_writes = bindings.shared_memory.buffer != nullptr ||
|
||||
@@ -457,7 +457,8 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
auto& pipeline = m_context.GetPipelineCache().GetComputePipeline(input_info, compute_program);
|
||||
auto& bindings = m_compute_bindings;
|
||||
PrepareBindings(input_info.stage, bindings);
|
||||
FindBuffers(bindings);
|
||||
PreparedBindings* descriptor_stage = &bindings;
|
||||
FindBuffers(std::span {&descriptor_stage, 1u});
|
||||
const auto& program = *input_info.stage.program;
|
||||
if (program.info.uses_dma) {
|
||||
m_context.PrepareBda();
|
||||
@@ -469,7 +470,6 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
args_addr, sizeof(vk::DispatchIndirectCommand), false);
|
||||
EXIT_IF(args_buffer == nullptr || (args_offset & 3u) != 0);
|
||||
RebindBuffers(bindings);
|
||||
PreparedBindings* descriptor_stage = &bindings;
|
||||
CommitBindings(buffer, vk::PipelineBindPoint::eCompute, pipeline,
|
||||
std::span {&descriptor_stage, 1u});
|
||||
const auto vk_buffer = buffer.Handle();
|
||||
|
||||
@@ -379,6 +379,12 @@ static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuf
|
||||
vk_buffer.setDepthTestEnable(depth.depth_test_enable ? VK_TRUE : VK_FALSE);
|
||||
vk_buffer.setDepthWriteEnable(depth.depth_write_enable ? VK_TRUE : VK_FALSE);
|
||||
vk_buffer.setDepthCompareOp(depth.depth_compare_op);
|
||||
#if !defined(__APPLE__)
|
||||
vk_buffer.setDepthBoundsTestEnable(depth.depth_bounds_test_enable ? VK_TRUE : VK_FALSE);
|
||||
if (depth.depth_bounds_test_enable) {
|
||||
vk_buffer.setDepthBounds(ctx.GetDepthBoundsMin(), ctx.GetDepthBoundsMax());
|
||||
}
|
||||
#endif
|
||||
|
||||
const auto& mode = ctx.GetModeControl();
|
||||
const auto& poly_offset = ctx.GetPolyOffset();
|
||||
|
||||
@@ -205,7 +205,8 @@ Emitter::SpirvRequirements Emitter::AnalyzeProgramRequirements(const IR::Program
|
||||
if (memory_index >= program.memory_info.size()) {
|
||||
Fail(program, "address operation has invalid memory metadata");
|
||||
}
|
||||
const auto kind = program.memory_info[memory_index].kind;
|
||||
const auto& memory = program.memory_info[memory_index];
|
||||
const auto kind = memory.kind;
|
||||
if (kind == IR::ResourceKind::Scratch || kind == IR::ResourceKind::FlatLocal) {
|
||||
if (program.scratch_dwords == 0) {
|
||||
Fail(program, "scratch operation has no per-thread storage");
|
||||
@@ -217,6 +218,9 @@ Emitter::SpirvRequirements Emitter::AnalyzeProgramRequirements(const IR::Program
|
||||
}
|
||||
} else if (address_access == IR::AddressAccess::Write) {
|
||||
Fail(program, "writable FLAT/GLOBAL addresses require GPU ownership tracking");
|
||||
} else {
|
||||
// Descriptor stores can alias coherent physical-address loads.
|
||||
requirements.coherent_buffers |= memory.coherent;
|
||||
}
|
||||
}
|
||||
if (IR::BufferAccessOf(inst.GetOpcode()) != IR::BufferAccess::None) {
|
||||
|
||||
@@ -849,74 +849,76 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
return;
|
||||
}
|
||||
const auto key = ctx.Def(handle->Arg(0));
|
||||
if (state.flattened_srt_variable == 0 || image.indirect_search_iterations == 0u ||
|
||||
if (state.flattened_srt_variable == 0 ||
|
||||
image.indirect_resources.size() < 2u) {
|
||||
ctx.Fail(inst, "has no indirect image runtime mapping");
|
||||
return;
|
||||
}
|
||||
const auto selected = EmitIndirectResourceIndex(
|
||||
state, key, image.indirect_mapping_offset, image.indirect_search_iterations, 0u);
|
||||
const auto kind = *IR::DescriptorBindingForImage(image);
|
||||
const auto& binding = *IR::FindBinding(state.program.bindings, kind);
|
||||
uint32_t first_child = 0;
|
||||
bool homogeneous = true;
|
||||
struct SampleRun {
|
||||
uint32_t first;
|
||||
uint32_t count;
|
||||
uint32_t resource;
|
||||
uint32_t slot_bias;
|
||||
};
|
||||
std::vector<SampleRun> runs;
|
||||
for (uint32_t ordinal = 0; ordinal < image.indirect_resources.size(); ++ordinal) {
|
||||
const auto resource = image.indirect_resources[ordinal];
|
||||
const auto& candidate = state.program.info.images[resource];
|
||||
if (candidate.dimension != image.dimension || candidate.cube != image.cube ||
|
||||
IR::DescriptorBindingForImage(candidate) != kind ||
|
||||
candidate.mip_count != 1u) {
|
||||
homogeneous = false;
|
||||
break;
|
||||
const auto kind = *IR::DescriptorBindingForImage(candidate);
|
||||
const auto& binding = *IR::FindBinding(state.program.bindings, kind);
|
||||
if (!runs.empty()) {
|
||||
auto& run = runs.back();
|
||||
const auto& first = state.program.info.images[run.resource];
|
||||
const auto next_slot = run.slot_bias + ordinal;
|
||||
if (candidate.dimension == first.dimension && candidate.cube == first.cube &&
|
||||
IR::DescriptorBindingForImage(first) == kind &&
|
||||
candidate.mip_count == 1u && first.mip_count == 1u &&
|
||||
(ordinal == 1u || (next_slot < binding.resources.size() &&
|
||||
binding.resources[next_slot] == resource))) {
|
||||
// Native roots precede appended children; only the root may have a slot gap.
|
||||
if (ordinal == 1u)
|
||||
run.slot_bias = ResourceForDescriptor(state, kind, resource) - ordinal;
|
||||
++run.count;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (ordinal == 1u) first_child = ResourceForDescriptor(state, kind, resource);
|
||||
const auto child_slot = first_child + ordinal - 1u;
|
||||
if (ordinal > 1u && (child_slot >= binding.resources.size() ||
|
||||
binding.resources[child_slot] != resource)) {
|
||||
homogeneous = false;
|
||||
break;
|
||||
runs.push_back({ordinal, 1u, resource,
|
||||
ResourceForDescriptor(state, kind, resource) - ordinal});
|
||||
}
|
||||
const auto EmitRun = [&](uint32_t index) {
|
||||
const auto& run = runs[index];
|
||||
if (run.count == 1u) return EmitSample(run.resource);
|
||||
auto slot = Binary(state, spv::OpIAdd, TypeU32(state), selected,
|
||||
ConstantU32(state, run.slot_bias));
|
||||
if (run.first == 0u) {
|
||||
const auto kind = *IR::DescriptorBindingForImage(image);
|
||||
const auto root_slot = ResourceForDescriptor(state, kind, mem.resource);
|
||||
if (root_slot != run.slot_bias) {
|
||||
const auto is_root = Binary(state, spv::OpIEqual, TypeBool(state), selected,
|
||||
ConstantU32(state, 0u));
|
||||
const auto root_or_child = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpSelect, TypeU32(state), root_or_child,
|
||||
is_root, ConstantU32(state, root_slot), slot);
|
||||
slot = root_or_child;
|
||||
}
|
||||
}
|
||||
return EmitSample(run.resource, slot);
|
||||
};
|
||||
auto run_index = ConstantU32(state, 0u);
|
||||
for (uint32_t index = 1; index < runs.size(); ++index) {
|
||||
const auto at_or_after = Binary(state, spv::OpUGreaterThanEqual, TypeBool(state),
|
||||
selected, ConstantU32(state, runs[index].first));
|
||||
const auto next = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpSelect, TypeU32(state), next, at_or_after,
|
||||
ConstantU32(state, index), run_index);
|
||||
run_index = next;
|
||||
}
|
||||
if (homogeneous) {
|
||||
const auto child_index = Binary(state, spv::OpIAdd, TypeU32(state), selected,
|
||||
ConstantU32(state, first_child - 1u));
|
||||
const auto is_root = Binary(state, spv::OpIEqual, TypeBool(state), selected,
|
||||
ConstantU32(state, 0u));
|
||||
const auto array_index = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpSelect, TypeU32(state), array_index, is_root,
|
||||
ConstantU32(state, ResourceForDescriptor(state, kind, mem.resource)),
|
||||
child_index);
|
||||
const auto sample = EmitSample(mem.resource, array_index);
|
||||
const auto result = dref ? sample : UnpackImageTexel(ctx, mem, sample);
|
||||
ctx.Define(inst, ResultVector(ctx, result, numeric_class, dref, mem));
|
||||
return;
|
||||
}
|
||||
const auto default_label = state.builder.AllocateId();
|
||||
const auto merge_label = state.builder.AllocateId();
|
||||
std::vector<uint32_t> labels(image.indirect_resources.size() - 1u);
|
||||
std::vector<uint32_t> switch_words {spv::OpSwitch, selected, default_label};
|
||||
for (uint32_t candidate = 1; candidate < image.indirect_resources.size(); candidate++) {
|
||||
labels[candidate - 1u] = state.builder.AllocateId();
|
||||
switch_words.push_back(candidate);
|
||||
switch_words.push_back(labels[candidate - 1u]);
|
||||
}
|
||||
state.builder.AddFunction(spv::OpSelectionMerge, merge_label,
|
||||
spv::SelectionControlMaskNone);
|
||||
state.builder.AddFunction(switch_words);
|
||||
std::vector<uint32_t> phi_words {spv::OpPhi, result_type, state.builder.AllocateId()};
|
||||
EmitLabel(state, default_label);
|
||||
phi_words.push_back(EmitSample(image.indirect_resources[0]));
|
||||
phi_words.push_back(default_label);
|
||||
state.builder.AddFunction(spv::OpBranch, merge_label);
|
||||
for (uint32_t candidate = 1; candidate < image.indirect_resources.size(); candidate++) {
|
||||
EmitLabel(state, labels[candidate - 1u]);
|
||||
phi_words.push_back(EmitSample(image.indirect_resources[candidate]));
|
||||
phi_words.push_back(labels[candidate - 1u]);
|
||||
state.builder.AddFunction(spv::OpBranch, merge_label);
|
||||
}
|
||||
EmitLabel(state, merge_label);
|
||||
state.builder.AddFunction(phi_words);
|
||||
auto result = phi_words[2];
|
||||
auto result = runs.size() == 1u
|
||||
? EmitRun(0u)
|
||||
: EmitIndexSwitch(state, run_index, static_cast<uint32_t>(runs.size()),
|
||||
result_type, EmitRun);
|
||||
if (!dref) {
|
||||
result = UnpackImageTexel(ctx, mem, result);
|
||||
}
|
||||
|
||||
@@ -180,29 +180,46 @@ void RecordBdaFault(EmitterState& state, uint32_t page) {
|
||||
Binary(state, spv::OpBitwiseOr, TypeU32(state), value, bit));
|
||||
}
|
||||
|
||||
uint32_t LoadBdaDword(ValueEmitContext& ctx, uint32_t address) {
|
||||
uint32_t LoadBdaDword(ValueEmitContext& ctx, uint32_t address, bool coherent = false) {
|
||||
auto& state = ctx.state;
|
||||
const auto bda = GetBdaPointer(state, address);
|
||||
const auto present =
|
||||
Binary(state, spv::OpINotEqual, TypeBool(state), bda, ConstantU64(state, 0));
|
||||
return EmitValueOrZeroIfCondition(state, present, [&]() {
|
||||
const auto pointer = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer,
|
||||
bda);
|
||||
auto pointer = state.builder.AllocateId();
|
||||
if (coherent) {
|
||||
const auto block = state.builder.DecoratedType(
|
||||
spv::OpTypeStruct, {{spv::OpMemberDecorate, {0, spv::DecorationOffset, 0}},
|
||||
{spv::OpMemberDecorate, {0, spv::DecorationCoherent}},
|
||||
{spv::OpDecorate, {spv::DecorationBlock}}}, TypeU32(state));
|
||||
state.builder.AddFunction(
|
||||
spv::OpConvertUToPtr, TypePointer(state, spv::StorageClassPhysicalStorageBuffer, block),
|
||||
pointer, bda);
|
||||
const auto base = pointer;
|
||||
pointer = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpAccessChain, TypePhysicalU32Pointer(state), pointer,
|
||||
base, ConstantU32(state, 0));
|
||||
} else {
|
||||
state.builder.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer,
|
||||
bda);
|
||||
}
|
||||
const auto value = state.builder.AllocateId();
|
||||
constexpr uint32_t alignment = sizeof(uint32_t);
|
||||
state.builder.AddFunction(spv::OpLoad, TypeU32(state), value, pointer,
|
||||
spv::MemoryAccessAlignedMask, alignment);
|
||||
spv::MemoryAccessAlignedMask |
|
||||
(coherent ? spv::MemoryAccessVolatileMask : spv::MemoryAccessMaskNone),
|
||||
alignment);
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
uint32_t LoadBda(ValueEmitContext& ctx, uint32_t address, uint32_t active, uint32_t bits) {
|
||||
uint32_t LoadBda(ValueEmitContext& ctx, uint32_t address, uint32_t active, uint32_t bits,
|
||||
bool coherent = false) {
|
||||
auto& state = ctx.state;
|
||||
return EmitValueOrZeroIfCondition(state, active, [&]() {
|
||||
const auto aligned = Binary(state, spv::OpBitwiseAnd, TypeU64(state), address,
|
||||
ConstantU64(state, ~uint64_t {3}));
|
||||
const auto first = LoadBdaDword(ctx, aligned);
|
||||
const auto first = LoadBdaDword(ctx, aligned, coherent);
|
||||
const auto byte =
|
||||
Binary(state, spv::OpBitwiseAnd, TypeU32(state),
|
||||
Unary(state, spv::OpUConvert, TypeU32(state), address), ConstantU32(state, 3));
|
||||
@@ -212,7 +229,7 @@ uint32_t LoadBda(ValueEmitContext& ctx, uint32_t address, uint32_t active, uint3
|
||||
TypeBool(state), byte, ConstantU32(state, bits == 16u ? 2u : 0u));
|
||||
const auto second = EmitValueOrZeroIfCondition(state, crosses, [&]() {
|
||||
return LoadBdaDword(ctx, Binary(state, spv::OpIAdd, TypeU64(state), aligned,
|
||||
ConstantU64(state, sizeof(uint32_t))));
|
||||
ConstantU64(state, sizeof(uint32_t))), coherent);
|
||||
});
|
||||
const auto shift =
|
||||
Binary(state, spv::OpShiftLeftLogical, TypeU32(state), byte, ConstantU32(state, 3));
|
||||
@@ -1359,7 +1376,7 @@ void EmitLoadMemory(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
else if (address_info.access == IR::AddressAccess::Read &&
|
||||
mem.kind != IR::ResourceKind::Scratch)
|
||||
value = LoadBda(ctx, GuestAddress(ctx, inst, mem), ctx.Arg(inst, inst.NumArgs() - 1),
|
||||
address_info.data_bits);
|
||||
address_info.data_bits, mem.coherent);
|
||||
else if (op == IR::ValueOpcode::LoadBufferU32 && mem.formatted)
|
||||
value = mem.kind == IR::ResourceKind::IndirectBuffer ? LoadIndirectFormattedX(ctx, inst)
|
||||
: FormattedLoad(ctx, inst, mem);
|
||||
|
||||
@@ -44,6 +44,16 @@ uint32_t EmitIndirectResourceIndex(EmitterState& state, uint32_t key, uint32_t m
|
||||
};
|
||||
const auto mapping = ConstantU32(state, mapping_offset);
|
||||
const auto count = LoadMapping(mapping);
|
||||
if (search_iterations == 0u) {
|
||||
const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), key,
|
||||
ConstantU32(state, 2u));
|
||||
const auto valid = Binary(state, spv::OpULessThan, TypeBool(state), index, count);
|
||||
const auto safe = Select(state, TypeU32(state), valid, index, ConstantU32(state, 0u));
|
||||
const auto entry = Binary(state, spv::OpIAdd, TypeU32(state), safe,
|
||||
ConstantU32(state, mapping_offset + 1u));
|
||||
return Select(state, TypeU32(state), valid, LoadMapping(entry),
|
||||
ConstantU32(state, default_resource));
|
||||
}
|
||||
const auto entry_at = [&](uint32_t index) {
|
||||
return Binary(state, spv::OpIAdd, TypeU32(state), mapping,
|
||||
Binary(state, spv::OpIAdd, TypeU32(state),
|
||||
|
||||
@@ -343,7 +343,6 @@ void DecodeFlat(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
const uint32_t word0 = code[word_index];
|
||||
const uint32_t word1 = code[word_index + 1u];
|
||||
const uint32_t offset = word0 & 0xfffu;
|
||||
const uint32_t dlc = (word0 >> 12u) & 1u;
|
||||
const uint32_t lds = (word0 >> 13u) & 1u;
|
||||
const uint32_t seg = (word0 >> 14u) & 0x3u;
|
||||
const uint32_t opcode = (word0 >> 18u) & 0x7fu;
|
||||
@@ -355,6 +354,7 @@ void DecodeFlat(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
inst.pc = pc;
|
||||
inst.word_count = 2;
|
||||
inst.offset = seg == 0u ? (offset & 0x7ffu) : SignExtendU32(offset, 12u);
|
||||
inst.dlc = ((word0 >> 12u) & 1u) != 0;
|
||||
inst.glc = ((word0 >> 16u) & 1u) != 0;
|
||||
inst.slc = ((word0 >> 17u) & 1u) != 0;
|
||||
inst.family = Family::FLAT;
|
||||
@@ -364,7 +364,7 @@ void DecodeFlat(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
ApplyMemoryInfo(inst, info);
|
||||
SetRawWords(inst, code, word_index, 2);
|
||||
|
||||
if (dlc != 0 || lds != 0 || inst.glc || inst.slc || seg == 3u) {
|
||||
if (lds != 0 || inst.glc || inst.slc || seg == 3u) {
|
||||
SetUnsupported(inst, Family::FLAT, opcode, "FLAT modifiers or segment are not implemented");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ constexpr OpcodeMap SOP1_OPCODE_LIST[] = {
|
||||
{0x14u, Opcode::S_FF1_I32_B64},
|
||||
{0x15u, Opcode::S_FLBIT_I32_B32},
|
||||
{0x16u, Opcode::S_FLBIT_I32_B64},
|
||||
{0x1au, Opcode::S_SEXT_I32_I16},
|
||||
{0x1bu, Opcode::S_BITSET0_B32},
|
||||
{0x1cu, Opcode::S_BITSET0_B64},
|
||||
{0x1du, Opcode::S_BITSET1_B32},
|
||||
|
||||
@@ -503,6 +503,7 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
OperandToString(inst.dst).c_str(),
|
||||
OperandToString(inst.src0).c_str()));
|
||||
case Opcode::S_ABS_I32:
|
||||
case Opcode::S_SEXT_I32_I16:
|
||||
case Opcode::S_BREV_B32:
|
||||
case Opcode::S_BREV_B64:
|
||||
case Opcode::S_BCNT1_I32_B32:
|
||||
|
||||
@@ -45,6 +45,7 @@ enum class Opcode {
|
||||
S_CMOV_B64,
|
||||
S_MOVK_I32,
|
||||
S_ABS_I32,
|
||||
S_SEXT_I32_I16,
|
||||
S_ABSDIFF_I32,
|
||||
S_BREV_B32,
|
||||
S_BREV_B64,
|
||||
|
||||
@@ -129,8 +129,9 @@ IR::MemoryInfo MemoryInfoFromDecoded(const Decoder::Instruction& decoded) {
|
||||
// Vector loads use GLC/DLC to bypass L0/GL1; atomics use GLC only to return data.
|
||||
const bool buffer_atomic = decoded.opcode >= Decoder::Opcode::BUFFER_ATOMIC_SWAP &&
|
||||
decoded.opcode <= Decoder::Opcode::BUFFER_ATOMIC_FMAX;
|
||||
memory.coherent = memory.kind == ResourceKind::Buffer && !buffer_atomic &&
|
||||
(decoded.glc || decoded.dlc);
|
||||
memory.coherent = (memory.kind == ResourceKind::Buffer || memory.kind == ResourceKind::Flat ||
|
||||
memory.kind == ResourceKind::Global) &&
|
||||
!buffer_atomic && (decoded.glc || decoded.dlc);
|
||||
memory.resource = ResourceIndexFromOperand(decoded.src1);
|
||||
memory.sampler = ResourceIndexFromOperand(decoded.src2);
|
||||
if (memory.kind == ResourceKind::ScalarBuffer) {
|
||||
|
||||
@@ -134,6 +134,10 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
|
||||
|
||||
case O::S_ABS_I32:
|
||||
return SimpleInteger(inst, IR::ValueOpcode::IAbs32, IR::Type::U32, false, false, true);
|
||||
case O::S_SEXT_I32_I16:
|
||||
WriteOperand(inst.dst, IR::U32(ir.Emit(IR::ValueOpcode::BitFieldSExtract,
|
||||
{ReadU32(inst.src0), IR::Value(0u), IR::Value(16u)})));
|
||||
return;
|
||||
case O::S_MUL_I32:
|
||||
case O::S_MULK_I32:
|
||||
return SimpleInteger(inst, IR::ValueOpcode::IMul32, IR::Type::U32, false, false, false);
|
||||
|
||||
@@ -485,15 +485,21 @@ struct BlockInfo {
|
||||
|
||||
struct DescriptorSource {
|
||||
struct IndirectDescriptor {
|
||||
uint32_t material_source = UINT32_MAX;
|
||||
struct SelectorRead {
|
||||
uint32_t source = UINT32_MAX;
|
||||
uint32_t stride = 0;
|
||||
uint32_t offset = 0;
|
||||
|
||||
bool operator==(const SelectorRead& other) const = default;
|
||||
};
|
||||
std::optional<SelectorRead> selector;
|
||||
uint32_t table_source = 0;
|
||||
uint32_t selector_stride = 0;
|
||||
uint32_t selector_offset = 0;
|
||||
uint32_t table_offset = 0;
|
||||
uint32_t table_immediate = 0;
|
||||
uint32_t table_stride = 0;
|
||||
uint32_t table_record_bytes = 0;
|
||||
bool table_scalar = false;
|
||||
uint32_t workgroup_axis = UINT32_MAX;
|
||||
uint32_t selector_shift = 0;
|
||||
uint32_t selector_bits = UINT32_MAX;
|
||||
Value key_count;
|
||||
Value selector_first;
|
||||
Value selector_mask;
|
||||
|
||||
@@ -101,7 +101,7 @@ SharedMemoryResources CollectMemoryResources(const Program& program, std::vector
|
||||
|
||||
bool UsesFlattenedSrt(const Program& program) {
|
||||
const auto uses_mapping = [](const auto& resource) {
|
||||
return resource.indirect_search_iterations != 0u;
|
||||
return resource.indirect_root != UINT32_MAX;
|
||||
};
|
||||
return std::ranges::any_of(program.blocks, [](const Block* block) {
|
||||
return std::ranges::any_of(*block, [](const Inst& inst) {
|
||||
|
||||
@@ -238,6 +238,13 @@ bool NormalizeIndirectStoreBuffer(DescriptorValue& value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsBoundedDescriptorTable(const ResourcePlan& program,
|
||||
const DescriptorSource::IndirectDescriptor& indirect) {
|
||||
const auto* table = Source(program, indirect.table_source);
|
||||
return indirect.sources.empty() && !indirect.selector &&
|
||||
indirect.workgroup_axis == UINT32_MAX && table != nullptr && table->dword_count == 4u;
|
||||
}
|
||||
|
||||
template <typename Specialization, typename Normalize>
|
||||
bool MaterializeIndirectDescriptor(const ResourcePlan& program,
|
||||
const DescriptorSource::IndirectDescriptor& indirect,
|
||||
@@ -249,7 +256,24 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
uint32_t maximum_resources, Normalize&& normalize) {
|
||||
const auto descriptor_bytes = dword_count * sizeof(uint32_t);
|
||||
const auto& sources = indirect.sources;
|
||||
const auto* selector = indirect.selector ? &*indirect.selector : nullptr;
|
||||
auto& keys = program.material_keys;
|
||||
const auto children_begin = descriptors.size();
|
||||
const auto root_resource = specializations[resource_index];
|
||||
const auto intern_candidate = [&](const DescriptorValue& candidate, uint32_t& ordinal) {
|
||||
ordinal = 0u;
|
||||
if (candidate == descriptors[resource_index]) return true;
|
||||
const auto found = std::find(descriptors.begin() + children_begin, descriptors.end(), candidate);
|
||||
ordinal = static_cast<uint32_t>(found - descriptors.begin() - children_begin + 1u);
|
||||
if (found == descriptors.end()) {
|
||||
if (descriptors.size() >= maximum_resources) return false;
|
||||
descriptors.push_back(candidate);
|
||||
auto child = root_resource;
|
||||
child.indirect_root = resource_index;
|
||||
specializations.push_back(child);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const auto read_keys = [&](const ShaderBufferResource& material, uint64_t first,
|
||||
uint64_t step, uint64_t count) {
|
||||
keys.resize(count);
|
||||
@@ -269,8 +293,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
keys.clear();
|
||||
DescriptorValue material_value;
|
||||
DescriptorValue table_value;
|
||||
if ((indirect.material_source != UINT32_MAX &&
|
||||
!clean.EvaluateDescriptor(indirect.material_source, material_value)) ||
|
||||
if ((selector != nullptr &&
|
||||
!clean.EvaluateDescriptor(selector->source, material_value)) ||
|
||||
!clean.EvaluateDescriptor(indirect.table_source, table_value)) {
|
||||
return false;
|
||||
}
|
||||
@@ -283,7 +307,48 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (indirect.material_source == UINT32_MAX) {
|
||||
if (IsBoundedDescriptorTable(program, indirect)) {
|
||||
if (!indirect.table_scalar && table.Format() == Prospero::BufferFormat::kInvalid) {
|
||||
descriptors[resource_index] = {.dword_count = dword_count};
|
||||
return true;
|
||||
}
|
||||
if (table_size > UINT32_MAX ||
|
||||
(indirect.table_record_bytes != 0u &&
|
||||
(table.Type() != 0u || table.SwizzleEnabled() || table.AddTid() ||
|
||||
table.OutOfBounds() != 0u || (table_base & 3u) != 0u ||
|
||||
(table.Stride() & 3u) != 0u || table.Stride() < indirect.table_record_bytes)))
|
||||
return false;
|
||||
// Descriptor bytes are refreshed together. The GPU selects the DWORD address;
|
||||
// no material selector or unused table field is interpreted on the CPU.
|
||||
keys.resize(table_size / 4u);
|
||||
if (!ReadScalarTable(table_base, table_size, 0u, runtime, keys)) return false;
|
||||
const auto mapping_offset = snapshot.flattened_srt.size();
|
||||
descriptors[resource_index] = {.dword_count = dword_count};
|
||||
snapshot.flattened_srt.resize(mapping_offset + 2u, 0u);
|
||||
for (uint32_t word = 0; word < keys.size(); ++word) {
|
||||
DescriptorValue candidate {.dword_count = dword_count};
|
||||
std::copy_n(keys.begin() + word, std::min<size_t>(dword_count, keys.size() - word),
|
||||
candidate.dwords.begin());
|
||||
if (!normalize(candidate)) return false;
|
||||
uint32_t ordinal;
|
||||
if (!intern_candidate(candidate, ordinal)) return false;
|
||||
if (ordinal == 0u) continue;
|
||||
snapshot.flattened_srt.resize(mapping_offset + 2u + word, 0u);
|
||||
snapshot.flattened_srt[mapping_offset + 1u + word] = ordinal;
|
||||
}
|
||||
if (descriptors.size() == children_begin) {
|
||||
snapshot.flattened_srt.resize(mapping_offset);
|
||||
} else {
|
||||
snapshot.flattened_srt[mapping_offset] =
|
||||
static_cast<uint32_t>(snapshot.flattened_srt.size() - mapping_offset - 1u);
|
||||
auto& root = specializations[resource_index];
|
||||
root.indirect_root = resource_index;
|
||||
root.indirect_mapping_offset = static_cast<uint32_t>(mapping_offset);
|
||||
root.indirect_search_iterations = 0u;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (selector == nullptr) {
|
||||
uint32_t key_count = 0;
|
||||
if (indirect.workgroup_axis != UINT32_MAX) {
|
||||
if (indirect.workgroup_axis >= runtime.workgroup_counts.size()) return false;
|
||||
@@ -295,7 +360,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
if (std::bit_cast<int32_t>(key_count) <= 0) key_count = 0;
|
||||
}
|
||||
if (indirect.table_stride == 0u || key_count > MaxIndirectDescriptorProbes ||
|
||||
(key_count != 0u && uint64_t {indirect.table_offset} +
|
||||
(key_count != 0u && uint64_t {indirect.table_offset} + indirect.table_immediate +
|
||||
uint64_t {key_count - 1u} * indirect.table_stride + descriptor_bytes >
|
||||
UINT32_MAX + 1ull)) {
|
||||
return false;
|
||||
@@ -306,20 +371,18 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
ShaderBufferResource material;
|
||||
uint32_t first = 0, count = 0;
|
||||
if (!DecodeBufferDescriptor(material_value, material) || material.Type() != 0u ||
|
||||
((indirect.selector_shift != 0u || indirect.selector_bits != UINT32_MAX) &&
|
||||
(material.Base48() & 3u) != 0u) ||
|
||||
material.SwizzleEnabled() ||
|
||||
material.AddTid() || material.OutOfBounds() != 0u ||
|
||||
uint64_t {indirect.selector_offset} + 4u > material.Stride() ||
|
||||
uint64_t {selector->offset} + 4u > material.Stride() ||
|
||||
!clean.Evaluate(indirect.selector_first, first) ||
|
||||
!clean.Evaluate(indirect.key_count, count) || count > MaxIndirectDescriptorProbes ||
|
||||
uint64_t {first} + count > material.NumRecords())
|
||||
return false;
|
||||
if (count != 0u && (uint64_t {first} + count - 1u) * material.Stride() +
|
||||
indirect.selector_offset + 4u >
|
||||
selector->offset + 4u >
|
||||
uint64_t {UINT32_MAX} + 1u)
|
||||
return false;
|
||||
if (!read_keys(material, uint64_t {first} * material.Stride() + indirect.selector_offset,
|
||||
if (!read_keys(material, uint64_t {first} * material.Stride() + selector->offset,
|
||||
material.Stride(), count)) return false;
|
||||
} else if (!indirect.selector_mask.IsEmpty()) {
|
||||
uint32_t mask = 0;
|
||||
@@ -335,8 +398,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
keys.reserve(std::popcount(mask));
|
||||
while (mask != 0u) {
|
||||
const auto index = std::countr_zero(mask);
|
||||
const auto offset = static_cast<uint64_t>(indirect.selector_offset) +
|
||||
static_cast<uint64_t>(index) * indirect.selector_stride;
|
||||
const auto offset = static_cast<uint64_t>(selector->offset) +
|
||||
static_cast<uint64_t>(index) * selector->stride;
|
||||
if (offset > UINT32_MAX) return false;
|
||||
uint32_t key = 0;
|
||||
if (!ReadScalarTable(material_base, UINT64_MAX, static_cast<uint32_t>(offset),
|
||||
@@ -344,37 +407,14 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
keys.push_back(key);
|
||||
mask &= mask - 1u;
|
||||
}
|
||||
} else {
|
||||
ShaderBufferResource material;
|
||||
if (!DecodeBufferDescriptor(material_value, material) || table_value.dword_count != 4u) {
|
||||
return false;
|
||||
}
|
||||
// The first aligned offset includes the immediate added after shader U32 arithmetic.
|
||||
const auto step = std::max<uint64_t>(4u,
|
||||
std::gcd<uint64_t>(indirect.selector_stride, uint64_t {1} << 32u));
|
||||
const uint64_t first = indirect.selector_offset;
|
||||
const auto size = material.GetSize();
|
||||
const auto limit = std::min(first + (uint64_t {1} << 32u) - step, size >= 4u ? size - 4u : 0u);
|
||||
const auto probe_count = size >= 4u && first <= limit ? (limit - first) / step + 1u : 0u;
|
||||
if (probe_count > MaxIndirectDescriptorProbes) {
|
||||
return false;
|
||||
}
|
||||
if (!read_keys(material, first, step, probe_count)) return false;
|
||||
if (indirect.selector_shift == 0u) keys.push_back(0u);
|
||||
}
|
||||
if (indirect.material_source != UINT32_MAX) {
|
||||
for (auto& key: keys) key = (key >> indirect.selector_shift) & indirect.selector_bits;
|
||||
if (indirect.selector_shift != 0u || indirect.selector_bits != UINT32_MAX) {
|
||||
keys.push_back(0u); // An out-of-range material load returns zero.
|
||||
}
|
||||
} else return false;
|
||||
if (selector != nullptr) {
|
||||
std::ranges::sort(keys);
|
||||
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
|
||||
}
|
||||
}
|
||||
|
||||
const auto children_begin = descriptors.size();
|
||||
const auto mapping_offset = snapshot.flattened_srt.size();
|
||||
const auto root_resource = specializations[resource_index];
|
||||
const auto key_count = sources.empty() ? keys.size() : sources.size();
|
||||
snapshot.flattened_srt.resize(mapping_offset + 1u + key_count * 2u);
|
||||
snapshot.flattened_srt[mapping_offset] = static_cast<uint32_t>(key_count);
|
||||
@@ -383,8 +423,9 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
DescriptorValue candidate;
|
||||
candidate.dword_count = dword_count;
|
||||
if (sources.empty()) {
|
||||
const auto table_offset =
|
||||
static_cast<uint32_t>(key * indirect.table_stride) + indirect.table_offset;
|
||||
const uint64_t table_offset =
|
||||
uint64_t {(key * indirect.table_stride + indirect.table_offset) & ~3u} +
|
||||
(indirect.table_immediate & ~3u);
|
||||
if (!ReadScalarTable(table_base, table_size, table_offset, runtime,
|
||||
std::span(candidate.dwords).first(dword_count))) {
|
||||
return false;
|
||||
@@ -396,19 +437,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
uint32_t ordinal = 0;
|
||||
if (entry == 0) {
|
||||
descriptors[resource_index] = candidate;
|
||||
} else if (descriptors[resource_index] != candidate) {
|
||||
const auto found =
|
||||
std::find(descriptors.begin() + children_begin, descriptors.end(), candidate);
|
||||
ordinal = static_cast<uint32_t>(found - descriptors.begin() - children_begin + 1u);
|
||||
if (found == descriptors.end()) {
|
||||
if (descriptors.size() >= maximum_resources) {
|
||||
return false;
|
||||
}
|
||||
descriptors.push_back(candidate);
|
||||
auto child = root_resource;
|
||||
child.indirect_root = resource_index;
|
||||
specializations.push_back(child);
|
||||
}
|
||||
} else if (!intern_candidate(candidate, ordinal)) {
|
||||
return false;
|
||||
}
|
||||
snapshot.flattened_srt[mapping_offset + 1u + entry * 2u] = key;
|
||||
snapshot.flattened_srt[mapping_offset + 2u + entry * 2u] = ordinal;
|
||||
@@ -566,9 +596,9 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
||||
const auto key_count = root.indirect_mapping_offset < snapshot.flattened_srt.size()
|
||||
? snapshot.flattened_srt[root.indirect_mapping_offset]
|
||||
: 0u;
|
||||
if (root.indirect_search_iterations == 0u || key_count < 2u ||
|
||||
if (key_count == 0u ||
|
||||
static_cast<size_t>(root.indirect_mapping_offset) + 1u +
|
||||
static_cast<size_t>(key_count) * 2u >
|
||||
static_cast<size_t>(key_count) * (root.indirect_search_iterations == 0u ? 1u : 2u) >
|
||||
snapshot.flattened_srt.size()) {
|
||||
return SpecializationFail("indirect image specialization has an invalid key mapping");
|
||||
}
|
||||
@@ -1010,9 +1040,10 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
|
||||
if (!source.indirect_descriptor.has_value()) continue;
|
||||
const auto& indirect = *source.indirect_descriptor;
|
||||
plan.requires_specialization_memory = true;
|
||||
capture_indirect_reads |= indirect.material_source != UINT32_MAX || !indirect.sources.empty();
|
||||
MarkCleanFlatSlots(plan, Source(plan, indirect.material_source), plan.clean_flat_slots,
|
||||
indirect.selector_mask);
|
||||
capture_indirect_reads = true;
|
||||
if (indirect.selector)
|
||||
MarkCleanFlatSlots(plan, Source(plan, indirect.selector->source), plan.clean_flat_slots);
|
||||
MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.selector_mask);
|
||||
MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.selector_first);
|
||||
MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.key_count);
|
||||
if (indirect.sources.empty()) {
|
||||
@@ -1156,10 +1187,13 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
continue;
|
||||
}
|
||||
const auto& indirect = *source->indirect_descriptor;
|
||||
const bool bounded_table = IsBoundedDescriptorTable(program, indirect);
|
||||
if (!MaterializeIndirectDescriptor(
|
||||
program, indirect, i, 8u, observed, clean, snapshot, snapshot.images,
|
||||
specialization.images, UINT32_MAX, [&](DescriptorValue& value) {
|
||||
if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128))
|
||||
// A broad heap also contains resources for other typed image operations.
|
||||
if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128) ||
|
||||
(bounded_table && DescriptorDimension(value, image.dimension) != image.dimension))
|
||||
value.dwords.fill(0u);
|
||||
return true;
|
||||
})) {
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <algorithm>
|
||||
#include <fmt/format.h>
|
||||
#include <map>
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <tuple>
|
||||
@@ -374,11 +373,68 @@ public:
|
||||
}
|
||||
}
|
||||
for (const auto& plan: m_indirect_descriptors) {
|
||||
auto key = plan.key;
|
||||
if (plan.table_read != nullptr) {
|
||||
const auto* anchor = plan.table_indexed ? plan.handle : plan.table_read;
|
||||
auto* block = anchor->Parent();
|
||||
const auto where = std::ranges::find_if(block->Instructions(),
|
||||
[&](const Inst& inst) { return &inst == anchor; });
|
||||
const auto emit = [&](ValueOpcode op, std::initializer_list<Value> args) {
|
||||
return Value(&*block->PrependNewInst(where, op, args));
|
||||
};
|
||||
const auto& read = *plan.table_read;
|
||||
const auto& memory = m_program.memory_info[read.Flags<MemoryFlags>().index];
|
||||
Value offset;
|
||||
Value valid(true);
|
||||
if (plan.table_indexed) {
|
||||
const auto* table = read.Arg(0).ResolveInstruction();
|
||||
const auto stride = emit(ValueOpcode::BitFieldUExtract,
|
||||
{table->Arg(1), Value(16u), Value(14u)});
|
||||
const auto format = emit(ValueOpcode::BitFieldUExtract,
|
||||
{table->Arg(3), Value(12u), Value(7u)});
|
||||
offset = emit(ValueOpcode::IAdd32,
|
||||
{emit(ValueOpcode::IMul32, {read.Arg(1), stride}), Value(memory.offset)});
|
||||
valid = emit(ValueOpcode::LogicalAnd,
|
||||
{emit(ValueOpcode::ULessThan32, {read.Arg(1), table->Arg(2)}),
|
||||
emit(ValueOpcode::INotEqual32, {format, Value(0u)})});
|
||||
} else {
|
||||
const auto dynamic = emit(ValueOpcode::BitwiseAnd32, {read.Arg(1), Value(~3u)});
|
||||
offset = emit(ValueOpcode::IAdd32, {dynamic, Value(memory.offset & ~3u)});
|
||||
valid = emit(ValueOpcode::ULessThanEqual32, {dynamic, offset});
|
||||
}
|
||||
key = emit(ValueOpcode::SelectU32, {valid, offset, Value(UINT32_MAX)});
|
||||
if (!plan.table_indexed) {
|
||||
// NativeDescriptorSource proved this read reaches the image. Preserve its
|
||||
// SSA Phi edges while retaining descriptor DWORDs for other consumers.
|
||||
std::map<const Inst*, Value> projected;
|
||||
const auto project = [&](auto&& self, Value value) -> Value {
|
||||
value = value.Resolve();
|
||||
const auto* phi = value.TryInstruction();
|
||||
if (phi == plan.table_read) return key;
|
||||
if (phi == nullptr || phi->GetOpcode() != ValueOpcode::Phi) return Value(UINT32_MAX);
|
||||
if (const auto found = projected.find(phi); found != projected.end()) return found->second;
|
||||
auto* parent = phi->Parent();
|
||||
const auto position = std::ranges::find_if(parent->Instructions(),
|
||||
[&](const Inst& inst) { return &inst == phi; });
|
||||
auto& selected = *parent->PrependNewInst(position, ValueOpcode::Phi);
|
||||
selected.SetFlags(Type::U32);
|
||||
projected.emplace(phi, Value(&selected));
|
||||
for (uint32_t arm = 0; arm < phi->NumArgs(); ++arm)
|
||||
selected.AddPhiOperand(phi->PhiBlock(arm), self(self, phi->Arg(arm)));
|
||||
return Value(&selected);
|
||||
};
|
||||
key = project(project, plan.handle->Arg(0));
|
||||
}
|
||||
plan.handle->SetArg(0, key);
|
||||
for (uint32_t word = 1; word < plan.handle->NumArgs(); ++word)
|
||||
plan.handle->SetArg(word, Value(0u));
|
||||
continue;
|
||||
}
|
||||
if (plan.handle->GetOpcode() == ValueOpcode::GetBufferResource) {
|
||||
plan.handle->SetArg(0, plan.key);
|
||||
for (uint32_t word = 1; word < 4u; ++word) plan.handle->SetArg(word, Value(0u));
|
||||
} else {
|
||||
if (plan.reads[0] == nullptr) plan.handle->SetArg(0, plan.key);
|
||||
if (plan.reads[0] == nullptr) plan.handle->SetArg(0, key);
|
||||
for (uint32_t dword = 0; dword < 4u; dword++) {
|
||||
plan.handle->SetArg(dword + 1u, plan.roots[dword + 4u]);
|
||||
}
|
||||
@@ -437,6 +493,10 @@ private:
|
||||
std::array<Value, 8> roots {};
|
||||
std::array<uint32_t, 8> memory {};
|
||||
std::array<const Inst*, 8> reads {};
|
||||
bool split_offsets = false;
|
||||
bool retain_reads = false;
|
||||
bool table_indexed = false;
|
||||
const Inst* table_read = nullptr;
|
||||
};
|
||||
|
||||
[[noreturn]] void Fail(uint32_t pc, const std::string& reason) const {
|
||||
@@ -834,7 +894,7 @@ private:
|
||||
|
||||
uint32_t InternSource(const DescriptorSource& descriptor) {
|
||||
for (uint32_t candidate = 0; candidate < m_sources.size(); candidate++) {
|
||||
const auto& current = m_sources[candidate];
|
||||
auto& current = m_sources[candidate];
|
||||
if (current.dword_count != descriptor.dword_count ||
|
||||
current.indirect_descriptor.has_value() != descriptor.indirect_descriptor.has_value()) {
|
||||
continue;
|
||||
@@ -842,10 +902,9 @@ private:
|
||||
if (current.indirect_descriptor.has_value()) {
|
||||
const auto& a = *current.indirect_descriptor;
|
||||
const auto& b = *descriptor.indirect_descriptor;
|
||||
if (a.material_source != b.material_source || a.table_source != b.table_source ||
|
||||
a.selector_stride != b.selector_stride || a.selector_offset != b.selector_offset ||
|
||||
a.selector_shift != b.selector_shift || a.selector_bits != b.selector_bits ||
|
||||
a.table_offset != b.table_offset || a.table_stride != b.table_stride ||
|
||||
if (a.selector != b.selector || a.table_source != b.table_source ||
|
||||
a.table_offset != b.table_offset || a.table_immediate != b.table_immediate ||
|
||||
a.table_stride != b.table_stride ||
|
||||
a.workgroup_axis != b.workgroup_axis || a.sources != b.sources ||
|
||||
!EquivalentValue(m_program, a.key_count, b.key_count) ||
|
||||
a.selector_first.IsEmpty() != b.selector_first.IsEmpty() ||
|
||||
@@ -860,6 +919,12 @@ private:
|
||||
same = same && EquivalentValue(m_program, current.dwords[i], descriptor.dwords[i]);
|
||||
}
|
||||
if (same) {
|
||||
if (current.indirect_descriptor) {
|
||||
current.indirect_descriptor->table_scalar |= descriptor.indirect_descriptor->table_scalar;
|
||||
current.indirect_descriptor->table_record_bytes = std::max(
|
||||
current.indirect_descriptor->table_record_bytes,
|
||||
descriptor.indirect_descriptor->table_record_bytes);
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
@@ -949,54 +1014,6 @@ private:
|
||||
return ValidateSource(descriptor, bad_dword);
|
||||
}
|
||||
|
||||
bool MatchMaterialOffset(Value value, uint32_t& stride, uint32_t& offset) const {
|
||||
stride = 1u;
|
||||
offset = 0u;
|
||||
value = value.Resolve();
|
||||
while (const auto* inst = value.TryInstruction()) {
|
||||
if (inst->NumArgs() != 2u) break;
|
||||
uint32_t constant = 0;
|
||||
Value remaining;
|
||||
if (inst->GetOpcode() == ValueOpcode::ShiftLeftLogical32 &&
|
||||
ImmediateU32(inst->Arg(1), constant) && constant < 32u) {
|
||||
stride *= 1u << constant;
|
||||
remaining = inst->Arg(0);
|
||||
} else if (inst->GetOpcode() == ValueOpcode::IAdd32 ||
|
||||
inst->GetOpcode() == ValueOpcode::IMul32) {
|
||||
if (ImmediateU32(inst->Arg(0), constant)) remaining = inst->Arg(1);
|
||||
else if (ImmediateU32(inst->Arg(1), constant)) remaining = inst->Arg(0);
|
||||
else break;
|
||||
if (inst->GetOpcode() == ValueOpcode::IAdd32) offset += stride * constant;
|
||||
else stride *= constant;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
value = remaining.Resolve();
|
||||
}
|
||||
// Treat the remaining GPU-derived scalar as any U32 value.
|
||||
return stride != 0u && value.GetType() == Type::U32 && !value.IsImmediate();
|
||||
}
|
||||
|
||||
Value MaterialKey(Value key, DescriptorSource::IndirectDescriptor& indirect) const {
|
||||
indirect.selector_shift = 0u;
|
||||
indirect.selector_bits = UINT32_MAX;
|
||||
const auto* inst = key.Resolve().TryInstruction();
|
||||
uint32_t immediate;
|
||||
if (inst != nullptr && inst->GetOpcode() == ValueOpcode::BitwiseAnd32) {
|
||||
if (ImmediateU32(inst->Arg(1), immediate)) key = inst->Arg(0);
|
||||
else if (ImmediateU32(inst->Arg(0), immediate)) key = inst->Arg(1);
|
||||
else return key;
|
||||
indirect.selector_bits = immediate;
|
||||
inst = key.Resolve().TryInstruction();
|
||||
}
|
||||
if (inst != nullptr && inst->GetOpcode() == ValueOpcode::ShiftRightLogical32 &&
|
||||
ImmediateU32(inst->Arg(1), immediate) && immediate != 0u && immediate < 32u) {
|
||||
indirect.selector_shift = immediate;
|
||||
key = inst->Arg(0);
|
||||
}
|
||||
return key.Resolve();
|
||||
}
|
||||
|
||||
enum class LaneQuantifier { Any, All };
|
||||
struct EdgePredicate {
|
||||
Value condition;
|
||||
@@ -1443,9 +1460,9 @@ private:
|
||||
static_cast<uint64_t>(UINT32_MAX) + 1u) return false;
|
||||
const auto mask = BoundedSetBitMask(offset.index, active);
|
||||
if (mask.IsEmpty()) return false;
|
||||
indirect.material_source = InternSource(material_source);
|
||||
indirect.selector_stride = static_cast<uint32_t>(offset.stride);
|
||||
indirect.selector_offset = static_cast<uint32_t>(offset.offset + memory.offset);
|
||||
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead{.source = InternSource(material_source),
|
||||
.stride = static_cast<uint32_t>(offset.stride),
|
||||
.offset = static_cast<uint32_t>(offset.offset + memory.offset)});
|
||||
indirect.key_count = Value(32u);
|
||||
indirect.selector_mask = mask;
|
||||
return true;
|
||||
@@ -1606,8 +1623,9 @@ private:
|
||||
bool MatchDescriptorTable(Inst& handle, const DescriptorSource& descriptor,
|
||||
IndirectDescriptorPlan& plan,
|
||||
DescriptorSource& table_source, Value& key, uint32_t& table_offset,
|
||||
uint32_t& table_stride) {
|
||||
uint32_t& table_stride, uint32_t& table_immediate) {
|
||||
Inst* table_handle = nullptr;
|
||||
Value table_guard;
|
||||
for (uint32_t dword = 0; dword < handle.NumArgs(); ++dword) {
|
||||
auto* read = descriptor.dwords[dword].Resolve().TryInstruction();
|
||||
if (read == nullptr) {
|
||||
@@ -1615,7 +1633,31 @@ private:
|
||||
}
|
||||
uint32_t memory_index = 0;
|
||||
const auto* memory = ScalarReadMemory(*read, memory_index);
|
||||
if (memory == nullptr || memory->offset > INT32_MAX || (memory->offset & 3u) != 0u ||
|
||||
uint32_t component = 0;
|
||||
const bool indexed = memory == nullptr;
|
||||
if (indexed) {
|
||||
if (handle.NumArgs() != 8u) return false;
|
||||
const auto* selected = UniformizedMaterialValue(descriptor.dwords[dword], handle);
|
||||
if (selected == nullptr) return false;
|
||||
const auto* extract = selected->Arg(1).Resolve().TryInstruction();
|
||||
if (extract == nullptr || extract->GetOpcode() != ValueOpcode::CompositeExtractU32x4 ||
|
||||
!ImmediateU32(extract->Arg(1), component) || component >= 4u) return false;
|
||||
read = extract->Arg(0).Resolve().TryInstruction();
|
||||
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32x4) return false;
|
||||
memory_index = read->Flags<MemoryFlags>().index;
|
||||
if (memory_index >= m_program.memory_info.size()) return false;
|
||||
memory = &m_program.memory_info[memory_index];
|
||||
uint32_t offset = 1u, scalar = 1u;
|
||||
if (memory->kind != ResourceKind::Buffer || memory->typed || memory->formatted ||
|
||||
!memory->idxen || memory->offen || memory->data_bits != 32u ||
|
||||
memory->data_dwords != 4u ||
|
||||
!ImmediateU32(read->Arg(2), offset) || offset != 0u ||
|
||||
!ImmediateU32(read->Arg(3), scalar) || scalar != 0u ||
|
||||
!EquivalentValue(m_program, read->Arg(4), selected->Arg(0)) ||
|
||||
(dword != 0u && !EquivalentValue(m_program, table_guard, read->Arg(4)))) return false;
|
||||
table_guard = read->Arg(4);
|
||||
}
|
||||
if (memory->offset > INT32_MAX || (memory->offset & 3u) != 0u ||
|
||||
!MemoryIndexBelongsTo(memory_index, *read)) {
|
||||
return false;
|
||||
}
|
||||
@@ -1630,23 +1672,32 @@ private:
|
||||
(memory->kind == ResourceKind::ScalarAddress && read->Parent() != handle.Parent()) ||
|
||||
(table_handle != nullptr &&
|
||||
!EquivalentValue(m_program, Value(table_handle), Value(current_handle))) ||
|
||||
!MatchTableOffset(read->Arg(1), current_key, offset, stride) ||
|
||||
memory->offset > UINT32_MAX - offset) {
|
||||
(!indexed && !MatchTableOffset(read->Arg(1), current_key, offset, stride))) {
|
||||
return false;
|
||||
}
|
||||
offset += memory->offset;
|
||||
if (indexed) {
|
||||
current_key = read->Arg(1);
|
||||
offset = component * 4u;
|
||||
}
|
||||
if (dword == 0u) {
|
||||
if (component != 0u) return false;
|
||||
plan.table_indexed = indexed;
|
||||
key = current_key;
|
||||
table_offset = offset;
|
||||
table_immediate = memory->offset;
|
||||
table_stride = stride;
|
||||
} else if (table_stride != stride || !EquivalentValue(m_program, key, current_key) ||
|
||||
static_cast<uint64_t>(table_offset) + dword * sizeof(uint32_t) != offset) {
|
||||
} else if (plan.table_indexed != indexed || table_stride != stride ||
|
||||
!EquivalentValue(m_program, key, current_key) ||
|
||||
uint64_t {table_offset} + table_immediate + dword * sizeof(uint32_t) !=
|
||||
uint64_t {offset} + memory->offset) {
|
||||
return false;
|
||||
}
|
||||
plan.split_offsets |= !indexed && offset != table_offset;
|
||||
table_handle = current_handle;
|
||||
if (handle.NumArgs() == 8u
|
||||
? WorkgroupAxis(key) == UINT32_MAX && !UsesOnlyImageDescriptors(*read) :
|
||||
std::ranges::any_of(read->Uses(), [](const Use& use) {
|
||||
if (handle.NumArgs() == 8u) {
|
||||
if (memory->kind == ResourceKind::ScalarAddress)
|
||||
plan.retain_reads |= !UsesOnlyImageDescriptors(*read);
|
||||
} else if (std::ranges::any_of(read->Uses(), [](const Use& use) {
|
||||
return use.user->GetOpcode() != ValueOpcode::GetBufferResource ||
|
||||
std::ranges::any_of(use.user->Uses(), [](const Use& consumer) {
|
||||
return consumer.user->GetOpcode() != ValueOpcode::StoreBufferU32;
|
||||
@@ -1664,34 +1715,6 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MatchUniformizedBufferKey(Value key, const Inst& image,
|
||||
DescriptorSource::IndirectDescriptor& indirect,
|
||||
DescriptorSource& material_source) {
|
||||
const auto* selected = UniformizedMaterialValue(key, image);
|
||||
if (selected == nullptr) return false;
|
||||
const auto value = MaterialKey(selected->Arg(1), indirect);
|
||||
if (indirect.selector_shift == 0u && indirect.selector_bits == UINT32_MAX) return false;
|
||||
const auto* loaded = value.TryInstruction();
|
||||
if (loaded == nullptr || loaded->GetOpcode() != ValueOpcode::SelectU32 ||
|
||||
!EquivalentValue(m_program, selected->Arg(0), loaded->Arg(0))) return false;
|
||||
const auto* read = loaded->Arg(1).Resolve().TryInstruction();
|
||||
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32) return false;
|
||||
const auto& memory = m_program.memory_info[read->Flags<MemoryFlags>().index];
|
||||
uint32_t offset = 1u, scalar = 1u;
|
||||
if (memory.kind != ResourceKind::Buffer || memory.typed || memory.formatted ||
|
||||
!memory.idxen || memory.offen ||
|
||||
!ImmediateU32(read->Arg(2), offset) || offset != 0u ||
|
||||
!ImmediateU32(read->Arg(3), scalar) || scalar != 0u ||
|
||||
!EquivalentValue(m_program, selected->Arg(0), read->Arg(4)) ||
|
||||
!MakeRuntimeTableSource(*read, material_source)) return false;
|
||||
// GPU index arithmetic may wrap. Every record in the bounded V# is a candidate.
|
||||
indirect.material_source = InternSource(material_source);
|
||||
indirect.selector_first = Value(0u);
|
||||
indirect.key_count = material_source.dwords[2];
|
||||
indirect.selector_offset = memory.offset;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryMakeIndirectImage(Inst& handle, const DescriptorSource& descriptor,
|
||||
IndirectDescriptorPlan& plan) {
|
||||
if (handle.GetOpcode() != ValueOpcode::GetImageResource || handle.NumArgs() != 8u) return false;
|
||||
@@ -1699,17 +1722,19 @@ private:
|
||||
Value key;
|
||||
uint32_t table_offset = 0;
|
||||
uint32_t table_stride = 0;
|
||||
uint32_t table_immediate = 0;
|
||||
if (!MatchDescriptorTable(handle, descriptor, plan, table_source, key, table_offset,
|
||||
table_stride)) return false;
|
||||
table_stride, table_immediate)) return false;
|
||||
DescriptorSource material_source;
|
||||
DescriptorSource::IndirectDescriptor indirect;
|
||||
indirect.table_offset = table_offset;
|
||||
indirect.table_immediate = table_immediate;
|
||||
indirect.table_stride = table_stride;
|
||||
indirect.workgroup_axis = WorkgroupAxis(key);
|
||||
if (indirect.workgroup_axis != UINT32_MAX) {
|
||||
if (indirect.workgroup_axis != UINT32_MAX && table_source.dword_count == 2u) {
|
||||
// The descriptor also supplies dimensions to shader arithmetic. Keep its reads;
|
||||
// only the image handle is projected onto the bounded workgroup key.
|
||||
plan.reads.fill(nullptr);
|
||||
plan.retain_reads = true;
|
||||
} else if (table_source.dword_count == 2u) {
|
||||
if (table_stride != 32u) return false;
|
||||
const auto* selector = key.Resolve().TryInstruction();
|
||||
@@ -1728,36 +1753,21 @@ private:
|
||||
}
|
||||
if ((table_offset & 3u) != 0u ||
|
||||
(bitscan && table_offset > UINT32_MAX - (32u * 32u - 1u))) return false;
|
||||
} else if (!MatchUniformizedBufferKey(key, handle, indirect, material_source)) {
|
||||
auto* material_read = MaterialKey(key, indirect).TryInstruction();
|
||||
uint32_t material_memory_index = 0;
|
||||
const auto* memory = material_read != nullptr
|
||||
? ScalarReadMemory(*material_read, material_memory_index) : nullptr;
|
||||
if (memory == nullptr || memory->kind != ResourceKind::ScalarBuffer ||
|
||||
memory->offset > INT32_MAX || !MemoryIndexBelongsTo(material_memory_index, *material_read)) {
|
||||
return false;
|
||||
}
|
||||
if (!MatchMaterialOffset(material_read->Arg(1), indirect.selector_stride,
|
||||
indirect.selector_offset)) {
|
||||
return false;
|
||||
}
|
||||
const auto step = std::gcd<uint64_t>(indirect.selector_stride, uint64_t {1} << 32u);
|
||||
indirect.selector_offset =
|
||||
(static_cast<uint32_t>(indirect.selector_offset % step) & ~3u) + (memory->offset & ~3u);
|
||||
const auto* material_handle = material_read->Arg(0).Resolve().TryInstruction();
|
||||
if (material_handle == nullptr ||
|
||||
material_handle->GetOpcode() != ValueOpcode::GetBufferResource ||
|
||||
!MakeRuntimeTableSource(*material_read, material_source)) {
|
||||
return false;
|
||||
}
|
||||
indirect.material_source = InternSource(material_source);
|
||||
} else {
|
||||
// A bounded V# supplies the complete image table. Leave every GPU selector
|
||||
// and descriptor read in the shader; the host only translates table bytes.
|
||||
if (plan.split_offsets) return false;
|
||||
indirect.table_stride = 0u;
|
||||
indirect.table_offset = 0u;
|
||||
indirect.table_immediate = 0u;
|
||||
indirect.workgroup_axis = UINT32_MAX;
|
||||
plan.table_read = plan.reads[0];
|
||||
if (plan.table_indexed) indirect.table_record_bytes = table_immediate + 32u;
|
||||
else indirect.table_scalar = true;
|
||||
plan.retain_reads = true;
|
||||
}
|
||||
// This plan combines shader byte-offset additions with the scalar immediate.
|
||||
// A nonzero combined offset is exact only when neither can cross U32 wrap.
|
||||
const auto maximum_key = (UINT32_MAX >> indirect.selector_shift) & indirect.selector_bits;
|
||||
if (indirect.workgroup_axis == UINT32_MAX && table_source.dword_count == 4u &&
|
||||
table_offset != 0u && uint64_t {maximum_key} * table_stride + table_offset + 28u > UINT32_MAX)
|
||||
return false;
|
||||
|
||||
if (plan.retain_reads) plan.reads.fill(nullptr);
|
||||
indirect.table_source = InternSource(table_source);
|
||||
DescriptorSource image_source;
|
||||
image_source.dword_count = 8u;
|
||||
@@ -1791,9 +1801,10 @@ private:
|
||||
Value key;
|
||||
uint32_t table_offset = 0;
|
||||
uint32_t table_stride = 0;
|
||||
uint32_t table_immediate = 0;
|
||||
if (!MatchDescriptorTable(handle, descriptor, plan, table_source, key, table_offset,
|
||||
table_stride) || table_stride != 16u ||
|
||||
table_source.dword_count != 4u || table_offset != 0u) return false;
|
||||
table_stride, table_immediate) || table_stride != 16u ||
|
||||
table_source.dword_count != 4u || table_offset != 0u || table_immediate != 0u) return false;
|
||||
const auto* lane_read = key.Resolve().TryInstruction();
|
||||
if (lane_read == nullptr || lane_read->GetOpcode() != ValueOpcode::ReadFirstLane)
|
||||
return false;
|
||||
@@ -1825,9 +1836,9 @@ private:
|
||||
indirect.table_stride = table_stride;
|
||||
if (!MakeRuntimeTableSource(*read, material_source) ||
|
||||
!BoundedSelectedIndex(read->Arg(1), read->Arg(4), indirect)) return false;
|
||||
indirect.material_source = InternSource(material_source);
|
||||
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead{.source = InternSource(material_source),
|
||||
.offset = memory.offset + lane * 4u});
|
||||
indirect.table_source = InternSource(table_source);
|
||||
indirect.selector_offset = memory.offset + lane * 4u;
|
||||
DescriptorSource source;
|
||||
source.dword_count = 4u;
|
||||
source.dwords.fill(Value(0u));
|
||||
|
||||
@@ -121,6 +121,9 @@ int KernelEqueuePrivate::GetTriggeredEvents(KernelEvent* ev, int num) {
|
||||
event.event = event.pending_events.front();
|
||||
event.pending_events.pop_front();
|
||||
event.triggered = true;
|
||||
} else {
|
||||
// A persistent level is delivered again by the next wait, not this batch.
|
||||
break;
|
||||
}
|
||||
|
||||
if (ret >= num) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef CHEAT_FILE_H
|
||||
#define CHEAT_FILE_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
namespace Cheats {
|
||||
|
||||
[[nodiscard]] bool IsSupportedTitleId(const QString& title_id);
|
||||
[[nodiscard]] QString PlanPath(const QString& title_id);
|
||||
|
||||
// Local files retain their existing defaults; remote imports receive additional validation.
|
||||
[[nodiscard]] QString ParseDocument(const QByteArray& data, QJsonDocument* document);
|
||||
[[nodiscard]] QString ValidateImport(const QJsonDocument& document, const QString& title_id,
|
||||
const QString& version, const QString& process);
|
||||
[[nodiscard]] QJsonDocument WithSelection(const QJsonDocument& document,
|
||||
const QList<bool>& enabled);
|
||||
// Refuse to replace a file changed since it was loaded; empty data also permits creation.
|
||||
[[nodiscard]] QString SaveDocument(const QString& path, const QJsonDocument& document,
|
||||
const QByteArray& expected_data);
|
||||
|
||||
} // namespace Cheats
|
||||
|
||||
#endif // CHEAT_FILE_H
|
||||
@@ -0,0 +1,66 @@
|
||||
#ifndef CHEAT_REPOSITORY_H
|
||||
#define CHEAT_REPOSITORY_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QList>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
|
||||
#include <functional>
|
||||
|
||||
class QNetworkReply;
|
||||
|
||||
namespace Cheats {
|
||||
|
||||
struct RemoteFile {
|
||||
QString name;
|
||||
QString title;
|
||||
QString version;
|
||||
QString format;
|
||||
|
||||
[[nodiscard]] bool IsSupported() const;
|
||||
[[nodiscard]] QUrl Url() const;
|
||||
};
|
||||
|
||||
struct CatalogResult {
|
||||
QList<RemoteFile> files;
|
||||
QString error;
|
||||
};
|
||||
|
||||
[[nodiscard]] CatalogResult ParseIndex(const QByteArray& data, const QString& format,
|
||||
const QString& title_id);
|
||||
|
||||
class CheatRepository final: public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CheatRepository(QObject* parent = nullptr);
|
||||
void Load(const QString& title_id);
|
||||
void Fetch(const RemoteFile& file);
|
||||
void CancelFetch();
|
||||
|
||||
signals:
|
||||
void CatalogLoaded(QList<Cheats::RemoteFile> files, QString error);
|
||||
void FileLoaded(QByteArray data, QString error);
|
||||
|
||||
private:
|
||||
QNetworkReply* Request(const QUrl& url, qint64 limit,
|
||||
std::function<void(QByteArray, QString)> complete);
|
||||
|
||||
QNetworkAccessManager m_network;
|
||||
QList<QNetworkReply*> m_catalog_replies;
|
||||
QPointer<QNetworkReply> m_file_reply;
|
||||
QList<RemoteFile> m_files;
|
||||
QStringList m_errors;
|
||||
int m_catalog_generation = 0;
|
||||
int m_fetch_generation = 0;
|
||||
int m_pending = 0;
|
||||
};
|
||||
|
||||
} // namespace Cheats
|
||||
|
||||
#endif // CHEAT_REPOSITORY_H
|
||||
@@ -1,30 +1,51 @@
|
||||
#ifndef PATCHES_DIALOG_H
|
||||
#define PATCHES_DIALOG_H
|
||||
|
||||
#include "cheatRepository.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDialog>
|
||||
#include <QJsonDocument>
|
||||
#include <QString>
|
||||
|
||||
class Configuration;
|
||||
class QLabel;
|
||||
class QListWidget;
|
||||
class QPushButton;
|
||||
class QTabWidget;
|
||||
class QTreeWidget;
|
||||
|
||||
class PatchesDialog final: public QDialog {
|
||||
public:
|
||||
explicit PatchesDialog(const Configuration& game, QWidget* parent = nullptr);
|
||||
~PatchesDialog() override = default;
|
||||
|
||||
[[nodiscard]] static bool IsSupportedTitleId(const QString& title_id);
|
||||
[[nodiscard]] static QString PatchPlanPath(const QString& title_id);
|
||||
|
||||
private:
|
||||
void Load();
|
||||
void Save();
|
||||
void LoadRemote();
|
||||
void PreviewRemote();
|
||||
void ImportRemote();
|
||||
|
||||
QString m_title_id;
|
||||
QListWidget* m_patches = nullptr;
|
||||
QLabel* m_status = nullptr;
|
||||
QPushButton* m_apply = nullptr;
|
||||
QString m_title_id;
|
||||
QString m_version;
|
||||
QString m_process;
|
||||
QByteArray m_local_data;
|
||||
QJsonDocument m_local_document;
|
||||
QJsonDocument m_remote_document;
|
||||
bool m_local_readable = false;
|
||||
Cheats::CheatRepository* m_repository = nullptr;
|
||||
QList<Cheats::RemoteFile> m_remote_files;
|
||||
QTabWidget* m_tabs = nullptr;
|
||||
QTreeWidget* m_files = nullptr;
|
||||
QListWidget* m_preview = nullptr;
|
||||
QLabel* m_remote_status = nullptr;
|
||||
QLabel* m_preview_status = nullptr;
|
||||
QPushButton* m_refresh = nullptr;
|
||||
QPushButton* m_import = nullptr;
|
||||
QListWidget* m_patches = nullptr;
|
||||
QLabel* m_status = nullptr;
|
||||
QPushButton* m_apply = nullptr;
|
||||
};
|
||||
|
||||
#endif // PATCHES_DIALOG_H
|
||||
@@ -0,0 +1,180 @@
|
||||
#include "cheatFile.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QSaveFile>
|
||||
|
||||
namespace Cheats {
|
||||
|
||||
namespace {
|
||||
|
||||
QString Tr(const char* text) {
|
||||
return QCoreApplication::translate("Cheats", text);
|
||||
}
|
||||
|
||||
QString ValidateDocument(const QJsonDocument& document) {
|
||||
if (!document.isObject()) {
|
||||
return Tr("Invalid cheat JSON.");
|
||||
}
|
||||
const auto mods = document.object().value(QStringLiteral("mods"));
|
||||
if (!mods.isArray()) {
|
||||
return Tr("Cheat JSON has no \"mods\" array.");
|
||||
}
|
||||
for (const auto& value: mods.toArray()) {
|
||||
const auto mod = value.toObject();
|
||||
const auto name = mod.value(QStringLiteral("name"));
|
||||
const auto enabled = mod.value(QStringLiteral("enabled"));
|
||||
if (!value.isObject() || !name.isString() || name.toString().isEmpty() ||
|
||||
(!enabled.isUndefined() && !enabled.isBool())) {
|
||||
return Tr("Invalid cheat entry.");
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsSupportedTitleId(const QString& title_id) {
|
||||
static const QRegularExpression pattern(QStringLiteral("\\APPSA[0-9]{5}\\z"));
|
||||
return pattern.match(title_id.trimmed().toUpper()).hasMatch();
|
||||
}
|
||||
|
||||
QString PlanPath(const QString& title_id) {
|
||||
if (!IsSupportedTitleId(title_id)) {
|
||||
return {};
|
||||
}
|
||||
return QDir(QCoreApplication::applicationDirPath())
|
||||
.filePath(QStringLiteral("_Patches/%1.json").arg(title_id.trimmed().toUpper()));
|
||||
}
|
||||
|
||||
QString ParseDocument(const QByteArray& data, QJsonDocument* document) {
|
||||
const auto parsed = QJsonDocument::fromJson(data);
|
||||
const auto error = ValidateDocument(parsed);
|
||||
*document = error.isEmpty() ? parsed : QJsonDocument();
|
||||
return error;
|
||||
}
|
||||
|
||||
QString ValidateImport(const QJsonDocument& document, const QString& title_id,
|
||||
const QString& version, const QString& process) {
|
||||
const auto parse_error = ValidateDocument(document);
|
||||
if (!parse_error.isEmpty()) {
|
||||
return parse_error;
|
||||
}
|
||||
const auto root = document.object();
|
||||
if (!IsSupportedTitleId(title_id) ||
|
||||
root.value(QStringLiteral("id"))
|
||||
.toString()
|
||||
.compare(title_id.trimmed(), Qt::CaseInsensitive) != 0) {
|
||||
return Tr("The cheat file does not match this game's title ID.");
|
||||
}
|
||||
if (version.isEmpty() || root.value(QStringLiteral("version")).toString() != version) {
|
||||
return Tr("The cheat file does not match this game's version (%1).").arg(version);
|
||||
}
|
||||
if (process.isEmpty() ||
|
||||
root.value(QStringLiteral("process")).toString().compare(process, Qt::CaseInsensitive) !=
|
||||
0) {
|
||||
return Tr("The cheat file does not match this game's executable (%1).").arg(process);
|
||||
}
|
||||
static const QRegularExpression offset_pattern(
|
||||
QStringLiteral("\\A(?:0[xX])?[0-9a-fA-F]{1,16}\\z"));
|
||||
static const QRegularExpression bytes_pattern(QStringLiteral("\\A(?:[0-9a-fA-F]{2})+\\z"));
|
||||
const auto mods = root.value(QStringLiteral("mods")).toArray();
|
||||
if (mods.isEmpty()) {
|
||||
return Tr("The cheat file contains no cheats.");
|
||||
}
|
||||
if (root.contains(QStringLiteral("master"))) {
|
||||
return Tr("This JSON uses master codes, which are not supported yet.");
|
||||
}
|
||||
for (const auto& value: mods) {
|
||||
const auto mod = value.toObject();
|
||||
const auto memory = mod.value(QStringLiteral("memory"));
|
||||
if (mod.contains(QStringLiteral("module_name"))) {
|
||||
return Tr("This JSON uses module-specific cheats, which are not supported yet.");
|
||||
}
|
||||
if (!memory.isArray() || memory.toArray().isEmpty()) {
|
||||
return Tr("Unsupported memory entries in cheat \"%1\".")
|
||||
.arg(mod.value(QStringLiteral("name")).toString());
|
||||
}
|
||||
for (const auto& entry: memory.toArray()) {
|
||||
const auto write = entry.toObject();
|
||||
const auto offset = write.value(QStringLiteral("offset")).toString();
|
||||
const auto off = write.value(QStringLiteral("off")).toString();
|
||||
const auto on = write.value(QStringLiteral("on")).toString();
|
||||
const auto absolute = write.value(QStringLiteral("absolute"));
|
||||
const auto expected = write.value(QStringLiteral("expected"));
|
||||
if ((!absolute.isUndefined() && (!absolute.isBool() || absolute.toBool())) ||
|
||||
(!expected.isUndefined() &&
|
||||
(!expected.isString() ||
|
||||
expected.toString().compare(off, Qt::CaseInsensitive) != 0)) ||
|
||||
write.contains(QStringLiteral("section")) ||
|
||||
write.contains(QStringLiteral("sectionName")) ||
|
||||
write.contains(QStringLiteral("sectionProtection")) ||
|
||||
write.contains(QStringLiteral("logicalOffset")) ||
|
||||
write.contains(QStringLiteral("sectionOffset"))) {
|
||||
return Tr("This JSON uses memory addressing features that are not supported yet.");
|
||||
}
|
||||
if (!entry.isObject() || !offset_pattern.match(offset).hasMatch() ||
|
||||
!bytes_pattern.match(off).hasMatch() || !bytes_pattern.match(on).hasMatch() ||
|
||||
off.size() != on.size()) {
|
||||
return Tr("Unsupported memory entries in cheat \"%1\".")
|
||||
.arg(mod.value(QStringLiteral("name")).toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonDocument WithSelection(const QJsonDocument& document, const QList<bool>& enabled) {
|
||||
auto root = document.object();
|
||||
auto mods = root.value(QStringLiteral("mods")).toArray();
|
||||
if (mods.size() != enabled.size()) {
|
||||
return {};
|
||||
}
|
||||
for (qsizetype index = 0; index < mods.size(); ++index) {
|
||||
auto mod = mods[index].toObject();
|
||||
mod.insert(QStringLiteral("enabled"), enabled[index]);
|
||||
mods[index] = mod;
|
||||
}
|
||||
root.insert(QStringLiteral("mods"), mods);
|
||||
return QJsonDocument(root);
|
||||
}
|
||||
|
||||
QString SaveDocument(const QString& path, const QJsonDocument& document,
|
||||
const QByteArray& expected_data) {
|
||||
if (path.isEmpty() || !document.isObject()) {
|
||||
return Tr("Invalid cheat file.");
|
||||
}
|
||||
QFile input(path);
|
||||
if (input.exists()) {
|
||||
if (!input.open(QIODevice::ReadOnly)) {
|
||||
return Tr("Could not read cheat file: %1").arg(input.errorString());
|
||||
}
|
||||
const auto current = input.readAll();
|
||||
if (input.error() != QFile::NoError) {
|
||||
return Tr("Could not read cheat file: %1").arg(input.errorString());
|
||||
}
|
||||
if (current != expected_data) {
|
||||
return Tr("Cheat file changed; reload the dialog.");
|
||||
}
|
||||
} else if (!expected_data.isEmpty()) {
|
||||
return Tr("Cheat file changed; reload the dialog.");
|
||||
}
|
||||
input.close();
|
||||
if (!QDir().mkpath(QFileInfo(path).absolutePath())) {
|
||||
return Tr("Could not create the local cheats directory.");
|
||||
}
|
||||
QSaveFile output(path);
|
||||
const auto data = document.toJson();
|
||||
if (!output.open(QIODevice::WriteOnly) || output.write(data) != data.size() ||
|
||||
!output.commit()) {
|
||||
return Tr("Could not save cheat file: %1").arg(output.errorString());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace Cheats
|
||||
@@ -0,0 +1,200 @@
|
||||
#include "cheatRepository.h"
|
||||
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
#include <QTimer>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
namespace Cheats {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr qint64 kIndexLimit = 2 * 1024 * 1024;
|
||||
constexpr qint64 kFileLimit = 8 * 1024 * 1024;
|
||||
|
||||
const QString kBaseUrl = QStringLiteral(
|
||||
"https://raw.githubusercontent.com/TeeKay87/HEN-Cheats-Collection/master/cheats/");
|
||||
|
||||
bool IsFormat(const QString& format) {
|
||||
return format == QStringLiteral("json") || format == QStringLiteral("mc4") ||
|
||||
format == QStringLiteral("shn");
|
||||
}
|
||||
|
||||
QRegularExpressionMatch MatchName(const QString& name) {
|
||||
static const QRegularExpression pattern(QStringLiteral(
|
||||
"\\A([A-Z]{4}[0-9]{5})_([0-9]+(?:\\.[0-9]+)+)_[A-Za-z0-9_.-]+\\.(json|mc4|shn)\\z"));
|
||||
return pattern.match(name);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool RemoteFile::IsSupported() const {
|
||||
return format == QStringLiteral("json") && !Url().isEmpty();
|
||||
}
|
||||
|
||||
QUrl RemoteFile::Url() const {
|
||||
const auto match = MatchName(name);
|
||||
if (!IsFormat(format) || !match.hasMatch() || match.captured(3) != format) {
|
||||
return {};
|
||||
}
|
||||
return QUrl(kBaseUrl + format + QLatin1Char('/') + name);
|
||||
}
|
||||
|
||||
CatalogResult ParseIndex(const QByteArray& data, const QString& format, const QString& title_id) {
|
||||
CatalogResult result;
|
||||
if (!IsFormat(format) || data.isEmpty() || data.size() > kIndexLimit) {
|
||||
result.error = CheatRepository::tr("Invalid cheat index.");
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto id = title_id.trimmed().toUpper();
|
||||
QSet<QString> seen;
|
||||
int invalid = 0;
|
||||
auto text = QString::fromUtf8(data);
|
||||
if (text.startsWith(QChar(0xfeff))) {
|
||||
text.remove(0, 1);
|
||||
}
|
||||
for (const auto& line: text.split(QLatin1Char('\n'))) {
|
||||
const auto trimmed = line.trimmed();
|
||||
if (trimmed.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
const auto separator = trimmed.indexOf(QLatin1Char('='));
|
||||
const auto name = trimmed.left(separator);
|
||||
const auto match = MatchName(name);
|
||||
if (separator < 0 || !match.hasMatch() || match.captured(3) != format) {
|
||||
invalid++;
|
||||
continue;
|
||||
}
|
||||
if (match.captured(1) != id || seen.contains(name)) {
|
||||
continue;
|
||||
}
|
||||
seen.insert(name);
|
||||
result.files.append(
|
||||
{name, trimmed.mid(separator + 1).trimmed(), match.captured(2), format});
|
||||
}
|
||||
if (invalid != 0) {
|
||||
result.error = CheatRepository::tr("Skipped %1 invalid cheat index entries.").arg(invalid);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
CheatRepository::CheatRepository(QObject* parent): QObject(parent), m_network(this) {}
|
||||
|
||||
QNetworkReply* CheatRepository::Request(const QUrl& url, qint64 limit,
|
||||
std::function<void(QByteArray, QString)> complete) {
|
||||
QNetworkRequest request(url);
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
|
||||
QNetworkRequest::SameOriginRedirectPolicy);
|
||||
request.setRawHeader("User-Agent", "Kyty-Launcher");
|
||||
request.setTransferTimeout(15000);
|
||||
auto* reply = m_network.get(request);
|
||||
reply->setReadBufferSize(limit + 1);
|
||||
connect(reply, &QNetworkReply::readyRead, this, [reply, limit]() {
|
||||
if (reply->bytesAvailable() > limit) {
|
||||
reply->setProperty("too_large", true);
|
||||
reply->abort();
|
||||
}
|
||||
});
|
||||
QTimer::singleShot(30000, reply, [reply]() {
|
||||
if (!reply->isFinished()) {
|
||||
reply->setProperty("timed_out", true);
|
||||
reply->abort();
|
||||
}
|
||||
});
|
||||
connect(reply, &QNetworkReply::finished, this,
|
||||
[this, reply, limit, complete = std::move(complete)]() {
|
||||
QString error;
|
||||
if (reply->property("too_large").toBool() || reply->bytesAvailable() > limit) {
|
||||
error = tr("Cheat download exceeds the size limit.");
|
||||
} else if (reply->property("timed_out").toBool()) {
|
||||
error = tr("Cheat download timed out.");
|
||||
} else if (reply->error() != QNetworkReply::NoError) {
|
||||
error = reply->errorString();
|
||||
} else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() !=
|
||||
200) {
|
||||
error = tr("Unexpected response from the cheat repository.");
|
||||
}
|
||||
const auto data = error.isEmpty() ? reply->readAll() : QByteArray();
|
||||
m_catalog_replies.removeAll(reply);
|
||||
if (m_file_reply == reply) {
|
||||
m_file_reply.clear();
|
||||
}
|
||||
reply->deleteLater();
|
||||
complete(data, error);
|
||||
});
|
||||
return reply;
|
||||
}
|
||||
|
||||
void CheatRepository::Load(const QString& title_id) {
|
||||
const auto generation = ++m_catalog_generation;
|
||||
for (auto* reply: std::exchange(m_catalog_replies, {})) {
|
||||
reply->abort();
|
||||
}
|
||||
CancelFetch();
|
||||
m_files.clear();
|
||||
m_errors.clear();
|
||||
m_pending = 3;
|
||||
for (const auto& format:
|
||||
{QStringLiteral("json"), QStringLiteral("mc4"), QStringLiteral("shn")}) {
|
||||
auto* reply =
|
||||
Request(QUrl(kBaseUrl + format + QStringLiteral(".txt")), kIndexLimit,
|
||||
[this, generation, format, title_id](const QByteArray& data, QString error) {
|
||||
if (generation != m_catalog_generation) {
|
||||
return;
|
||||
}
|
||||
if (error.isEmpty()) {
|
||||
auto result = ParseIndex(data, format, title_id);
|
||||
m_files.append(result.files);
|
||||
error = result.error;
|
||||
}
|
||||
if (!error.isEmpty()) {
|
||||
m_errors.append(format.toUpper() + QStringLiteral(": ") + error);
|
||||
}
|
||||
if (--m_pending == 0) {
|
||||
std::sort(m_files.begin(), m_files.end(),
|
||||
[](const RemoteFile& left, const RemoteFile& right) {
|
||||
if (left.version != right.version) {
|
||||
return left.version > right.version;
|
||||
}
|
||||
if (left.format != right.format) {
|
||||
return left.format < right.format;
|
||||
}
|
||||
return left.name < right.name;
|
||||
});
|
||||
emit CatalogLoaded(m_files, m_errors.join(QLatin1Char('\n')));
|
||||
}
|
||||
});
|
||||
m_catalog_replies.append(reply);
|
||||
}
|
||||
}
|
||||
|
||||
void CheatRepository::Fetch(const RemoteFile& file) {
|
||||
CancelFetch();
|
||||
if (!file.IsSupported()) {
|
||||
emit FileLoaded({}, tr("Only JSON cheat files are supported."));
|
||||
return;
|
||||
}
|
||||
const auto generation = m_fetch_generation;
|
||||
m_file_reply = Request(file.Url(), kFileLimit,
|
||||
[this, generation](const QByteArray& data, const QString& error) {
|
||||
if (generation == m_fetch_generation) {
|
||||
emit FileLoaded(data, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void CheatRepository::CancelFetch() {
|
||||
m_fetch_generation++;
|
||||
if (m_file_reply != nullptr) {
|
||||
auto* reply = m_file_reply.data();
|
||||
m_file_reply.clear();
|
||||
reply->abort();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Cheats
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "configurationListWidget.h"
|
||||
|
||||
#include "cheatFile.h"
|
||||
#include "common/archive.h"
|
||||
#include "compatibilityDatabase.h"
|
||||
#include "configuration.h"
|
||||
@@ -1015,7 +1016,7 @@ void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
|
||||
}
|
||||
});
|
||||
action_patches->setVisible(item != nullptr &&
|
||||
PatchesDialog::IsSupportedTitleId(item->GetInfo().title_id));
|
||||
Cheats::IsSupportedTitleId(item->GetInfo().title_id));
|
||||
QAction* action_remove_save_data =
|
||||
menu.addAction(style()->standardIcon(QStyle::SP_DialogDiscardButton),
|
||||
tr("Remove save data..."), this, SLOT(remove_save_data()));
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#include "mainDialog.h"
|
||||
|
||||
#include "cheatFile.h"
|
||||
#include "configuration.h"
|
||||
#include "configurationItem.h"
|
||||
#include "configurationListWidget.h"
|
||||
#include "controllerLightbar.h"
|
||||
#include "gameContent.h"
|
||||
#include "patchesDialog.h"
|
||||
#include "updateChecker.h"
|
||||
|
||||
#include <QApplication>
|
||||
@@ -285,7 +285,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
||||
}
|
||||
args << "--game" << game;
|
||||
|
||||
const auto patch_plan = PatchesDialog::PatchPlanPath(info.title_id);
|
||||
const auto patch_plan = Cheats::PlanPath(info.title_id);
|
||||
if (QFileInfo::exists(patch_plan)) {
|
||||
args << "--game-patch" << patch_plan;
|
||||
}
|
||||
|
||||
@@ -1,140 +1,309 @@
|
||||
#include "patchesDialog.h"
|
||||
|
||||
#include "cheatFile.h"
|
||||
#include "configuration.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QHeaderView>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSaveFile>
|
||||
#include <QSplitter>
|
||||
#include <QStringList>
|
||||
#include <QTabWidget>
|
||||
#include <QTreeWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
void ShowMods(QListWidget* list, const QJsonDocument& document, bool checkable) {
|
||||
list->clear();
|
||||
for (const auto& value: document.object().value(QStringLiteral("mods")).toArray()) {
|
||||
const auto mod = value.toObject();
|
||||
auto* item = new QListWidgetItem(mod.value(QStringLiteral("name")).toString(), list);
|
||||
if (checkable) {
|
||||
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||||
item->setCheckState(mod.value(QStringLiteral("enabled")).toBool(true) ? Qt::Checked
|
||||
: Qt::Unchecked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QLabel* StatusLabel(QWidget* parent) {
|
||||
auto* label = new QLabel(parent);
|
||||
label->setWordWrap(true);
|
||||
label->setTextFormat(Qt::PlainText);
|
||||
return label;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PatchesDialog::PatchesDialog(const Configuration& game, QWidget* parent)
|
||||
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()) {
|
||||
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()), m_version(game.gameVersion),
|
||||
m_process(QFileInfo(game.elf.isEmpty() ? QStringLiteral("eboot.bin") : game.elf).fileName()) {
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
setWindowTitle(tr("Cheats (experimental) - %1").arg(game.name));
|
||||
resize(640, 480);
|
||||
resize(800, 640);
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
m_patches = new QListWidget(this);
|
||||
m_status = new QLabel(this);
|
||||
m_apply = new QPushButton(tr("Apply selection"), this);
|
||||
auto* close = new QPushButton(tr("Close"), this);
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
auto* game_label = StatusLabel(this);
|
||||
game_label->setText(tr("%1 · Installed version: %2")
|
||||
.arg(m_title_id, m_version.isEmpty() ? tr("Unknown") : m_version));
|
||||
layout->addWidget(game_label);
|
||||
m_tabs = new QTabWidget(this);
|
||||
layout->addWidget(m_tabs);
|
||||
|
||||
m_status->setWordWrap(true);
|
||||
layout->addWidget(m_patches);
|
||||
layout->addWidget(m_status);
|
||||
auto* remote = new QWidget(m_tabs);
|
||||
auto* remote_layout = new QVBoxLayout(remote);
|
||||
m_tabs->addTab(remote, tr("Remote"));
|
||||
auto* credits = new QLabel(
|
||||
tr("Cheats by <a href=\"https://github.com/TeeKay87/HEN-Cheats-Collection\">TeeKay87 / HEN "
|
||||
"Cheats Collection</a> and its contributors."),
|
||||
remote);
|
||||
credits->setOpenExternalLinks(true);
|
||||
credits->setWordWrap(true);
|
||||
remote_layout->addWidget(credits);
|
||||
auto* support = StatusLabel(remote);
|
||||
support->setText(tr("Only JSON cheats are supported. MC4 and SHN files are shown disabled. "
|
||||
"Select a JSON file to preview its cheats."));
|
||||
remote_layout->addWidget(support);
|
||||
auto* split = new QSplitter(Qt::Vertical, remote);
|
||||
m_files = new QTreeWidget(split);
|
||||
m_files->setHeaderLabels({tr("Version"), tr("Format"), tr("File")});
|
||||
m_files->setRootIsDecorated(false);
|
||||
m_files->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_files->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||||
m_files->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
|
||||
m_files->header()->setSectionResizeMode(2, QHeaderView::Stretch);
|
||||
m_preview = new QListWidget(split);
|
||||
remote_layout->addWidget(split, 1);
|
||||
m_remote_status = StatusLabel(remote);
|
||||
m_preview_status = StatusLabel(remote);
|
||||
remote_layout->addWidget(m_remote_status);
|
||||
remote_layout->addWidget(m_preview_status);
|
||||
auto* remote_buttons = new QDialogButtonBox(remote);
|
||||
m_refresh = remote_buttons->addButton(tr("Refresh"), QDialogButtonBox::ActionRole);
|
||||
m_import = remote_buttons->addButton(tr("Import to Local"), QDialogButtonBox::ActionRole);
|
||||
m_import->setEnabled(false);
|
||||
remote_layout->addWidget(remote_buttons);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(this);
|
||||
buttons->addButton(m_apply, QDialogButtonBox::ActionRole);
|
||||
buttons->addButton(close, QDialogButtonBox::RejectRole);
|
||||
auto* local = new QWidget(m_tabs);
|
||||
auto* local_layout = new QVBoxLayout(local);
|
||||
m_tabs->addTab(local, tr("Local"));
|
||||
m_patches = new QListWidget(local);
|
||||
m_status = StatusLabel(local);
|
||||
local_layout->addWidget(m_patches, 1);
|
||||
local_layout->addWidget(m_status);
|
||||
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);
|
||||
local_layout->addWidget(local_buttons);
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
|
||||
layout->addWidget(buttons);
|
||||
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close);
|
||||
connect(reload, &QPushButton::clicked, this, &PatchesDialog::Load);
|
||||
connect(m_apply, &QPushButton::clicked, this, &PatchesDialog::Save);
|
||||
connect(close, &QPushButton::clicked, this, &QDialog::close);
|
||||
connect(m_refresh, &QPushButton::clicked, this, &PatchesDialog::LoadRemote);
|
||||
connect(m_import, &QPushButton::clicked, this, &PatchesDialog::ImportRemote);
|
||||
connect(m_files, &QTreeWidget::currentItemChanged, this, &PatchesDialog::PreviewRemote);
|
||||
|
||||
m_repository = new Cheats::CheatRepository(this);
|
||||
connect(
|
||||
m_repository, &Cheats::CheatRepository::CatalogLoaded, this,
|
||||
[this](QList<Cheats::RemoteFile> files, const QString& error) {
|
||||
std::stable_sort(files.begin(), files.end(), [this](const auto& a, const auto& b) {
|
||||
return (a.version == m_version) > (b.version == m_version);
|
||||
});
|
||||
m_remote_files = std::move(files);
|
||||
for (qsizetype index = 0; index < m_remote_files.size(); ++index) {
|
||||
const auto& file = m_remote_files[index];
|
||||
auto* item = new QTreeWidgetItem(
|
||||
m_files, {file.version == m_version ? tr("%1 (installed)").arg(file.version)
|
||||
: file.version,
|
||||
file.format.toUpper(), file.name});
|
||||
item->setData(0, Qt::UserRole, index);
|
||||
for (int column = 0; column < 3; ++column) {
|
||||
item->setToolTip(column, file.IsSupported()
|
||||
? file.title
|
||||
: tr("%1 — %2 is not supported.")
|
||||
.arg(file.title, file.format.toUpper()));
|
||||
}
|
||||
if (!file.IsSupported()) {
|
||||
item->setFlags(item->flags() & ~(Qt::ItemIsEnabled | Qt::ItemIsSelectable));
|
||||
}
|
||||
}
|
||||
auto status =
|
||||
m_remote_files.isEmpty() && error.isEmpty()
|
||||
? tr("No remote cheats found for %1.").arg(m_title_id)
|
||||
: tr("%1 remote file(s) for %2.").arg(m_remote_files.size()).arg(m_title_id);
|
||||
if (!error.isEmpty()) {
|
||||
status = tr("%1\nCould not load the complete collection: %2\nUse Refresh to retry.")
|
||||
.arg(status, error);
|
||||
}
|
||||
m_remote_status->setText(status);
|
||||
m_refresh->setEnabled(true);
|
||||
});
|
||||
connect(
|
||||
m_repository, &Cheats::CheatRepository::FileLoaded, this,
|
||||
[this](const QByteArray& data, QString error) {
|
||||
if (error.isEmpty()) {
|
||||
error = Cheats::ParseDocument(data, &m_remote_document);
|
||||
}
|
||||
if (!error.isEmpty()) {
|
||||
m_preview_status->setText(
|
||||
tr("Could not preview this file: %1\nSelect another file or refresh to retry.")
|
||||
.arg(error));
|
||||
return;
|
||||
}
|
||||
ShowMods(m_preview, m_remote_document, false);
|
||||
error = Cheats::ValidateImport(m_remote_document, m_title_id, m_version, m_process);
|
||||
const auto root = m_remote_document.object();
|
||||
QStringList creators;
|
||||
for (const auto& value: root.value(QStringLiteral("credits")).toArray()) {
|
||||
if (value.isString()) {
|
||||
creators.append(value.toString());
|
||||
}
|
||||
}
|
||||
const auto metadata =
|
||||
tr("%1\nCredits: %2 · Version: %3")
|
||||
.arg(root.value(QStringLiteral("name")).toString(),
|
||||
creators.isEmpty() ? root.value(QStringLiteral("author")).toString()
|
||||
: creators.join(QStringLiteral(", ")),
|
||||
root.value(QStringLiteral("version")).toString());
|
||||
m_preview_status->setText(
|
||||
metadata + QStringLiteral("\n\n") +
|
||||
(error.isEmpty() ? tr("Import to Local, then select the cheats to enable. Changes "
|
||||
"take effect on the next game launch.")
|
||||
: error));
|
||||
m_import->setEnabled(error.isEmpty());
|
||||
});
|
||||
Load();
|
||||
}
|
||||
|
||||
bool PatchesDialog::IsSupportedTitleId(const QString& title_id) {
|
||||
return title_id.trimmed().toUpper().startsWith(QStringLiteral("PPSA"));
|
||||
}
|
||||
|
||||
QString PatchesDialog::PatchPlanPath(const QString& title_id) {
|
||||
return QDir(QCoreApplication::applicationDirPath())
|
||||
.filePath(QStringLiteral("_Patches/%1.json").arg(title_id.trimmed().toUpper()));
|
||||
LoadRemote();
|
||||
}
|
||||
|
||||
void PatchesDialog::Load() {
|
||||
QFile file(PatchPlanPath(m_title_id));
|
||||
m_patches->clear();
|
||||
m_local_data.clear();
|
||||
m_local_document = {};
|
||||
m_local_readable = false;
|
||||
m_apply->setEnabled(false);
|
||||
const auto path = Cheats::PlanPath(m_title_id);
|
||||
if (path.isEmpty()) {
|
||||
m_status->setText(tr("Unsupported title ID."));
|
||||
return;
|
||||
}
|
||||
QFile file(path);
|
||||
if (!file.exists()) {
|
||||
m_local_readable = true;
|
||||
m_status->setText(tr("No local cheat file: %1").arg(path));
|
||||
return;
|
||||
}
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
m_status->setText(tr("No local cheat file: %1").arg(file.fileName()));
|
||||
m_apply->setEnabled(false);
|
||||
m_status->setText(tr("Could not read cheat file: %1").arg(file.errorString()));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto document = QJsonDocument::fromJson(file.readAll());
|
||||
if (document.isNull() || !document.isObject()) {
|
||||
m_status->setText(tr("Invalid cheat JSON in %1.").arg(file.fileName()));
|
||||
m_apply->setEnabled(false);
|
||||
m_local_data = file.readAll();
|
||||
if (file.error() != QFile::NoError) {
|
||||
m_status->setText(tr("Could not read cheat file: %1").arg(file.errorString()));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto root = document.object();
|
||||
if (!root.value(QStringLiteral("mods")).isArray()) {
|
||||
m_status->setText(tr("Cheat JSON has no \"mods\" array in %1.").arg(file.fileName()));
|
||||
m_apply->setEnabled(false);
|
||||
m_local_readable = true;
|
||||
const auto error = Cheats::ParseDocument(m_local_data, &m_local_document);
|
||||
if (!error.isEmpty()) {
|
||||
m_status->setText(tr("%1\n%2").arg(error, path));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto mods = root.value(QStringLiteral("mods")).toArray();
|
||||
for (const auto& value: mods) {
|
||||
if (!value.isObject()) {
|
||||
m_status->setText(tr("Invalid cheat entry in %1.").arg(file.fileName()));
|
||||
m_apply->setEnabled(false);
|
||||
m_patches->clear();
|
||||
return;
|
||||
}
|
||||
const auto mod = value.toObject();
|
||||
const auto name = mod.value(QStringLiteral("name"));
|
||||
const auto enabled = mod.value(QStringLiteral("enabled"));
|
||||
if (!name.isString() || name.toString().isEmpty() ||
|
||||
(!enabled.isUndefined() && !enabled.isBool())) {
|
||||
m_status->setText(tr("Invalid cheat entry in %1.").arg(file.fileName()));
|
||||
m_apply->setEnabled(false);
|
||||
m_patches->clear();
|
||||
return;
|
||||
}
|
||||
auto* item = new QListWidgetItem(name.toString(), m_patches);
|
||||
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||||
item->setCheckState(enabled.toBool(true) ? Qt::Checked : Qt::Unchecked);
|
||||
}
|
||||
|
||||
m_apply->setEnabled(!mods.isEmpty());
|
||||
m_status->setText(tr("Loaded %1 cheat(s) from %2.").arg(mods.size()).arg(file.fileName()));
|
||||
ShowMods(m_patches, m_local_document, true);
|
||||
m_apply->setEnabled(m_patches->count() > 0);
|
||||
m_status->setText(tr("Loaded %1 cheat(s) from %2.").arg(m_patches->count()).arg(path));
|
||||
}
|
||||
|
||||
void PatchesDialog::Save() {
|
||||
const auto path = PatchPlanPath(m_title_id);
|
||||
QFile input(path);
|
||||
if (!input.open(QIODevice::ReadOnly)) {
|
||||
QList<bool> enabled;
|
||||
for (int index = 0; index < m_patches->count(); ++index) {
|
||||
enabled.append(m_patches->item(index)->checkState() == Qt::Checked);
|
||||
}
|
||||
const auto document = Cheats::WithSelection(m_local_document, enabled);
|
||||
const auto error = Cheats::SaveDocument(Cheats::PlanPath(m_title_id), document, m_local_data);
|
||||
if (!error.isEmpty()) {
|
||||
m_status->setText(error);
|
||||
return;
|
||||
}
|
||||
m_local_document = document;
|
||||
m_local_data = document.toJson();
|
||||
m_status->setText(tr("Cheat selection saved. Changes take effect on the next game launch."));
|
||||
}
|
||||
|
||||
auto document = QJsonDocument::fromJson(input.readAll());
|
||||
input.close();
|
||||
const auto changed_message = tr("Cheat file changed; reload the dialog.");
|
||||
if (!document.isObject()) {
|
||||
m_status->setText(changed_message);
|
||||
void PatchesDialog::LoadRemote() {
|
||||
m_repository->CancelFetch();
|
||||
m_files->clear();
|
||||
m_remote_files.clear();
|
||||
m_preview->clear();
|
||||
m_remote_document = {};
|
||||
m_import->setEnabled(false);
|
||||
m_refresh->setEnabled(false);
|
||||
m_preview_status->clear();
|
||||
m_remote_status->setText(tr("Loading remote cheats…"));
|
||||
m_repository->Load(m_title_id);
|
||||
}
|
||||
|
||||
void PatchesDialog::PreviewRemote() {
|
||||
m_repository->CancelFetch();
|
||||
m_preview->clear();
|
||||
m_remote_document = {};
|
||||
m_import->setEnabled(false);
|
||||
m_preview_status->clear();
|
||||
const auto* item = m_files->currentItem();
|
||||
if (item == nullptr) {
|
||||
return;
|
||||
}
|
||||
const auto index = item->data(0, Qt::UserRole).toInt();
|
||||
if (index < 0 || index >= m_remote_files.size() || !m_remote_files[index].IsSupported()) {
|
||||
return;
|
||||
}
|
||||
m_preview_status->setText(tr("Loading cheat preview…"));
|
||||
m_repository->Fetch(m_remote_files[index]);
|
||||
}
|
||||
|
||||
void PatchesDialog::ImportRemote() {
|
||||
const auto error = Cheats::ValidateImport(m_remote_document, m_title_id, m_version, m_process);
|
||||
if (!error.isEmpty()) {
|
||||
m_preview_status->setText(error);
|
||||
return;
|
||||
}
|
||||
auto root = document.object();
|
||||
auto mods = root.value(QStringLiteral("mods")).toArray();
|
||||
if (mods.size() != m_patches->count()) {
|
||||
m_status->setText(changed_message);
|
||||
if (!m_local_readable) {
|
||||
m_preview_status->setText(
|
||||
tr("Could not read the local cheat file. Open Local and reload it before importing."));
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < mods.size(); index++) {
|
||||
auto mod = mods[index].toObject();
|
||||
mod.insert(QStringLiteral("enabled"), m_patches->item(index)->checkState() == Qt::Checked);
|
||||
mods[index] = mod;
|
||||
const auto path = Cheats::PlanPath(m_title_id);
|
||||
if (QFile::exists(path) &&
|
||||
QMessageBox::question(this, tr("Replace local cheats?"),
|
||||
tr("Replace the local cheat file and selections for %1? Imported "
|
||||
"cheats will start unchecked.")
|
||||
.arg(m_title_id),
|
||||
QMessageBox::Yes | QMessageBox::No,
|
||||
QMessageBox::No) != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
root.insert(QStringLiteral("mods"), mods);
|
||||
document.setObject(root);
|
||||
|
||||
QSaveFile output(path);
|
||||
if (!output.open(QIODevice::WriteOnly) || output.write(document.toJson()) < 0 ||
|
||||
!output.commit()) {
|
||||
qWarning() << "Could not save cheat file:" << output.errorString();
|
||||
m_status->setText(tr("Could not save cheat file: %1").arg(output.errorString()));
|
||||
const auto mods = m_remote_document.object().value(QStringLiteral("mods")).toArray();
|
||||
const auto document = Cheats::WithSelection(m_remote_document, QList<bool>(mods.size(), false));
|
||||
const auto save_error = Cheats::SaveDocument(path, document, m_local_data);
|
||||
if (!save_error.isEmpty()) {
|
||||
m_preview_status->setText(save_error);
|
||||
return;
|
||||
}
|
||||
m_status->setText(tr("Cheat selection saved."));
|
||||
Load();
|
||||
m_status->setText(
|
||||
tr("Imported %1 cheat(s). Select cheats and apply your selection for the next game launch.")
|
||||
.arg(mods.size()));
|
||||
m_tabs->setCurrentIndex(1);
|
||||
}
|
||||
+17
-5
@@ -2529,7 +2529,6 @@ static uint64_t decode_indirect_modifier_patch_offsets(uint64_t modifier, bool i
|
||||
if (indexed && (low & 0x2u) != 0) {
|
||||
base_vtx_loc |= static_cast<uint64_t>(sgpr_base + extract_modifier_bits(low, 14u, 5u))
|
||||
<< 16u;
|
||||
base_vtx_loc |= 1ull << 59u;
|
||||
}
|
||||
|
||||
return base_vtx_loc | (start_inst_loc << 32u);
|
||||
@@ -2787,13 +2786,15 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndexIndirect(CommandBuffer* buf, uint32_t dat
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
||||
const auto patch_offsets = decode_indirect_modifier_patch_offsets(modifier, true);
|
||||
// Single indexed draws store the index-offset enable in word 3, bit 28.
|
||||
const auto patch_offsets = decode_indirect_modifier_patch_offsets(modifier, true) |
|
||||
((modifier & 0x2ull) << 59u);
|
||||
|
||||
cmd[0] = KYTY_PM4(5, Pm4::IT_DRAW_INDEX_INDIRECT, 0u);
|
||||
cmd[1] = data_offset_in_bytes;
|
||||
cmd[2] = static_cast<uint32_t>(patch_offsets);
|
||||
cmd[3] = static_cast<uint32_t>(patch_offsets >> 32u);
|
||||
cmd[4] = decode_indirect_draw_initiator(modifier);
|
||||
cmd[4] = decode_draw_index_initiator(modifier);
|
||||
|
||||
return cmd;
|
||||
}
|
||||
@@ -2914,19 +2915,30 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndexIndirectMulti(CommandBuffer* buf,
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
|
||||
|
||||
const auto low = static_cast<uint32_t>(modifier);
|
||||
const auto sgpr_base = indirect_modifier_sgpr_base(low);
|
||||
const auto patch_offsets = decode_indirect_modifier_patch_offsets(modifier, true);
|
||||
const auto count_vaddr = reinterpret_cast<uint64_t>(count_addr);
|
||||
|
||||
uint32_t draw_index_location = 0x280u;
|
||||
if ((low & 0x8u) != 0) {
|
||||
draw_index_location = sgpr_base + extract_modifier_bits(low, 24u, 5u);
|
||||
}
|
||||
// Multi indexed draws store the index-offset enable in word 4, bit 28.
|
||||
const auto draw_control = draw_index_location | ((low & 0x10u) << 23u) |
|
||||
((low & 0x2u) << 27u) | ((count_indirect & 0x1u) << 30u) |
|
||||
((low & 0x8u) << 28u);
|
||||
|
||||
cmd[0] = KYTY_PM4(10, Pm4::IT_DRAW_INDEX_INDIRECT_MULTI, 0u);
|
||||
cmd[1] = data_offset_in_bytes;
|
||||
cmd[2] = static_cast<uint32_t>(patch_offsets);
|
||||
cmd[3] = static_cast<uint32_t>(patch_offsets >> 32u);
|
||||
cmd[4] = (count_indirect & 1u) << 30u;
|
||||
cmd[4] = draw_control;
|
||||
cmd[5] = max_count_or_count;
|
||||
cmd[6] = static_cast<uint32_t>(count_vaddr) & ~0x3u;
|
||||
cmd[7] = static_cast<uint32_t>(count_vaddr >> 32u);
|
||||
cmd[8] = stride_in_bytes;
|
||||
cmd[9] = decode_indirect_draw_initiator(modifier);
|
||||
cmd[9] = decode_draw_index_initiator(modifier);
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,46 @@ void CheckConcurrentResult(int result, const char* text) {
|
||||
Check(result == OK || result == KERNEL_ERROR_EBADF || result == KERNEL_ERROR_ENOENT, text);
|
||||
}
|
||||
|
||||
void TestLevelUserEventDelivery() {
|
||||
using Libs::LibKernel::KERNEL_ERROR_ETIMEDOUT;
|
||||
EventQueue::KernelEqueue queue = EventQueue::KERNEL_EQUEUE_INVALID;
|
||||
Check(EventQueue::KernelCreateEqueue(&queue, "level-user-events") == OK, "create user event queue");
|
||||
Check(EventQueue::KernelAddUserEvent(queue, 1) == OK &&
|
||||
EventQueue::KernelAddUserEvent(queue, 2) == OK &&
|
||||
EventQueue::KernelAddUserEventEdge(queue, 3) == OK, "add level and edge events");
|
||||
for (int id = 1; id <= 3; ++id)
|
||||
Check(EventQueue::KernelTriggerUserEvent(queue, id, reinterpret_cast<void*>(uintptr_t(id))) == OK,
|
||||
"trigger distinct user events");
|
||||
EventQueue::KernelEvent events[16] {};
|
||||
Libs::LibKernel::KernelUseconds timeout = 0;
|
||||
int out = 0;
|
||||
const auto find_event = [&](uintptr_t id) {
|
||||
return std::find_if(events, events + out, [=](const auto& event) { return event.ident == id; });
|
||||
};
|
||||
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 3,
|
||||
"level events are returned once per wait without starving following events");
|
||||
for (uintptr_t id = 1; id <= 3; ++id) {
|
||||
const auto* event = find_event(id);
|
||||
Check(event != events + out && event->data == static_cast<intptr_t>(id) &&
|
||||
event->udata == reinterpret_cast<void*>(id), "distinct event payloads");
|
||||
}
|
||||
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 2 &&
|
||||
find_event(1) != events + out && find_event(2) != events + out,
|
||||
"level persists while edge clears");
|
||||
Check(EventQueue::KernelDeleteUserEvent(queue, 1) == OK, "delete first level event");
|
||||
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 1 &&
|
||||
events[0].ident == 2, "remaining level event is delivered only once");
|
||||
Check(EventQueue::KernelDeleteUserEvent(queue, 2) == OK, "delete remaining level event");
|
||||
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == KERNEL_ERROR_ETIMEDOUT,
|
||||
"deleted levels and consumed edge do not reappear");
|
||||
Check(EventQueue::KernelTriggerUserEvent(queue, 3, nullptr) == OK, "retrigger edge event");
|
||||
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 1 &&
|
||||
events[0].ident == 3, "retriggered edge is delivered once");
|
||||
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == KERNEL_ERROR_ETIMEDOUT,
|
||||
"retriggered edge clears after delivery");
|
||||
Check(EventQueue::KernelDeleteEqueue(queue) == OK, "delete user event queue");
|
||||
}
|
||||
|
||||
void TestPeriodicTimerEvents() {
|
||||
using namespace std::chrono_literals;
|
||||
using Libs::LibKernel::KERNEL_ERROR_ETIMEDOUT;
|
||||
@@ -188,6 +228,12 @@ void TestDuplicateAddPreservesEventState() {
|
||||
"duplicate add updates deadline metadata");
|
||||
Check(timer_event.data == 0 && timer_event.udata == reinterpret_cast<void*>(0x2222),
|
||||
"deadline trigger retains updated duplicate metadata");
|
||||
for (uintptr_t value: {0x9abcu, 0xdef0u})
|
||||
Check(EventQueue::KernelTriggerEvent(queue, 17, EventQueue::KERNEL_EVFILT_VIDEO_OUT,
|
||||
reinterpret_cast<void*>(value)) == OK, "queue bounded read payload");
|
||||
for (intptr_t value: {0x9abcu, 0xdef0u})
|
||||
Check(EventQueue::KernelWaitEqueue(queue, events, 1, &out, &timeout) == OK && out == 1 &&
|
||||
events[0].data == value, "capacity-one reads preserve pending event order");
|
||||
|
||||
auto retained_owner = weak_original.lock();
|
||||
Check(retained_owner != nullptr, "original owner alive before delete");
|
||||
@@ -495,6 +541,7 @@ void TestConcurrentDelete() {
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TestLevelUserEventDelivery();
|
||||
TestPeriodicTimerEvents();
|
||||
TestDuplicateAddPreservesEventState();
|
||||
TestCallbackStateOutlivesPort();
|
||||
|
||||
+289
-352
@@ -206,10 +206,8 @@ bool ReadLinearTestMemory(void *userdata, uint64_t address, std::span<uint32_t>
|
||||
}
|
||||
|
||||
std::unique_ptr<Fixture>
|
||||
MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
|
||||
bool memory_backed_material = false, uint32_t member_offset = 0,
|
||||
uint32_t material_stride = 224, uint32_t selector_bits = UINT32_MAX,
|
||||
uint32_t table_stride = 32, uint32_t table_offset = 0) {
|
||||
MakeIndirectImageFixture(bool malformed, uint32_t table_stride = 32, uint32_t table_offset = 0,
|
||||
uint32_t table_shader_offset = 0, bool nested = false) {
|
||||
auto fixture = std::make_unique<Fixture>();
|
||||
std::array<Value, 4> material_words;
|
||||
std::array<Value, 4> heap_words;
|
||||
@@ -217,54 +215,38 @@ MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
|
||||
material_words[dword] = fixture->UserData(dword);
|
||||
heap_words[dword] = fixture->UserData(dword + 4u);
|
||||
}
|
||||
if (memory_backed_material) {
|
||||
const auto pointer_address =
|
||||
fixture->Address(fixture->UserData(9), fixture->UserData(10), 0x10b0);
|
||||
MemoryInfo pointer_word;
|
||||
pointer_word.kind = ResourceKind::ScalarAddress;
|
||||
const auto pointer =
|
||||
fixture->Emit(ValueOpcode::LoadAddressU32,
|
||||
{pointer_address, Value(0u), Value(0u), Value(true)},
|
||||
fixture->AddMemory(pointer_word, 0x10b0));
|
||||
const auto address = fixture->Address(pointer, Value(0u), 0x10c0);
|
||||
MemoryInfo descriptor_word;
|
||||
descriptor_word.kind = ResourceKind::ScalarAddress;
|
||||
material_words[0] =
|
||||
fixture->Emit(ValueOpcode::LoadAddressU32,
|
||||
{address, Value(0u), Value(0u), Value(true)},
|
||||
fixture->AddMemory(descriptor_word, 0x10c0));
|
||||
}
|
||||
const auto material = fixture->Buffer(material_words, 0x10d8);
|
||||
const auto heap = fixture->Buffer(heap_words, 0x10d8);
|
||||
if (memory_backed_material) {
|
||||
MemoryInfo shared_buffer;
|
||||
shared_buffer.kind = ResourceKind::Buffer;
|
||||
const auto load =
|
||||
fixture->Emit(ValueOpcode::LoadBufferU32,
|
||||
{material, Value(0u), Value(0u), Value(0u), Value(true)},
|
||||
fixture->AddMemory(shared_buffer, 0x10d8));
|
||||
fixture->Emit(ValueOpcode::ReferenceU32, {load});
|
||||
}
|
||||
const auto invocation = fixture->Emit(
|
||||
ValueOpcode::GetBuiltin,
|
||||
{Value(static_cast<uint32_t>(StageInputKind::GlobalInvocationId)), Value(0u)});
|
||||
const auto selector = fixture->Emit(ValueOpcode::ReadFirstLane,
|
||||
{invocation, Value(true)});
|
||||
const auto record =
|
||||
fixture->Emit(ValueOpcode::IMul32, {selector, Value(material_stride)});
|
||||
const auto member = member_offset == 0 ? record :
|
||||
fixture->Emit(ValueOpcode::IAdd32, {record, Value(member_offset)});
|
||||
Value member;
|
||||
if (nested) {
|
||||
std::array<Value, 4> root_words;
|
||||
for (uint32_t word = 0; word < 4u; ++word) root_words[word] = fixture->UserData(12u + word);
|
||||
const auto root = fixture->Buffer(root_words);
|
||||
MemoryInfo root_memory;
|
||||
root_memory.kind = ResourceKind::ScalarBuffer;
|
||||
const auto root_offset = fixture->Emit(ValueOpcode::IMul32, {selector, Value(32u)});
|
||||
const auto pointer = fixture->Emit(ValueOpcode::ReadConstBuffer, {root, root_offset},
|
||||
fixture->AddMemory(root_memory, 0x1000));
|
||||
const auto masked = fixture->Emit(ValueOpcode::BitwiseAnd32, {pointer, Value(0xfffffff1u)});
|
||||
member = fixture->Emit(ValueOpcode::IAdd32, {masked, Value(16u)});
|
||||
} else {
|
||||
member = fixture->Emit(ValueOpcode::IMul32, {selector, Value(224u)});
|
||||
}
|
||||
fixture->Emit(ValueOpcode::ReferenceU32, {member});
|
||||
MemoryInfo material_scalar;
|
||||
material_scalar.kind = ResourceKind::ScalarBuffer;
|
||||
material_scalar.offset = material_immediate;
|
||||
auto key =
|
||||
material_scalar.offset = 4u;
|
||||
const auto key =
|
||||
fixture->Emit(ValueOpcode::ReadConstBuffer, {material, member},
|
||||
fixture->AddMemory(material_scalar, 0x10d8));
|
||||
if (selector_bits != UINT32_MAX)
|
||||
key = fixture->Emit(ValueOpcode::BitwiseAnd32, {key, Value(selector_bits)});
|
||||
const auto heap_offset =
|
||||
fixture->Emit(ValueOpcode::IMul32, {key, Value(table_stride)});
|
||||
auto heap_offset = fixture->Emit(ValueOpcode::IMul32, {key, Value(table_stride)});
|
||||
if (table_shader_offset != 0u)
|
||||
heap_offset = fixture->Emit(ValueOpcode::IAdd32, {heap_offset, Value(table_shader_offset)});
|
||||
std::array<Value, 8> image_words;
|
||||
MemoryInfo heap_scalar;
|
||||
heap_scalar.kind = ResourceKind::ScalarBuffer;
|
||||
@@ -278,6 +260,10 @@ MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
|
||||
fixture->Emit(ValueOpcode::ReadConstBuffer, {heap, heap_offset},
|
||||
fixture->AddMemory(component, 0x10d8));
|
||||
}
|
||||
if (nested) {
|
||||
const auto field = fixture->Emit(ValueOpcode::BitwiseAnd32, {image_words[4], Value(7u)});
|
||||
fixture->Emit(ValueOpcode::ReferenceU32, {field});
|
||||
}
|
||||
const auto image = fixture->Image(image_words, 0x10f0);
|
||||
const auto sampler =
|
||||
fixture->Sampler({Value(0u), Value(0u), Value(0u), Value(0u)}, 0x10f0);
|
||||
@@ -294,331 +280,280 @@ MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
|
||||
}
|
||||
|
||||
void TestInvariantIndirectImageMaterialization() {
|
||||
auto fixture = MakeIndirectImageFixture(false);
|
||||
auto fixture = MakeIndirectImageFixture(false, 48u, 0u, 16u, true);
|
||||
fixture->PlanAndTrack();
|
||||
auto resource_plan = ExtractResourcePlan(fixture->program);
|
||||
auto plan = ExtractResourcePlan(fixture->program);
|
||||
EliminateDeadCode(fixture->program.blocks);
|
||||
ValidateProgram(fixture->program, true);
|
||||
|
||||
Check(fixture->program.info.buffers.size() == 1 &&
|
||||
fixture->program.info.images.size() == 1 &&
|
||||
std::ranges::any_of(*fixture->block, [](const Inst &inst) {
|
||||
return inst.GetOpcode() == ValueOpcode::ReadConstBuffer;
|
||||
}),
|
||||
"indirect image key was not retained as a scalar-buffer read");
|
||||
const auto source = fixture->program.info.images[0].source;
|
||||
Check(source < fixture->program.descriptor_sources.size() &&
|
||||
fixture->program.descriptor_sources[source]
|
||||
.indirect_descriptor.has_value(),
|
||||
"indirect image source was not retained for runtime proof");
|
||||
const auto image_handle =
|
||||
std::ranges::find_if(*fixture->block, [](const Inst &inst) {
|
||||
return inst.GetOpcode() == ValueOpcode::GetImageResource;
|
||||
});
|
||||
Check(image_handle != fixture->block->end() &&
|
||||
image_handle->Arg(0).ResolveInstruction() != nullptr &&
|
||||
image_handle->Arg(0).ResolveInstruction()->GetOpcode() ==
|
||||
ValueOpcode::ReadConstBuffer,
|
||||
"indirect image handle discarded the live material key");
|
||||
|
||||
std::array<uint32_t, 9> user_data{0x1000u, 224u << 16u, 2u, 0u, 0x2000u,
|
||||
16u << 16u, 4u, 0u, 7u};
|
||||
const auto &source = *plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor;
|
||||
Check(!source.selector && fixture->program.info.buffers.size() == 3u,
|
||||
"bounded image table retained CPU selector traversal or lost live loads");
|
||||
std::array<uint32_t, 8> descriptor{};
|
||||
descriptor[0] = 0x20u;
|
||||
descriptor[1] = static_cast<uint32_t>(Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float) << 20u;
|
||||
descriptor[2] = 3u | (3u << 14u);
|
||||
descriptor[3] = Libs::Graphics::DstSel(4, 5, 6, 7) |
|
||||
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
|
||||
std::array<uint32_t, 16> data{0x3000u, 16u << 16u, 0x200000u, 0u,
|
||||
0x2000u, 48u << 16u, 3u, 0u};
|
||||
std::copy_n(data.begin(), 4u, data.begin() + 12u);
|
||||
LinearTestMemory memory;
|
||||
std::array<uint32_t, 8> image_descriptor{};
|
||||
image_descriptor[0] = 0x20u;
|
||||
image_descriptor[1] =
|
||||
static_cast<uint32_t>(
|
||||
Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float)
|
||||
<< 20u;
|
||||
image_descriptor[2] = 3u | (3u << 14u);
|
||||
image_descriptor[3] =
|
||||
Libs::Graphics::DstSel(4, 5, 6, 7) |
|
||||
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D)
|
||||
<< 28u);
|
||||
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
|
||||
memory.words[(0x2000u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
memory.words[(0x2020u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
}
|
||||
memory.words[(0x2020u - memory.base) / 4u] ^= 1u;
|
||||
|
||||
SrtRuntime runtime{.user_data = user_data,
|
||||
.userdata = &memory,
|
||||
memory.fail_address = 0x3000u; // GPU-only material backing must never be read on the CPU.
|
||||
memory.watched_address = 0x2000u;
|
||||
const auto put = [&](uint32_t offset, uint32_t identity) {
|
||||
auto value = descriptor;
|
||||
value[0] += identity;
|
||||
std::copy(value.begin(), value.end(), memory.words.begin() + (0x1000u + offset) / 4u);
|
||||
};
|
||||
put(16u, 0u);
|
||||
put(64u, 1u);
|
||||
SrtRuntime runtime{.user_data = data, .userdata = &memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
Check(MaterializeResources(resource_plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 1 &&
|
||||
std::equal(image_descriptor.begin(), image_descriptor.end(),
|
||||
snapshot.images[0].dwords.begin()),
|
||||
"invariant indirect image table did not materialize");
|
||||
|
||||
user_data[5] = 0u;
|
||||
user_data[6] = 19u;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 3u && memory.watched_reads == 1u &&
|
||||
memory.watched_dwords == 36u && memory.reads == 1u,
|
||||
"image table was not read once independently of its GPU selector chain");
|
||||
const auto root = specialization.images[0];
|
||||
const auto mapping = root.indirect_mapping_offset;
|
||||
Check(root.indirect_search_iterations == 0u && snapshot.flattened_srt[mapping] == 17u &&
|
||||
snapshot.flattened_srt[mapping + 1u] == 0u &&
|
||||
snapshot.flattened_srt[mapping + 5u] == 1u &&
|
||||
snapshot.flattened_srt[mapping + 17u] == 2u &&
|
||||
snapshot.specialization_reads == std::vector<std::pair<uint64_t, uint64_t>>{{0x2000u, 144u}},
|
||||
"dense image mapping lost its null slot, trimmed length or coherent read dependency");
|
||||
const auto original_specialization = specialization;
|
||||
std::fill_n(memory.words.begin() + 0x1040u / 4u, 8u, 0u);
|
||||
put(112u, 1u);
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
specialization == original_specialization && memory.watched_reads == 2u &&
|
||||
snapshot.flattened_srt[mapping] == 29u,
|
||||
"moving a table entry changed shader specialization or reused stale mapping bytes");
|
||||
put(112u, 7u);
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images[2].dwords[0] == descriptor[0] + 7u,
|
||||
"changed table descriptor was not refreshed");
|
||||
// Scalar reads align each DWORD and return zero beyond the V# byte size.
|
||||
data[5] = 0u;
|
||||
data[6] = 19u;
|
||||
std::fill(memory.words.begin() + 0x1000u / 4u, memory.words.end(), 0u);
|
||||
put(0u, 0u);
|
||||
memory.fail_address = 0x2010u;
|
||||
memory.watched_address = 0x2000u;
|
||||
Check(MaterializeResources(resource_plan, runtime, snapshot, specialization) &&
|
||||
memory.watched_dwords == 4u &&
|
||||
std::equal(image_descriptor.begin(), image_descriptor.end(),
|
||||
snapshot.images[0].dwords.begin()),
|
||||
"partial scalar-buffer descriptor read crossed bounds instead of zeroing its tail");
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
memory.watched_dwords == 4u && snapshot.images.size() == 2u &&
|
||||
snapshot.images[1].dwords == descriptor,
|
||||
"partial descriptor prefix did not zero its out-of-bounds DWORDs");
|
||||
memory.fail_address = 0x2008u;
|
||||
Check(!MaterializeResources(resource_plan, runtime, snapshot, specialization),
|
||||
"unreadable memory inside the descriptor prefix was accepted");
|
||||
user_data[5] = 16u << 16u;
|
||||
user_data[6] = 4u;
|
||||
memory.watched_address = UINT64_MAX;
|
||||
|
||||
memory.fail_address = 0x1004u;
|
||||
Check(!MaterializeResources(resource_plan, runtime, snapshot,
|
||||
specialization),
|
||||
"unreadable material-table selector was accepted");
|
||||
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"unreadable in-range table bytes were accepted");
|
||||
memory.fail_address = UINT64_MAX;
|
||||
|
||||
memory.words[(0x1000u - memory.base + 36u) / 4u] = 1u;
|
||||
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
|
||||
memory.words[(0x2000u - memory.base) / 4u + dword] = 0u;
|
||||
memory.words[(0x2020u - memory.base) / 4u + dword] = 0u;
|
||||
for (uint32_t size : {0u, 3u, 4u}) {
|
||||
data[6] = size;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 1u &&
|
||||
specialization.images[0].indirect_root == ImageResource::NoIndirectImage &&
|
||||
std::ranges::all_of(snapshot.images[0].dwords, [](uint32_t word) { return word == 0u; }),
|
||||
"empty image table did not specialize as a safe null image");
|
||||
}
|
||||
memory.words[(0x2000u - memory.base) / 4u + 1u] = image_descriptor[1];
|
||||
memory.words[(0x2000u - memory.base) / 4u + 3u] = image_descriptor[3];
|
||||
memory.words[(0x2020u - memory.base) / 4u + 1u] = image_descriptor[1];
|
||||
memory.words[(0x2020u - memory.base) / 4u + 3u] =
|
||||
image_descriptor[3] ^ (1u << 28u);
|
||||
ResourceSnapshot null_snapshot;
|
||||
ResourceSpecialization null_specialization;
|
||||
Check(MaterializeResources(resource_plan, runtime, null_snapshot,
|
||||
null_specialization) &&
|
||||
std::ranges::all_of(null_snapshot.images[0].dwords,
|
||||
[](uint32_t dword) { return dword == 0u; }),
|
||||
"stale typed null image descriptors were not canonicalized");
|
||||
|
||||
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
|
||||
memory.words[(0x2000u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
memory.words[(0x2020u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
// The GPU computes these offsets; host materialization must not enumerate the key.
|
||||
for (const auto [shader, immediate, key, expected] :
|
||||
{std::array{16u, 0u, 0x55555555u, 0u},
|
||||
std::array{0u, 16u, 0x55555555u, UINT32_MAX},
|
||||
std::array{0xfffffff0u, 32u, 0u, UINT32_MAX}}) {
|
||||
auto wrap = MakeIndirectImageFixture(false, 48u, immediate, shader);
|
||||
wrap->PlanAndTrack();
|
||||
for (auto &inst : *wrap->block) {
|
||||
if (inst.GetOpcode() == ValueOpcode::ReadConstBuffer) {
|
||||
inst.ReplaceUsesWith(Value(key));
|
||||
break;
|
||||
}
|
||||
}
|
||||
const auto handle = std::ranges::find_if(*wrap->block, [](const Inst &inst) {
|
||||
return inst.GetOpcode() == ValueOpcode::GetImageResource;
|
||||
});
|
||||
SrtWalker walker(wrap->program, runtime);
|
||||
uint32_t offset = 0;
|
||||
Check(walker.Evaluate(handle->Arg(0), offset) && offset == expected,
|
||||
"GPU descriptor offset projection lost U32 wrap or scalar immediate carry");
|
||||
}
|
||||
memory.words[(0x2020u - memory.base) / 4u] ^= 1u;
|
||||
memory.words[(0x1000u - memory.base + 36u) / 4u] = 1u;
|
||||
ResourceSnapshot dynamic_snapshot;
|
||||
ResourceSpecialization dynamic_specialization;
|
||||
Check(MaterializeResources(resource_plan, runtime, dynamic_snapshot,
|
||||
dynamic_specialization) &&
|
||||
dynamic_snapshot.images.size() == 2 &&
|
||||
dynamic_specialization.images.size() == 2,
|
||||
"dynamic indirect image table did not materialize");
|
||||
const auto second_image = (0x2020u - memory.base) / 4u;
|
||||
memory.words[second_image + 1u] |= 3u << 30u;
|
||||
memory.words[second_image + 2u] = 3u << 14u;
|
||||
memory.words[second_image + 3u] =
|
||||
Libs::Graphics::DstSel(4, 5, 6, 7) |
|
||||
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kCube) << 28u);
|
||||
memory.words[second_image + 4u] = 11u;
|
||||
ResourceSnapshot mixed_snapshot;
|
||||
ResourceSpecialization mixed_specialization;
|
||||
Check(MaterializeResources(resource_plan, runtime, mixed_snapshot,
|
||||
mixed_specialization) &&
|
||||
mixed_snapshot.images.size() == 2 &&
|
||||
mixed_specialization.images.size() == 2 &&
|
||||
mixed_specialization.images[0].dimension ==
|
||||
Decoder::ImageDimension::Dim2D &&
|
||||
!mixed_specialization.images[0].cube &&
|
||||
mixed_specialization.images[1].dimension ==
|
||||
Decoder::ImageDimension::Dim2DArray &&
|
||||
mixed_specialization.images[1].cube &&
|
||||
std::equal(mixed_snapshot.images[1].dwords.begin(),
|
||||
mixed_snapshot.images[1].dwords.end(),
|
||||
memory.words.begin() + second_image),
|
||||
"mixed 2D and cube candidates were rejected or discarded");
|
||||
memory.words[second_image + 1u] =
|
||||
static_cast<uint32_t>(
|
||||
Libs::Graphics::Prospero::BufferFormat::k32_32_32_32UInt)
|
||||
<< 20u |
|
||||
(3u << 30u);
|
||||
Check(!MaterializeResources(resource_plan, runtime, mixed_snapshot,
|
||||
mixed_specialization),
|
||||
"indirect images with different numeric classes were accepted");
|
||||
for (const auto dword : {1u, 2u, 3u, 4u}) {
|
||||
memory.words[second_image + dword] = image_descriptor[dword];
|
||||
}
|
||||
ApplyResourceSpecialization(fixture->program, dynamic_specialization);
|
||||
Check(fixture->program.info.images.size() == 2 &&
|
||||
fixture->program.info.images[0].indirect_root == 0 &&
|
||||
fixture->program.info.images[0].indirect_search_iterations != 0 &&
|
||||
fixture->program.info.images[0].indirect_resources.size() == 2 &&
|
||||
dynamic_snapshot.images.size() == 2,
|
||||
"dynamic indirect image table was not specialized transactionally");
|
||||
const auto &mapping = dynamic_specialization.images[0];
|
||||
const auto key_count = dynamic_snapshot.flattened_srt[mapping.indirect_mapping_offset];
|
||||
Check(mapping.indirect_search_iterations == std::bit_width(key_count) &&
|
||||
mapping.indirect_mapping_offset + 1u + key_count * 2u ==
|
||||
dynamic_snapshot.flattened_srt.size(),
|
||||
"indirect image mapping retained worst-case padding");
|
||||
|
||||
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
|
||||
memory.words[(0x2000u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
memory.words[(0x2020u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
}
|
||||
memory.words[(0x2000u - memory.base) / 4u] += 0x100u;
|
||||
memory.words[(0x2020u - memory.base) / 4u] += 0x101u;
|
||||
ResourceSnapshot rebound_snapshot;
|
||||
ResourceSpecialization rebound_specialization;
|
||||
Check(MaterializeResources(resource_plan, runtime, rebound_snapshot,
|
||||
rebound_specialization) &&
|
||||
rebound_specialization == dynamic_specialization,
|
||||
"stable indirect key mapping did not accept changed image addresses");
|
||||
memory.words[(0x2020u - memory.base) / 4u] =
|
||||
memory.words[(0x2000u - memory.base) / 4u];
|
||||
Check(MaterializeResources(resource_plan, runtime, rebound_snapshot,
|
||||
rebound_specialization) &&
|
||||
rebound_specialization != dynamic_specialization,
|
||||
"collapsed indirect candidates did not select a new specialization");
|
||||
const auto collapsed_specialization = rebound_specialization;
|
||||
ResourceSnapshot capacity_snapshot;
|
||||
ResourceSpecialization capacity_specialization;
|
||||
for (const uint32_t records : {1u, 3u}) {
|
||||
user_data[2] = records;
|
||||
Check(MaterializeResources(resource_plan, runtime, capacity_snapshot,
|
||||
capacity_specialization),
|
||||
"runtime indirect key mapping rejected a valid material-table size");
|
||||
}
|
||||
user_data[2] = 2u;
|
||||
memory.words[(0x2020u - memory.base) / 4u] =
|
||||
memory.words[(0x2000u - memory.base) / 4u] + 1u;
|
||||
memory.words[(0x2040u - memory.base) / 4u] =
|
||||
memory.words[(0x2000u - memory.base) / 4u] + 2u;
|
||||
for (uint32_t dword = 1; dword < image_descriptor.size(); dword++) {
|
||||
memory.words[(0x2040u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
}
|
||||
memory.words[(0x1000u - memory.base + 68u) / 4u] = 2u;
|
||||
Check(MaterializeResources(resource_plan, runtime, rebound_snapshot,
|
||||
rebound_specialization) &&
|
||||
rebound_specialization != collapsed_specialization,
|
||||
"larger indirect candidate topology reused the old specialization");
|
||||
|
||||
auto memory_backed = MakeIndirectImageFixture(false, 4u, true);
|
||||
memory_backed->PlanAndTrack();
|
||||
auto memory_backed_plan = ExtractResourcePlan(memory_backed->program);
|
||||
EliminateDeadCode(memory_backed->program.blocks);
|
||||
std::array<uint32_t, 11> memory_backed_user_data{0x1000u, 224u << 16u, 2u, 0u,
|
||||
0x2000u, 16u << 16u, 4u, 0u,
|
||||
7u, 0x3100u, 0u};
|
||||
memory.words[(0x3100u - memory.base) / 4u] = 0x3000u;
|
||||
memory.words[(0x3000u - memory.base) / 4u] = 0x1000u;
|
||||
memory.fail_address = 0x3100u;
|
||||
SrtRuntime memory_backed_runtime{.user_data = memory_backed_user_data,
|
||||
.userdata = &memory,
|
||||
.read_specialization_memory =
|
||||
ReadLinearTestMemory};
|
||||
Check(!MaterializeResources(memory_backed_plan, memory_backed_runtime,
|
||||
snapshot, specialization),
|
||||
"unreadable indirect table descriptor was accepted");
|
||||
memory.fail_address = UINT64_MAX;
|
||||
|
||||
memory.watched_address = 0x3100u;
|
||||
memory.watched_reads = 0;
|
||||
Check(MaterializeResources(memory_backed_plan, memory_backed_runtime,
|
||||
snapshot, specialization) && memory.watched_reads == 1u,
|
||||
"descriptor, flattened SRT and indirect-table roots repeated a clean pointer read");
|
||||
const auto* buffer_storage = snapshot.buffers.data();
|
||||
const auto* image_storage = snapshot.images.data();
|
||||
const auto* flat_storage = snapshot.flattened_srt.data();
|
||||
const auto* user_data_storage = snapshot.user_data.data();
|
||||
const auto* buffer_specialization_storage = specialization.buffers.data();
|
||||
const auto* image_specialization_storage = specialization.images.data();
|
||||
const auto old_address = snapshot.images[0].dwords[0];
|
||||
memory.words[(0x2000u - memory.base) / 4u] += 0x100u;
|
||||
memory.watched_reads = 0;
|
||||
Check(MaterializeResources(memory_backed_plan, memory_backed_runtime,
|
||||
snapshot, specialization) && memory.watched_reads == 1u &&
|
||||
snapshot.images[0].dwords[0] == old_address + 0x100u,
|
||||
"cache refresh reused stale table contents or repeated its clean pointer read");
|
||||
Check(snapshot.buffers.data() == buffer_storage && snapshot.images.data() == image_storage &&
|
||||
snapshot.flattened_srt.data() == flat_storage &&
|
||||
snapshot.user_data.data() == user_data_storage &&
|
||||
specialization.buffers.data() == buffer_specialization_storage &&
|
||||
specialization.images.data() == image_specialization_storage,
|
||||
"a same-shape refresh discarded the runtime output storage");
|
||||
|
||||
auto malformed = MakeIndirectImageFixture(true);
|
||||
|
||||
CheckFatal([&] { malformed->PlanAndTrack(); }, "not a valid runtime value",
|
||||
"malformed indirect image pattern was accepted");
|
||||
Check(!malformed->program.resource_tracking_complete &&
|
||||
malformed->program.info.images.empty() &&
|
||||
malformed->program.descriptor_sources.empty(),
|
||||
"malformed indirect image pattern was partially accepted");
|
||||
"noncontiguous descriptor DWORDs entered the table plan");
|
||||
Check(!malformed->program.resource_tracking_complete && malformed->program.descriptor_sources.empty(),
|
||||
"malformed image table tracking was not transactional");
|
||||
}
|
||||
|
||||
auto negative_immediate = MakeIndirectImageFixture(false, 0xfffffffcu);
|
||||
CheckFatal([&] { negative_immediate->PlanAndTrack(); },
|
||||
"not a valid runtime value",
|
||||
"negative scalar immediate entered the indirect image proof");
|
||||
|
||||
for (const auto [immediate, member, first, stride] :
|
||||
{std::array{0u, 4u, 4u, 224u}, std::array{4u, 4u, 8u, 224u},
|
||||
std::array{36u, 0u, 36u, 224u}, std::array{1u, 3u, 0u, 224u},
|
||||
std::array{1u, 3u, 0u, 1u}, std::array{1u, 3u, 0u, 2u}}) {
|
||||
auto split_offset = MakeIndirectImageFixture(false, immediate, false, member, stride);
|
||||
split_offset->PlanAndTrack();
|
||||
const auto split_plan = ExtractResourcePlan(split_offset->program);
|
||||
LinearTestMemory split_memory;
|
||||
std::copy(image_descriptor.begin(), image_descriptor.end(),
|
||||
split_memory.words.begin() + 0x1000u / 4u);
|
||||
split_memory.fail_address = first == 36u ? 0x1004u : UINT64_MAX;
|
||||
split_memory.watched_address = split_memory.base + first;
|
||||
user_data[1] = stride << 16u;
|
||||
user_data[2] = stride < 4u ? 8u / stride : 1u;
|
||||
SrtRuntime split_runtime{.user_data = user_data,
|
||||
.userdata = &split_memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
Check(MaterializeResources(split_plan, split_runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 1 &&
|
||||
snapshot.images[0].dwords == image_descriptor &&
|
||||
split_memory.watched_reads == 1u,
|
||||
"indirect scalar offsets did not align and bound their components independently");
|
||||
void TestBoundedImageViewEligibility() {
|
||||
using Type = Libs::Graphics::Prospero::ImageType;
|
||||
auto fixture = MakeIndirectImageFixture(false, 48u, 0u, 16u);
|
||||
fixture->PlanAndTrack();
|
||||
auto plan = ExtractResourcePlan(fixture->program);
|
||||
std::array<uint32_t, 8> data{0x3000u, 16u << 16u, 1u, 0u,
|
||||
0x2000u, 48u << 16u, 5u, 0u};
|
||||
LinearTestMemory memory;
|
||||
memory.fail_address = 0x3000u;
|
||||
const auto descriptor = [](uint32_t identity, Type type) {
|
||||
return std::array<uint32_t, 8>{identity, 75u << 20u, 3u | (3u << 14u),
|
||||
Libs::Graphics::DstSel(4, 5, 6, 7) | (static_cast<uint32_t>(type) << 28u),
|
||||
0u, 0u, 0u, 0u};
|
||||
};
|
||||
const std::array types{Type::kColor2D, Type::kCube, Type::kColor3D,
|
||||
Type::kColor2DArray, Type::kColor2D};
|
||||
for (uint32_t row = 0; row < types.size(); ++row) {
|
||||
const auto value = descriptor(0x20u + row, types[row]);
|
||||
std::copy(value.begin(), value.end(), memory.words.begin() + (0x1010u + row * 48u) / 4u);
|
||||
}
|
||||
SrtRuntime runtime{.user_data = data, .userdata = &memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 4u && memory.reads == 1u,
|
||||
"unrelated cube/3D heap entries prevented a typed 2D table view");
|
||||
const auto mapping = specialization.images[0].indirect_mapping_offset;
|
||||
Check(snapshot.flattened_srt[mapping] == 53u &&
|
||||
snapshot.flattened_srt[mapping + 5u] == 1u &&
|
||||
snapshot.flattened_srt[mapping + 17u] == 0u &&
|
||||
snapshot.flattened_srt[mapping + 29u] == 0u &&
|
||||
snapshot.flattened_srt[mapping + 41u] == 2u &&
|
||||
snapshot.flattened_srt[mapping + 53u] == 3u &&
|
||||
snapshot.images[1].dwords == descriptor(0x20u, Type::kColor2D) &&
|
||||
snapshot.images[2].dwords == descriptor(0x23u, Type::kColor2DArray) &&
|
||||
snapshot.images[3].dwords == descriptor(0x24u, Type::kColor2D),
|
||||
"typed table eligibility changed byte offsets or discarded compatible views");
|
||||
|
||||
// The shader packs flags above a 15-bit cube-image key in the final DWORD of
|
||||
// each 64-byte material record; the image occupies bytes 16..47 of its record.
|
||||
auto packed = MakeIndirectImageFixture(false, 60u, false, 0u, 64u, 0x7fffu, 48u, 16u);
|
||||
packed->PlanAndTrack();
|
||||
const auto packed_plan = ExtractResourcePlan(packed->program);
|
||||
LinearTestMemory packed_memory;
|
||||
std::array<uint32_t, 8> packed_data{0x1000u, 64u << 16u, 3u, 0u,
|
||||
0x2000u, 48u << 16u, 2u, 0u};
|
||||
for (uint32_t i = 0; i < 3u; ++i)
|
||||
packed_memory.words[(i * 64u + 60u) / 4u] = (0xffffu << 15u) | (i & 1u);
|
||||
for (uint32_t i = 0; i < 2u; ++i) {
|
||||
auto descriptor = image_descriptor;
|
||||
descriptor[0] += i;
|
||||
std::copy(descriptor.begin(), descriptor.end(),
|
||||
packed_memory.words.begin() + (0x1000u + i * 48u + 16u) / 4u);
|
||||
// An explicitly selected descriptor is not an unrelated entry in a broad heap.
|
||||
const auto image_source = plan.info.images[0].source;
|
||||
std::vector<uint32_t> selected;
|
||||
for (auto type : {Type::kColor2D, Type::kColor3D}) {
|
||||
DescriptorSource source;
|
||||
source.dword_count = 8u;
|
||||
const auto value = descriptor(0x30u, type);
|
||||
for (uint32_t word = 0; word < value.size(); ++word) source.dwords[word] = Value(value[word]);
|
||||
selected.push_back(static_cast<uint32_t>(plan.descriptor_sources.size()));
|
||||
plan.descriptor_sources.push_back(source);
|
||||
}
|
||||
SrtRuntime packed_runtime{.user_data = packed_data, .userdata = &packed_memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
Check(MaterializeResources(packed_plan, packed_runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 2u && snapshot.images[0].dwords == image_descriptor &&
|
||||
snapshot.images[1].dwords[0] == image_descriptor[0] + 1u &&
|
||||
snapshot.flattened_srt[specialization.images[0].indirect_mapping_offset] == 2u,
|
||||
"packed material flags changed the table key or 48-byte descriptor addressing");
|
||||
packed_memory.words[60u / 4u] = 0xffff8002u;
|
||||
packed_memory.fail_address = 0x2070u;
|
||||
Check(MaterializeResources(packed_plan, packed_runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 3u &&
|
||||
std::ranges::all_of(snapshot.images[2].dwords, [](uint32_t word) { return word == 0u; }),
|
||||
"masked out-of-range descriptor did not return zero without reading past table bounds");
|
||||
packed_memory.fail_address = 0x205cu;
|
||||
Check(!MaterializeResources(packed_plan, packed_runtime, snapshot, specialization),
|
||||
"unreadable in-range DWORD in a 48-byte descriptor record was accepted");
|
||||
auto overflowing = MakeIndirectImageFixture(false, 60u, false, 0u, 64u,
|
||||
UINT32_MAX, 48u, 16u);
|
||||
CheckFatal([&] { overflowing->PlanAndTrack(); }, "not a valid runtime value",
|
||||
"unbounded table offset conflated shader U32 wrap with scalar immediate addition");
|
||||
plan.descriptor_sources[image_source].indirect_descriptor->sources = std::move(selected);
|
||||
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"explicitly selected incompatible descriptor was silently normalized to null");
|
||||
}
|
||||
|
||||
void TestWaterfallImageTable() {
|
||||
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
|
||||
const auto make = [](bool equality, bool scalar) {
|
||||
auto fixture = std::make_unique<Fixture>();
|
||||
auto *entry = fixture->block;
|
||||
auto *body = fixture->AddBlock();
|
||||
auto *exit = fixture->AddBlock();
|
||||
entry->AddBranch(body);
|
||||
entry->AddBranch(exit);
|
||||
body->AddBranch(exit);
|
||||
std::array<Value, 4> words;
|
||||
for (uint32_t i = 0; i < 4u; ++i) words[i] = fixture->UserData(i);
|
||||
const auto table = fixture->Buffer(words);
|
||||
const auto lane_value = fixture->Emit(ValueOpcode::GetAttribute, {Value(0u), Value(0u)});
|
||||
const auto active = fixture->Emit(ValueOpcode::INotEqual32, {lane_value, Value(0u)});
|
||||
const auto index = fixture->UserData(4u);
|
||||
std::array<Value, 8> image_words;
|
||||
auto guard = active;
|
||||
for (uint32_t group = 0; group < 2u; ++group) {
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::Buffer;
|
||||
memory.idxen = true;
|
||||
memory.data_dwords = 4u;
|
||||
memory.offset = 16u + group * 16u;
|
||||
const auto loaded = fixture->Emit(ValueOpcode::LoadBufferU32x4,
|
||||
{table, index, Value(0u), Value(0u), active}, fixture->AddMemory(memory, 0x850u));
|
||||
for (uint32_t component = 0; component < 4u; ++component) {
|
||||
const auto word = group * 4u + component;
|
||||
const auto local = fixture->Emit(ValueOpcode::SelectU32,
|
||||
{active, fixture->Emit(ValueOpcode::CompositeExtractU32x4,
|
||||
{loaded, Value(component)}), Value(0xdeadu)});
|
||||
image_words[word] = fixture->Emit(ValueOpcode::ReadLane, {local, Value(0u)});
|
||||
if (equality || word != 7u)
|
||||
guard = fixture->Emit(ValueOpcode::LogicalAnd,
|
||||
{guard, fixture->Emit(ValueOpcode::IEqual32, {image_words[word], local})});
|
||||
}
|
||||
}
|
||||
fixture->program.block_info[0].condition = guard;
|
||||
fixture->program.block_info[0].terminator = {
|
||||
.kind = CFG::TerminatorKind::ConditionalBranch, .true_block = 1u, .false_block = 2u};
|
||||
const auto sample = [&](Value image, Block *block) {
|
||||
const auto sampler = fixture->Sampler({Value(0u), Value(0u), Value(0u), Value(0u)});
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::Image;
|
||||
memory.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
const auto value = fixture->Emit(ValueOpcode::ImageSampleRaw,
|
||||
{image, sampler, fixture->ImageAddress()}, fixture->AddMemory(memory, 0x92cu), block);
|
||||
fixture->Emit(ValueOpcode::ReferenceU32,
|
||||
{fixture->Emit(ValueOpcode::CompositeExtractU32x4, {value, Value(0u)}, 0, block)}, 0, block);
|
||||
};
|
||||
if (scalar) {
|
||||
const auto key = fixture->Emit(ValueOpcode::ReadFirstLane, {lane_value, active});
|
||||
const auto offset = fixture->Emit(ValueOpcode::IAdd32,
|
||||
{fixture->Emit(ValueOpcode::IMul32, {key, Value(48u)}), Value(16u)});
|
||||
std::array<Value, 8> scalar_words;
|
||||
for (uint32_t word = 0; word < 8u; ++word) {
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::ScalarBuffer;
|
||||
memory.offset = word * 4u;
|
||||
scalar_words[word] = fixture->Emit(ValueOpcode::ReadConstBuffer, {table, offset},
|
||||
fixture->AddMemory(memory, 0x7bcu));
|
||||
}
|
||||
sample(fixture->Image(scalar_words), entry);
|
||||
}
|
||||
const auto image = fixture->Emit(ValueOpcode::GetImageResource,
|
||||
{image_words[0], image_words[1], image_words[2], image_words[3],
|
||||
image_words[4], image_words[5], image_words[6], image_words[7]}, 0, body);
|
||||
sample(image, body);
|
||||
return fixture;
|
||||
};
|
||||
auto fixture = make(true, true);
|
||||
fixture->PlanAndTrack();
|
||||
auto plan = ExtractResourcePlan(fixture->program);
|
||||
Check(plan.info.images.size() == 1u &&
|
||||
plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor->table_record_bytes == 48u,
|
||||
"scalar and waterfall uses did not share their native image table");
|
||||
std::array<uint32_t, 5> data{0x2000u, 48u << 16u, 2u, 0x5204u, 1u};
|
||||
LinearTestMemory memory;
|
||||
const std::array<uint32_t, 8> descriptor{0x20u, 75u << 20u, 3u | (3u << 14u),
|
||||
Libs::Graphics::DstSel(4, 5, 6, 7) | (9u << 28u), 0u, 0u, 0u, 0u};
|
||||
std::copy(descriptor.begin(), descriptor.end(), memory.words.begin() + 0x1010u / 4u);
|
||||
SrtRuntime runtime{.user_data = data, .userdata = &memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) && memory.reads == 1u &&
|
||||
snapshot.images.size() == 2u,
|
||||
"shared scalar/waterfall table repeated its coherent bulk read");
|
||||
const auto handle = std::ranges::find_if(*fixture->program.blocks[1], [](const Inst &inst) {
|
||||
return inst.GetOpcode() == ValueOpcode::GetImageResource;
|
||||
});
|
||||
for (const auto [index, format, expected] :
|
||||
{std::array{1u, 0x5204u, 64u}, std::array{2u, 0x5204u, UINT32_MAX},
|
||||
std::array{UINT32_MAX, 0x5204u, UINT32_MAX}, std::array{0u, 0u, UINT32_MAX}}) {
|
||||
data[4] = index;
|
||||
data[3] = format;
|
||||
SrtWalker walker(fixture->program, runtime);
|
||||
uint32_t offset = 0;
|
||||
Check(walker.Evaluate(handle->Arg(0), offset) && offset == expected,
|
||||
"VMEM table projection lost record bounds, format disable or multiplication carry");
|
||||
}
|
||||
auto disabled = make(true, false);
|
||||
disabled->PlanAndTrack();
|
||||
const auto disabled_plan = ExtractResourcePlan(disabled->program);
|
||||
data[3] = 0u;
|
||||
memory.fail_address = 0x2000u;
|
||||
Check(MaterializeResources(disabled_plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 1u &&
|
||||
specialization.images[0].indirect_root == ImageResource::NoIndirectImage,
|
||||
"disabled VMEM descriptor table read its unavailable backing");
|
||||
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"VMEM format disable incorrectly disabled a shared scalar descriptor read");
|
||||
auto malformed = make(false, false);
|
||||
CheckFatal([&] { malformed->PlanAndTrack(); }, "not a valid runtime value",
|
||||
"waterfall without all descriptor equality witnesses was accepted");
|
||||
}
|
||||
|
||||
void TestGuardedDirectImageTable() {
|
||||
@@ -709,8 +644,8 @@ void TestGuardedDirectImageTable() {
|
||||
fixture.PlanAndTrack();
|
||||
const auto source = fixture.program.info.images[0].source;
|
||||
const auto &indirect = fixture.program.descriptor_sources[source].indirect_descriptor;
|
||||
Check(indirect && indirect->material_source == UINT32_MAX &&
|
||||
indirect->selector_stride == 0u && indirect->table_offset == 344u &&
|
||||
Check(indirect && !indirect->selector.has_value() &&
|
||||
indirect->table_offset == 344u &&
|
||||
indirect->key_count.Resolve().IsImmediate() &&
|
||||
indirect->key_count.Resolve().U32() == 32u &&
|
||||
fixture.program.descriptor_sources[indirect->table_source].dword_count == 2u,
|
||||
@@ -1007,7 +942,7 @@ void TestBoundedComputeImageLoop() {
|
||||
fixture.PlanAndTrack();
|
||||
const auto source = fixture.program.info.images[0].source;
|
||||
const auto &indirect = fixture.program.descriptor_sources[source].indirect_descriptor;
|
||||
Check(indirect && indirect->material_source == UINT32_MAX &&
|
||||
Check(indirect && !indirect->selector.has_value() &&
|
||||
indirect->table_offset == 0x6b0u &&
|
||||
indirect->key_count.Resolve() == count.Resolve(),
|
||||
"bounded compute loop lost its runtime image count");
|
||||
@@ -1362,8 +1297,8 @@ void TestUniformizedMaterialImageKeys() {
|
||||
fixture.PlanAndTrack();
|
||||
const auto source = fixture.program.info.images[0].source;
|
||||
const auto &indirect = fixture.program.descriptor_sources[source].indirect_descriptor;
|
||||
Check(indirect && indirect->selector_stride == 0x90u &&
|
||||
indirect->selector_offset == 0xc00u &&
|
||||
Check(indirect && indirect->selector->stride == 0x90u &&
|
||||
indirect->selector->offset == 0xc00u &&
|
||||
indirect->table_offset == 0x20e0u &&
|
||||
!indirect->selector_mask.IsEmpty() &&
|
||||
indirect->key_count.Resolve().IsImmediate() &&
|
||||
@@ -3637,6 +3572,8 @@ int main() {
|
||||
Run("gather LOD sampler validation", TestGatherLodSamplerValidation);
|
||||
Run("dynamic storage mips", TestDynamicStorageMipTracking);
|
||||
Run("invariant indirect images", TestInvariantIndirectImageMaterialization);
|
||||
Run("bounded image view eligibility", TestBoundedImageViewEligibility);
|
||||
Run("waterfall image table", TestWaterfallImageTable);
|
||||
Run("guarded direct image table", TestGuardedDirectImageTable);
|
||||
Run("bounded compute image loop", TestBoundedComputeImageLoop);
|
||||
Run("uniformized material image keys", TestUniformizedMaterialImageKeys);
|
||||
|
||||
@@ -10161,6 +10161,46 @@ public:
|
||||
|
||||
void CheckRenderExecutorColorMetadataClear() {
|
||||
constexpr const char *name = "RenderExecutorColorMetadataClear";
|
||||
struct PackedCase {
|
||||
vk::Format format;
|
||||
uint32_t word;
|
||||
std::array<float, 4> channels;
|
||||
};
|
||||
const std::array packed_cases{
|
||||
PackedCase{vk::Format::eR16Sfloat, 0x7bff7bffu, {65504.0f}},
|
||||
PackedCase{vk::Format::eR16G16Sfloat, 0xbc003c00u, {1.0f, -1.0f}},
|
||||
PackedCase{vk::Format::eR16Sfloat, 0xffff0001u, {0x1p-24f}},
|
||||
PackedCase{vk::Format::eR16Sfloat, 0x00008001u, {-0x1p-24f}},
|
||||
PackedCase{vk::Format::eR16Sfloat, 0x000003ffu, {1023.0f * 0x1p-24f}},
|
||||
PackedCase{vk::Format::eR16Sfloat, 0x00000400u, {0x1p-14f}},
|
||||
PackedCase{vk::Format::eR16G16Sfloat, 0x80000000u, {0.0f, -0.0f}},
|
||||
PackedCase{vk::Format::eR16G16Sfloat, 0xfc007c00u,
|
||||
{std::numeric_limits<float>::infinity(),
|
||||
-std::numeric_limits<float>::infinity()}},
|
||||
PackedCase{vk::Format::eR16Unorm, 0x0000ffffu, {1.0f}},
|
||||
PackedCase{vk::Format::eR16G16Unorm, 0x0000ffffu, {1.0f, 0.0f}},
|
||||
PackedCase{vk::Format::eR16G16Unorm, 0x80000001u,
|
||||
{1.0f / 65535.0f, 32768.0f / 65535.0f}},
|
||||
};
|
||||
for (const auto &test : packed_cases) {
|
||||
vk::ClearColorValue clear{};
|
||||
Require(name, "packed 16-bit clear",
|
||||
DecodePackedColorClear(test.format, test.word, clear) &&
|
||||
std::bit_cast<std::array<uint32_t, 4>>(clear.float32) ==
|
||||
std::bit_cast<std::array<uint32_t, 4>>(test.channels),
|
||||
"16-bit clear lost channel order, sign, range or unused-channel zeros");
|
||||
}
|
||||
vk::ClearColorValue clear{};
|
||||
Require(name, "half NaN clear",
|
||||
DecodePackedColorClear(vk::Format::eR16Sfloat, 0x7e00u, clear) &&
|
||||
std::isnan(clear.float32[0]),
|
||||
"half NaN did not remain NaN");
|
||||
for (const auto format : {vk::Format::eR16Sfloat, vk::Format::eR16Unorm}) {
|
||||
Require(name, "R16 DWORD fill",
|
||||
!DecodeColorDwordFill(format, 0xbc003c00u, clear) &&
|
||||
DecodeColorDwordFill(format, 0x3c003c00u, clear),
|
||||
"alternating R16 pixels were treated as a uniform clear");
|
||||
}
|
||||
constexpr uintptr_t base = 0x0000000204100000ull;
|
||||
constexpr uint64_t allocation_size = 0x200000;
|
||||
constexpr uint64_t allocation_alignment = 0x10000;
|
||||
@@ -10170,6 +10210,9 @@ public:
|
||||
bool reuse_unorm = false;
|
||||
bool cmask = false;
|
||||
bool reuse_mips = false;
|
||||
Prospero::ChannelLayout layout = Prospero::ChannelLayout::k16_16_16_16;
|
||||
Prospero::ChannelType type = Prospero::ChannelType::kFloat;
|
||||
uint32_t clear_word = 0;
|
||||
};
|
||||
constexpr std::array cases{
|
||||
FillCase{0x40404040u, {0, 0x3c000000u}},
|
||||
@@ -10177,6 +10220,16 @@ public:
|
||||
FillCase{0x40404040u, {0, 0x3c000000u}, true},
|
||||
FillCase{0, {0x804020ffu, 0}, false, true},
|
||||
FillCase{0x40404040u, {0, 0x3c000000u}, false, false, true},
|
||||
FillCase{.fill = 0x20202020u, .texel = {0x7bff7bffu},
|
||||
.layout = Prospero::ChannelLayout::k16, .clear_word = 0x7bff7bffu},
|
||||
FillCase{.fill = 0x20202020u, .texel = {0xbc003c00u},
|
||||
.layout = Prospero::ChannelLayout::k16_16, .clear_word = 0xbc003c00u},
|
||||
FillCase{.fill = 0x20202020u, .texel = {0x80008000u},
|
||||
.layout = Prospero::ChannelLayout::k16,
|
||||
.type = Prospero::ChannelType::kUNorm, .clear_word = 0x80008000u},
|
||||
FillCase{.fill = 0x20202020u, .texel = {0x0000ffffu},
|
||||
.layout = Prospero::ChannelLayout::k16_16,
|
||||
.type = Prospero::ChannelType::kUNorm, .clear_word = 0x0000ffffu},
|
||||
};
|
||||
EnsureRuntimeContext();
|
||||
// Astro's generic metadata fill, through S_ENDPGM; trailing debug data is omitted.
|
||||
@@ -10214,10 +10267,6 @@ public:
|
||||
{.type = Prospero::ShaderBinaryType::kCs,
|
||||
.code_size_bytes = sizeof(native_fill)});
|
||||
}
|
||||
TileSizeAlign dcc_size{};
|
||||
(void)TileGetDccSize(512, 256, 1, 8, 1,
|
||||
Prospero::TileMode::kRenderTarget, dcc_size, 0);
|
||||
|
||||
int64_t direct_offset = -1;
|
||||
Require(name, "direct allocation",
|
||||
Libs::LibKernel::Memory::KernelAllocateDirectMemory(
|
||||
@@ -10234,11 +10283,19 @@ public:
|
||||
std::memset(mapped, 0, allocation_size);
|
||||
|
||||
for (const auto &fill_case : cases) {
|
||||
const auto layout = fill_case.cmask ? Prospero::ChannelLayout::k8_8_8_8 : fill_case.layout;
|
||||
const auto type = fill_case.cmask ? Prospero::ChannelType::kUNorm : fill_case.type;
|
||||
const auto bytes_per_pixel = TextureGetRenderTargetFormat(
|
||||
layout, type, Prospero::ChannelOrder::kStandard).bytes_per_element;
|
||||
const uint32_t probe_width = bytes_per_pixel == 2 ? 2 : 1;
|
||||
TileSizeAlign dcc_size{};
|
||||
(void)TileGetDccSize(512, 256, 1, bytes_per_pixel, 1,
|
||||
Prospero::TileMode::kRenderTarget, dcc_size, 0);
|
||||
const uint64_t dcc_address = base + (fill_case.reuse_mips ? 0x160000 : 0x100000);
|
||||
const uint32_t selected_layer = fill_case.cmask ? 1 : 0;
|
||||
const uint64_t metadata_size = fill_case.cmask ? 0x2000 : dcc_size.size;
|
||||
const std::vector<u32> expected(
|
||||
fill_case.texel.begin(), fill_case.texel.begin() + (fill_case.cmask ? 1 : 2));
|
||||
fill_case.texel.begin(), fill_case.texel.begin() + std::max(bytes_per_pixel / 4u, 1u));
|
||||
RenderContext context(m_runtime_context);
|
||||
HW::Context registers{};
|
||||
HW::UserConfig user_config{};
|
||||
@@ -10251,10 +10308,8 @@ public:
|
||||
registers.SetColorInfo(
|
||||
0, {.cmask_fast_clear_enable = !fill_case.reuse_unorm,
|
||||
.dcc_compression_enable = !fill_case.cmask,
|
||||
.format = fill_case.cmask ? Prospero::ChannelLayout::k8_8_8_8
|
||||
: Prospero::ChannelLayout::k16_16_16_16,
|
||||
.channel_type = fill_case.cmask ? Prospero::ChannelType::kUNorm
|
||||
: Prospero::ChannelType::kFloat,
|
||||
.format = layout,
|
||||
.channel_type = type,
|
||||
.channel_order = Prospero::ChannelOrder::kStandard});
|
||||
registers.SetColorView(0, {.base_array_slice_index = selected_layer,
|
||||
.last_array_slice_index = selected_layer});
|
||||
@@ -10265,7 +10320,7 @@ public:
|
||||
.metadata_pipe_aligned = true});
|
||||
registers.SetColorDccAddr(0, {.addr = dcc_address});
|
||||
registers.SetColorCmask(0, {.addr = dcc_address});
|
||||
registers.SetColorClearWord0(0, {.word0 = fill_case.cmask ? expected[0] : 0});
|
||||
registers.SetColorClearWord0(0, {.word0 = fill_case.cmask ? expected[0] : fill_case.clear_word});
|
||||
registers.SetColorClearWord1(0, {.word1 = 0});
|
||||
registers.SetRenderTargetMask(0x0f);
|
||||
scheduler.Begin(registers, user_config, shaders);
|
||||
@@ -10354,12 +10409,13 @@ public:
|
||||
!texture_cache.IsMeta(dcc_address),
|
||||
"color metadata did not retain its type and full native allocation");
|
||||
const auto read_texel = [&] {
|
||||
return ReadCachedTexel(name, context, color.image_id, {}, {1, 1, 1}, selected_layer);
|
||||
return ReadCachedTexel(name, context, color.image_id, {}, {probe_width, 1, 1}, selected_layer);
|
||||
};
|
||||
Require(name, "GPU color clear value",
|
||||
read_texel() == expected &&
|
||||
ReadCachedTexel(name, context, color.image_id, {511, 255, 0},
|
||||
{1, 1, 1}, selected_layer) == expected,
|
||||
ReadCachedTexel(name, context, color.image_id,
|
||||
{static_cast<int32_t>(512 - probe_width), 255, 0},
|
||||
{probe_width, 1, 1}, selected_layer) == expected,
|
||||
"native metadata did not materialize the expected color");
|
||||
if (fill_case.reuse_mips) {
|
||||
const auto &image = texture_cache.GetImage(color.image_id);
|
||||
@@ -10405,6 +10461,9 @@ public:
|
||||
painted = {0x0000ffffu, 0xffffffffu};
|
||||
} else if (fill_case.cmask) {
|
||||
painted = {0xffff00ffu};
|
||||
} else if (bytes_per_pixel <= 4) {
|
||||
const uint32_t one = type == Prospero::ChannelType::kFloat ? 0x3c00u : 0xffffu;
|
||||
painted = {bytes_per_pixel == 2 ? one | (one << 16u) : one};
|
||||
}
|
||||
const auto retains_painted_texel = [&] {
|
||||
const auto texels = ReadCachedTexel(name, context, color.image_id, {},
|
||||
@@ -11711,7 +11770,8 @@ public:
|
||||
value.dword_count = 4;
|
||||
PreparedBindings buffer_bindings;
|
||||
executor.PrepareBindings(buffer_runtime, buffer_bindings);
|
||||
executor.FindBuffers(buffer_bindings);
|
||||
std::array<PreparedBindings *, 1> buffer_stages{&buffer_bindings};
|
||||
executor.FindBuffers(buffer_stages);
|
||||
const auto original_id = buffer_bindings.buffer_sources[0].id;
|
||||
|
||||
auto &buffer_cache = resources.GetBufferCache();
|
||||
@@ -11744,7 +11804,8 @@ public:
|
||||
ShaderStageRuntime runtime{&buffer_program, &snapshot};
|
||||
PreparedBindings bindings;
|
||||
executor.PrepareBindings(runtime, bindings);
|
||||
executor.FindBuffers(bindings);
|
||||
std::array<PreparedBindings *, 1> stages{&bindings};
|
||||
executor.FindBuffers(stages);
|
||||
executor.RebindBuffers(bindings);
|
||||
const auto &owner = resources.GetBufferCache().GetBuffer(bindings.buffer_sources[0].id);
|
||||
Require(name, "halfword buffer base and exact tail",
|
||||
@@ -11755,6 +11816,74 @@ public:
|
||||
"native binding lost the byte adjustment or rounded the final halfword range");
|
||||
}
|
||||
|
||||
{
|
||||
constexpr uint64_t address = base + allocation_size - 0x4000 + 12;
|
||||
constexpr uint64_t mapped_tail = base + allocation_size - address;
|
||||
auto writer = make_buffer_program(ShaderType::Vertex, {.written = true});
|
||||
ShaderRecompiler::IR::CompiledShaderInfo reader{};
|
||||
reader.stage = ShaderType::Pixel;
|
||||
ShaderRecompiler::IR::ResourceSnapshot writer_snapshot, reader_snapshot;
|
||||
ShaderBufferResource descriptor{};
|
||||
descriptor.UpdateAddress48(address);
|
||||
descriptor.fields[2] = UINT32_MAX;
|
||||
auto &value = writer_snapshot.buffers.emplace_back();
|
||||
std::copy_n(descriptor.fields, 4, value.dwords.begin());
|
||||
value.dword_count = 4;
|
||||
ShaderStageRuntime writer_runtime{&writer, &writer_snapshot};
|
||||
ShaderStageRuntime reader_runtime{&reader, &reader_snapshot};
|
||||
PreparedBindings writer_bindings, reader_bindings;
|
||||
const auto check_range = [&](uint64_t expected, const char *description) {
|
||||
executor.PrepareBindings(writer_runtime, writer_bindings);
|
||||
executor.PrepareBindings(reader_runtime, reader_bindings);
|
||||
std::array<PreparedBindings *, 2> stages{&reader_bindings, &writer_bindings};
|
||||
executor.FindBuffers(stages);
|
||||
executor.RebindBuffers(writer_bindings);
|
||||
const auto &source = writer_bindings.buffer_sources[0];
|
||||
const auto &binding = writer_bindings.buffers[0];
|
||||
const auto &owner = resources.GetBufferCache().GetBuffer(source.id);
|
||||
Require(name, description, source.address == address && source.size == expected &&
|
||||
binding.buffer == owner.Handle() && binding.offset <= owner.Offset(address) &&
|
||||
binding.range == expected + owner.Offset(address) - binding.offset,
|
||||
"descriptor discovery or rebind changed the write boundary or base adjustment");
|
||||
};
|
||||
writer_snapshot.specialization_reads = {{address + 0x500, 4},
|
||||
{address - 0x40, 0x40}, {address + 0x80, 0}};
|
||||
reader_snapshot.specialization_reads = {{address + 0x300, 4}, {address + 0x100, 4}};
|
||||
check_range(0x100, "unbounded write stops before nearest cross-stage read");
|
||||
reader_snapshot.specialization_reads.clear();
|
||||
check_range(0x500, "unbounded write stops before same-stage read");
|
||||
writer_snapshot.specialization_reads = {{address - 0x40, 0x40},
|
||||
{address + mapped_tail + 0x100, 4}};
|
||||
check_range(mapped_tail, "earlier and unmapped reads preserve mapped tail");
|
||||
writer_snapshot.specialization_reads.clear();
|
||||
check_range(mapped_tail, "unbounded write without reads preserves mapped tail");
|
||||
reader_snapshot.specialization_reads = {{address + 0x100, 4}};
|
||||
value.dwords[2] = 0x300;
|
||||
check_range(0x300, "finite write preserves its declared range");
|
||||
value.dwords[2] = UINT32_MAX;
|
||||
writer.info.buffers[0].written = false;
|
||||
writer.info.buffers[0].read = true;
|
||||
check_range(mapped_tail, "read-only sentinel preserves mapped tail");
|
||||
auto &graphics = context.GetGraphics();
|
||||
auto &storage_limit = graphics.physical_device_properties.limits.maxStorageBufferRange;
|
||||
const auto original_limit = storage_limit;
|
||||
storage_limit = 0x2000;
|
||||
value.dwords[1] |= 16u << 16u;
|
||||
check_range(storage_limit - (graphics.StorageMinAlignment() - 1),
|
||||
"read-only structured sentinel reserves native alignment prefix");
|
||||
Require(name, "read-only native range fits device", writer_bindings.buffers[0].range <= storage_limit,
|
||||
"read-only cap omitted the native descriptor alignment prefix");
|
||||
value.dwords[2] = 0x300; // A finite 12 KiB descriptor remains finite despite the mock limit.
|
||||
std::array<PreparedBindings *, 1> stages{&writer_bindings};
|
||||
executor.FindBuffers(stages);
|
||||
Require(name, "finite read exceeds mock cap unchanged", writer_bindings.buffer_sources[0].size == 0x3000,
|
||||
"read-only window changed a finite descriptor's declared range");
|
||||
storage_limit = original_limit;
|
||||
check_range(0x3000, "finite structured read preserves native range");
|
||||
value.dwords[2] = UINT32_MAX;
|
||||
check_range(mapped_tail, "read-only structured sentinel preserves smaller mapping");
|
||||
}
|
||||
|
||||
for (const auto format : {Prospero::BufferFormat::k8UNorm,
|
||||
Prospero::BufferFormat::k16UNorm}) {
|
||||
const uint64_t alias_address = base + 0xe00000 +
|
||||
@@ -14462,7 +14591,8 @@ public:
|
||||
plane_dependency.memoryBarrierCount = 1;
|
||||
plane_dependency.pMemoryBarriers = &plane_barrier;
|
||||
const auto write_plane = [&] {
|
||||
executor.FindBuffers(plane_bindings);
|
||||
std::array<PreparedBindings *, 1> stages{&plane_bindings};
|
||||
executor.FindBuffers(stages);
|
||||
executor.RebindBuffers(plane_bindings);
|
||||
const auto &plane_buffer = plane_bindings.buffers[0];
|
||||
const vk::BufferCopy plane_copy{0, plane_buffer.offset, plane_size};
|
||||
@@ -14879,7 +15009,8 @@ public:
|
||||
const Image *sampled_image = nullptr,
|
||||
const Image *storage_image = nullptr,
|
||||
const Image *storage_image_uint = nullptr,
|
||||
vk::Sampler sampler = nullptr) {
|
||||
vk::Sampler sampler = nullptr,
|
||||
std::span<const Image> sampled_resources = {}) {
|
||||
using Kind = ShaderRecompiler::IR::DescriptorBindingKind;
|
||||
const auto &layout = compiled.program.bindings;
|
||||
auto shader_data = compiled.packed_user_data;
|
||||
@@ -15155,7 +15286,7 @@ public:
|
||||
write.pBufferInfo = &gds_info;
|
||||
writes.push_back(write);
|
||||
}
|
||||
const ShaderRecompiler::IR::DescriptorBinding *sampled = nullptr;
|
||||
std::vector<const ShaderRecompiler::IR::DescriptorBinding *> sampled_bindings;
|
||||
const ShaderRecompiler::IR::DescriptorBinding *storage = nullptr;
|
||||
const ShaderRecompiler::IR::DescriptorBinding *storage_uint = nullptr;
|
||||
const ShaderRecompiler::IR::DescriptorBinding *storage_atomic = nullptr;
|
||||
@@ -15168,10 +15299,7 @@ public:
|
||||
const auto &image =
|
||||
compiled.program.info.images.at(binding.resources.front());
|
||||
if (resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled) {
|
||||
Require(test.name, "dispatch", sampled == nullptr,
|
||||
"Vulkan test harness needs separate sampled images for mixed "
|
||||
"descriptor classes");
|
||||
sampled = &binding;
|
||||
sampled_bindings.push_back(&binding);
|
||||
} else if (image.atomic) {
|
||||
storage_atomic = &binding;
|
||||
} else if (image.numeric_class == Prospero::TextureNumericClass::Float) {
|
||||
@@ -15180,32 +15308,41 @@ public:
|
||||
storage_uint = &binding;
|
||||
}
|
||||
}
|
||||
if (sampled != nullptr) {
|
||||
Require(test.name, "dispatch", sampled_image != nullptr,
|
||||
size_t sampled_count = 0;
|
||||
for (const auto *binding : sampled_bindings) sampled_count += binding->resources.size();
|
||||
sampled_infos.reserve(sampled_count);
|
||||
Require(test.name, "dispatch", sampled_resources.empty() ||
|
||||
sampled_resources.size() == compiled.program.info.images.size(),
|
||||
"sampled resources must match the logical images");
|
||||
for (const auto *sampled : sampled_bindings) {
|
||||
Require(test.name, "dispatch", sampled_image != nullptr || !sampled_resources.empty(),
|
||||
"sampled image descriptor requested but no sampled image was "
|
||||
"provided");
|
||||
sampled_infos.resize(sampled->resources.size());
|
||||
const auto first_info = sampled_infos.size();
|
||||
sampled_infos.resize(first_info + sampled->resources.size());
|
||||
std::vector<u32> mip_indices(compiled.program.info.images.size());
|
||||
for (u32 slot = 0; slot < sampled_infos.size(); slot++) {
|
||||
auto &info = sampled_infos[slot];
|
||||
info.imageView = sampled_image->view;
|
||||
info.imageLayout = sampled_image->layout;
|
||||
for (u32 slot = 0; slot < sampled->resources.size(); slot++) {
|
||||
auto &info = sampled_infos[first_info + slot];
|
||||
const auto resource = sampled->resources[slot];
|
||||
const auto *source_image = sampled_resources.empty() ? sampled_image
|
||||
: &sampled_resources[resource];
|
||||
info.imageView = source_image->view;
|
||||
info.imageLayout = source_image->layout;
|
||||
if (compiled.program.info.images[resource].mip_mode ==
|
||||
ShaderRecompiler::IR::ImageMipMode::Dynamic) {
|
||||
const auto mip = test.sampled_image_view_base_mip + mip_indices[resource]++;
|
||||
Require(test.name, "dispatch", mip < sampled_image->mip_levels,
|
||||
Require(test.name, "dispatch", mip < source_image->mip_levels,
|
||||
"sampled mip descriptor exceeds the supplied image");
|
||||
vk::ImageViewCreateInfo view{};
|
||||
view.image = sampled_image->image;
|
||||
view.image = source_image->image;
|
||||
view.viewType = test.sampled_image_view_type;
|
||||
view.format = sampled_image->format;
|
||||
view.format = source_image->format;
|
||||
view.subresourceRange = {
|
||||
vk::ImageAspectFlagBits::eColor, mip, 1,
|
||||
test.sampled_image_view_base_layer,
|
||||
test.sampled_image_view_layers != 0
|
||||
? test.sampled_image_view_layers
|
||||
: sampled_image->layers - test.sampled_image_view_base_layer};
|
||||
: source_image->layers - test.sampled_image_view_base_layer};
|
||||
RequireVk(test.name, "dispatch",
|
||||
m_device.createImageView(&view, nullptr, &info.imageView),
|
||||
"vkCreateImageView(sampled mip)");
|
||||
@@ -15216,9 +15353,9 @@ public:
|
||||
write.sType = vk::StructureType::eWriteDescriptorSet;
|
||||
write.dstSet = descriptor_set;
|
||||
write.dstBinding = Native(sampled->kind);
|
||||
write.descriptorCount = static_cast<u32>(sampled_infos.size());
|
||||
write.descriptorCount = static_cast<u32>(sampled->resources.size());
|
||||
write.descriptorType = vk::DescriptorType::eSampledImage;
|
||||
write.pImageInfo = sampled_infos.data();
|
||||
write.pImageInfo = sampled_infos.data() + first_info;
|
||||
writes.push_back(write);
|
||||
}
|
||||
const auto BindStorage =
|
||||
@@ -15619,6 +15756,12 @@ public:
|
||||
cmd.setDepthTestEnable(depth.depth_test_enable);
|
||||
cmd.setDepthWriteEnable(depth.depth_write_enable);
|
||||
cmd.setDepthCompareOp(depth.depth_compare_op);
|
||||
#if !defined(__APPLE__)
|
||||
cmd.setDepthBoundsTestEnable(depth.depth_bounds_test_enable);
|
||||
if (depth.depth_bounds_test_enable) {
|
||||
cmd.setDepthBounds(registers.GetDepthBoundsMin(), registers.GetDepthBoundsMax());
|
||||
}
|
||||
#endif
|
||||
cmd.setDepthBiasEnable(false);
|
||||
cmd.setStencilTestEnable(depth.stencil_test_enable);
|
||||
if (depth.stencil_test_enable) {
|
||||
@@ -15675,6 +15818,26 @@ public:
|
||||
"depth feedback lost a prior write or the read-only draw changed depth");
|
||||
}
|
||||
}
|
||||
depth.depth_test_enable = false;
|
||||
for (uint32_t pass = 0; pass < 3; pass++) {
|
||||
depth.depth_bounds_test_enable = pass != 2;
|
||||
registers.SetDepthBoundsMin(pass == 0 ? 0.5f : 0.625f);
|
||||
registers.SetDepthBoundsMax(pass == 0 ? 0.5625f : 0.6875f);
|
||||
Require(name, "dynamic depth bounds pipeline reuse",
|
||||
pipeline(true, 2, 2).pipeline == filled.pipeline,
|
||||
"changing depth bounds or their enable state created another pipeline");
|
||||
draw(filled);
|
||||
const auto bounded_pixels = read_color();
|
||||
for (size_t index = 0; index < stored_depth.size(); index++) {
|
||||
const auto z = std::bit_cast<float>(stored_depth[index]);
|
||||
const bool passes = !depth.depth_bounds_test_enable ||
|
||||
(z >= registers.GetDepthBoundsMin() &&
|
||||
z <= registers.GetDepthBoundsMax());
|
||||
Require(name, "dynamic stored-depth bounds",
|
||||
bounded_pixels[index * 4] == (passes ? stored_depth[index] : 0u),
|
||||
"depth bounds did not filter stored depth independently of the depth test");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auto &wireframe = pipeline(true, 1, 1);
|
||||
Require(name, "pipeline cache", filled.pipeline != wireframe.pipeline &&
|
||||
@@ -17876,6 +18039,7 @@ private:
|
||||
"vertex layer output is not supported");
|
||||
m_rasterization_supported = available_features.fillModeNonSolid &&
|
||||
available_features.tessellationShader &&
|
||||
available_features.depthBounds &&
|
||||
available_depth_clip.depthClipEnable &&
|
||||
available_clip_control.depthClipControl &&
|
||||
available_color_write.colorWriteEnable &&
|
||||
@@ -17958,6 +18122,7 @@ private:
|
||||
device_features.shaderFloat64 = available_features.shaderFloat64;
|
||||
device_features.fillModeNonSolid = m_rasterization_supported;
|
||||
device_features.tessellationShader = m_rasterization_supported;
|
||||
device_features.depthBounds = m_rasterization_supported;
|
||||
device_info.pEnabledFeatures = &device_features;
|
||||
std::vector<const char *> device_extensions{
|
||||
VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME,
|
||||
@@ -20170,6 +20335,57 @@ TestCase ScalarBrevB32PreservesScc() {
|
||||
O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
|
||||
}
|
||||
|
||||
TestCase ScalarSextI16Captured() {
|
||||
using O = ShaderOpcode;
|
||||
TestCase test;
|
||||
test.name = "ScalarSextI16Captured";
|
||||
constexpr u32 wave_size = 64, high = 0x80000001u;
|
||||
test.initial = {0xdead0000u, 0xabcd7fffu, 0x12348000u, 0xabcfffffu};
|
||||
test.expected = test.initial;
|
||||
auto &code = test.code;
|
||||
for (u32 i = 0; i < 4u; ++i) {
|
||||
const auto extended = static_cast<u32>(static_cast<int32_t>(static_cast<int16_t>(test.initial[i])));
|
||||
for (const bool scc : {false, true}) {
|
||||
AppendVMovU32(&code, 30, i * 4u);
|
||||
AppendBufferLoadDword(&code, 3, 30);
|
||||
code.push_back(EncodeVop1(0x02, 11, Vgpr(3)));
|
||||
AppendSMovLiteral(&code, 107, high);
|
||||
code.push_back(EncodeSopc(0x06, InlineU32(scc ? 1u : 0u), InlineU32(1)));
|
||||
code.push_back(EncodeSop1(0x04, 126, 128)); // Scalar operations execute with empty EXEC.
|
||||
code.push_back(0xbeea1a0bu); // Captured s_sext_i32_i16 vcc_lo, s11.
|
||||
code.push_back(EncodeSMovB32(20, 106));
|
||||
code.push_back(EncodeSMovB32(21, 107));
|
||||
code.push_back(EncodeSMovB32(22, 253));
|
||||
code.push_back(EncodeSop1(0x04, 126, 193));
|
||||
AppendVMovU32(&code, 1, 1);
|
||||
code.push_back(EncodeVop2(0x01, 2, InlineU32(0), 1));
|
||||
const u32 expected[] = {extended, high, scc ? 1u : 0u};
|
||||
for (u32 word = 0; word < 3u; ++word) {
|
||||
AppendStoreSgprAtLaneDwordOffset(&code, 20u + word, 0, test.expected.size());
|
||||
test.expected.insert(test.expected.end(), wave_size, expected[word]);
|
||||
}
|
||||
AppendStoreVgprAtLaneDwordOffset(&code, 2, 0, test.expected.size());
|
||||
const uint64_t mask = (uint64_t{high} << 32u) | extended;
|
||||
for (u32 lane = 0; lane < wave_size; ++lane)
|
||||
test.expected.push_back(static_cast<u32>((mask >> lane) & 1u));
|
||||
}
|
||||
}
|
||||
AppendEnd(&code);
|
||||
test.initial.resize(test.expected.size());
|
||||
test.opcodes = {O::BUFFER_LOAD_DWORD, O::V_READFIRSTLANE_B32, O::S_MOV_B32,
|
||||
O::S_MOV_B64, O::S_CMP_EQ_U32, O::S_SEXT_I32_I16,
|
||||
O::V_CNDMASK_B32, O::V_MOV_B32, O::V_ADD_NC_U32,
|
||||
O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
test.decoded_counts = {{"S_SEXT_I32_I16", 8u}};
|
||||
test.required_spirv = {"OpBitFieldSExtract"};
|
||||
test.compute_info.threads_num[0] = wave_size;
|
||||
test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1;
|
||||
test.compute_info.thread_ids_num = 1;
|
||||
test.compute_info.wave_size = wave_size;
|
||||
test.has_compute_info = true;
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase ScalarBrevB64OperandsAndMasks(u32 wave_size) {
|
||||
using O = ShaderOpcode;
|
||||
TestCase test;
|
||||
@@ -29438,6 +29654,41 @@ TestCase TBufferStoreVariants() {
|
||||
O::S_ENDPGM}};
|
||||
}
|
||||
|
||||
TestCase FlatLoadDlcCaptured(u32 wave_size) {
|
||||
using O = ShaderOpcode;
|
||||
TestCase test;
|
||||
test.name = wave_size == 64 ? "FlatLoadDlcCapturedWave64" : "FlatLoadDlcCapturedWave32";
|
||||
auto &code = test.code;
|
||||
code.push_back(EncodeSop1(0x04, 126, InlineU32(1)));
|
||||
AppendVMovU32(&code, 4, 0);
|
||||
AppendVMovU32(&code, 5, 31);
|
||||
// PPSA04288 CS 05b58beb842bacbf:0x638, FLAT_LOAD_DWORDX2 v[0:1], v[4:5] DLC.
|
||||
code.insert(code.end(), {0xdc341000u, 0x007d0004u});
|
||||
AppendStoreVgpr(&code, 0, 0);
|
||||
AppendStoreVgpr(&code, 1, 1);
|
||||
// The descriptor store aliases the physical address read by the next load.
|
||||
AppendStoreVgpr(&code, 1, 8);
|
||||
code.push_back(0xbbfd0000u); // S_WAITCNT_VSCNT null, 0.
|
||||
code.insert(code.end(), {0xdc341000u, 0x007d0004u});
|
||||
AppendStoreVgpr(&code, 0, 2);
|
||||
AppendStoreVgpr(&code, 1, 3);
|
||||
AppendEnd(&code);
|
||||
test.initial = {0, 0, 0, 0, 0, 0, 0, 0, 0x11223344u, 0x55667788u};
|
||||
test.expected = {0x11223344u, 0x55667788u, 0x55667788u, 0x55667788u,
|
||||
0, 0, 0, 0, 0x55667788u, 0x55667788u};
|
||||
test.bda_mappings = {{0x1f00000000ull, 32}};
|
||||
test.opcodes = {O::S_MOV_B64, O::V_MOV_B32, O::FLAT_LOAD_DWORDX2,
|
||||
O::BUFFER_STORE_DWORD, O::S_WAITCNT_VSCNT, O::S_ENDPGM};
|
||||
test.required_spirv = {"OpDecorate %buffers Coherent", " 0 Coherent", "Volatile|Aligned 4"};
|
||||
test.decoded_counts = {{"dlc=1", 2}};
|
||||
test.compute_info.threads_num[0] = wave_size;
|
||||
test.compute_info.threads_num[1] = 1;
|
||||
test.compute_info.threads_num[2] = 1;
|
||||
test.compute_info.wave_size = wave_size;
|
||||
test.has_compute_info = true;
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase FlatLoadVariants() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
@@ -33348,7 +33599,7 @@ void CheckIndirectBufferStore(VulkanHarness &vulkan) {
|
||||
std::printf("[compute] %-32s ok\n", test.name);
|
||||
}
|
||||
|
||||
void CheckIndirectImageKeySwitch() {
|
||||
void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
|
||||
constexpr const char *name = "IndirectImageKeySwitch";
|
||||
constexpr uint32_t mapping_capacity = 1793u;
|
||||
using namespace ShaderRecompiler::IR;
|
||||
@@ -33401,8 +33652,10 @@ void CheckIndirectImageKeySwitch() {
|
||||
program.descriptor_sources.resize(2);
|
||||
program.descriptor_sources[0].dword_count = 8;
|
||||
program.descriptor_sources[0].indirect_descriptor =
|
||||
DescriptorSource::IndirectDescriptor{.material_source = 0, .table_source = 0,
|
||||
.selector_stride = 224, .selector_offset = 12, .table_stride = 32};
|
||||
DescriptorSource::IndirectDescriptor{
|
||||
.selector = DescriptorSource::IndirectDescriptor::SelectorRead{
|
||||
.source = 0, .stride = 224, .offset = 12},
|
||||
.table_source = 0, .table_stride = 32};
|
||||
program.descriptor_sources[1].dword_count = 4;
|
||||
|
||||
ImageResource root{};
|
||||
@@ -33451,6 +33704,19 @@ void CheckIndirectImageKeySwitch() {
|
||||
sampled_image != 0u && std::ranges::find(nonuniform, sampled_image) != nonuniform.end(),
|
||||
"the sample operand lacks its nonuniform decoration");
|
||||
|
||||
program.info.images[0].indirect_search_iterations = 0;
|
||||
program.binding_layout_complete = false;
|
||||
AllocateBindings(program);
|
||||
spirv = ShaderRecompiler::Spirv::EmitProgram(program, {.compute = &compute});
|
||||
ValidateSpirv("DenseIndirectImageTable", spirv);
|
||||
Require(name, "dense image map disassembly", tools.Disassemble(spirv, &text),
|
||||
"failed to disassemble dense image table shader");
|
||||
Require(name, "bounded dense image selection",
|
||||
CountText(text, "OpULessThan") == 1 &&
|
||||
CountText(text, "OpImageSampleExplicitLod") == 1 &&
|
||||
text.find("SampledImageArrayNonUniformIndexing") != std::string::npos,
|
||||
"dense image selection lost its bounds check or native nonuniform array");
|
||||
|
||||
// Materialized children follow all native roots, including unrelated images.
|
||||
root.indirect_resources = {0u};
|
||||
program.info.images = {root, candidate};
|
||||
@@ -33471,6 +33737,17 @@ void CheckIndirectImageKeySwitch() {
|
||||
CountText(text, "OpImageSampleExplicitLod") == 1,
|
||||
"materialized images expanded into per-candidate samples");
|
||||
|
||||
program.info.images[root.indirect_resources[103]].dimension =
|
||||
ShaderRecompiler::Decoder::ImageDimension::Dim2DArray;
|
||||
program.info.images[root.indirect_resources[103]].cube = true;
|
||||
program.binding_layout_complete = false;
|
||||
AllocateBindings(program);
|
||||
spirv = ShaderRecompiler::Spirv::EmitProgram(program, {.compute = &compute});
|
||||
ValidateSpirv(name, spirv);
|
||||
Require(name, "mixed contiguous image runs", tools.Disassemble(spirv, &text) &&
|
||||
CountText(text, "OpImageSampleExplicitLod") == 3,
|
||||
"one cube split the native arrays into per-image samples");
|
||||
|
||||
program.memory_info[0].image_dimension =
|
||||
ShaderRecompiler::Decoder::ImageDimension::Dim2DArray;
|
||||
program.memory_info[0].image_address_components = 4;
|
||||
@@ -33527,6 +33804,82 @@ void CheckIndirectImageKeySwitch() {
|
||||
Require(name, "mixed sample count", samples == 3u,
|
||||
"mixed candidate switch did not retain every image");
|
||||
}
|
||||
// Keep an unrelated native root between the null image and appended children,
|
||||
// then select both sides of a cube boundary and the unmapped suffix on the GPU.
|
||||
root.indirect_resources = {0u, 2u, 3u, 4u, 5u, 6u};
|
||||
root.indirect_search_iterations = 0;
|
||||
program.info.images.assign(7, candidate);
|
||||
program.info.images[0] = root;
|
||||
program.info.images[1].indirect_root = ImageResource::NoIndirectImage;
|
||||
program.info.images[4].dimension =
|
||||
ShaderRecompiler::Decoder::ImageDimension::Dim2DArray;
|
||||
program.info.images[4].cube = true;
|
||||
for (u32 component = 0; component < 4u; ++component) {
|
||||
constexpr std::array values{1.5f, 1.5f, 0.0f, 0.0f};
|
||||
address.SetArg(component, Value(std::bit_cast<u32>(values[component])));
|
||||
}
|
||||
auto &byte_offset = *block->PrependNewInst(
|
||||
std::next(block->begin()), ValueOpcode::IMul32, {Value(&key), Value(4u)});
|
||||
image.SetArg(0, Value(&byte_offset));
|
||||
auto &output = block->AppendNewInst(ValueOpcode::GetBufferResource,
|
||||
{Value(0u), Value(0u), Value(0u), Value(0u)});
|
||||
output.SetFlags<u32>(0u);
|
||||
program.memory_info.push_back({.kind = ResourceKind::Buffer, .offen = true});
|
||||
auto &store = block->AppendNewInst(
|
||||
ValueOpcode::StoreBufferU32,
|
||||
{Value(&output), Value(0u), Value(&byte_offset), Value(0u), Value(&sample_x), Value(true)});
|
||||
store.SetFlags(MemoryFlags{1u, 0u});
|
||||
program.info.buffers.push_back({.packed_stride = 1, .written = true});
|
||||
program.binding_layout_complete = false;
|
||||
AllocateBindings(program);
|
||||
|
||||
TestCase test;
|
||||
test.name = "IndirectImageContiguousRuns";
|
||||
CompiledShader compiled;
|
||||
compiled.program = std::move(program);
|
||||
compiled.packed_user_data.resize(compiled.program.bindings.ShaderDataDwords());
|
||||
constexpr std::array ordinals{0u, 1u, 2u, 3u, 4u, 5u, 2u, 0u};
|
||||
compiled.resources.flattened_srt.push_back(ordinals.size());
|
||||
compiled.resources.flattened_srt.insert(compiled.resources.flattened_srt.end(),
|
||||
ordinals.begin(), ordinals.end());
|
||||
std::vector<VulkanHarness::Image> textures;
|
||||
for (u32 resource = 0; resource < 7u; ++resource) {
|
||||
const auto layers = resource == 4u ? 6u : 1u;
|
||||
std::vector<u32> pixels(layers * 4u, std::bit_cast<u32>(float(resource)));
|
||||
textures.push_back(vulkan.CreateImageMips(
|
||||
test.name, 1, 1, vk::Format::eR32G32B32A32Sfloat,
|
||||
vk::ImageUsageFlagBits::eSampled, {pixels}, 4u,
|
||||
vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageType::e2D,
|
||||
resource == 4u ? vk::ImageViewType::e2DArray : vk::ImageViewType::e2D, layers));
|
||||
}
|
||||
const auto native_sampler = vulkan.CreateSampler(test.name);
|
||||
for (const u32 wave_size : {32u, 64u}) {
|
||||
compute.wave_size = wave_size;
|
||||
compute.host_subgroup_size = vulkan.SubgroupSize();
|
||||
compute.threads_num[0] = wave_size;
|
||||
compute.threads_num[1] = compute.threads_num[2] = 1u;
|
||||
compiled.program.wave_size = wave_size;
|
||||
compiled.spirv = ShaderRecompiler::Spirv::EmitProgram(compiled.program, {.compute = &compute});
|
||||
ValidateSpirv(test.name, compiled.spirv);
|
||||
const auto halves = wave_size > compute.host_subgroup_size ? 2u : 1u;
|
||||
Require(test.name, "native array run samples", tools.Disassemble(compiled.spirv, &text) &&
|
||||
CountText(text, "OpImageSampleExplicitLod") == 3u * halves,
|
||||
"mixed runs expanded into per-image samples");
|
||||
test.initial.assign(wave_size, 0xdeadbeefu);
|
||||
test.expected.assign(wave_size, 0u);
|
||||
for (u32 lane = 0; lane < ordinals.size(); ++lane) {
|
||||
test.expected[lane] = std::bit_cast<u32>(float(root.indirect_resources[ordinals[lane]]));
|
||||
}
|
||||
auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size());
|
||||
vulkan.Dispatch(test, compiled, buffer, nullptr, nullptr, nullptr, nullptr,
|
||||
native_sampler, textures);
|
||||
const auto actual = vulkan.ReadBuffer(test.name, buffer, test.expected.size());
|
||||
vulkan.DestroyBuffer(&buffer);
|
||||
CompareWords(test, "root, native slots, cube and mapping bounds", test.expected, actual);
|
||||
}
|
||||
vulkan.Device().destroySampler(native_sampler);
|
||||
for (auto &texture : textures) vulkan.DestroyImage(&texture);
|
||||
std::printf("[compute] %-32s ok\n", test.name);
|
||||
}
|
||||
|
||||
TestCase ImageStoreMipSelectsPpsa01340Descriptor() {
|
||||
@@ -34873,6 +35226,7 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(ScalarBitcmpB64DynamicOperands);
|
||||
AddCase(ScalarBitcmpB64IntegerConstants);
|
||||
AddCase(ScalarBrevB32PreservesScc);
|
||||
AddCase(ScalarSextI16Captured);
|
||||
cases.push_back(ScalarBrevB64OperandsAndMasks(32));
|
||||
cases.push_back(ScalarBrevB64OperandsAndMasks(64));
|
||||
AddCase(ScalarBfeI32CapturedRawSignExtends);
|
||||
@@ -35154,6 +35508,8 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(TBufferStoreFormatXy88IntegerComponents);
|
||||
AddCase(TBufferLoadFormatXy88IntegerComponents);
|
||||
AddCase(TBufferStoreVariants);
|
||||
cases.push_back(FlatLoadDlcCaptured(32));
|
||||
cases.push_back(FlatLoadDlcCaptured(64));
|
||||
AddCase(FlatLoadVariants);
|
||||
AddCase(FlatSubdwordLoadsApplyByteOffset);
|
||||
cases.push_back(GlobalLoadShortD16Captured(32));
|
||||
@@ -37856,15 +38212,6 @@ void CheckImageSamplerSpecialization() {
|
||||
options.user_data = user_data;
|
||||
options.input_info.compute = &compute;
|
||||
auto translated = ShaderRecompiler::TranslateProgram(code, options);
|
||||
if (scalar) {
|
||||
bool key_phi = false;
|
||||
for (const auto *block : translated.program.blocks)
|
||||
for (const auto &inst : *block)
|
||||
key_phi |= inst.GetOpcode() == ValueOpcode::GetImageResource &&
|
||||
inst.Arg(0).Resolve().IsPhi();
|
||||
Require(name, "descriptor key reconvergence", key_phi,
|
||||
"native crossed exits did not retain a descriptor key Phi");
|
||||
}
|
||||
auto plan = ExtractResourcePlan(translated.program);
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
@@ -37876,56 +38223,57 @@ void CheckImageSamplerSpecialization() {
|
||||
snapshot.images.size() == count + 1u,
|
||||
"GPU-selected material descriptors did not expand beyond native root capacity");
|
||||
const auto offset = specialization.images[0].indirect_mapping_offset;
|
||||
const auto ordinal = [&](u32 key) {
|
||||
for (u32 i = 0; i < snapshot.flattened_srt[offset]; ++i)
|
||||
if (snapshot.flattened_srt[offset + 1u + 2u * i] == key)
|
||||
return snapshot.flattened_srt[offset + 2u + 2u * i];
|
||||
return UINT32_MAX;
|
||||
const auto ordinal = [&](u32 byte_offset) {
|
||||
const auto word = byte_offset >> 2u;
|
||||
return word < snapshot.flattened_srt[offset]
|
||||
? snapshot.flattened_srt[offset + 1u + word] : 0u;
|
||||
};
|
||||
Require(name, "shift, wrap and whole-record read",
|
||||
snapshot.flattened_srt[offset] == count + 2u && ordinal(1u) != UINT32_MAX &&
|
||||
ordinal(0x08000001u) == ordinal(1u) && ordinal(count) < snapshot.images.size() &&
|
||||
snapshot.images[ordinal(count)].dwords[0] == 0u &&
|
||||
std::ranges::count(snapshot.specialization_reads,
|
||||
std::pair<uint64_t, uint64_t>{material_base, (count + 3u) * 4u}) == 1,
|
||||
"material keys lost shift/dedup, scalar wrap/OOB, or bulk strict-read semantics");
|
||||
Require(name, "dense table map and coherent snapshot",
|
||||
specialization.images[0].indirect_search_iterations == 0u &&
|
||||
snapshot.flattened_srt[offset] == (count - 1u) * 8u + 1u &&
|
||||
ordinal(32u) != 0u &&
|
||||
ordinal(count * 32u) == 0u && snapshot.images[0].dwords[0] == 0u &&
|
||||
snapshot.specialization_reads ==
|
||||
std::vector<std::pair<uint64_t, uint64_t>>{{table_base, count * 32u}},
|
||||
"image table lost its dense offsets, explicit null or single coherent read");
|
||||
const auto previous = specialization;
|
||||
memory[table_base / 4u + 8u] += 0x10000u;
|
||||
Require(name, "runtime descriptor refresh",
|
||||
MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
specialization == previous && snapshot.images[1].dwords[0] == 0x11100u,
|
||||
specialization == previous && snapshot.images[ordinal(32u)].dwords[0] == 0x11100u,
|
||||
"descriptor addresses were frozen or entered the shader specialization");
|
||||
auto compiled = ShaderRecompiler::CompileProgram(std::move(translated), options, specialization);
|
||||
ValidateSpirv(name, compiled.spirv);
|
||||
if (!scalar) {
|
||||
user_data[4] += 1u;
|
||||
Require(name, "misaligned material rejection",
|
||||
!MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"unaligned raw buffer base was treated as separately aligned scalar memory");
|
||||
Require(name, "GPU material reads remain on GPU",
|
||||
MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
specialization == previous && snapshot.specialization_reads ==
|
||||
std::vector<std::pair<uint64_t, uint64_t>>{{table_base, count * 32u}},
|
||||
"GPU material addressing reentered host descriptor selection");
|
||||
user_data[4] -= 1u;
|
||||
}
|
||||
auto dirty = runtime;
|
||||
dirty.read_specialization_memory = +[](void*, uint64_t, std::span<u32>) { return false; };
|
||||
Require(name, "dirty material rejection",
|
||||
Require(name, "unreadable image table rejection",
|
||||
!MaterializeResources(plan, dirty, snapshot, specialization),
|
||||
"GPU-selected texture materialization bypassed strict read provenance");
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
if (scalar) {
|
||||
ExpectFatal("ScalarImageDescriptorNumericConsumer", [&] {
|
||||
code.insert(code.begin() + table_read_end, EncodeVop1(0x01, 18, 12));
|
||||
code.pop_back();
|
||||
AppendBufferStoreDword(&code, 18, 0);
|
||||
code[2] = EncodeSopp(0x08, static_cast<int16_t>(code.size() - 3u));
|
||||
AppendEnd(&code);
|
||||
(void)ShaderRecompiler::TranslateProgram(code, options);
|
||||
});
|
||||
code.insert(code.begin() + table_read_end, EncodeVop1(0x01, 18, 12));
|
||||
code.pop_back();
|
||||
AppendBufferStoreDword(&code, 18, 0);
|
||||
code[2] = EncodeSopp(0x08, static_cast<int16_t>(code.size() - 3u));
|
||||
AppendEnd(&code);
|
||||
} else {
|
||||
ExpectFatal("MaterialImageWithoutEntryWitness", [&] {
|
||||
code[2] = EncodeSopp(0x00, 0); // S_NOP removes the original nonempty-EXEC witness.
|
||||
(void)ShaderRecompiler::TranslateProgram(code, options);
|
||||
});
|
||||
code[2] = EncodeSopp(0x00, 0); // GPU selectors do not need a host nonempty-EXEC witness.
|
||||
}
|
||||
#endif
|
||||
auto retained = ShaderRecompiler::TranslateProgram(code, options);
|
||||
auto retained_plan = ExtractResourcePlan(retained.program);
|
||||
Require(name, "retained descriptor consumers",
|
||||
MaterializeResources(retained_plan, runtime, snapshot, specialization),
|
||||
"retained GPU descriptor reads could not use the shared image table");
|
||||
compiled = ShaderRecompiler::CompileProgram(std::move(retained), options, specialization);
|
||||
ValidateSpirv(name, compiled.spirv);
|
||||
}
|
||||
|
||||
std::printf("[host] %-32s ok\n", "ImageSpecializationPipelineId");
|
||||
@@ -39789,6 +40137,64 @@ void CheckPm4DrawIndirectMultiPacket(RenderContext &renderer) {
|
||||
packet_mismatch &&
|
||||
!Gen5::AgcIsInternalDataPacket(0xc0017904u, invalid_payload),
|
||||
"indirect multi-draw packet differs from the expected stream");
|
||||
|
||||
constexpr uint32_t data_offset = 0x5c08;
|
||||
constexpr uint32_t stride = 32;
|
||||
std::array<uint32_t, data_offset / 4 + stride / 4 + 5> arguments{};
|
||||
const std::array<uint32_t, 5> first{0, 1, 3, 0xfffffffeu, 9};
|
||||
const std::array<uint32_t, 5> second{0, 2, 5, 7, 11};
|
||||
std::copy(first.begin(), first.end(), arguments.begin() + data_offset / 4);
|
||||
std::copy(second.begin(), second.end(), arguments.begin() + (data_offset + stride) / 4);
|
||||
std::array<uint32_t, 8> indices{};
|
||||
processor.BufferInit();
|
||||
processor.SetDrawIndirectArgsBaseAddress(reinterpret_cast<uint64_t>(arguments.data()));
|
||||
processor.SetIndexBaseAddress(reinterpret_cast<uint64_t>(indices.data()));
|
||||
processor.SetIndexType(0);
|
||||
std::array<uint32_t, 5> captured{0xc0032500u, data_offset, 0x94, 0x95, 0};
|
||||
Require("Pm4DrawIndirectMulti", "captured indexed single execution",
|
||||
execute(captured.data(), captured.size()),
|
||||
"indexed indirect draw rejected native source-select zero");
|
||||
// Zero primitive counts still traverse CP argument decoding and renderer entry,
|
||||
// while avoiding unrelated shader and attachment requirements.
|
||||
for (const bool indexed : {false, true}) {
|
||||
for (const bool multi : {false, true}) {
|
||||
for (const uint64_t draw_modifier : {0ull, 0x100ull, 1ull << 32u, (1ull << 32u) | 0x100ull}) {
|
||||
packet.fill(0);
|
||||
dcb.cursor_up = packet.data();
|
||||
auto *buffer = reinterpret_cast<Gen5::CommandBuffer *>(&dcb);
|
||||
alignas(4) uint32_t draw_count = 3;
|
||||
if (multi) {
|
||||
emitted = indexed ? Gen5::AgcDcbDrawIndexIndirectMulti(buffer, data_offset, 1, 2,
|
||||
&draw_count, stride, draw_modifier)
|
||||
: Gen5::AgcDcbDrawIndirectMulti(buffer, data_offset, 1, 2,
|
||||
&draw_count, stride, draw_modifier);
|
||||
} else {
|
||||
emitted = indexed ? Gen5::AgcDcbDrawIndexIndirect(buffer, data_offset, draw_modifier)
|
||||
: Gen5::AgcDcbDrawIndirect(buffer, data_offset, draw_modifier);
|
||||
}
|
||||
const uint32_t begin = multi && !indexed ? 3 : 0;
|
||||
const uint32_t words = multi ? (indexed ? 10 : 16) : 5;
|
||||
const uint32_t opcode = multi ? (indexed ? 0xc0083800u : 0xc0082c00u)
|
||||
: (indexed ? 0xc0032500u : 0xc0032400u);
|
||||
const uint32_t initiator = (indexed ? 0 : 2) |
|
||||
(draw_modifier == 0x100 ? 0x20 : 0);
|
||||
Require("Pm4DrawIndirectMulti", "indexed/auto initiator emission",
|
||||
emitted == packet.data() && dcb.cursor_up == packet.data() + words &&
|
||||
packet[begin] == opcode && packet[begin + 1] == data_offset &&
|
||||
packet[begin + (multi ? 9 : 4)] == initiator,
|
||||
"indirect opcode, source select, or modifier bits differ from the native packet");
|
||||
Require("Pm4DrawIndirectMulti", "single/multi argument execution",
|
||||
execute(packet.data(), words),
|
||||
"indirect draw failed shared argument decoding or padded/count-limited traversal");
|
||||
if (multi) {
|
||||
draw_count = 0;
|
||||
Require("Pm4DrawIndirectMulti", "zero indirect count execution",
|
||||
execute(packet.data(), words), "zero indirect draw count did not complete");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
renderer.GetCommandScheduler().Finish();
|
||||
std::printf("[host] %-32s ok\n", "Pm4DrawIndirectMulti");
|
||||
}
|
||||
|
||||
@@ -40460,8 +40866,15 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--scalar-sext-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, ScalarSextI16Captured());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--flat-d16-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, FlatLoadDlcCaptured(32));
|
||||
RunCase(&vulkan, FlatLoadDlcCaptured(64));
|
||||
RunCase(&vulkan, GlobalLoadShortD16Captured(32));
|
||||
RunCase(&vulkan, GlobalLoadShortD16Captured(64));
|
||||
RunCase(&vulkan, FlatLoadShortD16AddressSegments());
|
||||
@@ -40933,8 +41346,8 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--indirect-image-only") == 0) {
|
||||
CheckImageSamplerSpecialization();
|
||||
CheckIndirectImageKeySwitch();
|
||||
VulkanHarness vulkan;
|
||||
CheckIndirectImageKeySwitch(vulkan);
|
||||
RunCase(&vulkan, ImageCubeGradientsPreserveDerivatives());
|
||||
return 0;
|
||||
}
|
||||
@@ -41096,7 +41509,7 @@ int main(int argc, char **argv) {
|
||||
CheckIndirectBufferStore(vulkan);
|
||||
CheckRuntimeBufferRecords(vulkan);
|
||||
CheckComputeThreadDimensions(vulkan);
|
||||
CheckIndirectImageKeySwitch();
|
||||
CheckIndirectImageKeySwitch(vulkan);
|
||||
CheckWave64WholeWaveResults();
|
||||
CheckPs5GameExampleImageClearRuntimeShape();
|
||||
vulkan.CheckSchedulerTimeline();
|
||||
|
||||
Reference in new issue
Block a user