mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-19 18:36:53 +02:00
Compare commits
20 Commits
099de8dea6
...
e5f0ee7fc2
| Author | SHA1 | Date | |
|---|---|---|---|
| e5f0ee7fc2 | |||
| 5f29374d5b | |||
| 2c730e58ea | |||
| e87fb8c859 | |||
| 440245f616 | |||
| 05d25ef28c | |||
| cad6aafdc5 | |||
| defb2f11d3 | |||
| 75d7a4311d | |||
| bb505a3ae8 | |||
| ab8566591f | |||
| b2d3ce6dfc | |||
| 09c993dfe0 | |||
| de81300b28 | |||
| ac3711ce7c | |||
| 8b51370ca1 | |||
| e9bcc77e0b | |||
| 8639e0de81 | |||
| 7675579e79 | |||
| 831862a50b |
@@ -189,10 +189,6 @@ bool SyncRawImageBuffersEnabled() {
|
||||
return g_config->sync_raw_image_buffers;
|
||||
}
|
||||
|
||||
bool SupportZmaskEnabled() {
|
||||
return g_config->support_zmask;
|
||||
}
|
||||
|
||||
bool TessellationEnabled() {
|
||||
return g_config->tessellation_enabled;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,6 @@ struct ConfigOptions {
|
||||
bool renderdoc_enabled = false;
|
||||
bool readback_linear_images = false;
|
||||
bool sync_raw_image_buffers = false;
|
||||
bool support_zmask = false;
|
||||
bool tessellation_enabled = false;
|
||||
bool trophy_enabled = true;
|
||||
uint32_t trophy_duration_seconds = 6;
|
||||
@@ -152,7 +151,6 @@ bool GpuAssistedValidationEnabled();
|
||||
bool RenderDocEnabled();
|
||||
bool ReadbackLinearImagesEnabled();
|
||||
bool SyncRawImageBuffersEnabled();
|
||||
bool SupportZmaskEnabled();
|
||||
bool TessellationEnabled();
|
||||
bool TrophyEnabled();
|
||||
uint32_t GetTrophyDurationSeconds();
|
||||
|
||||
@@ -367,7 +367,7 @@ bool SysFileDeleteFile(const std::filesystem::path& name) {
|
||||
|
||||
bool SysFileFlush(sys_file_t& f) {
|
||||
if (f.type == SYS_FILE_FILE && f.f != nullptr) {
|
||||
return (fflush(f.f) == 0);
|
||||
return fflush(f.f) == 0 && fsync(fileno(f.f)) == 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
+3
-8
@@ -263,6 +263,9 @@ void TextureCache::DeleteImage(ImageId id) {
|
||||
EXIT("TextureCache: deleting a GPU-modified image without resolving its contents\n");
|
||||
}
|
||||
m_download_images.erase(id);
|
||||
if (image->info.metadata.kind == ImageMetadataKind::Htile) {
|
||||
m_surface_metas.erase(image->info.metadata.range.address);
|
||||
}
|
||||
UnregisterImage(id);
|
||||
if (m_scheduler.Active()) {
|
||||
m_scheduler.DeferOperation([this, id] { m_slot_images.erase(id); });
|
||||
@@ -716,13 +719,6 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingTyp
|
||||
"TextureCache: unsupported unequal-sample depth overlap copy (%u -> %u)\n",
|
||||
cached.backing.samples, replacement.backing.samples);
|
||||
}
|
||||
if (info.IsDepth() && info.metadata.kind == ImageMetadataKind::Htile) {
|
||||
// Copied depth supersedes pending clears from the previous native image.
|
||||
const auto metadata = m_surface_metas.find(info.metadata.range.address);
|
||||
if (metadata != m_surface_metas.end()) {
|
||||
metadata->second.clear_mask.reset();
|
||||
}
|
||||
}
|
||||
FreeImage(cached_id);
|
||||
return replacement_id;
|
||||
}
|
||||
@@ -1880,7 +1876,6 @@ void TextureCache::UnmapMemory(uint64_t address, uint64_t size) {
|
||||
EXIT("TextureCache: invalid unmap range\n");
|
||||
}
|
||||
std::scoped_lock lock {m_lock};
|
||||
// HTILE belongs to guest memory, independently of native image replacements.
|
||||
for (auto metadata = m_surface_metas.begin(); metadata != m_surface_metas.end();) {
|
||||
const auto base = metadata->first;
|
||||
if (base >= address && base < address + size) {
|
||||
|
||||
@@ -72,7 +72,16 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
|
||||
return;
|
||||
}
|
||||
const auto samples = render_sample_count(rt.attrib.num_fragments);
|
||||
if (samples == 0 || rt.attrib.num_samples != rt.attrib.num_fragments) {
|
||||
// TODO(PPSA24156): Replace this 2x MSAA approximation with proper 4-coverage/2-fragment EQAA.
|
||||
const bool approximate_eqaa = rt.attrib.num_samples == 2 && rt.attrib.num_fragments == 1;
|
||||
if (approximate_eqaa) {
|
||||
static std::atomic_bool logged = false;
|
||||
if (!logged.exchange(true, std::memory_order_relaxed)) {
|
||||
Log::WriteToConsoleAndLog(
|
||||
"Warning: approximating 4-coverage/2-fragment EQAA with 2x MSAA.\n");
|
||||
}
|
||||
}
|
||||
if (samples == 0 || (rt.attrib.num_samples != rt.attrib.num_fragments && !approximate_eqaa)) {
|
||||
EXIT("unsupported render-target sample configuration: samples=%u fragments=%u\n",
|
||||
rt.attrib.num_samples, rt.attrib.num_fragments);
|
||||
}
|
||||
|
||||
@@ -353,8 +353,12 @@ void Image::CopyImage(Image& source) {
|
||||
std::vector<vk::ImageCopy> copies;
|
||||
copies.reserve(levels);
|
||||
for (uint32_t level = 0; level < levels; level++) {
|
||||
const auto width = std::max(source.backing.extent.width >> level, 1u);
|
||||
const auto height = std::max(source.backing.extent.height >> level, 1u);
|
||||
// Overlapping cache images can differ in size; copying the source extent into a smaller
|
||||
// destination writes out of bounds and loses the device.
|
||||
const auto width = std::min(std::max(source.backing.extent.width >> level, 1u),
|
||||
std::max(backing.extent.width >> level, 1u));
|
||||
const auto height = std::min(std::max(source.backing.extent.height >> level, 1u),
|
||||
std::max(backing.extent.height >> level, 1u));
|
||||
const auto depth = std::max(base_depth >> level, 1u);
|
||||
const auto [source_layers, destination_layers] = SanitizeCopyLayers(source, *this, depth);
|
||||
vk::ImageCopy copy {};
|
||||
|
||||
@@ -126,13 +126,9 @@ NativeStorageBuffer(RenderContext& context, const PreparedBindings::BufferSource
|
||||
if (size > graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange) {
|
||||
EXIT("storage buffer range is unsupported\n");
|
||||
}
|
||||
const bool metadata_read = resource.read && Config::SupportZmaskEnabled() &&
|
||||
(address & 0x7fffu) == 0 &&
|
||||
context.GetTextureCache().IsMeta(address);
|
||||
auto [buffer, offset] = context.GetBufferCache().ObtainBuffer(
|
||||
address, size, resource.written || metadata_read,
|
||||
auto [buffer, offset] = context.GetBufferCache().ObtainBuffer(
|
||||
address, size, resource.written,
|
||||
resource.formatted || (resource.read && Config::SyncRawImageBuffersEnabled()), id);
|
||||
if (metadata_read) buffer->Fill(offset, size, 0xf); // HTILE ZMask::kUncompressed.
|
||||
const auto aligned_offset = Common::AlignDown(offset, alignment);
|
||||
const auto adjustment = offset - aligned_offset;
|
||||
const auto max_range = graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange;
|
||||
@@ -768,7 +764,10 @@ void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
|
||||
size = std::min({size, uint64_t {256} * 1024 * 1024, limit});
|
||||
}
|
||||
}
|
||||
size = Libs::LibKernel::Memory::ClampRangeSize(address, size);
|
||||
// Finite GPU descriptors can span sparse PRT holes; clamp only oversized/sentinel ranges.
|
||||
if (size >= (uint64_t {1} << 30u) || descriptor.NumRecords() == UINT32_MAX) {
|
||||
size = Libs::LibKernel::Memory::ClampRangeSize(address, size);
|
||||
}
|
||||
source = {address, size, cache.FindBuffer(address, size)};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,6 +275,7 @@ struct PipelineCache::ProgramCache {
|
||||
ShaderRecompiler::IR::ResourceSnapshot resources;
|
||||
ShaderRecompiler::IR::ResourceSpecialization specialization;
|
||||
std::vector<Permutation> permutations;
|
||||
bool skip_dispatch = false;
|
||||
};
|
||||
|
||||
struct ProgramKeyHash {
|
||||
@@ -360,6 +361,7 @@ struct PipelineCache::ProgramCache {
|
||||
bool source_changed = false;
|
||||
if (entry != programs.end()) {
|
||||
auto& cached = entry->second;
|
||||
if (cached.skip_dispatch) return {};
|
||||
Permutation* permutation = nullptr;
|
||||
if (cached.call_source) {
|
||||
source_changed = cached.RefreshCallSource(runtime);
|
||||
@@ -419,10 +421,18 @@ struct PipelineCache::ProgramCache {
|
||||
DumpShaderOriginal(stage_name, options.shader_hash, params.code);
|
||||
ShaderRecompiler::ShaderSource source;
|
||||
if (entry == programs.end()) source = ShaderRecompiler::PrepareShaderSource(params.code, options);
|
||||
if (source.unsupported_call_pc != UINT32_MAX) {
|
||||
PipelineCacheLog("!!! TEMPORARY COMPUTE DISPATCH SKIP !!! shader=0x{:016x} pc=0x{:08x} "
|
||||
"reason=unsupported GPU-selected callable target; rendering is incomplete",
|
||||
params.hash, source.unsupported_call_pc);
|
||||
programs.try_emplace(lookup_key, ShaderRecompiler::IR::ResourcePlan{}, nullptr)
|
||||
.first->second.skip_dispatch = true;
|
||||
return {};
|
||||
}
|
||||
auto* current = entry == programs.end() ? &source : entry->second.call_source.get();
|
||||
ShaderRecompiler::TranslateResult translated;
|
||||
if (current != nullptr) {
|
||||
if (current->call) {
|
||||
if (!current->calls.empty()) {
|
||||
if (entry == programs.end()) ShaderRecompiler::RefreshShaderSource(*current, runtime);
|
||||
translated = ShaderRecompiler::TranslateProgram(current->linked->decoded, options);
|
||||
translated.program.source_reads = current->reads;
|
||||
@@ -433,7 +443,7 @@ struct PipelineCache::ProgramCache {
|
||||
translated = ShaderRecompiler::TranslateProgram(params.code, options);
|
||||
}
|
||||
if (entry == programs.end()) {
|
||||
auto retained = source.call ? std::make_unique<ShaderRecompiler::ShaderSource>(std::move(source)) : nullptr;
|
||||
auto retained = !source.calls.empty() ? std::make_unique<ShaderRecompiler::ShaderSource>(std::move(source)) : nullptr;
|
||||
entry = programs.try_emplace(lookup_key,
|
||||
ShaderRecompiler::IR::ExtractResourcePlan(translated.program), std::move(retained)).first;
|
||||
EXIT_IF(!ShaderRecompiler::IR::MaterializeResources(
|
||||
|
||||
@@ -196,21 +196,27 @@ static void CreateDescriptorLayout(GraphicContext& graphics, PipelineCache::Pipe
|
||||
const auto& limits = graphics.GetPhysicalDeviceProperties().limits;
|
||||
uint64_t descriptor_count = 0;
|
||||
uint64_t storage_buffers = 0;
|
||||
std::array<uint64_t, 32> stage_buffers {}, stage_resources {};
|
||||
uint64_t samplers = 0;
|
||||
std::array<uint64_t, 32> stage_buffers {}, stage_resources {}, stage_samplers {};
|
||||
for (const auto& binding: bindings) {
|
||||
descriptor_count += binding.descriptorCount;
|
||||
const bool storage = binding.descriptorType == vk::DescriptorType::eStorageBuffer;
|
||||
const bool sampler = binding.descriptorType == vk::DescriptorType::eSampler;
|
||||
if (storage) storage_buffers += binding.descriptorCount;
|
||||
if (sampler) samplers += binding.descriptorCount;
|
||||
for (auto stages = static_cast<uint32_t>(binding.stageFlags); stages != 0; stages &= stages - 1u) {
|
||||
const auto stage = std::countr_zero(stages);
|
||||
if (storage) stage_buffers[stage] += binding.descriptorCount;
|
||||
if (binding.descriptorType != vk::DescriptorType::eSampler)
|
||||
if (sampler) stage_samplers[stage] += binding.descriptorCount;
|
||||
else
|
||||
stage_resources[stage] += binding.descriptorCount;
|
||||
if (stage_buffers[stage] > limits.maxPerStageDescriptorStorageBuffers ||
|
||||
stage_resources[stage] > limits.maxPerStageResources) {
|
||||
stage_resources[stage] > limits.maxPerStageResources ||
|
||||
stage_samplers[stage] > limits.maxPerStageDescriptorSamplers) {
|
||||
EXIT("native descriptor stage %u exceeds device limits: buffers=%" PRIu64
|
||||
"/%u resources=%" PRIu64 "/%u\n", stage, stage_buffers[stage],
|
||||
limits.maxPerStageDescriptorStorageBuffers, stage_resources[stage], limits.maxPerStageResources);
|
||||
"/%u resources=%" PRIu64 "/%u samplers=%" PRIu64 "/%u\n", stage, stage_buffers[stage],
|
||||
limits.maxPerStageDescriptorStorageBuffers, stage_resources[stage], limits.maxPerStageResources,
|
||||
stage_samplers[stage], limits.maxPerStageDescriptorSamplers);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,6 +224,10 @@ static void CreateDescriptorLayout(GraphicContext& graphics, PipelineCache::Pipe
|
||||
EXIT("native storage-buffer descriptors exceed device limit: %" PRIu64 "/%u\n",
|
||||
storage_buffers, limits.maxDescriptorSetStorageBuffers);
|
||||
}
|
||||
if (samplers > limits.maxDescriptorSetSamplers) {
|
||||
EXIT("native sampler descriptors exceed device limit: %" PRIu64 "/%u\n",
|
||||
samplers, limits.maxDescriptorSetSamplers);
|
||||
}
|
||||
pipeline.uses_push_descriptors = descriptor_count <= graphics.max_push_descriptors;
|
||||
|
||||
vk::DescriptorSetLayoutCreateInfo create {};
|
||||
|
||||
@@ -201,8 +201,8 @@ private:
|
||||
void BindImage(ImageId id, bool storage);
|
||||
void BindRenderTarget(ImageId id);
|
||||
void ResetBindings();
|
||||
void UpdateComputeMetaClears(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer);
|
||||
[[nodiscard]] bool TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer);
|
||||
[[nodiscard]] bool TryConsumeComputeFill(const ShaderComputeInputInfo& input,
|
||||
CommandBuffer& command, uint32_t group_x,
|
||||
uint32_t group_y, uint32_t group_z, uint32_t mode);
|
||||
|
||||
@@ -51,8 +51,8 @@ static bool FillSourcesDisjoint(std::span<const ShaderRecompiler::IR::Descriptor
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer) {
|
||||
bool RenderExecutor::TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
|
||||
const CommandBuffer& buffer) {
|
||||
const auto& program = *input.stage.program;
|
||||
const auto& resources = *input.stage.resources;
|
||||
if (resources.buffers.size() != program.info.buffers.size()) {
|
||||
@@ -62,9 +62,10 @@ void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input
|
||||
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
|
||||
const auto& resource = program.info.buffers[i];
|
||||
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
|
||||
// Reads and transforms of metadata do not establish a clear state.
|
||||
// A metadata resource that is also read is not proven to be a full overwrite. Execute it
|
||||
// conservatively instead of replacing the dispatch with a coarse full-surface clear.
|
||||
if ((!resource.written || resource.read) && cache.IsMeta(descriptor.Base48())) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,10 +75,13 @@ void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input
|
||||
if (resource.written) {
|
||||
const auto descriptor =
|
||||
DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
|
||||
(void)cache.ClearMeta(descriptor.Base48());
|
||||
if (cache.ClearMeta(descriptor.Base48())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t group_x,
|
||||
@@ -309,6 +313,7 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
}
|
||||
const auto compute_program =
|
||||
m_context.GetPipelineCache().GetComputeProgram(cs_regs, sh_regs, input_info);
|
||||
if (!compute_program) return;
|
||||
if (use_thread_dimensions) {
|
||||
input_info.dispatch_threads_num[0] = thread_group_x;
|
||||
input_info.dispatch_threads_num[1] = thread_group_y;
|
||||
@@ -317,14 +322,12 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
|
||||
const auto& program = *input_info.stage.program;
|
||||
const auto& resources = *input_info.stage.resources;
|
||||
if (resources.specialization_reads.empty()) {
|
||||
// Cached metadata addresses can be reused as ordinary buffers; preserve their guest writes.
|
||||
UpdateComputeMetaClears(input_info, buffer);
|
||||
if (TryConsumeComputeFill(input_info, buffer, thread_group_x, thread_group_y,
|
||||
thread_group_z, mode)) {
|
||||
ResetBindings();
|
||||
return;
|
||||
}
|
||||
if (resources.specialization_reads.empty() &&
|
||||
(TryConsumeComputeMetaClear(input_info, buffer) ||
|
||||
TryConsumeComputeFill(input_info, buffer, thread_group_x, thread_group_y,
|
||||
thread_group_z, mode))) {
|
||||
ResetBindings();
|
||||
return;
|
||||
}
|
||||
|
||||
if (use_thread_dimensions) {
|
||||
@@ -476,6 +479,7 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
input_info.indirect_args_address = args_addr;
|
||||
const auto compute_program = m_context.GetPipelineCache().GetComputeProgram(
|
||||
cs_regs, buffer.GetRegisters().GetShaderRegisters(), input_info);
|
||||
if (!compute_program) return;
|
||||
buffer.EndRendering();
|
||||
auto& pipeline = m_context.GetPipelineCache().GetComputePipeline(input_info, compute_program);
|
||||
auto& bindings = m_compute_bindings;
|
||||
|
||||
@@ -481,18 +481,14 @@ Decoder::Program DecodeFusedProgram(std::span<const uint32_t> front, std::span<c
|
||||
return result;
|
||||
}
|
||||
|
||||
void PrepareCallTarget(ShaderSource& source, const CompileOptions& options) {
|
||||
void PrepareCallTargets(ShaderSource& source, const CompileOptions& options) {
|
||||
using namespace IR;
|
||||
EXIT_NOT_IMPLEMENTED(options.stage != ShaderType::Compute);
|
||||
for (uint32_t i = 0; i < source.decoded.instructions.size(); ++i) {
|
||||
if (source.decoded.instructions[i].opcode != Decoder::Opcode::S_SWAPPC_B64) continue;
|
||||
EXIT_NOT_IMPLEMENTED(source.call.has_value() || options.stage != ShaderType::Compute);
|
||||
source.call = ShaderSource::Call {.instruction = i};
|
||||
source.calls.push_back({.instruction = i});
|
||||
}
|
||||
EXIT_IF(options.input_info.compute == nullptr);
|
||||
const auto& call = source.decoded.instructions[source.call->instruction];
|
||||
EXIT_NOT_IMPLEMENTED(call.src0.kind != Decoder::OperandKind::Sgpr ||
|
||||
call.dst.kind != Decoder::OperandKind::Sgpr ||
|
||||
call.src0.reg != call.dst.reg || call.dst.reg >= 105u);
|
||||
const auto graph = CFG::BuildGraph(source.decoded);
|
||||
Program query;
|
||||
query.stage = options.stage;
|
||||
@@ -524,7 +520,11 @@ void PrepareCallTarget(ShaderSource& source, const CompileOptions& options) {
|
||||
return value;
|
||||
};
|
||||
Register = [&](uint32_t before, uint32_t code) -> Value {
|
||||
const auto index = CFG::FindScalarDefinition(source.decoded, graph, before, code);
|
||||
std::vector<uint32_t> crossed_calls;
|
||||
const auto index = CFG::FindScalarDefinition(source.decoded, graph, before, code, crossed_calls);
|
||||
for (const auto crossed: crossed_calls) {
|
||||
std::ranges::find(source.calls, crossed, &ShaderSource::Call::instruction)->preserved_registers.set(code);
|
||||
}
|
||||
if (index == UINT32_MAX) {
|
||||
EXIT_IF(code < query.user_data_base || code - query.user_data_base >= query.user_data_count);
|
||||
return IREmitter(&block).GetUserData(static_cast<ScalarReg>(code));
|
||||
@@ -557,18 +557,30 @@ void PrepareCallTarget(ShaderSource& source, const CompileOptions& options) {
|
||||
EXIT_IF(!producer.ready || !producer.values.contains(code));
|
||||
return producer.values.at(code).Resolve();
|
||||
};
|
||||
for (uint32_t i = 0; i < 2u; ++i) {
|
||||
const auto value = Register(source.call->instruction, call.src0.reg + i);
|
||||
EXIT_IF(!ValidateRuntimeValue(query, value, RuntimeValueType::Integer));
|
||||
auto& root = query.value_storage.emplace_back(ValueOpcode::ReferenceU32);
|
||||
root.SetArg(0, value);
|
||||
source.call->target[i] = Value(&root);
|
||||
for (auto& site: source.calls) {
|
||||
const auto& call = source.decoded.instructions[site.instruction];
|
||||
EXIT_NOT_IMPLEMENTED(call.src0.kind != Decoder::OperandKind::Sgpr ||
|
||||
call.dst.kind != Decoder::OperandKind::Sgpr ||
|
||||
call.src0.reg != call.dst.reg || call.dst.reg >= 105u);
|
||||
for (uint32_t i = 0; i < 2u; ++i) {
|
||||
const auto value = Register(site.instruction, call.src0.reg + i);
|
||||
if (!ValidateRuntimeValue(query, value, RuntimeValueType::Integer)) {
|
||||
EXIT_IF(value.Instruction()->GetOpcode() != ValueOpcode::ReadConstBuffer);
|
||||
source.unsupported_call_pc = call.pc;
|
||||
source.calls.clear();
|
||||
return;
|
||||
}
|
||||
auto& root = query.value_storage.emplace_back(ValueOpcode::ReferenceU32);
|
||||
root.SetArg(0, value);
|
||||
site.target[i] = Value(&root);
|
||||
}
|
||||
}
|
||||
RewriteToSsa(query.blocks);
|
||||
ConstantPropagationPass(query.blocks, query.wave_size);
|
||||
RemoveIdentities(query);
|
||||
EliminateDeadCode(query.blocks);
|
||||
for (auto& value: source.call->target) value = value.Instruction()->Arg(0).Resolve();
|
||||
for (auto& site: source.calls)
|
||||
for (auto& value: site.target) value = value.Instruction()->Arg(0).Resolve();
|
||||
query.value_storage.clear();
|
||||
for (auto& inst: block) {
|
||||
inst.SetParent(nullptr);
|
||||
@@ -595,7 +607,7 @@ ShaderSource PrepareShaderSource(std::span<const uint32_t> code, const CompileOp
|
||||
Decoder::DecodeProgram(code, source.decoded);
|
||||
}
|
||||
if (source.decoded.has_swap_pc) {
|
||||
PrepareCallTarget(source, options);
|
||||
PrepareCallTargets(source, options);
|
||||
source.code.assign(code.begin(), code.end());
|
||||
source.decoded.code = source.code;
|
||||
}
|
||||
@@ -603,88 +615,122 @@ ShaderSource PrepareShaderSource(std::span<const uint32_t> code, const CompileOp
|
||||
}
|
||||
|
||||
const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtRuntime& runtime) {
|
||||
EXIT_IF(source.unsupported_call_pc != UINT32_MAX);
|
||||
auto& reads = source.reads;
|
||||
reads.clear();
|
||||
IR::SrtReadCapture capture(runtime, reads);
|
||||
const auto clean = IR::CleanRuntime(capture.ObservedRuntime());
|
||||
IR::SrtWalker walker(source.call_targets, clean);
|
||||
uint32_t low = 0, high = 0;
|
||||
const auto& call = source.decoded.instructions[source.call->instruction];
|
||||
if (!walker.Evaluate(source.call->target[0], low) || !walker.Evaluate(source.call->target[1], high))
|
||||
EXIT("shader call at pc 0x%08x has an unavailable scalar target", call.pc);
|
||||
// SWAPPC reads the old aliased pair and ignores the target's low two bits.
|
||||
const auto address = ((uint64_t {high} << 32u) | low) & ~uint64_t {3};
|
||||
EXIT_IF(address == 0u);
|
||||
for (auto& site: source.calls) {
|
||||
uint32_t low = 0, high = 0;
|
||||
if (!walker.Evaluate(site.target[0], low) || !walker.Evaluate(site.target[1], high))
|
||||
EXIT("shader call at pc 0x%08x has an unavailable scalar target",
|
||||
source.decoded.instructions[site.instruction].pc);
|
||||
// SWAPPC reads the old aliased pair and ignores the target's low two bits.
|
||||
const auto address = ((uint64_t {high} << 32u) | low) & ~uint64_t {3};
|
||||
EXIT_IF(address == 0u);
|
||||
site.address = address;
|
||||
}
|
||||
const auto query_reads = reads.size();
|
||||
if (source.linked) {
|
||||
auto& linked = *source.linked;
|
||||
const auto function = std::span(linked.code).subspan(source.code.size());
|
||||
if (clean.read_memory(clean.userdata, address, linked.observed_function) &&
|
||||
std::ranges::equal(function, linked.observed_function)) {
|
||||
return linked.decoded;
|
||||
bool unchanged = true;
|
||||
for (size_t i = 0; i < source.calls.size(); ++i) {
|
||||
const auto& function = linked.functions[i];
|
||||
const auto expected = std::span(linked.code).subspan(function.offset, function.size);
|
||||
const auto preceding = std::span(source.calls).first(i);
|
||||
const auto previous = std::ranges::find(preceding, source.calls[i].address, &ShaderSource::Call::address);
|
||||
if (previous != preceding.end()) {
|
||||
const auto& observed = linked.functions[previous - preceding.begin()];
|
||||
if (std::ranges::equal(expected, std::span(linked.code).subspan(observed.offset, observed.size))) continue;
|
||||
unchanged = false;
|
||||
break;
|
||||
}
|
||||
linked.observed_function.resize(function.size);
|
||||
if (!clean.read_memory(clean.userdata, source.calls[i].address, linked.observed_function) ||
|
||||
!std::ranges::equal(expected, linked.observed_function)) {
|
||||
unchanged = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (unchanged) return linked.decoded;
|
||||
reads.resize(query_reads);
|
||||
}
|
||||
ShaderSource::Linked linked {.code = source.code,
|
||||
.decoded = source.decoded};
|
||||
ShaderSource::Linked linked {.code = source.code, .decoded = source.decoded};
|
||||
const auto& last = source.decoded.instructions.back();
|
||||
EXIT_NOT_IMPLEMENTED(last.opcode != Decoder::Opcode::S_ENDPGM);
|
||||
// Keep the caller's data/footer bytes at their native offsets. The leaf's virtual CFG
|
||||
// PCs follow decoded instructions; its physical code is independently read and owned.
|
||||
const auto function_pc = last.pc + last.word_count * sizeof(uint32_t);
|
||||
for (uint32_t index = 0;;) {
|
||||
// The observed queue helper is a scalar leaf. Scalar encodings need at most one literal.
|
||||
EXIT_IF(index >= 64u || address > UINT64_MAX - (uint64_t {index} + 2u) * sizeof(uint32_t));
|
||||
std::array<uint32_t, 2> words {};
|
||||
if (!clean.read_memory(clean.userdata, address + uint64_t {index} * sizeof(uint32_t),
|
||||
std::span(words).first(1)))
|
||||
EXIT("shader call at pc 0x%08x cannot read leaf code", call.pc);
|
||||
const auto family = Decoder::GetInstructionFamily(words[0]);
|
||||
EXIT_NOT_IMPLEMENTED(family != Decoder::Family::SOP1 && family != Decoder::Family::SOP2 &&
|
||||
family != Decoder::Family::SOPC && family != Decoder::Family::SOPK &&
|
||||
family != Decoder::Family::SOPP);
|
||||
Decoder::Instruction inst;
|
||||
Decoder::DecodeInstruction(words, 0u, inst);
|
||||
if (inst.word_count == 2u) {
|
||||
if (!clean.read_memory(clean.userdata, address + uint64_t {index + 1u} * sizeof(uint32_t),
|
||||
std::span(words).last(1)))
|
||||
EXIT("shader call at pc 0x%08x cannot read leaf literal", call.pc);
|
||||
inst = {};
|
||||
Decoder::DecodeInstruction(words, 0u, inst);
|
||||
// Keep the caller's data/footer at native offsets. Expand leaf CFGs per call site
|
||||
// before SSA, so return edges neither require a dispatcher nor merge resource roots.
|
||||
uint32_t function_pc = last.pc + last.word_count * sizeof(uint32_t);
|
||||
std::map<uint64_t, std::vector<Decoder::Instruction>> functions;
|
||||
for (size_t i = 0; i < source.calls.size(); ++i) {
|
||||
const auto& site = source.calls[i];
|
||||
const auto& call = source.decoded.instructions[site.instruction];
|
||||
const auto address = source.calls[i].address;
|
||||
auto [entry, inserted] = functions.try_emplace(address);
|
||||
auto& function = entry->second;
|
||||
if (inserted) {
|
||||
for (uint32_t index = 0;;) {
|
||||
std::array<uint32_t, Decoder::MaxInstructionRawWords> words {};
|
||||
EXIT_IF(index > UINT32_MAX / sizeof(uint32_t) - words.size() ||
|
||||
address > UINT64_MAX - (uint64_t {index} + words.size()) * sizeof(uint32_t));
|
||||
uint32_t count = 1;
|
||||
Decoder::Instruction inst;
|
||||
for (;;) {
|
||||
if (!clean.read_memory(clean.userdata, address + uint64_t {index} * sizeof(uint32_t),
|
||||
std::span(words).first(count)))
|
||||
EXIT("shader call at pc 0x%08x cannot read leaf code", call.pc);
|
||||
inst = {};
|
||||
Decoder::DecodeInstruction(words, 0u, inst);
|
||||
if (inst.word_count <= count) break;
|
||||
count = inst.word_count;
|
||||
}
|
||||
inst.pc = index * sizeof(uint32_t);
|
||||
if (Decoder::IsDirectBranch(inst.opcode)) inst.branch_target += inst.pc;
|
||||
index += inst.word_count;
|
||||
EXIT_NOT_IMPLEMENTED(inst.opcode == Decoder::Opcode::S_SWAPPC_B64 ||
|
||||
inst.opcode == Decoder::Opcode::S_GETPC_B64 ||
|
||||
inst.opcode == Decoder::Opcode::S_ENDPGM);
|
||||
function.push_back(inst);
|
||||
if (inst.opcode == Decoder::Opcode::S_SETPC_B64) break;
|
||||
}
|
||||
}
|
||||
inst.pc = index * sizeof(uint32_t);
|
||||
if (Decoder::IsDirectBranch(inst.opcode)) inst.branch_target += inst.pc;
|
||||
linked.code.insert(linked.code.end(), inst.raw, inst.raw + inst.word_count);
|
||||
index += inst.word_count;
|
||||
EXIT_NOT_IMPLEMENTED(inst.opcode == Decoder::Opcode::S_SWAPPC_B64 ||
|
||||
inst.opcode == Decoder::Opcode::S_GETPC_B64 ||
|
||||
inst.opcode == Decoder::Opcode::S_ENDPGM);
|
||||
EXIT_IF(CFG::MayWriteScalarRegister(inst, call.dst.reg) || CFG::MayWriteScalarRegister(inst, call.dst.reg + 1u));
|
||||
const bool returns = inst.opcode == Decoder::Opcode::S_SETPC_B64;
|
||||
if (returns) {
|
||||
EXIT_NOT_IMPLEMENTED(inst.src0.kind != Decoder::OperandKind::Sgpr || inst.src0.reg != call.dst.reg);
|
||||
inst.opcode = Decoder::Opcode::S_BRANCH;
|
||||
inst.branch_target = call.pc + call.word_count * sizeof(uint32_t);
|
||||
} else if (Decoder::IsDirectBranch(inst.opcode)) {
|
||||
EXIT_IF(inst.branch_target > UINT32_MAX - function_pc);
|
||||
inst.branch_target += function_pc;
|
||||
const uint32_t function_size = function.back().pc + function.back().word_count * sizeof(uint32_t);
|
||||
EXIT_IF(function_pc > UINT32_MAX - function_size);
|
||||
const auto function_offset = static_cast<uint32_t>(linked.code.size());
|
||||
for (auto inst: function) {
|
||||
EXIT_IF(CFG::MayWriteScalarRegister(inst, call.dst.reg) ||
|
||||
CFG::MayWriteScalarRegister(inst, call.dst.reg + 1u));
|
||||
for (uint32_t code = 0; code < site.preserved_registers.size(); ++code) {
|
||||
if (site.preserved_registers[code] && CFG::MayWriteScalarRegister(inst, code))
|
||||
EXIT("shader call at pc 0x%08x clobbers scalar target dependency s%u", call.pc, code);
|
||||
}
|
||||
linked.code.insert(linked.code.end(), inst.raw, inst.raw + inst.word_count);
|
||||
if (inst.opcode == Decoder::Opcode::S_SETPC_B64) {
|
||||
EXIT_NOT_IMPLEMENTED(inst.src0.kind != Decoder::OperandKind::Sgpr || inst.src0.reg != call.dst.reg);
|
||||
inst.opcode = Decoder::Opcode::S_BRANCH;
|
||||
inst.branch_target = call.pc + call.word_count * sizeof(uint32_t);
|
||||
} else if (Decoder::IsDirectBranch(inst.opcode)) {
|
||||
EXIT_IF(inst.branch_target >= function_size);
|
||||
inst.branch_target += function_pc;
|
||||
}
|
||||
inst.pc += function_pc;
|
||||
linked.decoded.instructions.push_back(std::move(inst));
|
||||
}
|
||||
inst.pc += function_pc;
|
||||
linked.decoded.instructions.push_back(std::move(inst));
|
||||
if (returns) break;
|
||||
}
|
||||
const auto function_end = linked.decoded.instructions.back().pc +
|
||||
linked.decoded.instructions.back().word_count * sizeof(uint32_t);
|
||||
for (size_t i = source.decoded.instructions.size(); i + 1u < linked.decoded.instructions.size(); ++i) {
|
||||
const auto& inst = linked.decoded.instructions[i];
|
||||
if (Decoder::IsDirectBranch(inst.opcode))
|
||||
EXIT_IF(inst.branch_target < function_pc || inst.branch_target >= function_end);
|
||||
linked.functions.push_back({function_offset, function_size / 4u});
|
||||
linked.decoded.instructions[site.instruction].branch_target = function_pc;
|
||||
function_pc += function_size;
|
||||
}
|
||||
reads.resize(query_reads);
|
||||
reads.emplace_back(address, (linked.code.size() - source.code.size()) * sizeof(uint32_t));
|
||||
linked.decoded.instructions[source.call->instruction].branch_target = function_pc;
|
||||
for (size_t i = 0; i < source.calls.size(); ++i) {
|
||||
const auto preceding = std::span(source.calls).first(i);
|
||||
if (std::ranges::find(preceding, source.calls[i].address, &ShaderSource::Call::address) == preceding.end())
|
||||
reads.emplace_back(source.calls[i].address, linked.functions[i].size * sizeof(uint32_t));
|
||||
}
|
||||
linked.decoded.code = linked.code;
|
||||
linked.observed_function.resize(linked.code.size() - source.code.size());
|
||||
linked.observed_function.resize(std::ranges::max_element(linked.functions, {},
|
||||
&ShaderSource::Linked::Function::size)->size);
|
||||
|
||||
source.linked = std::move(linked);
|
||||
++source.revision;
|
||||
return source.linked->decoded;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "graphics/shader/shader.h"
|
||||
|
||||
#include <array>
|
||||
#include <bitset>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
@@ -47,16 +48,21 @@ struct ShaderSource {
|
||||
struct Call {
|
||||
uint32_t instruction;
|
||||
std::array<IR::Value, 2> target;
|
||||
std::bitset<128> preserved_registers;
|
||||
uint64_t address = 0;
|
||||
};
|
||||
struct Linked {
|
||||
struct Function { uint32_t offset; uint32_t size; };
|
||||
std::vector<uint32_t> code;
|
||||
Decoder::Program decoded;
|
||||
std::vector<Function> functions;
|
||||
std::vector<uint32_t> observed_function;
|
||||
};
|
||||
std::vector<uint32_t> code;
|
||||
Decoder::Program decoded;
|
||||
IR::ResourcePlan call_targets;
|
||||
std::optional<Call> call;
|
||||
std::vector<Call> calls;
|
||||
uint32_t unsupported_call_pc = UINT32_MAX;
|
||||
std::optional<Linked> linked;
|
||||
uint64_t revision = 0;
|
||||
std::vector<std::pair<uint64_t, uint64_t>> reads;
|
||||
|
||||
@@ -160,19 +160,6 @@ uint32_t EmitFPMad32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
|
||||
return sum;
|
||||
}
|
||||
|
||||
uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
|
||||
const auto min_ab = EmitMinMaxF32Value(state, a, b, false);
|
||||
const auto min3 = EmitMinMaxF32Value(state, min_ab, c, false);
|
||||
const auto max_ab = EmitMinMaxF32Value(state, a, b, true);
|
||||
const auto high_min = EmitMinMaxF32Value(state, max_ab, c, false);
|
||||
const auto median = EmitMinMaxF32Value(state, min_ab, high_min, true);
|
||||
const auto nan_ab = Binary(state, spv::OpLogicalOr, TypeBool(state),
|
||||
EmitClassifyF32(state, a).nan, EmitClassifyF32(state, b).nan);
|
||||
const auto any_nan =
|
||||
Binary(state, spv::OpLogicalOr, TypeBool(state), nan_ab, EmitClassifyF32(state, c).nan);
|
||||
return Select(state, TypeF32(state), any_nan, min3, median);
|
||||
}
|
||||
|
||||
uint32_t EmitFindUMsb64(EmitterState& state, uint32_t value) {
|
||||
// GLSL FindUMsb is restricted to 32-bit components.
|
||||
const auto unpacked = Unary(state, spv::OpBitcast, TypeU32Vector(state, 2), value);
|
||||
|
||||
@@ -100,10 +100,9 @@ uint32_t EmitClassMaskF16(EmitterState& state, uint32_t bits, uint32_t mask) {
|
||||
return EmitClassMaskBits(state, bits, mask, true);
|
||||
}
|
||||
|
||||
uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) {
|
||||
const auto lhs_class = EmitClassifyF32(state, lhs);
|
||||
const auto rhs_class = EmitClassifyF32(state, rhs);
|
||||
|
||||
static uint32_t EmitMinMaxF32(EmitterState& state, uint32_t lhs, uint32_t rhs,
|
||||
const F32Class& lhs_class, const F32Class& rhs_class,
|
||||
bool max_value) {
|
||||
const auto numeric_cond = state.builder.AllocateId();
|
||||
state.builder.AddFunction(max_value ? spv::OpFOrdGreaterThanEqual : spv::OpFOrdLessThan,
|
||||
TypeBool(state), numeric_cond, lhs, rhs);
|
||||
@@ -121,6 +120,34 @@ uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, boo
|
||||
return EmitBitcastU32ToF32(state, result_bits);
|
||||
}
|
||||
|
||||
uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) {
|
||||
const auto lhs_class = EmitClassifyF32(state, lhs);
|
||||
const auto rhs_class = EmitClassifyF32(state, rhs);
|
||||
return EmitMinMaxF32(state, lhs, rhs, lhs_class, rhs_class, max_value);
|
||||
}
|
||||
|
||||
uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
|
||||
const auto a_class = EmitClassifyF32(state, a);
|
||||
const auto b_class = EmitClassifyF32(state, b);
|
||||
const auto c_class = EmitClassifyF32(state, c);
|
||||
const auto min_ab = EmitMinMaxF32(state, a, b, a_class, b_class, false);
|
||||
const auto min_ab_class = EmitClassifyF32(state, min_ab);
|
||||
const auto min3 = EmitMinMaxF32(state, min_ab, c, min_ab_class, c_class, false);
|
||||
const auto max_ab = EmitMinMaxF32(state, a, b, a_class, b_class, true);
|
||||
const auto max_ab_class = EmitClassifyF32(state, max_ab);
|
||||
const auto max3 = EmitMinMaxF32(state, max_ab, c, max_ab_class, c_class, true);
|
||||
const auto max_bc = EmitMinMaxF32(state, b, c, b_class, c_class, true);
|
||||
const auto max_ac = EmitMinMaxF32(state, a, c, a_class, c_class, true);
|
||||
// RDNA2 compares numerically against MAX3, including ties between signed zeros.
|
||||
const auto a_is_max = Binary(state, spv::OpFOrdEqual, TypeBool(state), max3, a);
|
||||
const auto b_is_max = Binary(state, spv::OpFOrdEqual, TypeBool(state), max3, b);
|
||||
const auto median = Select(state, TypeF32(state), a_is_max, max_bc,
|
||||
Select(state, TypeF32(state), b_is_max, max_ac, max_ab));
|
||||
const auto nan_ab = EmitLogicalOrBool(state, a_class.nan, b_class.nan);
|
||||
const auto any_nan = EmitLogicalOrBool(state, nan_ab, c_class.nan);
|
||||
return Select(state, TypeF32(state), any_nan, min3, median);
|
||||
}
|
||||
|
||||
uint32_t EmitFlushF32DenormToSignedZero(EmitterState& state, uint32_t value) {
|
||||
const auto bits = state.builder.AllocateId();
|
||||
const auto abs_bits = state.builder.AllocateId();
|
||||
|
||||
@@ -97,10 +97,14 @@ uint32_t GatherMip(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Memory
|
||||
ConstantF32Value(state, 1.0f / 256.0f));
|
||||
};
|
||||
const auto sampler_control = ctx.Arg(inst, 6);
|
||||
const auto mip_filter = Extract(sampler_control, 26, 2);
|
||||
const auto preclamp = Binary(state, spv::OpINotEqual, TypeBool(state),
|
||||
Extract(sampler_control, 28, 1), ConstantU32(state, 0));
|
||||
const auto half = ConstantF32Value(state, 0.5f);
|
||||
const auto zero = ZeroF32(state);
|
||||
// Point selects the nearest mip; linear gathers return the lower, primary mip only.
|
||||
const auto rounding = Select(state, TypeF32(state),
|
||||
Binary(state, spv::OpIEqual, TypeBool(state), mip_filter, ConstantU32(state, 1)),
|
||||
ConstantF32Value(state, 0.5f), zero);
|
||||
const auto base = Unary(state, spv::OpConvertUToF, TypeF32(state),
|
||||
Extract(ctx.Arg(inst, 4), 12, 4));
|
||||
const auto minimum = Binary(state, spv::OpFSub, TypeF32(state),
|
||||
@@ -108,19 +112,19 @@ uint32_t GatherMip(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Memory
|
||||
const auto last = ConstantF32Value(state, static_cast<float>(mip_count - 1u));
|
||||
auto lod = Binary(state, spv::OpFAdd, TypeF32(state),
|
||||
AddressF32(ctx, mem, address, layout.lod),
|
||||
Select(state, TypeF32(state), preclamp, half, zero));
|
||||
Select(state, TypeF32(state), preclamp, rounding, zero));
|
||||
lod = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(
|
||||
state, lod, LodFixed(ctx.Arg(inst, 5), 0), LodFixed(ctx.Arg(inst, 5), 12));
|
||||
// T# minimum LOD is in physical mip space and applies after the S# view-relative clamp.
|
||||
lod = EmitGlsl<GLSLstd450FMax, IR::Type::F32>(state, lod, minimum);
|
||||
lod = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(state, lod, zero, last);
|
||||
lod = Binary(state, spv::OpFAdd, TypeF32(state), lod,
|
||||
Select(state, TypeF32(state), preclamp, zero, half));
|
||||
Select(state, TypeF32(state), preclamp, zero, rounding));
|
||||
// The clamped value is nonnegative, so conversion performs the integer mip rounding.
|
||||
const auto mip = EmitUMin32(state, Unary(state, spv::OpConvertFToU, TypeU32(state), lod),
|
||||
ConstantU32(state, mip_count - 1u));
|
||||
const auto mip_none = Binary(state, spv::OpIEqual, TypeBool(state),
|
||||
Extract(sampler_control, 26, 2), ConstantU32(state, 0));
|
||||
mip_filter, ConstantU32(state, 0));
|
||||
return Select(state, TypeU32(state), mip_none, ConstantU32(state, 0), mip);
|
||||
}
|
||||
|
||||
@@ -284,10 +288,10 @@ uint32_t ResultVector(ValueEmitContext& ctx, uint32_t value,
|
||||
}
|
||||
|
||||
uint32_t QueryDimensions(ValueEmitContext& ctx, const IR::MemoryInfo& mem,
|
||||
const IR::Inst& address) {
|
||||
const auto dimension = ctx.state.program.info.images.at(mem.resource).dimension;
|
||||
const IR::Inst& address, uint32_t resource, uint32_t array_index) {
|
||||
const auto dimension = ctx.state.program.info.images.at(resource).dimension;
|
||||
const auto& info = ImageDimensionInfoFor(dimension);
|
||||
const auto image = LoadImageDescriptor(ctx.state, mem.resource);
|
||||
const auto image = LoadImageDescriptor(ctx.state, resource, 0u, array_index);
|
||||
const auto size = ctx.state.builder.AllocateId();
|
||||
if (info.multisampled != 0u) {
|
||||
ctx.state.builder.AddFunction(spv::OpImageQuerySize,
|
||||
@@ -689,68 +693,58 @@ auto EmitImageAccess(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t resul
|
||||
}
|
||||
const auto selected = EmitIndirectResourceIndex(
|
||||
state, key, image.indirect_mapping_offset, image.indirect_search_iterations, 0u);
|
||||
struct ImageRun {
|
||||
uint32_t first;
|
||||
uint32_t count;
|
||||
uint32_t resource;
|
||||
uint32_t slot_bias;
|
||||
const auto SameType = [&](uint32_t lhs, uint32_t rhs) {
|
||||
const auto& first = state.program.info.images[lhs];
|
||||
const auto& second = state.program.info.images[rhs];
|
||||
return first.dimension == second.dimension && first.cube == second.cube &&
|
||||
IR::DescriptorBindingForImage(first) == IR::DescriptorBindingForImage(second) &&
|
||||
(inst.GetOpcode() != IR::ValueOpcode::ImageWrite ||
|
||||
first.shader_swizzle == second.shader_swizzle) &&
|
||||
first.mip_count == 1u && second.mip_count == 1u;
|
||||
};
|
||||
std::vector<ImageRun> 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];
|
||||
const auto kind = *IR::DescriptorBindingForImage(candidate);
|
||||
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 &&
|
||||
(inst.GetOpcode() != IR::ValueOpcode::ImageWrite ||
|
||||
candidate.shader_swizzle == first.shader_swizzle) &&
|
||||
candidate.mip_count == 1u && first.mip_count == 1u &&
|
||||
(ordinal == 1u || candidate.descriptor_index == next_slot)) {
|
||||
// Native roots precede appended children; only the root may have a slot gap.
|
||||
if (ordinal == 1u)
|
||||
run.slot_bias = candidate.descriptor_index - ordinal;
|
||||
++run.count;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
runs.push_back({ordinal, 1u, resource,
|
||||
candidate.descriptor_index - ordinal});
|
||||
// Descriptor order is unrelated to image type. Share each typed access and
|
||||
// select its descriptor slot independently, including gaps between native roots.
|
||||
struct ImageGroup { uint32_t resource; uint32_t count; };
|
||||
std::vector<ImageGroup> groups;
|
||||
std::vector<uint32_t> ordinal_groups;
|
||||
for (const auto resource: image.indirect_resources) {
|
||||
const auto found = std::ranges::find_if(groups, [&](const ImageGroup& group) {
|
||||
return SameType(resource, group.resource);
|
||||
});
|
||||
ordinal_groups.push_back(static_cast<uint32_t>(found - groups.begin()));
|
||||
if (found == groups.end()) groups.push_back({resource, 1u});
|
||||
else ++found->count;
|
||||
}
|
||||
const auto EmitRun = [&](uint32_t index) {
|
||||
const auto& run = runs[index];
|
||||
if (run.count == 1u) return emit(run.resource, 0u);
|
||||
auto slot = Binary(state, spv::OpIAdd, TypeU32(state), selected,
|
||||
ConstantU32(state, run.slot_bias));
|
||||
if (run.first == 0u) {
|
||||
const auto root_slot = image.descriptor_index;
|
||||
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 emit(run.resource, slot);
|
||||
};
|
||||
auto run_index = ConstantU32(state, 0u);
|
||||
for (uint32_t index = 1; index < runs.size(); ++index) {
|
||||
const bool dynamic_slot = std::ranges::any_of(groups, [](const ImageGroup& group) {
|
||||
return group.count > 1u;
|
||||
});
|
||||
auto group_index = ConstantU32(state, 0u);
|
||||
auto slot_bias = ConstantU32(state, image.descriptor_index);
|
||||
auto previous_bias = image.descriptor_index;
|
||||
for (uint32_t ordinal = 1; ordinal < image.indirect_resources.size(); ++ordinal) {
|
||||
const auto& candidate = state.program.info.images[image.indirect_resources[ordinal]];
|
||||
const auto bias = candidate.descriptor_index - ordinal;
|
||||
const bool select_group = ordinal_groups[ordinal] != ordinal_groups[ordinal - 1u];
|
||||
const bool select_slot = dynamic_slot && bias != previous_bias;
|
||||
previous_bias = bias;
|
||||
if (!select_group && !select_slot) continue;
|
||||
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;
|
||||
selected, ConstantU32(state, ordinal));
|
||||
if (select_group) group_index = Select(state, TypeU32(state), at_or_after,
|
||||
ConstantU32(state, ordinal_groups[ordinal]), group_index);
|
||||
if (select_slot) slot_bias = Select(state, TypeU32(state), at_or_after,
|
||||
ConstantU32(state, bias), slot_bias);
|
||||
}
|
||||
return runs.size() == 1u
|
||||
? EmitRun(0u)
|
||||
: EmitIndexSwitch(state, run_index, static_cast<uint32_t>(runs.size()),
|
||||
result_type, EmitRun);
|
||||
const auto slot = dynamic_slot
|
||||
? Binary(state, spv::OpIAdd, TypeU32(state), selected, slot_bias) : 0u;
|
||||
const auto EmitGroup = [&](uint32_t index) {
|
||||
const auto& group = groups[index];
|
||||
return emit(group.resource, group.count == 1u ? 0u : slot);
|
||||
};
|
||||
return groups.size() == 1u
|
||||
? EmitGroup(0u)
|
||||
: EmitIndexSwitch(state, group_index, static_cast<uint32_t>(groups.size()),
|
||||
result_type, EmitGroup);
|
||||
}
|
||||
|
||||
template <typename Emit>
|
||||
@@ -852,7 +846,10 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
const auto* address = ctx.ImageAddress(inst.Arg(image_info.needs_sampler ? 2 : 1));
|
||||
if (op == IR::ValueOpcode::ImageQueryDimensions) {
|
||||
state.builder.RequireCapability(spv::CapabilityImageQuery);
|
||||
ctx.Define(inst, QueryDimensions(ctx, mem, *address));
|
||||
ctx.Define(inst, EmitImageAccess(ctx, inst, TypeU32Vector(state, 4),
|
||||
[&](uint32_t resource, uint32_t array_index) {
|
||||
return QueryDimensions(ctx, mem, *address, resource, array_index);
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (image_info.needs_sampler &&
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <deque>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <span>
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::CFG {
|
||||
@@ -1650,6 +1652,32 @@ Graph BuildGraph(const Decoder::Program& program) {
|
||||
}
|
||||
SortUnique(graph.code_table_load_pcs);
|
||||
|
||||
if (!indirect_setpc && std::ranges::any_of(program.instructions, [](const auto& inst) {
|
||||
return inst.opcode == Opcode::S_SWAPPC_B64 && inst.branch_target != UINT32_MAX;
|
||||
})) {
|
||||
// Linked leaf bodies follow the caller in native PC order. Visit them in
|
||||
// execution order so returning to the caller does not manufacture goto loops.
|
||||
std::vector<bool> visited(graph.blocks.size(), false);
|
||||
std::vector<std::pair<uint32_t, bool>> pending {{graph.entry_block, false}};
|
||||
std::vector<BasicBlock> ordered;
|
||||
ordered.reserve(graph.blocks.size());
|
||||
while (!pending.empty()) {
|
||||
const auto [id, finished] = pending.back();
|
||||
pending.pop_back();
|
||||
if (finished) {
|
||||
ordered.push_back(std::move(graph.blocks[id]));
|
||||
continue;
|
||||
}
|
||||
if (visited[id]) continue;
|
||||
visited[id] = true;
|
||||
pending.emplace_back(id, true);
|
||||
for (const auto successor: graph.blocks[id].successors)
|
||||
pending.emplace_back(successor, false);
|
||||
}
|
||||
std::ranges::reverse(ordered);
|
||||
ApplyBlockOrder(graph, std::move(ordered));
|
||||
}
|
||||
|
||||
RecomputeAnalyses(graph);
|
||||
|
||||
if (indirect_setpc) {
|
||||
@@ -1681,33 +1709,56 @@ Graph Structurize(const Graph& graph) {
|
||||
}
|
||||
|
||||
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code) {
|
||||
uint32_t destination = UINT32_MAX;
|
||||
// Scalar ALU pair widths are not all decoded. The typed producer must confirm this word.
|
||||
return InstructionWritesScalarCode(inst, code) ||
|
||||
(ScalarOperandCode(inst.dst, destination) && code == destination + 1u);
|
||||
// Vector comparisons and carry results can write a scalar pair in wave64.
|
||||
if (inst.family == Decoder::Family::VOP1 || inst.family == Decoder::Family::VOP2 ||
|
||||
inst.family == Decoder::Family::VOPC || inst.family == Decoder::Family::VOP3 ||
|
||||
inst.family == Decoder::Family::VOP3P) {
|
||||
uint32_t destination = 0;
|
||||
for (const auto* operand: {&inst.dst, &inst.dst2})
|
||||
if (ScalarOperandCode(*operand, destination) && code >= destination && code <= destination + 1u)
|
||||
return true;
|
||||
}
|
||||
return InstructionWritesScalarCode(inst, code);
|
||||
}
|
||||
|
||||
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
|
||||
uint32_t before, uint32_t code) {
|
||||
uint32_t before, uint32_t code, std::vector<uint32_t>& crossed_calls) {
|
||||
EXIT_IF(before >= program.instructions.size());
|
||||
const auto found = std::ranges::find_if(graph.blocks, [before](const auto& block) {
|
||||
return block.inst_begin <= before && before < block.inst_end;
|
||||
});
|
||||
EXIT_IF(found == graph.blocks.end());
|
||||
const auto* block = &*found;
|
||||
for (size_t depth = 0; depth < graph.blocks.size(); ++depth) {
|
||||
for (uint32_t i = before; i > block->inst_begin;) {
|
||||
const auto& inst = program.instructions[--i];
|
||||
EXIT_IF(inst.opcode == Opcode::S_SWAPPC_B64);
|
||||
if (MayWriteScalarRegister(inst, code)) return i;
|
||||
}
|
||||
if (block->id == graph.entry_block) return UINT32_MAX;
|
||||
EXIT_IF(block->predecessors.size() != 1u);
|
||||
block = graph.FindBlock(block->predecessors[0]);
|
||||
std::vector<std::pair<uint32_t, uint32_t>> pending {{found->id, before}};
|
||||
std::set<std::pair<uint32_t, uint32_t>> visited;
|
||||
std::optional<uint32_t> definition;
|
||||
while (!pending.empty()) {
|
||||
const auto point = pending.back();
|
||||
pending.pop_back();
|
||||
if (!visited.insert(point).second) continue;
|
||||
const auto* block = graph.FindBlock(point.first);
|
||||
EXIT_IF(block == nullptr);
|
||||
before = block->inst_end;
|
||||
uint32_t index = UINT32_MAX;
|
||||
for (uint32_t i = point.second; i > block->inst_begin;) {
|
||||
const auto& inst = program.instructions[--i];
|
||||
if (inst.opcode == Opcode::S_SWAPPC_B64) {
|
||||
EXIT_NOT_IMPLEMENTED(MayWriteScalarRegister(inst, code));
|
||||
crossed_calls.push_back(i);
|
||||
continue;
|
||||
}
|
||||
if (MayWriteScalarRegister(inst, code)) { index = i; break; }
|
||||
}
|
||||
if (index != UINT32_MAX || block->id == graph.entry_block) {
|
||||
EXIT_IF(definition && *definition != index);
|
||||
definition = index;
|
||||
continue;
|
||||
}
|
||||
EXIT_IF(block->predecessors.empty());
|
||||
for (const auto predecessor: block->predecessors) {
|
||||
pending.emplace_back(predecessor, graph.blocks[predecessor].inst_end);
|
||||
}
|
||||
}
|
||||
EXIT("scalar shader call source has cyclic reaching definitions");
|
||||
EXIT_IF(!definition);
|
||||
return *definition;
|
||||
}
|
||||
|
||||
std::string BranchConditionToString(BranchCondition condition) {
|
||||
|
||||
@@ -115,7 +115,7 @@ struct Graph {
|
||||
Graph BuildGraph(const Decoder::Program& program);
|
||||
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code);
|
||||
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
|
||||
uint32_t before, uint32_t code);
|
||||
uint32_t before, uint32_t code, std::vector<uint32_t>& crossed_calls);
|
||||
// Returns structured control flow or failure diagnostics without changing the native graph.
|
||||
// On failure, failure_block is an original block ID or UINT32_MAX.
|
||||
Graph Structurize(const Graph& graph);
|
||||
|
||||
@@ -164,9 +164,23 @@ void DecodeSop1(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
switch (inst.opcode) {
|
||||
case Opcode::S_SWAPPC_B64:
|
||||
inst.branch_target = UINT32_MAX;
|
||||
inst.data_dwords = 2;
|
||||
[[fallthrough]];
|
||||
case Opcode::S_MOV_B64:
|
||||
case Opcode::S_CMOV_B64:
|
||||
case Opcode::S_NOT_B64:
|
||||
case Opcode::S_WQM_B64:
|
||||
case Opcode::S_BREV_B64:
|
||||
case Opcode::S_BITSET0_B64:
|
||||
case Opcode::S_BITSET1_B64:
|
||||
case Opcode::S_AND_SAVEEXEC_B64:
|
||||
case Opcode::S_ORN2_SAVEEXEC_B64:
|
||||
case Opcode::S_QUADMASK_B64:
|
||||
case Opcode::S_ANDN1_SAVEEXEC_B64:
|
||||
case Opcode::S_BITREPLICATE_B64_B32:
|
||||
inst.data_dwords = 2u;
|
||||
break;
|
||||
case Opcode::S_GETPC_B64:
|
||||
inst.data_dwords = 2u;
|
||||
inst.src_count = 0;
|
||||
DecodeScalarDestination(sdst, pc, inst.dst);
|
||||
return;
|
||||
@@ -206,6 +220,8 @@ void DecodeSop2(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
|
||||
DecodeScalarDestination(sdst, pc, inst.dst);
|
||||
DecodeBinarySources(pc, code, word_index, inst, ssrc0, ssrc1);
|
||||
if (opcode == 0x0bu || (opcode >= 0x0fu && opcode <= 0x25u && (opcode & 1u)) || opcode == 0x29u)
|
||||
inst.data_dwords = 2u;
|
||||
}
|
||||
|
||||
void DecodeSopk(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
|
||||
|
||||
@@ -1029,6 +1029,19 @@ void ConstantPropagationPass(const BlockList& blocks, uint32_t wave_size) {
|
||||
}
|
||||
drain();
|
||||
} while (normalized);
|
||||
// Repeated register reads share a bitcast only within the same SSA block.
|
||||
std::unordered_map<const Inst*, Inst*> bitcasts;
|
||||
for (auto* block: blocks) {
|
||||
bitcasts.clear();
|
||||
for (auto& inst: *block) {
|
||||
if (inst.GetOpcode() != ValueOpcode::BitCastF32U32 &&
|
||||
inst.GetOpcode() != ValueOpcode::BitCastU32F32) continue;
|
||||
const auto* source = Arg(inst, 0).TryInstruction();
|
||||
if (source == nullptr) continue;
|
||||
const auto [entry, inserted] = bitcasts.try_emplace(source, &inst);
|
||||
if (!inserted) Replace(inst, Value(entry->second));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
|
||||
@@ -30,32 +30,62 @@ void RemoveIdentities(Program& program) {
|
||||
}
|
||||
|
||||
void EliminateDeadCode(const BlockList& blocks) {
|
||||
std::vector<Inst*> pending;
|
||||
for (auto* block: blocks) {
|
||||
for (auto& inst: *block) {
|
||||
// Retained resource-planning instructions live outside the block list.
|
||||
inst.live = inst.MayHaveSideEffects() || std::ranges::any_of(inst.Uses(),
|
||||
[](const Use& use) { return use.user->Parent() == nullptr; });
|
||||
if (inst.live) pending.push_back(&inst);
|
||||
}
|
||||
}
|
||||
while (!pending.empty()) {
|
||||
const auto* inst = pending.back();
|
||||
pending.pop_back();
|
||||
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) {
|
||||
auto* input = inst->Arg(arg).TryInstruction();
|
||||
if (input != nullptr && input->Parent() != nullptr && !input->live) {
|
||||
input->live = true;
|
||||
pending.push_back(input);
|
||||
bool changed;
|
||||
do {
|
||||
std::vector<Inst*> pending;
|
||||
for (auto* block: blocks) {
|
||||
for (auto& inst: *block) {
|
||||
// Retained resource-planning instructions live outside the block list.
|
||||
inst.live = inst.MayHaveSideEffects() || std::ranges::any_of(inst.Uses(),
|
||||
[](const Use& use) { return use.user->Parent() == nullptr; });
|
||||
if (inst.live) pending.push_back(&inst);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Unlink every dead operand before freeing nodes in a dead Phi cycle.
|
||||
for (auto* block: blocks)
|
||||
for (auto& inst: *block)
|
||||
if (!inst.live) inst.Invalidate();
|
||||
for (auto* block: blocks)
|
||||
std::erase_if(block->Instructions(), [](const Inst& inst) { return !inst.live; });
|
||||
while (!pending.empty()) {
|
||||
const auto* inst = pending.back();
|
||||
pending.pop_back();
|
||||
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) {
|
||||
auto* input = inst->Arg(arg).TryInstruction();
|
||||
if (input != nullptr && input->Parent() != nullptr && !input->live) {
|
||||
input->live = true;
|
||||
pending.push_back(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Unlink every dead operand before freeing nodes in a dead Phi cycle.
|
||||
for (auto* block: blocks)
|
||||
for (auto& inst: *block)
|
||||
if (!inst.live) inst.Invalidate();
|
||||
for (auto* block: blocks)
|
||||
std::erase_if(block->Instructions(), [](const Inst& inst) { return !inst.live; });
|
||||
|
||||
changed = false;
|
||||
for (auto* block: blocks) {
|
||||
const auto& term = block->terminator;
|
||||
if (term.kind != CFG::TerminatorKind::ConditionalBranch || term.loop_header ||
|
||||
block->condition.IsImmediate()) continue;
|
||||
for (const bool take_true: {false, true}) {
|
||||
const auto* empty = take_true ? term.false_block : term.true_block;
|
||||
const auto* merge = take_true ? term.true_block : term.false_block;
|
||||
if (empty == nullptr || merge == nullptr || !empty->empty() ||
|
||||
empty->terminator.loop_header || merge->terminator.loop_header ||
|
||||
empty->terminator.kind != CFG::TerminatorKind::Branch ||
|
||||
empty->terminator.true_block != merge ||
|
||||
std::ranges::any_of(*merge, [](const Inst& inst) {
|
||||
return inst.GetOpcode() == ValueOpcode::Phi;
|
||||
})) continue;
|
||||
// Keep the CFG and SSA edges intact; only the empty detour's predicate is dead.
|
||||
for (auto& inst: *block) {
|
||||
if (inst.GetOpcode() == ValueOpcode::Reference &&
|
||||
inst.Arg(0).Resolve() == block->condition.Resolve())
|
||||
inst.SetArg(0, Value(take_true));
|
||||
}
|
||||
block->condition = Value(take_true);
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (changed);
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
|
||||
@@ -292,11 +292,23 @@ bool NormalizeIndirectBuffer(DescriptorValue& value, bool indexed, const BufferR
|
||||
bool IsBoundedDescriptorTable(const ResourcePlan& program,
|
||||
const DescriptorSource::IndirectDescriptor& indirect) {
|
||||
const auto* table = Source(program, indirect.table_source);
|
||||
return indirect.sources.empty() && !indirect.selector &&
|
||||
return indirect.sources.empty() && (!indirect.selector || indirect.table_scalar) &&
|
||||
(indirect.key_count.IsEmpty() || indirect.table_maximum) && indirect.workgroup_axis == UINT32_MAX &&
|
||||
table != nullptr && table->dword_count == 4u;
|
||||
}
|
||||
|
||||
bool CanReadIndexedSelector(const ShaderBufferResource& material, uint32_t offset) {
|
||||
if (material.Type() != 0u || material.SwizzleEnabled() || material.AddTid() ||
|
||||
material.OutOfBounds() != 0u) return false;
|
||||
if (material.Format() == Prospero::BufferFormat::kInvalid || material.NumRecords() == 0u ||
|
||||
offset >= material.Stride()) return true;
|
||||
return material.Base48() != 0u &&
|
||||
((material.Base48() | material.Stride() | offset) & 3u) == 0u &&
|
||||
material.NumRecords() <= MaxIndirectDescriptorProbes &&
|
||||
uint64_t {material.NumRecords() - 1u} * material.Stride() + offset + 4u <=
|
||||
uint64_t {UINT32_MAX} + 1u;
|
||||
}
|
||||
|
||||
bool EvaluateIndirectCount(const DescriptorSource::IndirectDescriptor& indirect,
|
||||
const SrtRuntime& runtime, SrtWalker& clean, uint32_t& count) {
|
||||
if (indirect.workgroup_axis != UINT32_MAX) {
|
||||
@@ -490,6 +502,15 @@ bool ResolveSelectorKeys(const DescriptorSource::IndirectDescriptor& indirect,
|
||||
keys.push_back(key);
|
||||
mask &= mask - 1u;
|
||||
}
|
||||
} else if (selector->stride == 0u) {
|
||||
ShaderBufferResource material;
|
||||
if (!DecodeBufferDescriptor(material_value, material) ||
|
||||
!CanReadIndexedSelector(material, selector->offset)) return false;
|
||||
if (material.Format() != Prospero::BufferFormat::kInvalid && material.NumRecords() != 0u &&
|
||||
selector->offset < material.Stride() &&
|
||||
!read_keys(material.Base48(), material.GetSize(), selector->offset,
|
||||
material.Stride(), material.NumRecords())) return false;
|
||||
keys.push_back(0u); // Every out-of-bounds indexed load returns zero.
|
||||
} else {
|
||||
ShaderBufferResource material;
|
||||
if (!DecodeBufferDescriptor(material_value, material) || selector->stride < 4u ||
|
||||
@@ -524,29 +545,29 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
ResourceSnapshot& snapshot,
|
||||
std::vector<DescriptorValue>& descriptors,
|
||||
std::vector<Specialization>& specializations,
|
||||
uint32_t maximum_resources, Normalize&& normalize) {
|
||||
Normalize&& normalize) {
|
||||
const auto& indirect = *program.descriptor_sources[source_index].indirect_descriptor;
|
||||
const auto descriptor_bytes = read_dwords * sizeof(uint32_t);
|
||||
const auto& sources = indirect.sources;
|
||||
const auto* selector = indirect.selector ? &*indirect.selector : nullptr;
|
||||
const bool byte_keys = IsBoundedDescriptorTable(program, indirect);
|
||||
auto* selector_keys = selector != nullptr
|
||||
? &program.selector_keys[program.selector_groups[source_index]] : nullptr;
|
||||
auto& keys = selector_keys != nullptr ? selector_keys->values : program.material_keys;
|
||||
auto& byte_offsets = 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 intern_candidate = [&](const DescriptorValue& candidate) {
|
||||
if (candidate == descriptors[resource_index]) return 0u;
|
||||
const auto found = std::find(descriptors.begin() + children_begin, descriptors.end(), candidate);
|
||||
ordinal = static_cast<uint32_t>(found - descriptors.begin() - children_begin + 1u);
|
||||
const auto 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;
|
||||
return ordinal;
|
||||
};
|
||||
uint64_t table_base = 0;
|
||||
uint64_t table_size = UINT64_MAX; // Scalar addresses have no buffer descriptor bounds.
|
||||
@@ -558,7 +579,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
"indirect {} resource={} source={} table_source={} words={:08x},{:08x},{:08x},{:08x} "
|
||||
"base={:#x} size={} type={} runtime_stride={} records={} swizzle={} addtid={} oob={} "
|
||||
"maximum={} offset={} immediate={} record_bytes={} child_count={} child_stride={} "
|
||||
"child_offset={} indexed_pointer={} candidates={} capacity={} {}",
|
||||
"child_offset={} indexed_pointer={} candidates={} {}",
|
||||
reason, resource_index, source_index, indirect.table_source,
|
||||
table_value.dwords[0], table_value.dwords[1], table_value.dwords[2], table_value.dwords[3],
|
||||
table_base, table_size, table.Type(), table.Stride(), table.NumRecords(),
|
||||
@@ -567,7 +588,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
indirect.table_offset, indirect.table_immediate, indirect.table_record_bytes,
|
||||
pointer != nullptr ? pointer->count : 1u, pointer != nullptr ? pointer->stride : 0u,
|
||||
pointer != nullptr ? pointer->offset : 0u, pointer != nullptr && pointer->indexed,
|
||||
descriptors.size(), maximum_resources, details));
|
||||
descriptors.size(), details));
|
||||
};
|
||||
const auto read_failed = [&](std::string_view reason, uint32_t row, uint32_t child,
|
||||
uint64_t base, uint64_t offset, size_t bytes) {
|
||||
@@ -583,7 +604,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
if (sources.empty()) {
|
||||
if (selector == nullptr) keys.clear();
|
||||
DescriptorValue material_value;
|
||||
if ((selector != nullptr && !selector_keys->resolved &&
|
||||
if ((selector != nullptr && (byte_keys || !selector_keys->resolved) &&
|
||||
!clean.EvaluateDescriptor(selector->source, material_value)) ||
|
||||
!clean.EvaluateDescriptor(indirect.table_source, table_value)) {
|
||||
return false;
|
||||
@@ -596,16 +617,40 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (IsBoundedDescriptorTable(program, indirect)) {
|
||||
if (byte_keys && table_size > UINT32_MAX) return failed("table geometry");
|
||||
bool use_selector = selector != nullptr;
|
||||
if (byte_keys && selector != nullptr) {
|
||||
ShaderBufferResource material;
|
||||
if (!DecodeBufferDescriptor(material_value, material)) return false;
|
||||
use_selector = CanReadIndexedSelector(material, selector->offset);
|
||||
if (use_selector && material.Format() != Prospero::BufferFormat::kInvalid &&
|
||||
material.NumRecords() != 0u && selector->offset < material.Stride()) {
|
||||
const auto begin = material.Base48() + selector->offset;
|
||||
const auto end = begin + uint64_t {material.NumRecords() - 1u} * material.Stride() + 4u;
|
||||
for (uint32_t i = 0; i < program.info.buffers.size(); ++i) {
|
||||
const auto& buffer = program.info.buffers[i];
|
||||
if (!buffer.written) continue;
|
||||
ShaderBufferResource written;
|
||||
if (program.descriptor_sources[buffer.source].indirect_descriptor ||
|
||||
!DecodeBufferDescriptor(snapshot.buffers[i], written) ||
|
||||
(written.Base48() != 0u && written.GetSize() != 0u &&
|
||||
begin < written.Base48() + written.GetSize() &&
|
||||
written.Base48() < end)) {
|
||||
use_selector = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (byte_keys && !use_selector) {
|
||||
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 &&
|
||||
if (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)))
|
||||
(table.Stride() & 3u) != 0u || table.Stride() < indirect.table_record_bytes))
|
||||
return failed("table geometry");
|
||||
const auto* pointer = indirect.pointer_table ? &*indirect.pointer_table : nullptr;
|
||||
const bool indexed_pointer = pointer != nullptr && pointer->indexed;
|
||||
@@ -640,10 +685,11 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
return failed("candidate count", fmt::format("rows={} limit={}", count, MaxIndirectDescriptorProbes));
|
||||
// Affine SMEM offsets wrap in U32 before adding the aligned immediate.
|
||||
// Image heaps retain one bulk snapshot and their absolute byte-offset mapping.
|
||||
auto& heap_words = program.material_keys;
|
||||
if (image_heap) {
|
||||
keys.resize(table_size / 4u);
|
||||
if (!ReadScalarTable(table_base, table_size, 0u, runtime, keys))
|
||||
return read_failed("image heap read", 0u, 0u, table_base, 0u, keys.size() * sizeof(uint32_t));
|
||||
heap_words.resize(table_size / 4u);
|
||||
if (!ReadScalarTable(table_base, table_size, 0u, runtime, heap_words))
|
||||
return read_failed("image heap read", 0u, 0u, table_base, 0u, heap_words.size() * sizeof(uint32_t));
|
||||
}
|
||||
const auto mapping_offset = snapshot.flattened_srt.size();
|
||||
descriptors[resource_index] = {.dword_count = dword_count};
|
||||
@@ -670,15 +716,14 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
std::span(candidate.dwords).first(read_dwords))) return false;
|
||||
} else {
|
||||
const auto table_word = table_offset / 4u;
|
||||
std::copy_n(keys.begin() + table_word,
|
||||
std::min<size_t>(read_dwords, keys.size() - table_word),
|
||||
std::copy_n(heap_words.begin() + table_word,
|
||||
std::min<size_t>(read_dwords, heap_words.size() - table_word),
|
||||
candidate.dwords.begin());
|
||||
}
|
||||
if (!normalize(candidate))
|
||||
return normalization_failed(table_offset, word, step,
|
||||
indirect.table_maximum ? "key" : "probe");
|
||||
uint32_t ordinal;
|
||||
if (!intern_candidate(candidate, ordinal)) return false;
|
||||
const auto ordinal = intern_candidate(candidate);
|
||||
if (ordinal == 0u) continue;
|
||||
const auto mapping_word = image_heap ? table_offset / 4u : word * child_count + child;
|
||||
snapshot.flattened_srt.resize(mapping_offset + 2u + mapping_word, 0u);
|
||||
@@ -722,21 +767,38 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
selector_keys->resolved = true;
|
||||
}
|
||||
}
|
||||
if (byte_keys) {
|
||||
// Keep cached selector values independent of each destination table's
|
||||
// U32 arithmetic, and use the same byte keys as the full-heap GPU path.
|
||||
byte_offsets.clear();
|
||||
byte_offsets.reserve(keys.size());
|
||||
for (const auto key: keys) {
|
||||
const auto offset = uint64_t {(key * indirect.table_stride + indirect.table_offset) & ~3u} +
|
||||
(indirect.table_immediate & ~3u);
|
||||
byte_offsets.push_back(offset <= UINT32_MAX ? static_cast<uint32_t>(offset) : UINT32_MAX);
|
||||
}
|
||||
std::ranges::sort(byte_offsets);
|
||||
byte_offsets.erase(std::unique(byte_offsets.begin(), byte_offsets.end()), byte_offsets.end());
|
||||
}
|
||||
}
|
||||
|
||||
const auto mapping_offset = snapshot.flattened_srt.size();
|
||||
const auto key_count = sources.empty() ? keys.size() : sources.size();
|
||||
const auto selected_keys = byte_keys ? std::span<const uint32_t>(byte_offsets)
|
||||
: std::span<const uint32_t>(keys);
|
||||
const auto key_count = sources.empty() ? selected_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);
|
||||
for (uint32_t entry = 0; entry < key_count; ++entry) {
|
||||
const auto key = sources.empty() ? keys[entry] : entry;
|
||||
const auto key = sources.empty() ? selected_keys[entry] : entry;
|
||||
uint64_t table_offset = 0;
|
||||
DescriptorValue candidate;
|
||||
candidate.dword_count = dword_count;
|
||||
if (sources.empty()) {
|
||||
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,
|
||||
table_offset = byte_keys ? key
|
||||
: uint64_t {(key * indirect.table_stride + indirect.table_offset) & ~3u} +
|
||||
(indirect.table_immediate & ~3u);
|
||||
if (!(byte_keys && key == UINT32_MAX) &&
|
||||
!ReadScalarTable(table_base, table_size, table_offset, runtime,
|
||||
std::span(candidate.dwords).first(read_dwords))) {
|
||||
return read_failed("table descriptor read", key, 0u, table_base, table_offset, descriptor_bytes);
|
||||
}
|
||||
@@ -746,10 +808,10 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
if (!normalize(candidate))
|
||||
return normalization_failed(table_offset, key, indirect.table_stride, "key");
|
||||
uint32_t ordinal = 0;
|
||||
if (entry == 0) {
|
||||
if (entry == 0 && !byte_keys) {
|
||||
descriptors[resource_index] = candidate;
|
||||
} else if (!intern_candidate(candidate, ordinal)) {
|
||||
return false;
|
||||
} else {
|
||||
ordinal = intern_candidate(candidate);
|
||||
}
|
||||
snapshot.flattened_srt[mapping_offset + 1u + entry * 2u] = key;
|
||||
snapshot.flattened_srt[mapping_offset + 2u + entry * 2u] = ordinal;
|
||||
@@ -772,9 +834,8 @@ struct SamplerPlan {
|
||||
uint32_t source;
|
||||
SamplerClass type;
|
||||
};
|
||||
std::array<std::array<uint32_t, 2>, ShaderInfo::MaxSamplers> mapping;
|
||||
std::array<Binding, ShaderInfo::MaxSamplers> bindings;
|
||||
uint32_t sampler_count = 0;
|
||||
std::vector<std::array<uint32_t, 2>> mapping;
|
||||
std::vector<Binding> bindings;
|
||||
};
|
||||
|
||||
template <typename T, typename Keep>
|
||||
@@ -913,6 +974,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
||||
if (i >= program.info.images.size() && source != nullptr &&
|
||||
source->indirect_descriptor &&
|
||||
IsBoundedDescriptorTable(program, *source->indirect_descriptor) &&
|
||||
specialization.images[base_index].indirect_search_iterations == 0u &&
|
||||
!source->indirect_descriptor->pointer_table) {
|
||||
const auto& indirect = *source->indirect_descriptor;
|
||||
const size_t mapping = specialization.images[base_index].indirect_mapping_offset;
|
||||
@@ -997,7 +1059,6 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
||||
is_1d_or_2d(image_class.dimension);
|
||||
if ((!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension)) &&
|
||||
!unfiltered_coordinates) ||
|
||||
image.mip_count != image_class.mip_count ||
|
||||
image.conversion_format != image_class.conversion_format ||
|
||||
(!storage_swizzle && image.shader_swizzle != image_class.shader_swizzle)) {
|
||||
return SpecializationFail(
|
||||
@@ -1011,11 +1072,9 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
||||
}
|
||||
|
||||
bool BuildSamplerPlan(const ShaderInfo& base, SamplerPlan& plan) {
|
||||
if (base.samplers.size() > plan.mapping.size()) {
|
||||
return false;
|
||||
}
|
||||
std::array<uint8_t, ShaderInfo::MaxSamplers> usage {};
|
||||
plan.sampler_count = static_cast<uint32_t>(base.samplers.size());
|
||||
plan.mapping.resize(base.samplers.size());
|
||||
plan.bindings.resize(base.samplers.size());
|
||||
std::vector<uint8_t> usage(base.samplers.size());
|
||||
for (const auto& pair: base.sampled_pairs) {
|
||||
if (pair.image >= base.images.size() || pair.sampler >= base.samplers.size()) {
|
||||
return false;
|
||||
@@ -1037,8 +1096,8 @@ bool BuildSamplerPlan(const ShaderInfo& base, SamplerPlan& plan) {
|
||||
bool first = true;
|
||||
for (uint32_t type = 0; type < mapping.size(); type++) {
|
||||
if ((classes & (1u << type)) == 0u) continue;
|
||||
const auto target = first ? index : plan.sampler_count++;
|
||||
if (target >= ShaderInfo::MaxSamplers) return false;
|
||||
const auto target = first ? index : static_cast<uint32_t>(plan.bindings.size());
|
||||
if (!first) plan.bindings.emplace_back();
|
||||
mapping[type] = target;
|
||||
plan.bindings[target] = {index, static_cast<SamplerClass>(type)};
|
||||
first = false;
|
||||
@@ -1544,9 +1603,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
return true;
|
||||
};
|
||||
const auto resource_failed = [&](std::string_view kind, uint32_t resource, uint32_t source,
|
||||
size_t candidates, uint32_t capacity) {
|
||||
return SpecializationFail(fmt::format("indirect {} resource={} source={} candidates={} capacity={}",
|
||||
kind, resource, source, candidates, capacity));
|
||||
size_t candidates) {
|
||||
return SpecializationFail(fmt::format("indirect {} resource={} source={} candidates={}",
|
||||
kind, resource, source, candidates));
|
||||
};
|
||||
snapshot.buffers.resize(program.info.buffers.size());
|
||||
specialization.buffers.assign(program.info.buffers.size(), {});
|
||||
@@ -1563,12 +1622,12 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
if (active.empty() || active[base.source]) {
|
||||
if (!MaterializeIndirectDescriptor(
|
||||
program, base.source, i, 4u, 4u, observed, clean, snapshot,
|
||||
snapshot.buffers, specialization.buffers, UINT32_MAX,
|
||||
snapshot.buffers, specialization.buffers,
|
||||
[&](DescriptorValue& value) {
|
||||
return NormalizeIndirectBuffer(
|
||||
value, source->indirect_descriptor->indexed, base);
|
||||
}))
|
||||
return resource_failed("buffer", i, base.source, snapshot.buffers.size(), UINT32_MAX);
|
||||
return resource_failed("buffer", i, base.source, snapshot.buffers.size());
|
||||
}
|
||||
} else if (!evaluate(base.source, snapshot.buffers[i], base.written)) {
|
||||
return false;
|
||||
@@ -1627,14 +1686,14 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
const bool bounded_table = IsBoundedDescriptorTable(program, indirect);
|
||||
if (!MaterializeIndirectDescriptor(
|
||||
program, image.source, i, 8u, image.r128 ? 4u : 8u, observed, clean, snapshot, snapshot.images,
|
||||
specialization.images, UINT32_MAX, [&](DescriptorValue& value) {
|
||||
specialization.images, [&](DescriptorValue& value) {
|
||||
// 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;
|
||||
})) {
|
||||
return resource_failed("image", i, image.source, snapshot.images.size(), UINT32_MAX);
|
||||
return resource_failed("image", i, image.source, snapshot.images.size());
|
||||
}
|
||||
} else {
|
||||
if (!evaluate(image.source, snapshot.images[i], image.written)) {
|
||||
@@ -1660,9 +1719,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
snapshot.samplers[i] = {.dword_count = 4u};
|
||||
if ((active.empty() || active[source]) && !MaterializeIndirectDescriptor(
|
||||
program, source, i, 4u, 4u, observed, clean, snapshot, snapshot.samplers,
|
||||
specialization.samplers, ShaderInfo::MaxSamplers,
|
||||
specialization.samplers,
|
||||
[](DescriptorValue&) { return true; }))
|
||||
return resource_failed("sampler", i, source, snapshot.samplers.size(), ShaderInfo::MaxSamplers);
|
||||
return resource_failed("sampler", i, source, snapshot.samplers.size());
|
||||
} else {
|
||||
if (indirect != nullptr) source = indirect->sources[0];
|
||||
if (!evaluate(source, snapshot.samplers[i])) return false;
|
||||
@@ -1671,11 +1730,13 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
if (program.info.samplers[base_index].gather_lod) {
|
||||
const auto control = snapshot.samplers[i].dwords[2];
|
||||
const auto filter = (control >> 26u) & 3u;
|
||||
// MipNone always selects the base level. Explicit point gathers currently require
|
||||
// encoded-zero primary and secondary bias; linear primary-mip selection is unsupported.
|
||||
if (filter > 1u || (filter == 1u && (control & 0xfffffu) != 0u)) {
|
||||
const auto perf_mip = (snapshot.samplers[i].dwords[1] >> 24u) & 15u;
|
||||
// MipNone always selects the base level. Point/linear selection requires zero
|
||||
// primary/secondary bias; linear mip snapping is not implemented.
|
||||
if (filter > 2u || (filter != 0u && (control & 0xfffffu) != 0u) ||
|
||||
(filter == 2u && perf_mip != 0u)) {
|
||||
return SpecializationFail(
|
||||
"explicit-LOD gather requires mip filtering None or Point with zero LOD biases");
|
||||
"explicit-LOD gather requires None, unbiased Point, or unbiased Linear without mip snapping");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1768,8 +1829,8 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
||||
SamplerPlan sampler_plan;
|
||||
EXIT_IF(!BuildSamplerPlan(program.info, sampler_plan));
|
||||
const auto original_sampler_count = samplers.size();
|
||||
samplers.reserve(sampler_plan.sampler_count);
|
||||
for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) {
|
||||
samplers.reserve(sampler_plan.bindings.size());
|
||||
for (uint32_t index = 0; index < sampler_plan.bindings.size(); index++) {
|
||||
const auto& binding = sampler_plan.bindings[index];
|
||||
if (index >= original_sampler_count) {
|
||||
samplers.push_back(samplers[binding.source]);
|
||||
@@ -1778,7 +1839,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
||||
samplers[index].force_point_filtering = binding.type == SamplerClass::Integer;
|
||||
samplers[index].integer_border = binding.type != SamplerClass::Float;
|
||||
}
|
||||
for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) {
|
||||
for (uint32_t index = 0; index < sampler_plan.bindings.size(); index++) {
|
||||
const auto& binding = sampler_plan.bindings[index];
|
||||
for (auto& candidate: samplers[index].indirect_resources) {
|
||||
candidate = sampler_plan.mapping[candidate][static_cast<uint32_t>(binding.type)];
|
||||
@@ -1905,6 +1966,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
|
||||
EXIT_IF(image.indirect_root == memory.resource &&
|
||||
inst.GetOpcode() != ValueOpcode::ImageSampleRaw &&
|
||||
inst.GetOpcode() != ValueOpcode::ImageGatherRaw &&
|
||||
inst.GetOpcode() != ValueOpcode::ImageQueryDimensions &&
|
||||
inst.GetOpcode() != ValueOpcode::ImageRead &&
|
||||
inst.GetOpcode() != ValueOpcode::ImageWrite);
|
||||
}
|
||||
|
||||
@@ -1558,14 +1558,49 @@ private:
|
||||
return read;
|
||||
}
|
||||
|
||||
const Inst* IndexedMaterialRead(const Inst* read, Value active,
|
||||
DescriptorSource::IndirectDescriptor::SelectorRead& selector) const {
|
||||
uint32_t lane = 0;
|
||||
if (read != nullptr && read->GetOpcode() == ValueOpcode::CompositeExtractU32x2) {
|
||||
if (!ImmediateU32(read->Arg(1), lane) || lane >= 2u) return nullptr;
|
||||
read = read->Arg(0).Resolve().TryInstruction();
|
||||
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32x2) return nullptr;
|
||||
} else if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto& memory = m_program.memory_info[read->Flags<MemoryFlags>().index];
|
||||
if (memory.kind != ResourceKind::Buffer || memory.typed || memory.formatted ||
|
||||
!memory.idxen || memory.offen || memory.offset > UINT32_MAX - lane * 4u ||
|
||||
read->Arg(2).Resolve() != Value(0u) || read->Arg(3).Resolve() != Value(0u) ||
|
||||
!EquivalentValue(m_program, active, read->Arg(4))) return nullptr;
|
||||
selector.offset = memory.offset + lane * 4u;
|
||||
return read;
|
||||
}
|
||||
|
||||
bool MatchUniformizedMaterialKey(Value key, const Inst& image,
|
||||
DescriptorSource::IndirectDescriptor& indirect,
|
||||
DescriptorSource& material_source) {
|
||||
DescriptorSource& material_source, bool indexed) {
|
||||
const auto* selected = UniformizedMaterialValue(key, image);
|
||||
if (selected == nullptr) return false;
|
||||
const auto active = selected->Arg(0).Resolve();
|
||||
DescriptorSource::IndirectDescriptor::SelectorRead selector;
|
||||
const auto* read = SelectorReadValue(selected->Arg(1), active, selector);
|
||||
if (indexed) {
|
||||
read = IndexedMaterialRead(read, active, selector);
|
||||
// This optional domain is checked against the dispatch's writable buffers.
|
||||
// Other stages and unbounded/image writes need the original heap domain.
|
||||
if (read == nullptr || m_program.stage != ShaderType::Compute ||
|
||||
std::ranges::any_of(m_program.blocks, [](const Block* block) {
|
||||
return std::ranges::any_of(*block, [](const Inst& inst) {
|
||||
const auto op = inst.GetOpcode();
|
||||
return ImageOpcodeInfoOf(op).resource_class == ImageResourceClass::Storage ||
|
||||
AddressOpcodeInfoOf(op).access == AddressAccess::Write;
|
||||
});
|
||||
}) || !MakeRuntimeTableSource(*read, material_source)) return false;
|
||||
selector.source = InternSource(material_source);
|
||||
indirect.selector = std::move(selector);
|
||||
return true;
|
||||
}
|
||||
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadAddressU32 ||
|
||||
read->NumArgs() != 4u || !Implies(active, read->Arg(3)))
|
||||
return false;
|
||||
@@ -2815,7 +2850,7 @@ private:
|
||||
} else result.maximum = std::max(result.maximum, SelectorMaximum(key, {}, 0u, active));
|
||||
}
|
||||
}
|
||||
if (lane != nullptr) result = SelectedFiniteDomain(*lane, handle != nullptr ? lane->Arg(1) : Value {});
|
||||
if (lane != nullptr) result = SelectedSelectorDomain(*lane, handle != nullptr ? lane->Arg(1) : Value {});
|
||||
else if (handle == nullptr) result.maximum = SelectorMaximum(key);
|
||||
if (result.maximum > UINT32_MAX / 4u ||
|
||||
uint64_t {result.maximum} * indirect.table_stride + indirect.table_offset > UINT32_MAX)
|
||||
@@ -2971,6 +3006,13 @@ private:
|
||||
// Keep live descriptor payloads, but project descriptor-only SMEM onto
|
||||
// the GPU key using the same plan as other indirect image reads.
|
||||
plan.retain_reads |= table_source.dword_count == 4u;
|
||||
} else if (!sampler && !plan.table_indexed && table_source.dword_count == 4u &&
|
||||
MatchUniformizedMaterialKey(key, handle, indirect, material_source, true)) {
|
||||
// Preserve the heap's byte-offset key so runtime geometry can choose the
|
||||
// tighter selector domain without changing the GPU's descriptor reads.
|
||||
indirect.table_scalar = true;
|
||||
plan.table_read = plan.reads[0];
|
||||
plan.retain_reads = true;
|
||||
} else if (sampler && table_source.dword_count == 4u && !plan.table_indexed &&
|
||||
SetExactTableMaximum(key, indirect, &handle)) {
|
||||
indirect.table_scalar = true;
|
||||
@@ -3020,7 +3062,7 @@ private:
|
||||
}
|
||||
}
|
||||
if (indirect.key_count.IsEmpty() &&
|
||||
!MatchUniformizedMaterialKey(key, handle, indirect, material_source) &&
|
||||
!MatchUniformizedMaterialKey(key, handle, indirect, material_source, false) &&
|
||||
!MatchReversedImageRange(key, indirect)) {
|
||||
const auto maximum = SelectorMaximum(key);
|
||||
if (maximum >= MaxIndirectDescriptorProbes) return false;
|
||||
@@ -3064,6 +3106,10 @@ private:
|
||||
indirect.selector_mask = selected.selector_mask;
|
||||
indirect.signed_count = selected.signed_count;
|
||||
indirect.workgroup_axis = selected.workgroup_axis;
|
||||
if (selected.table_scalar) {
|
||||
indirect.table_scalar = true;
|
||||
plan.table_read = plan.reads[0];
|
||||
}
|
||||
material_source = m_sources[selected.selector->source];
|
||||
shared_selector = true;
|
||||
break;
|
||||
@@ -3264,26 +3310,13 @@ private:
|
||||
if (value == nullptr || (selected != nullptr && selected != value)) return false;
|
||||
selected = value;
|
||||
}
|
||||
const auto* read = selected->Arg(1).Resolve().TryInstruction();
|
||||
uint32_t lane = 0;
|
||||
if (read != nullptr && read->GetOpcode() == ValueOpcode::CompositeExtractU32x2) {
|
||||
if (!ImmediateU32(read->Arg(1), lane) || lane >= 2u) return false;
|
||||
read = read->Arg(0).Resolve().TryInstruction();
|
||||
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32x2) return false;
|
||||
} else 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 || memory.offset > UINT32_MAX - lane * 4u ||
|
||||
!ImmediateU32(read->Arg(2), offset) || offset != 0u ||
|
||||
!ImmediateU32(read->Arg(3), scalar) || scalar != 0u ||
|
||||
!EquivalentValue(m_program, selected->Arg(0), read->Arg(4))) return false;
|
||||
if (!MakeRuntimeTableSource(*read, material_source) ||
|
||||
DescriptorSource::IndirectDescriptor::SelectorRead selector;
|
||||
const auto* read = IndexedMaterialRead(selected->Arg(1).Resolve().TryInstruction(),
|
||||
selected->Arg(0), selector);
|
||||
if (read == nullptr || !MakeRuntimeTableSource(*read, material_source) ||
|
||||
!BoundedSelectedIndex(read->Arg(1), read->Arg(4), indirect)) return false;
|
||||
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead{.source = InternSource(material_source),
|
||||
.offset = memory.offset + lane * 4u});
|
||||
selector.source = InternSource(material_source);
|
||||
indirect.selector = std::move(selector);
|
||||
return finish();
|
||||
}
|
||||
|
||||
@@ -3372,10 +3405,12 @@ private:
|
||||
return found ? SelectorDomain {maximum, counted ? count : Value {}} : SelectorDomain {};
|
||||
}
|
||||
|
||||
SelectorDomain SelectedFiniteDomain(const Inst& lane, Value guard) const {
|
||||
SelectorDomain SelectedSelectorDomain(const Inst& lane, Value guard) const {
|
||||
if (guard.IsEmpty() ? !HasActiveLane(lane.Arg(1), lane.Parent())
|
||||
: !Implies(guard, lane.Arg(1))) return {};
|
||||
return FiniteSelectorDomain(lane.Arg(0), lane.Arg(1));
|
||||
auto result = FiniteSelectorDomain(lane.Arg(0), lane.Arg(1));
|
||||
if (result.maximum == UINT32_MAX) result.maximum = PossibleU32Bits(lane.Arg(0));
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t SelectorMaximum(Value value, SelectorBounds bounds = {}, uint32_t depth = 0,
|
||||
@@ -3392,7 +3427,7 @@ private:
|
||||
};
|
||||
switch (inst->GetOpcode()) {
|
||||
case ValueOpcode::ReadFirstLane:
|
||||
return SelectedFiniteDomain(*inst, guard).maximum;
|
||||
return SelectedSelectorDomain(*inst, guard).maximum;
|
||||
case ValueOpcode::ReadLane:
|
||||
// ReadLane ignores EXEC; only context-free bounds on every source lane apply.
|
||||
return PossibleU32Bits(inst->Arg(0));
|
||||
|
||||
@@ -159,9 +159,11 @@ bool IsRuntimeUniformOp(ValueOpcode op) {
|
||||
case ValueOpcode::ULessThan32:
|
||||
case ValueOpcode::ULessThanEqual32:
|
||||
case ValueOpcode::IEqual32:
|
||||
case ValueOpcode::IEqual64:
|
||||
case ValueOpcode::UGreaterThan32:
|
||||
case ValueOpcode::SGreaterThanEqual32:
|
||||
case ValueOpcode::INotEqual32:
|
||||
case ValueOpcode::INotEqual64:
|
||||
case ValueOpcode::LogicalOr:
|
||||
case ValueOpcode::LogicalAnd:
|
||||
case ValueOpcode::LogicalXor:
|
||||
@@ -1053,6 +1055,11 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case ValueOpcode::IEqual64:
|
||||
case ValueOpcode::INotEqual64:
|
||||
if (!binary()) return false;
|
||||
result = inst.GetOpcode() == ValueOpcode::IEqual64 ? a == b : a != b;
|
||||
return true;
|
||||
case ValueOpcode::ULessThan32:
|
||||
// No U32 is below zero, including an unknown loop-carried value.
|
||||
if (!Arg(inst, 1, b)) return false;
|
||||
|
||||
@@ -1326,6 +1326,27 @@ int KYTY_SYSV_ABI KernelFtruncate(int d, int64_t length) {
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI KernelFsync(int d) {
|
||||
PRINT_NAME();
|
||||
|
||||
if (d < DESCRIPTOR_MIN) {
|
||||
return KERNEL_ERROR_EBADF;
|
||||
}
|
||||
|
||||
auto* file = g_files->GetFile(d);
|
||||
if (file == nullptr || !file->opened) {
|
||||
return KERNEL_ERROR_EBADF;
|
||||
}
|
||||
|
||||
Common::LockGuard lock(file->mutex);
|
||||
if (file->f.IsInvalid() || !file->f.Flush()) {
|
||||
return KERNEL_ERROR_EIO;
|
||||
}
|
||||
|
||||
LOGF("\tFsync: %s\n", Common::PathToString(file->real_name).c_str());
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI KernelUnlink(const char* path) {
|
||||
PRINT_NAME();
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ int KYTY_SYSV_ABI KernelStat(const char* path, FileStat* sb);
|
||||
int KYTY_SYSV_ABI KernelFstat(int d, FileStat* sb);
|
||||
int KYTY_SYSV_ABI KernelTruncate(const char* path, int64_t length);
|
||||
int KYTY_SYSV_ABI KernelFtruncate(int d, int64_t length);
|
||||
int KYTY_SYSV_ABI KernelFsync(int d);
|
||||
int KYTY_SYSV_ABI KernelUnlink(const char* path);
|
||||
int KYTY_SYSV_ABI KernelRename(const char* from, const char* to);
|
||||
int KYTY_SYSV_ABI KernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep);
|
||||
|
||||
+74
-48
@@ -343,14 +343,21 @@ static bool KernelClockGettimeSpecial(KernelClockid clock_id, KernelTimespec* tp
|
||||
#endif
|
||||
|
||||
struct PthreadMutexPrivate {
|
||||
struct Waiter {
|
||||
Pthread thread = nullptr;
|
||||
std::condition_variable cv;
|
||||
Waiter* previous = nullptr;
|
||||
Waiter* next = nullptr;
|
||||
};
|
||||
uint8_t reserved[256];
|
||||
std::string name;
|
||||
std::mutex m;
|
||||
std::condition_variable cv;
|
||||
Pthread owner = nullptr;
|
||||
uint32_t count = 0;
|
||||
int type = 1;
|
||||
int pprotocol = PTHREAD_PRIO_NONE;
|
||||
Waiter* first_waiter = nullptr;
|
||||
Waiter* last_waiter = nullptr;
|
||||
};
|
||||
|
||||
struct PthreadMutexattrPrivate {
|
||||
@@ -1242,6 +1249,20 @@ static int PthreadRwlockInitNamed(PthreadRwlock* rwlock, const PthreadRwlockattr
|
||||
const char* name);
|
||||
static int PthreadCondInitNamed(PthreadCond* cond, const PthreadCondattr* attr, const char* name);
|
||||
|
||||
static void NativeMutexRemoveWaiter(PthreadMutexPrivate* mutex,
|
||||
PthreadMutexPrivate::Waiter* waiter) {
|
||||
if (waiter->previous != nullptr) {
|
||||
waiter->previous->next = waiter->next;
|
||||
} else {
|
||||
mutex->first_waiter = waiter->next;
|
||||
}
|
||||
if (waiter->next != nullptr) {
|
||||
waiter->next->previous = waiter->previous;
|
||||
} else {
|
||||
mutex->last_waiter = waiter->previous;
|
||||
}
|
||||
}
|
||||
|
||||
static int NativeMutexLock(PthreadMutexPrivate* mutex, KernelUseconds* timeout_us) {
|
||||
EXIT_IF(mutex == nullptr);
|
||||
|
||||
@@ -1258,54 +1279,53 @@ static int NativeMutexLock(PthreadMutexPrivate* mutex, KernelUseconds* timeout_u
|
||||
mutex->count++;
|
||||
return OK;
|
||||
}
|
||||
if (timeout_us != nullptr) {
|
||||
if (*timeout_us > 0) {
|
||||
mutex->cv.wait_for(lock, std::chrono::microseconds(*timeout_us));
|
||||
}
|
||||
return ETIMEDOUT;
|
||||
}
|
||||
return EDEADLK;
|
||||
}
|
||||
|
||||
if (timeout_us == nullptr) {
|
||||
while (mutex->owner != nullptr) {
|
||||
mutex->cv.wait_for(lock, std::chrono::microseconds(SIGNAL_APC_POLL_MICROS));
|
||||
if (mutex->owner != nullptr) {
|
||||
lock.unlock();
|
||||
KernelDispatchPendingSignalForCurrentThread();
|
||||
lock.lock();
|
||||
}
|
||||
}
|
||||
} else if (*timeout_us == 0) {
|
||||
if (mutex->owner != nullptr) {
|
||||
return ETIMEDOUT;
|
||||
}
|
||||
} else {
|
||||
const auto deadline =
|
||||
std::chrono::steady_clock::now() + std::chrono::microseconds(*timeout_us);
|
||||
while (mutex->owner != nullptr) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now >= deadline) {
|
||||
return ETIMEDOUT;
|
||||
}
|
||||
|
||||
const auto remaining = deadline - now;
|
||||
const auto poll = (remaining < std::chrono::microseconds(SIGNAL_APC_POLL_MICROS)
|
||||
? remaining
|
||||
: std::chrono::steady_clock::duration(
|
||||
std::chrono::microseconds(SIGNAL_APC_POLL_MICROS)));
|
||||
mutex->cv.wait_for(lock, poll);
|
||||
if (mutex->owner != nullptr) {
|
||||
lock.unlock();
|
||||
KernelDispatchPendingSignalForCurrentThread();
|
||||
lock.lock();
|
||||
}
|
||||
}
|
||||
if (mutex->owner == nullptr) {
|
||||
mutex->owner = self;
|
||||
mutex->count = 1;
|
||||
return OK;
|
||||
}
|
||||
if (timeout_us != nullptr && *timeout_us == 0) {
|
||||
return ETIMEDOUT;
|
||||
}
|
||||
|
||||
mutex->owner = self;
|
||||
mutex->count = 1;
|
||||
return OK;
|
||||
// Guest mutex waits are not cancellation points, unlike the host condition wait.
|
||||
int previous_cancel_state = 0;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &previous_cancel_state);
|
||||
PthreadMutexPrivate::Waiter waiter;
|
||||
waiter.thread = self;
|
||||
waiter.previous = mutex->last_waiter;
|
||||
if (mutex->last_waiter != nullptr) {
|
||||
mutex->last_waiter->next = &waiter;
|
||||
} else {
|
||||
mutex->first_waiter = &waiter;
|
||||
}
|
||||
mutex->last_waiter = &waiter;
|
||||
const auto deadline = timeout_us != nullptr
|
||||
? std::chrono::steady_clock::now() +
|
||||
std::chrono::microseconds(*timeout_us)
|
||||
: std::chrono::steady_clock::time_point::max();
|
||||
int result = OK;
|
||||
while (mutex->owner != self) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now >= deadline) {
|
||||
NativeMutexRemoveWaiter(mutex, &waiter);
|
||||
result = ETIMEDOUT;
|
||||
break;
|
||||
}
|
||||
waiter.cv.wait_until(lock, std::min(deadline, now +
|
||||
std::chrono::microseconds(SIGNAL_APC_POLL_MICROS)));
|
||||
if (mutex->owner != self) {
|
||||
lock.unlock();
|
||||
KernelDispatchPendingSignalForCurrentThread();
|
||||
lock.lock();
|
||||
}
|
||||
}
|
||||
lock.unlock();
|
||||
pthread_setcancelstate(previous_cancel_state, nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
static int NativeMutexTrylock(PthreadMutexPrivate* mutex) {
|
||||
@@ -1358,10 +1378,16 @@ static int NativeMutexUnlock(PthreadMutexPrivate* mutex, uint32_t* recurse = nul
|
||||
return OK;
|
||||
}
|
||||
|
||||
mutex->owner = nullptr;
|
||||
mutex->count = 0;
|
||||
lock.unlock();
|
||||
mutex->cv.notify_one();
|
||||
if (auto* waiter = mutex->first_waiter) {
|
||||
NativeMutexRemoveWaiter(mutex, waiter);
|
||||
// Transfer ownership in FIFO order before waking the selected waiter.
|
||||
mutex->owner = waiter->thread;
|
||||
mutex->count = 1;
|
||||
waiter->cv.notify_one();
|
||||
} else {
|
||||
mutex->owner = nullptr;
|
||||
mutex->count = 0;
|
||||
}
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
@@ -493,12 +493,6 @@
|
||||
<property name="text"><string>Accurate texture synchronization</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_support_zmask">
|
||||
<property name="toolTip"><string>Can restore missing geometry (High on Life 2).</string></property>
|
||||
<property name="text"><string>Support ZMask</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -127,7 +127,6 @@ public:
|
||||
bool hide_cursor_enabled = false;
|
||||
bool readback_linear_images = false;
|
||||
bool sync_raw_image_buffers = false;
|
||||
bool support_zmask = false;
|
||||
bool tessellation_enabled = false;
|
||||
bool trophy_enabled = true;
|
||||
int trophy_duration_seconds = 6;
|
||||
@@ -167,7 +166,6 @@ public:
|
||||
hide_cursor_enabled = other.hide_cursor_enabled;
|
||||
readback_linear_images = other.readback_linear_images;
|
||||
sync_raw_image_buffers = other.sync_raw_image_buffers;
|
||||
support_zmask = other.support_zmask;
|
||||
tessellation_enabled = other.tessellation_enabled;
|
||||
trophy_enabled = other.trophy_enabled;
|
||||
trophy_duration_seconds = other.trophy_duration_seconds;
|
||||
|
||||
@@ -14,7 +14,6 @@ QVariantMap Configuration::GameSettings() const {
|
||||
KYTY_CFG_SET(hide_cursor_enabled);
|
||||
KYTY_CFG_SET(readback_linear_images);
|
||||
KYTY_CFG_SET(sync_raw_image_buffers);
|
||||
KYTY_CFG_SET(support_zmask);
|
||||
KYTY_CFG_SET(tessellation_enabled);
|
||||
KYTY_CFG_SET(trophy_enabled);
|
||||
KYTY_CFG_SET(trophy_duration_seconds);
|
||||
@@ -64,7 +63,6 @@ void Configuration::ReadGameSettingsValues(const Settings& s) {
|
||||
KYTY_CFG_GET(hide_cursor_enabled);
|
||||
KYTY_CFG_GET(readback_linear_images);
|
||||
KYTY_CFG_GET(sync_raw_image_buffers);
|
||||
KYTY_CFG_GET(support_zmask);
|
||||
KYTY_CFG_GET(tessellation_enabled);
|
||||
trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool();
|
||||
KYTY_CFG_GET(trophy_duration_seconds);
|
||||
|
||||
@@ -287,7 +287,6 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
|
||||
m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled);
|
||||
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
|
||||
m_ui->checkBox_sync_raw_image_buffers->setChecked(info.sync_raw_image_buffers);
|
||||
m_ui->checkBox_support_zmask->setChecked(info.support_zmask);
|
||||
m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled);
|
||||
m_ui->checkBox_skip_notice_screen->setChecked(info.skip_notice_screen);
|
||||
m_ui->checkBox_network_connected->setChecked(info.network_connected);
|
||||
@@ -481,7 +480,6 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
|
||||
info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked();
|
||||
info.readback_linear_images = ui.checkBox_readback->isChecked();
|
||||
info.sync_raw_image_buffers = ui.checkBox_sync_raw_image_buffers->isChecked();
|
||||
info.support_zmask = ui.checkBox_support_zmask->isChecked();
|
||||
info.tessellation_enabled = ui.checkBox_tessellation->isChecked();
|
||||
info.skip_notice_screen = ui.checkBox_skip_notice_screen->isChecked();
|
||||
info.network_connected = ui.checkBox_network_connected->isChecked();
|
||||
|
||||
@@ -262,7 +262,6 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
||||
}
|
||||
args << "--readback-linear-images" << BoolArg(info.readback_linear_images);
|
||||
args << "--sync-raw-image-buffers" << BoolArg(info.sync_raw_image_buffers);
|
||||
args << "--support-zmask" << BoolArg(info.support_zmask);
|
||||
args << "--trophy-notifications" << BoolArg(info.trophy_enabled);
|
||||
args << "--trophy-duration" << QString::number(info.trophy_duration_seconds);
|
||||
args << "--perf-overlay" << BoolArg(info.perf_overlay_enabled);
|
||||
|
||||
@@ -1341,14 +1341,6 @@ static int KYTY_SYSV_ABI KernelGetOperationMode(int* mode, int* submode) {
|
||||
return OK;
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI KernelFsync(int fd) {
|
||||
PRINT_NAME();
|
||||
|
||||
LOGF("\t fd = %d\n", fd);
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
static void KYTY_SYSV_ABI KernelSync() {
|
||||
PRINT_NAME();
|
||||
}
|
||||
@@ -1986,6 +1978,12 @@ int KYTY_SYSV_ABI ftruncate(int d, int64_t length) {
|
||||
return POSIX_CALL(LibKernel::FileSystem::KernelFtruncate(d, length));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI fsync(int d) {
|
||||
PRINT_NAME();
|
||||
|
||||
return POSIX_CALL(LibKernel::FileSystem::KernelFsync(d));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI socket(int family, int type, int protocol) {
|
||||
PRINT_NAME();
|
||||
return Network::Net::Socket(family, type, protocol);
|
||||
@@ -2174,6 +2172,7 @@ LIB_DEFINE(InitLibKernel_1_Posix) {
|
||||
LIB_FUNC("E6ao34wPw+U", stat);
|
||||
LIB_FUNC("JGMio+21L4c", mkdir);
|
||||
LIB_FUNC("ih4CD9-gghM", Posix::ftruncate);
|
||||
LIB_FUNC("juWbTNM+8hw", Posix::fsync);
|
||||
LIB_FUNC("8nY19bKoiZk", Posix::fcntl);
|
||||
LIB_FUNC("pDuPEf3m4fI", Posix::sem_init);
|
||||
LIB_FUNC("cDW233RAwWo", Posix::sem_destroy);
|
||||
@@ -3394,7 +3393,7 @@ LIB_DEFINE(InitLibKernel_1) {
|
||||
LIB_FUNC("959qrazPIrg", LibKernel::KernelGetProcParam);
|
||||
LIB_FUNC("tU5e3f9gSiU", LibKernel::KernelIsTrinityMode);
|
||||
LIB_FUNC("NH6xARDOVv8", LibKernel::KernelGetOperationMode);
|
||||
LIB_FUNC("fTx66l5iWIA", LibKernel::KernelFsync);
|
||||
LIB_FUNC("fTx66l5iWIA", FileSystem::KernelFsync);
|
||||
LIB_FUNC("uvT2iYBBnkY", LibKernel::KernelSync);
|
||||
LIB_FUNC("HoLVWNanBBc", LibKernel::getpid);
|
||||
LIB_FUNC("9BcDykPmo1I", LibKernel::get_error_addr);
|
||||
|
||||
@@ -24,10 +24,15 @@ static int KYTY_SYSV_ABI RazorCpuPushMarker(const char*, uint32_t, uint32_t) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI RazorCpuNamedSync(const char*) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
LIB_DEFINE(InitRazorCpu_1) {
|
||||
LIB_FUNC("EboejOQvLL4", LibRazorCpu::RazorCpuIsCapturing);
|
||||
LIB_FUNC("YpkGsMXP3ew", LibRazorCpu::RazorCpuPopMarker);
|
||||
LIB_FUNC("zw+celG7zSI", LibRazorCpu::RazorCpuPushMarker);
|
||||
LIB_FUNC("XCuZoBSVFG8", LibRazorCpu::RazorCpuNamedSync);
|
||||
}
|
||||
|
||||
} // namespace LibRazorCpu
|
||||
|
||||
@@ -91,8 +91,6 @@ static void PrintUsage() {
|
||||
" --readback-linear-images <true|false> Read back writable linear images on submit.\n");
|
||||
::printf(
|
||||
" --sync-raw-image-buffers <true|false> Synchronize raw reads of GPU images. Default: false.\n");
|
||||
::printf(
|
||||
" --support-zmask <true|false> Fill HTILE buffers with uncompressed ZMask. Default: false.\n");
|
||||
::printf(
|
||||
" --trophy-notifications <true|false> Show trophy unlock toasts and play their sound.\n");
|
||||
::printf(
|
||||
@@ -479,11 +477,6 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
||||
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--support-zmask") {
|
||||
if (!ParseBool(value, options.config.support_zmask)) {
|
||||
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--trophy-notifications") {
|
||||
if (!ParseBool(value, options.config.trophy_enabled)) {
|
||||
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
|
||||
|
||||
@@ -6,15 +6,18 @@
|
||||
#include "common/file.h"
|
||||
#include "ArchiveTestFixture.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/singleton.h"
|
||||
#include "common/subsystems.h"
|
||||
#include "common/threads.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "graphics/presentation/window/windowInternal.h"
|
||||
#include "kernel/eventQueue.h"
|
||||
#include "kernel/fileSystem.h"
|
||||
#include "kernel/pthread.h"
|
||||
#include "libs/errno.h"
|
||||
#include "libs/network.h"
|
||||
#include "loader/additionalContent.h"
|
||||
#include "loader/runtimeLinker.h"
|
||||
#include "loader/symbolDatabase.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -85,6 +88,148 @@ void Check(bool value, const char *text) {
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Predicate>
|
||||
void WaitForMutexTest(Predicate ready) {
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
while (!ready()) {
|
||||
Check(std::chrono::steady_clock::now() < deadline, "mutex waiter made progress");
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
|
||||
struct MutexWaiter {
|
||||
Libs::LibKernel::PthreadMutex *mutex;
|
||||
Libs::LibKernel::KernelUseconds timeout = 0;
|
||||
bool cancel = false;
|
||||
Libs::LibKernel::Pthread thread = nullptr;
|
||||
std::vector<uint8_t> stack = std::vector<uint8_t>(1024 * 1024);
|
||||
std::atomic<unsigned> parked{0}, resume{0};
|
||||
std::atomic<bool> acquired{false}, release{false}, finished{false};
|
||||
int result = 0;
|
||||
};
|
||||
|
||||
thread_local MutexWaiter *current_mutex_waiter = nullptr;
|
||||
|
||||
void KYTY_SYSV_ABI ParkMutexWaiter(int, void *) {
|
||||
auto &waiter = *current_mutex_waiter;
|
||||
const auto ticket = waiter.parked.fetch_add(1) + 1;
|
||||
WaitForMutexTest([&] { return waiter.resume.load() >= ticket; });
|
||||
}
|
||||
|
||||
void *KYTY_SYSV_ABI RunMutexWaiter(void *arg) {
|
||||
using namespace Libs::LibKernel;
|
||||
auto &waiter = *static_cast<MutexWaiter *>(arg);
|
||||
current_mutex_waiter = &waiter;
|
||||
waiter.result = waiter.timeout == 0 ? PthreadMutexLock(waiter.mutex)
|
||||
: PthreadMutexTimedlock(waiter.mutex, waiter.timeout);
|
||||
if (waiter.result == OK) {
|
||||
waiter.acquired = true;
|
||||
WaitForMutexTest([&] { return waiter.release.load(); });
|
||||
Check(PthreadMutexUnlock(waiter.mutex) == OK, "waiter unlocks owned mutex");
|
||||
}
|
||||
waiter.finished = true;
|
||||
if (waiter.cancel) {
|
||||
PthreadTestcancel();
|
||||
Check(false, "deferred mutex waiter cancellation remains pending");
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void QueueMutexWaiter(MutexWaiter &waiter) {
|
||||
using namespace Libs::LibKernel;
|
||||
PthreadAttr attr = nullptr;
|
||||
Check(PthreadAttrInit(&attr) == OK &&
|
||||
PthreadAttrSetstack(&attr, waiter.stack.data(), waiter.stack.size()) == OK &&
|
||||
PthreadCreate(&waiter.thread, &attr, RunMutexWaiter, &waiter, "MutexTest") == OK &&
|
||||
PthreadAttrDestroy(&attr) == OK,
|
||||
"create guest mutex waiter");
|
||||
// Pending guest signals are dispatched inside the contended lock, after enqueueing.
|
||||
// Holding that callback proves queue order without letting the waiter acquire.
|
||||
PthreadQueuePendingSignal(waiter.thread, 30);
|
||||
WaitForMutexTest([&] { return waiter.parked.load() == 1; });
|
||||
}
|
||||
|
||||
void TestMutexHandoff() {
|
||||
using namespace Libs::LibKernel;
|
||||
Common::Singleton<Loader::RuntimeLinker>::Instance();
|
||||
Initialize();
|
||||
PthreadInitSelfForMainThread();
|
||||
Loader::SymbolDatabase symbols;
|
||||
Libs::InitLibKernel_1(&symbols);
|
||||
const auto *install_symbol = symbols.FindByNid("WkwEd3N7w0Y", Loader::SymbolType::Func);
|
||||
const auto *remove_symbol = symbols.FindByNid("Qhv5ARAoOEc", Loader::SymbolType::Func);
|
||||
Check(install_symbol != nullptr && remove_symbol != nullptr, "mutex signal exports resolve");
|
||||
const auto install = reinterpret_cast<int(KYTY_SYSV_ABI *)(int, void *)>(install_symbol->vaddr);
|
||||
const auto remove = reinterpret_cast<int(KYTY_SYSV_ABI *)(int)>(remove_symbol->vaddr);
|
||||
Check(install(30, reinterpret_cast<void *>(ParkMutexWaiter)) == OK, "install mutex test signal");
|
||||
|
||||
PthreadMutexattr attr = nullptr;
|
||||
Check(PthreadMutexattrInit(&attr) == OK && PthreadMutexattrSettype(&attr, 2) == OK,
|
||||
"create recursive mutex attributes");
|
||||
for (unsigned timed = 0; timed < 3; ++timed) {
|
||||
PthreadMutex mutex = nullptr;
|
||||
Check(PthreadMutexInit(&mutex, &attr, "MutexHandoff") == OK &&
|
||||
PthreadMutexLock(&mutex) == OK && PthreadMutexLock(&mutex) == OK,
|
||||
"lock recursive mutex twice");
|
||||
std::array<MutexWaiter, 3> waiters{{{&mutex}, {&mutex}, {&mutex}}};
|
||||
waiters[timed].timeout = 5000;
|
||||
for (auto &waiter : waiters) {
|
||||
QueueMutexWaiter(waiter);
|
||||
}
|
||||
waiters[timed].resume = 1;
|
||||
Check(PthreadJoin(waiters[timed].thread, nullptr) == OK &&
|
||||
waiters[timed].result == KERNEL_ERROR_ETIMEDOUT,
|
||||
"timed waiter unlinks from queue head, middle and tail");
|
||||
Check(PthreadMutexUnlock(&mutex) == OK && PthreadMutexTrylock(&mutex) == OK &&
|
||||
PthreadMutexUnlock(&mutex) == OK,
|
||||
"partial recursive unlock retains ownership");
|
||||
Check(PthreadMutexUnlock(&mutex) == OK &&
|
||||
PthreadMutexTrylock(&mutex) == KERNEL_ERROR_EBUSY,
|
||||
"unlock reserves ownership before queued waiter runs");
|
||||
for (auto &waiter : waiters) {
|
||||
if (&waiter == &waiters[timed]) {
|
||||
continue;
|
||||
}
|
||||
waiter.resume = 1;
|
||||
WaitForMutexTest([&] { return waiter.acquired.load(); });
|
||||
Check(PthreadMutexTrylock(&mutex) == KERNEL_ERROR_EBUSY, "FIFO waiter owns mutex");
|
||||
waiter.release = true;
|
||||
WaitForMutexTest([&] { return waiter.finished.load(); });
|
||||
Check(PthreadJoin(waiter.thread, nullptr) == OK && waiter.result == OK,
|
||||
"FIFO waiter completes");
|
||||
}
|
||||
Check(PthreadMutexLock(&mutex) == OK && PthreadMutexUnlock(&mutex) == OK &&
|
||||
PthreadMutexDestroy(&mutex) == OK,
|
||||
"mutex remains usable after timed waiter removal");
|
||||
}
|
||||
Check(PthreadMutexattrDestroy(&attr) == OK, "destroy recursive mutex attributes");
|
||||
|
||||
PthreadMutex mutex = nullptr;
|
||||
Check(PthreadMutexInit(&mutex, nullptr, "MutexCancel") == OK &&
|
||||
PthreadMutexLock(&mutex) == OK &&
|
||||
PthreadMutexTimedlock(&mutex, 5000) == KERNEL_ERROR_EDEADLK,
|
||||
"nonrecursive self timed lock reports deadlock");
|
||||
MutexWaiter waiter{&mutex};
|
||||
waiter.cancel = true;
|
||||
QueueMutexWaiter(waiter);
|
||||
Check(PthreadCancel(waiter.thread) == OK, "cancel queued mutex waiter");
|
||||
PthreadQueuePendingSignal(waiter.thread, 30);
|
||||
waiter.resume = 1;
|
||||
WaitForMutexTest([&] { return waiter.parked.load() == 2; });
|
||||
Check(PthreadMutexUnlock(&mutex) == OK &&
|
||||
PthreadMutexTrylock(&mutex) == KERNEL_ERROR_EBUSY,
|
||||
"canceled waiter retains its queue position");
|
||||
waiter.release = true;
|
||||
waiter.resume = 2;
|
||||
void *result = nullptr;
|
||||
Check(PthreadJoin(waiter.thread, &result) == OK &&
|
||||
result == reinterpret_cast<void *>(-1) && waiter.acquired && waiter.finished,
|
||||
"mutex lock defers cancellation until explicit cancellation point");
|
||||
Check(PthreadMutexLock(&mutex) == OK && PthreadMutexUnlock(&mutex) == OK &&
|
||||
PthreadMutexDestroy(&mutex) == OK && remove(30) == OK,
|
||||
"mutex remains usable after waiter cancellation");
|
||||
}
|
||||
|
||||
class TempDirectory {
|
||||
public:
|
||||
TempDirectory() {
|
||||
@@ -483,6 +628,27 @@ void TestSaveOpenVisibility() {
|
||||
Check(FileSystem::KernelClose(truncated) == OK, "close truncated save file");
|
||||
}
|
||||
|
||||
void TestFsync() {
|
||||
Loader::SymbolDatabase symbols;
|
||||
Libs::InitLibKernel_1(&symbols);
|
||||
const auto* symbol = symbols.Find(
|
||||
{"juWbTNM+8hw", "Posix", 1, "libkernel", 1, 1, Loader::SymbolType::Func});
|
||||
Check(symbol != nullptr, "POSIX fsync export resolves");
|
||||
const auto fsync = reinterpret_cast<int (KYTY_SYSV_ABI *)(int)>(symbol->vaddr);
|
||||
Check(fsync(std::numeric_limits<int>::min()) == -1 &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EBADF,
|
||||
"invalid fsync descriptor reports POSIX EBADF");
|
||||
|
||||
constexpr char Path[] = "/savedata0/fsync.dat";
|
||||
const int fd = FileSystem::KernelOpen(Path, 0x602, 0777);
|
||||
Check(fd >= 3 && FileSystem::KernelWrite(fd, "save", 4) == 4 && fsync(fd) == OK &&
|
||||
FileSystem::KernelLseek(fd, 0, 1) == 4,
|
||||
"fsync flushes a writable file without changing its offset");
|
||||
Check(FileSystem::KernelClose(fd) == OK && fsync(fd) == -1 &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EBADF,
|
||||
"closed fsync descriptor reports POSIX EBADF");
|
||||
}
|
||||
|
||||
void TestSaveTruncate() {
|
||||
namespace Kernel = Libs::LibKernel;
|
||||
Loader::SymbolDatabase symbols;
|
||||
@@ -973,6 +1139,9 @@ void CheckArchiveMount(const std::filesystem::path &root) {
|
||||
std::equal(bytes.begin(), bytes.begin() + 9, payload.end() - 9) &&
|
||||
FileSystem::KernelRead(fd, bytes.data(), bytes.size()) == 0,
|
||||
"archive reads stop at the member boundary");
|
||||
Check(FileSystem::KernelFsync(fd) == OK &&
|
||||
FileSystem::KernelLseek(fd, 0, 1) == payload.size(),
|
||||
"fsync succeeds on an immutable archive member without changing its offset");
|
||||
Check(FileSystem::KernelWrite(fd, bytes.data(), 1) ==
|
||||
Libs::LibKernel::KERNEL_ERROR_EBADF &&
|
||||
FileSystem::KernelPwrite(fd, bytes.data(), 1, 0) ==
|
||||
@@ -2236,7 +2405,7 @@ void CheckSocketWakeup() {
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int, char**) {
|
||||
int main(int argc, char** argv) {
|
||||
Common::InitializeThreads();
|
||||
Common::Subsystems subsystems;
|
||||
subsystems.Initialize<Config::Lifecycle>();
|
||||
@@ -2245,6 +2414,13 @@ int main(int, char**) {
|
||||
Config::Load(options);
|
||||
subsystems.Initialize<Log::Lifecycle>();
|
||||
|
||||
TestMutexHandoff();
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--mutex-only") {
|
||||
subsystems.Destroy();
|
||||
std::printf("KernelFileSystemTests: mutex cases passed\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
Check(SDL_InitSubSystem(SDL_INIT_VIDEO), "initialize Vulkan test video");
|
||||
auto graphics = std::make_unique<Libs::Graphics::WindowContext>();
|
||||
graphics->graphic_ctx.screen_width = 64;
|
||||
@@ -2271,6 +2447,7 @@ int main(int, char**) {
|
||||
CheckAprPaths(temporary.Path());
|
||||
FileSystem::Mount(temporary.Path(), "/savedata0");
|
||||
TestSaveOpenVisibility();
|
||||
TestFsync();
|
||||
TestSaveTruncate();
|
||||
TestSaveWritev();
|
||||
TestAioBatches();
|
||||
|
||||
+415
-17
@@ -593,8 +593,9 @@ void TestExactUnfilteredImageCandidates() {
|
||||
second[3] |= 1u << 16u;
|
||||
fill();
|
||||
plan.info.images[0].mip_mode = ImageMipMode::Dynamic;
|
||||
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"mixed-dimension acceptance discarded mip-count compatibility");
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
specialization.images[0].mip_count == 1u && specialization.images[1].mip_count == 2u,
|
||||
"mixed-dimension candidates lost their individual mip counts");
|
||||
}
|
||||
// The same image may be merged from several uses; one query keeps the guard strict.
|
||||
auto merged = MakeIndirectImageFixture(false, 32u, 0u, 0u, false, ValueOpcode::ImageRead, true);
|
||||
@@ -889,7 +890,7 @@ void TestNestedPointerImageTable() {
|
||||
"nested pointer descriptors lost their row geometry or captured the GPU selector payload");
|
||||
}
|
||||
|
||||
constexpr uint32_t table = 0x2000u, objects = 0x4000u, stride = 592u, count = 6u;
|
||||
constexpr uint32_t table = 0x2000u, objects = 0x10000u, stride = 592u, count = 59u;
|
||||
LinearTestMemory memory;
|
||||
memory.words.resize((objects + count * 0x430u - memory.base) / 4u);
|
||||
memory.fail_address = 0x1800u; // The GPU selector's contents are unavailable to the CPU.
|
||||
@@ -898,7 +899,8 @@ void TestNestedPointerImageTable() {
|
||||
const auto object = objects + row * 0x430u;
|
||||
*at(table + row * stride) = object;
|
||||
const std::array<uint32_t, 8> image{10u + row,
|
||||
uint32_t(Libs::Graphics::Prospero::BufferFormat::k32Float) << 20u, 0u,
|
||||
uint32_t(row == count - 1u ? Libs::Graphics::Prospero::BufferFormat::k32UInt
|
||||
: Libs::Graphics::Prospero::BufferFormat::k32Float) << 20u, 0u,
|
||||
Libs::Graphics::DstSel(4, 0, 0, 1) | (9u << 28u), 0u, 0u, 0u, 0u};
|
||||
std::copy(image.begin(), image.end(), at(object + 0x3d0u));
|
||||
const std::array<uint32_t, 4> sampler{row, 0x00fff000u, 0u, 0u};
|
||||
@@ -917,6 +919,21 @@ void TestNestedPointerImageTable() {
|
||||
Check(snapshot.images[row + 1u].dwords[0] == 10u + row &&
|
||||
snapshot.samplers[row + 1u].dwords[0] == row,
|
||||
"nested pointer table mixed rows or pointee members");
|
||||
auto typed = make();
|
||||
typed->PlanAndTrack();
|
||||
ApplyResourceSpecialization(typed->program, specialization);
|
||||
const auto& samplers = typed->program.info.samplers;
|
||||
Check(samplers.size() == 2u * (count + 1u) && samplers[0].indirect_resources.size() == count + 1u &&
|
||||
samplers[count + 1u].indirect_resources.size() == count + 1u &&
|
||||
snapshot.samplers[0].dwords[1] == 0u,
|
||||
"expanded sampler table lost its null root or numeric-class variants");
|
||||
for (uint32_t index = 0; index <= count; ++index) {
|
||||
const auto& integer = samplers[index + count + 1u];
|
||||
Check(samplers[index].snapshot_index == index && !samplers[index].integer_border &&
|
||||
integer.snapshot_index == index && integer.integer_border && integer.force_point_filtering &&
|
||||
samplers[count + 1u].indirect_resources[index] == index + count + 1u,
|
||||
"large sampler class mapping changed descriptor identity or integer filtering");
|
||||
}
|
||||
const auto populated = specialization;
|
||||
auto equivalent = make(0u, Address::Equivalent);
|
||||
equivalent->PlanAndTrack();
|
||||
@@ -939,11 +956,11 @@ void TestNestedPointerImageTable() {
|
||||
specialization == populated,
|
||||
"empty-to-positive pointer table refresh did not restore its specialization");
|
||||
*at(table) = objects + 0x430u;
|
||||
*at(objects + 0x430u + 0x3d0u) = 42u;
|
||||
*at(objects + 0x430u + 0x3f8u) = 7u;
|
||||
*at(objects + 0x430u + 0x3d0u) = 10u + count;
|
||||
*at(objects + 0x430u + 0x3f8u) = count;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == count && snapshot.samplers.size() == count &&
|
||||
snapshot.images[1].dwords[0] == 42u && snapshot.samplers[1].dwords[0] == 7u,
|
||||
snapshot.images[1].dwords[0] == 10u + count && snapshot.samplers[1].dwords[0] == count,
|
||||
"nested descriptor refresh retained stale pointers or failed to deduplicate new pointees");
|
||||
*at(table) = objects;
|
||||
|
||||
@@ -3164,6 +3181,243 @@ void TestUniformizedMaterialImageKeys() {
|
||||
"packed selector accepted an unproved OR guard, offset domain, or field extraction");
|
||||
}
|
||||
|
||||
void TestIndexedMaterialImageKeys() {
|
||||
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
|
||||
const auto make_program = [](ShaderType stage = ShaderType::Compute, bool shared = false) {
|
||||
Fixture f(stage);
|
||||
auto *entry = f.block, *body = f.AddBlock();
|
||||
auto *later = shared ? f.AddBlock() : nullptr;
|
||||
auto *done = f.AddBlock();
|
||||
entry->AddBranch(body);
|
||||
entry->AddBranch(done);
|
||||
body->AddBranch(shared ? later : done);
|
||||
if (shared) later->AddBranch(done);
|
||||
const auto lane = f.Emit(ValueOpcode::GetBuiltin,
|
||||
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
|
||||
const auto active = f.Emit(ValueOpcode::INotEqual32, {lane, Value(UINT32_MAX)});
|
||||
entry->condition = f.Emit(ValueOpcode::ConditionRef, {active}, CFG::BranchCondition::ExecNonZero);
|
||||
entry->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch,
|
||||
.true_block = body, .false_block = done};
|
||||
body->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = shared ? later : done};
|
||||
if (shared) later->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = done};
|
||||
done->terminator.kind = CFG::TerminatorKind::Return;
|
||||
const auto material = f.Buffer({f.UserData(0), f.UserData(1), f.UserData(2), f.UserData(3)});
|
||||
const auto heap = f.Buffer({f.UserData(4), f.UserData(5), f.UserData(6), f.UserData(7)});
|
||||
const auto output = f.Buffer({f.UserData(8), f.UserData(9), f.UserData(10), f.UserData(11)});
|
||||
const auto sampler = f.Sampler({Value(0u), Value(0u), Value(1u << 26u), Value(0u)});
|
||||
const auto address = f.ImageAddress();
|
||||
Value owner_key, shared_image;
|
||||
for (uint32_t index = 0; index < (shared ? 3u : 2u); ++index) {
|
||||
const uint32_t field = index == 1u ? 68u : 64u;
|
||||
auto *use = index == 2u ? later : body;
|
||||
auto key = owner_key;
|
||||
if (index < 2u) {
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::Buffer;
|
||||
memory.idxen = true;
|
||||
memory.offset = field;
|
||||
const auto loaded = f.Emit(ValueOpcode::LoadBufferU32,
|
||||
{material, lane, Value(0u), Value(0u), active}, f.AddMemory(memory, field), entry);
|
||||
const auto selected = f.Emit(ValueOpcode::SelectU32, {active, loaded, Value(0xdeadbeefu)}, 0, entry);
|
||||
key = shared && index == 0u
|
||||
? f.Emit(ValueOpcode::ReadLane, {selected, Value(0u)}, 0, entry)
|
||||
: f.Emit(ValueOpcode::ReadFirstLane, {selected, active}, 0, body);
|
||||
if (shared && index == 0u) {
|
||||
owner_key = key;
|
||||
const auto equal = f.Emit(ValueOpcode::IEqual32, {key, selected}, 0, entry);
|
||||
const auto guard = f.Emit(ValueOpcode::LogicalAnd, {active, equal}, 0, entry);
|
||||
entry->condition = f.Emit(ValueOpcode::ConditionRef, {guard}, CFG::BranchCondition::ExecNonZero, entry);
|
||||
}
|
||||
}
|
||||
const auto offset = f.Emit(ValueOpcode::ShiftLeftLogical32, {key, Value(5u)}, 0, use);
|
||||
std::array<Value, 8> words;
|
||||
for (uint32_t word = 0; word < words.size(); ++word) {
|
||||
MemoryInfo scalar;
|
||||
scalar.kind = ResourceKind::ScalarBuffer;
|
||||
scalar.offset = word * 4u;
|
||||
words[word] = f.Emit(ValueOpcode::ReadConstBuffer, {heap, offset},
|
||||
f.AddMemory(scalar, 0x100u + field), use);
|
||||
}
|
||||
const auto image_handle = [&] {
|
||||
return f.Emit(ValueOpcode::GetImageResource,
|
||||
{words[0], words[1], words[2], words[3], words[4], words[5], words[6], words[7]}, 0, use);
|
||||
};
|
||||
MemoryInfo gather;
|
||||
gather.kind = ResourceKind::Image;
|
||||
gather.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
if (index == 0u)
|
||||
f.Emit(ValueOpcode::ImageQueryDimensions, {image_handle(), address}, f.AddMemory(gather, 0x180u), body);
|
||||
const auto image = image_handle();
|
||||
if (index == 2u) shared_image = image;
|
||||
gather.image_sample_flags = Decoder::ImageSampleFlagLod;
|
||||
f.Emit(ValueOpcode::ImageGatherRaw,
|
||||
{image, sampler, address, words[1], words[3], Value(0u), Value(1u << 26u), active},
|
||||
f.AddMemory(gather, 0x200u + field), use);
|
||||
}
|
||||
MemoryInfo store;
|
||||
store.kind = ResourceKind::Buffer;
|
||||
f.Emit(ValueOpcode::StoreBufferU32,
|
||||
{output, Value(0u), Value(0u), Value(0u), Value(1u), Value(true)},
|
||||
f.AddMemory(store, 0x300u), done);
|
||||
f.PlanAndTrack();
|
||||
if (shared) {
|
||||
owner_key.ResolveInstruction()->ReplaceUsesWith(Value(12994u));
|
||||
uint32_t offset = 0u;
|
||||
Check(SrtWalker(f.program, {}).Evaluate(shared_image.ResolveInstruction()->Arg(0), offset) &&
|
||||
offset == 12994u * 32u,
|
||||
"inherited indexed selector changed GPU byte offsets into raw material keys");
|
||||
}
|
||||
return std::move(f.program);
|
||||
};
|
||||
auto program = make_program();
|
||||
auto plan = ExtractResourcePlan(program);
|
||||
Check(plan.info.images.size() == 3u && plan.selector_keys.size() == 2u,
|
||||
"indexed material fields did not share their query/gather selector domain");
|
||||
for (const auto &image : plan.info.images) {
|
||||
const auto &source = *plan.descriptor_sources[image.source].indirect_descriptor;
|
||||
Check(source.table_scalar && source.selector && source.selector->stride == 0u &&
|
||||
source.key_count.IsEmpty() && source.selector_first.IsEmpty(),
|
||||
"indexed material field fell back to an unconditional heap scan");
|
||||
}
|
||||
constexpr uint32_t base = 0x1000u, heap = 0x2000u, records = 14517u;
|
||||
struct Memory {
|
||||
std::vector<uint32_t> words = std::vector<uint32_t>((heap - base + records * 32u) / 4u);
|
||||
uint32_t selectors = 0, descriptors = 0;
|
||||
bool narrow = true, unreadable = false;
|
||||
} memory;
|
||||
const auto put = [&](uint32_t key, uint32_t mips) {
|
||||
auto *words = memory.words.data() + (heap - base + key * 32u) / 4u;
|
||||
words[0] = 0x100u + key;
|
||||
words[1] = static_cast<uint32_t>(Libs::Graphics::Prospero::BufferFormat::k16UNorm) << 20u;
|
||||
words[3] = (9u << 28u) | ((mips - 1u) << 16u) | Libs::Graphics::DstSel(4, 5, 6, 7);
|
||||
};
|
||||
for (uint32_t key = 0; key < 64u; ++key) put(key, 1u);
|
||||
put(12994u, 8u);
|
||||
put(12996u, 8u);
|
||||
for (uint32_t row = 0; row < 10u; ++row) {
|
||||
memory.words[(row * 96u + 64u) / 4u] = 12994u;
|
||||
memory.words[(row * 96u + 68u) / 4u] = 12996u;
|
||||
}
|
||||
std::array<uint32_t, 12> user_data{base, 96u << 16u, 10u, 0x5204u,
|
||||
heap, 32u << 16u, records, 0x5204u, 0x90000u, 4u << 16u, 16u, 0x5204u};
|
||||
const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
|
||||
auto &m = *static_cast<Memory *>(data);
|
||||
if (address < base || address - base + words.size_bytes() > m.words.size() * 4u) return false;
|
||||
if (address < heap) {
|
||||
++m.selectors;
|
||||
if (m.unreadable) return false;
|
||||
} else {
|
||||
++m.descriptors;
|
||||
const auto key = (address - heap) / 32u;
|
||||
if (m.narrow && (words.size() != 8u || (key != 0u && key != 12994u && key != 12996u))) return false;
|
||||
}
|
||||
std::copy_n(m.words.begin() + (address - base) / 4u, words.size(), words.begin());
|
||||
return true;
|
||||
};
|
||||
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
|
||||
.read_specialization_memory = read};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
memory.selectors == 2u && memory.descriptors == 6u && snapshot.images.size() == 9u,
|
||||
"indexed selector scanned the broad heap or did not share its captured keys");
|
||||
ApplyResourceSpecialization(program, specialization);
|
||||
Check(program.info.images[0].indirect_resources.size() == 3u,
|
||||
"indirect dimension query lost its specialized candidates");
|
||||
for (uint32_t root = 0; root < plan.info.images.size(); ++root) {
|
||||
const auto &image = specialization.images[root];
|
||||
const auto offset = image.indirect_mapping_offset;
|
||||
Check(snapshot.images[root].dwords[0] == 0u && image.indirect_search_iterations == 2u &&
|
||||
snapshot.flattened_srt[offset] == 2u && snapshot.flattened_srt[offset + 1u] == 0u &&
|
||||
snapshot.flattened_srt[offset + 3u] == (root == 2u ? 12996u : 12994u) * 32u,
|
||||
"byte-key selector lost native OOB zero, sorted offsets, or the null default");
|
||||
}
|
||||
Check(std::ranges::count_if(specialization.images, [](const auto &image) {
|
||||
return image.mip_count == 8u;
|
||||
}) == 2u, "indirect gathers lost candidate-specific mip counts");
|
||||
for (const uint32_t field : {64u, 68u})
|
||||
Check(std::ranges::find(snapshot.specialization_reads,
|
||||
std::pair<uint64_t, uint64_t>{base + field, 868u}) != snapshot.specialization_reads.end(),
|
||||
"indexed selector batch escaped writable-alias tracking");
|
||||
auto shared_program = make_program(ShaderType::Compute, true);
|
||||
auto shared = ExtractResourcePlan(shared_program);
|
||||
Check(MaterializeResources(shared, runtime, snapshot, specialization) && snapshot.images.size() == 9u,
|
||||
"inherited indexed selector lost its bounded material domain");
|
||||
memory.words[64u / 4u] = 12996u;
|
||||
memory.selectors = 0;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
memory.selectors == 2u && snapshot.images.size() == 11u,
|
||||
"changed material rows reused stale selector keys");
|
||||
memory.words[64u / 4u] = 12994u;
|
||||
memory.unreadable = true;
|
||||
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"unreadable eligible selector silently fell back to the heap");
|
||||
memory.narrow = false;
|
||||
const auto original = user_data;
|
||||
for (const uint32_t variant : {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u}) {
|
||||
user_data = original;
|
||||
if (variant == 0u) user_data[1] |= 1u << 31u;
|
||||
if (variant == 1u) user_data[3] |= 1u << 23u;
|
||||
if (variant == 2u) user_data[3] |= 1u << 28u;
|
||||
if (variant == 3u) user_data[0] += 1u;
|
||||
if (variant == 4u) user_data[2] = MaxIndirectDescriptorProbes + 1u;
|
||||
if (variant == 5u) user_data[8] = base + 68u;
|
||||
if (variant == 6u) user_data[1] = 66u << 16u;
|
||||
if (variant == 7u) user_data[0] = 0u;
|
||||
memory.selectors = 0;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
memory.selectors == 0u && snapshot.images.size() > 100u,
|
||||
"unsupported geometry or writable selector lost the original heap domain");
|
||||
}
|
||||
user_data = original;
|
||||
auto graphics_program = make_program(ShaderType::Pixel);
|
||||
auto graphics = ExtractResourcePlan(graphics_program);
|
||||
Check(MaterializeResources(graphics, runtime, snapshot, specialization) && memory.selectors == 0u,
|
||||
"graphics material lookup acquired unproved selector read dependencies");
|
||||
memory.unreadable = false;
|
||||
for (uint32_t row = 0; row < 10u; ++row)
|
||||
memory.words[(row * 96u + 64u) / 4u] = 0x08000000u; // U32 key*32 wraps to zero.
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.images.size() == 7u,
|
||||
"indexed selector lost wrapped scalar descriptor offsets");
|
||||
for (auto &source : plan.descriptor_sources) {
|
||||
if (!source.indirect_descriptor) continue;
|
||||
source.indirect_descriptor->table_offset = 0xffffffe0u;
|
||||
source.indirect_descriptor->table_immediate = 32u;
|
||||
}
|
||||
for (uint32_t row = 0; row < 10u; ++row) {
|
||||
memory.words[(row * 96u + 64u) / 4u] = row == 0u ? 1u : 0u;
|
||||
memory.words[(row * 96u + 68u) / 4u] = 0u;
|
||||
}
|
||||
memory.selectors = memory.descriptors = 0u;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
memory.selectors == 2u && memory.descriptors == 2u && snapshot.images.size() == 5u,
|
||||
"overflowed scalar immediate read a descriptor or lost valid wrapped keys");
|
||||
for (uint32_t root = 0; root < 2u; ++root) {
|
||||
const auto offset = specialization.images[root].indirect_mapping_offset;
|
||||
Check(snapshot.images[root].dwords[0] == 0u && snapshot.flattened_srt[offset] == 2u &&
|
||||
snapshot.flattened_srt[offset + 1u] == 32u && snapshot.flattened_srt[offset + 3u] == UINT32_MAX &&
|
||||
snapshot.flattened_srt[offset + 4u] == 0u,
|
||||
"invalid byte key selected a real texture instead of the null root");
|
||||
}
|
||||
for (auto &source : plan.descriptor_sources) {
|
||||
if (!source.indirect_descriptor) continue;
|
||||
source.indirect_descriptor->table_offset = source.indirect_descriptor->table_immediate = 0u;
|
||||
}
|
||||
memory.narrow = true;
|
||||
memory.unreadable = true;
|
||||
for (const uint32_t variant : {0u, 1u, 2u, 3u}) {
|
||||
user_data = original;
|
||||
if (variant == 0u) user_data[2] = 0u;
|
||||
if (variant == 1u) user_data[1] = 0u;
|
||||
if (variant == 2u) user_data[3] = 0u;
|
||||
if (variant == 3u) user_data[1] = 64u << 16u;
|
||||
memory.selectors = 0;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) && memory.selectors == 0u &&
|
||||
snapshot.images.size() == 6u,
|
||||
"native zero/OOB indexed load read its unavailable payload");
|
||||
}
|
||||
}
|
||||
|
||||
void TestImageDescriptorFields() {
|
||||
constexpr std::array<std::pair<uint32_t, uint32_t>, 5> reserved{
|
||||
{{1u, 0x20000000u}, {2u, 0x70003000u}, {4u, 0xe000e000u},
|
||||
@@ -5177,19 +5431,24 @@ void TestGatherLodSamplerValidation() {
|
||||
Libs::Graphics::Prospero::BufferFormat::k32Float) << 20u;
|
||||
user_data[3] = Libs::Graphics::DstSel(4, 5, 6, 7) |
|
||||
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
|
||||
user_data[9] = 0xfff000u;
|
||||
constexpr std::array sampler_controls{
|
||||
0u, 0x4001u, 1u << 26u, (1u << 26u) | 256u,
|
||||
(1u << 26u) | (1u << 14u), 2u << 26u, 3u << 26u};
|
||||
(1u << 26u) | (1u << 14u), 2u << 26u, (2u << 26u) | 256u,
|
||||
(2u << 26u) | (1u << 14u), 0x0a500000u, 0x1a500000u, 3u << 26u};
|
||||
for (const auto control : sampler_controls) {
|
||||
user_data[10] = control;
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
const bool supported = !explicit_lod || (control >> 26u) == 0 ||
|
||||
control == (1u << 26u);
|
||||
Check(MaterializeResources(plan, {.user_data = user_data}, snapshot,
|
||||
specialization) == supported,
|
||||
"explicit gather accepted an unsupported sampler or rejected a valid one");
|
||||
for (const uint32_t perf_mip : {0u, 1u}) {
|
||||
user_data[9] = 0xfff000u | (perf_mip << 24u);
|
||||
user_data[10] = control;
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
const auto filter = (control >> 26u) & 3u;
|
||||
const bool supported = !explicit_lod || filter == 0 ||
|
||||
(filter <= 2 && (control & 0xfffffu) == 0 &&
|
||||
(filter != 2 || perf_mip == 0));
|
||||
Check(MaterializeResources(plan, {.user_data = user_data}, snapshot,
|
||||
specialization) == supported,
|
||||
"explicit gather accepted an unsupported sampler or rejected a valid one");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5408,6 +5667,65 @@ void TestSrtFlatteningAndRuntimeMemoization() {
|
||||
"flattened typed SRT reads were not collected");
|
||||
}
|
||||
|
||||
void TestScalarDataReadControlFlow() {
|
||||
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
|
||||
for (const auto comparison : {ValueOpcode::Void, ValueOpcode::IEqual64,
|
||||
ValueOpcode::INotEqual64}) {
|
||||
Fixture fixture;
|
||||
const auto low = fixture.UserData(0);
|
||||
const auto high = fixture.UserData(1);
|
||||
const auto address = fixture.Address(low, high);
|
||||
auto* use = fixture.block;
|
||||
if (comparison != ValueOpcode::Void) {
|
||||
const auto pointer = fixture.Emit(ValueOpcode::CompositeConstructU64, {low, high});
|
||||
auto enabled = fixture.Emit(comparison, {pointer, Value(uint64_t{0})});
|
||||
if (comparison == ValueOpcode::IEqual64)
|
||||
enabled = fixture.Emit(ValueOpcode::LogicalNot, {enabled});
|
||||
const auto lane = fixture.Emit(ValueOpcode::GetBuiltin,
|
||||
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
|
||||
const auto varying = fixture.Emit(ValueOpcode::INotEqual32, {lane, Value(0u)});
|
||||
fixture.block->condition = fixture.Emit(ValueOpcode::LogicalAnd, {varying, enabled});
|
||||
use = fixture.AddBlock();
|
||||
auto* done = fixture.AddBlock();
|
||||
fixture.block->AddBranch(use);
|
||||
fixture.block->AddBranch(done);
|
||||
use->AddBranch(done);
|
||||
fixture.block->terminator = {.kind = CFG::TerminatorKind::ConditionalBranch,
|
||||
.true_block = use, .false_block = done};
|
||||
use->terminator = {.kind = CFG::TerminatorKind::Branch, .true_block = done};
|
||||
done->terminator.kind = CFG::TerminatorKind::Return;
|
||||
}
|
||||
MemoryInfo scalar;
|
||||
scalar.kind = ResourceKind::ScalarAddress;
|
||||
fixture.Emit(ValueOpcode::LoadAddressU32,
|
||||
{address, Value(0u), Value(0u), Value(true)}, fixture.AddMemory(scalar, 4), use);
|
||||
fixture.PlanAndTrack();
|
||||
auto plan = ExtractResourcePlan(fixture.program);
|
||||
Check(plan.srt_reads.size() == 1 && !fixture.program.info.uses_dma &&
|
||||
bool(plan.srt_code) == (comparison == ValueOpcode::Void),
|
||||
"scalar data lost flattening or conditional reads entered the unconditional JIT");
|
||||
uint32_t reads = 0;
|
||||
std::array<uint32_t, 2> user_data {};
|
||||
SrtRuntime runtime {.user_data = user_data,
|
||||
.read_memory = +[](void* data, uint64_t address, std::span<uint32_t> words) {
|
||||
++*static_cast<uint32_t*>(data);
|
||||
if (address == 0) return false;
|
||||
words[0] = 42;
|
||||
return true;
|
||||
}, .userdata = &reads};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
for (const uint64_t pointer : {uint64_t{0}, uint64_t{0x1000}, uint64_t{1} << 32u}) {
|
||||
if (pointer == 0 && comparison == ValueOpcode::Void) continue;
|
||||
user_data = {static_cast<uint32_t>(pointer), static_cast<uint32_t>(pointer >> 32u)};
|
||||
reads = 0;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
reads == (pointer != 0) && snapshot.flattened_srt[0] == (pointer ? 42u : 0u),
|
||||
"scalar data refresh ignored a mixed EXEC/64-bit null-pointer branch");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TestDynamicSrtReadRemainsExplicit() {
|
||||
Fixture fixture;
|
||||
const auto base =
|
||||
@@ -7062,6 +7380,83 @@ void TestFiniteImagePhiCycle() {
|
||||
}
|
||||
}
|
||||
|
||||
void TestPackedDescriptorWaterfall() {
|
||||
for (const uint32_t variant : {0u, 1u, 2u}) {
|
||||
Fixture f(ShaderType::Pixel);
|
||||
const auto packed = f.Emit(ValueOpcode::GetInterpolationParameter,
|
||||
{Value(1u), Value(1u), Value(2u)});
|
||||
const auto active = f.Emit(ValueOpcode::ULessThan32,
|
||||
{f.Emit(ValueOpcode::LaneId), Value(16u)});
|
||||
const auto image_key = f.Emit(ValueOpcode::BitwiseAnd32, {packed, Value(65535u)});
|
||||
const auto sampler_key = variant == 2u ? packed :
|
||||
f.Emit(ValueOpcode::ShiftRightLogical32, {packed, Value(16u)});
|
||||
auto selected_active = active;
|
||||
const auto descriptor = [&](Value key, uint32_t first, uint32_t count, uint32_t shift) {
|
||||
const auto selected = f.Emit(ValueOpcode::ReadFirstLane, {key, active});
|
||||
selected_active = f.Emit(ValueOpcode::LogicalAnd, {selected_active,
|
||||
f.Emit(ValueOpcode::IEqual32, {selected, key})});
|
||||
const auto offset = f.Emit(ValueOpcode::ShiftLeftLogical32, {selected, Value(shift)});
|
||||
const auto table = f.Buffer({f.UserData(first), f.UserData(first + 1u),
|
||||
f.UserData(first + 2u), f.UserData(first + 3u)});
|
||||
std::array<Value, 8> words;
|
||||
for (uint32_t word = 0; word < count; ++word)
|
||||
words[word] = f.Emit(ValueOpcode::ReadConstBuffer, {table, offset},
|
||||
f.AddMemory({.kind = ResourceKind::ScalarBuffer, .offset = word * 4u}, 0x58u));
|
||||
return words;
|
||||
};
|
||||
const auto image = f.Image(descriptor(image_key, 0u, 8u, 5u));
|
||||
const auto sampler_words = descriptor(sampler_key, 4u, 4u, 4u);
|
||||
const auto sampler = f.Sampler({sampler_words[0], sampler_words[1],
|
||||
sampler_words[2], sampler_words[3]});
|
||||
f.Emit(ValueOpcode::ImageSampleRaw,
|
||||
{image, sampler, f.ImageAddress(), variant == 1u ? Value(true) : selected_active},
|
||||
f.AddMemory({.kind = ResourceKind::Image,
|
||||
.image_dimension = Decoder::ImageDimension::Dim2D}, 0x6cu));
|
||||
if (variant != 0u) {
|
||||
CheckFatal([&] { f.PlanAndTrack(); }, "not a valid runtime value",
|
||||
"packed waterfall accepted widened EXEC or an unbounded sampler key");
|
||||
continue;
|
||||
}
|
||||
f.PlanAndTrack();
|
||||
const auto plan = ExtractResourcePlan(f.program);
|
||||
for (const auto source : {plan.info.images[0].source, plan.info.samplers[0].source}) {
|
||||
const auto& value = plan.descriptor_sources[source];
|
||||
const auto& table = value.indirect_descriptor;
|
||||
Check(table && !table->selector && table->table_scalar &&
|
||||
table->table_stride == value.dword_count * 4u &&
|
||||
(value.dword_count != 4u || table->table_maximum == 65535u),
|
||||
"packed waterfall lost its static sampler bit bound or captured the varying key");
|
||||
}
|
||||
LinearTestMemory memory;
|
||||
const std::array<uint32_t, 8> user_data{
|
||||
uint32_t(memory.base) + 256u, 32u << 16u, 2u, 0x5204u,
|
||||
uint32_t(memory.base) + 512u, 16u << 16u, 59u, 0x5204u};
|
||||
for (uint32_t row = 0; row < 2u; ++row) {
|
||||
const auto word = (256u + row * 32u) / 4u;
|
||||
memory.words[word] = 0x1000u + row * 0x1000u;
|
||||
memory.words[word + 1u] = uint32_t(Libs::Graphics::Prospero::BufferFormat::k32Float) << 20u;
|
||||
memory.words[word + 3u] = Libs::Graphics::DstSel(4, 0, 0, 1) | (9u << 28u);
|
||||
}
|
||||
for (uint32_t row = 0; row < 59u; ++row) {
|
||||
const auto word = (512u + row * 16u) / 4u;
|
||||
memory.words[word] = row;
|
||||
memory.words[word + 1u] = 0x00fff000u;
|
||||
memory.words[word + 2u] = (row & 1u) ? 0x0a500000u : 0u;
|
||||
}
|
||||
const SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
|
||||
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 3u && snapshot.samplers.size() == 60u,
|
||||
"packed waterfall ignored the two-image/59-sampler heap bounds or lost the null roots");
|
||||
Check(std::ranges::all_of(snapshot.specialization_reads, [&](const auto& span) {
|
||||
return (span.first >= memory.base + 256u && span.first + span.second <= memory.base + 320u) ||
|
||||
(span.first >= memory.base + 512u && span.first + span.second <= memory.base + 512u + 59u * 16u);
|
||||
}), "packed waterfall materialization read outside the descriptor heaps");
|
||||
}
|
||||
}
|
||||
|
||||
void TestFiniteImageWaterfall() {
|
||||
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
|
||||
enum class Variant { Valid, Staggered, Cycle, DifferentMask, MixedWord, DifferentSelect, WiderExec, BadLeaf };
|
||||
@@ -8302,6 +8697,7 @@ int main() {
|
||||
Run("guarded direct image table", TestGuardedDirectImageTable);
|
||||
Run("bounded compute image loop", TestBoundedComputeImageLoop);
|
||||
Run("uniformized material image keys", TestUniformizedMaterialImageKeys);
|
||||
Run("indexed material image keys", TestIndexedMaterialImageKeys);
|
||||
Run("image descriptor fields", TestImageDescriptorFields);
|
||||
Run("draw-uniform scalar image", TestUniformScalarBufferImage);
|
||||
Run("compact scalar image table", TestCompactScalarImageTable);
|
||||
@@ -8311,6 +8707,7 @@ int main() {
|
||||
Run("shared append selector bounds", TestSharedAppendSelectorBounds);
|
||||
Run("descending image range", TestDescendingImageRange);
|
||||
Run("SRT runtime", TestSrtFlatteningAndRuntimeMemoization);
|
||||
Run("scalar data control flow", TestScalarDataReadControlFlow);
|
||||
Run("dynamic SRT", TestDynamicSrtReadRemainsExplicit);
|
||||
Run("writable descriptor phi", TestWritableDescriptorPhi);
|
||||
Run("bounded formatted buffer store", TestBoundedFormattedBufferStore);
|
||||
@@ -8325,6 +8722,7 @@ int main() {
|
||||
Run("packed scalar buffer selection", TestPackedScalarBufferSelection);
|
||||
Run("conditional sampler phi", TestConditionalSamplerPhi);
|
||||
Run("finite image phi cycle", TestFiniteImagePhiCycle);
|
||||
Run("packed descriptor waterfall", TestPackedDescriptorWaterfall);
|
||||
Run("finite image waterfall", TestFiniteImageWaterfall);
|
||||
Run("finite inline sampler", TestFiniteInlineSampler);
|
||||
Run("finite image bit scan sentinel", TestFiniteImageBitScanSentinel);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+115
-7
@@ -45,6 +45,7 @@
|
||||
#include <span>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
@@ -10804,9 +10805,14 @@ void TestMemoryFedScalarLeafCall() {
|
||||
};
|
||||
IR::SrtRuntime runtime{.read_memory = read, .userdata = &memory, .read_specialization_memory = read};
|
||||
const auto options = MakeCompileOptions(ShaderType::Compute);
|
||||
const uint32_t gpu_callable[] = {EncodeVop1(0x02, 4, 256),
|
||||
EncodeSmem0(0x09, 14, 0), 4u << 25u, EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
|
||||
const auto skipped = PrepareShaderSource(gpu_callable, options);
|
||||
Check(skipped.unsupported_call_pc == 12u && skipped.calls.empty(),
|
||||
"GPU-buffer callable did not report its unsupported call site");
|
||||
auto source = PrepareShaderSource(shader, options);
|
||||
Check(source.call && source.decoded.has_swap_pc &&
|
||||
source.decoded.instructions[source.call->instruction].branch_target == UINT32_MAX,
|
||||
Check(source.calls.size() == 1 && source.decoded.has_swap_pc &&
|
||||
source.decoded.instructions[source.calls.front().instruction].branch_target == UINT32_MAX,
|
||||
"memory-fed SWAPPC did not retain an unresolved target");
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
ExpectFatal([&] { (void)TranslateProgram(source.decoded, options); },
|
||||
@@ -10814,7 +10820,7 @@ void TestMemoryFedScalarLeafCall() {
|
||||
#endif
|
||||
const auto& linked = RefreshShaderSource(source, runtime);
|
||||
Check(linked.has_swap_pc && source.revision == 1 && linked.instructions.back().branch_target == 0x40 &&
|
||||
linked.instructions[source.call->instruction].branch_target == 0x44 &&
|
||||
linked.instructions[source.calls.front().instruction].branch_target == 0x44 &&
|
||||
source.reads.back() == std::pair<uint64_t, uint64_t>{base + 0x100, 12},
|
||||
"aliased call target, exact leaf range, or continuation was lost");
|
||||
const auto* storage = linked.instructions.data();
|
||||
@@ -10899,14 +10905,14 @@ void TestMemoryFedScalarLeafCall() {
|
||||
memory.words[96] = EncodeSMovB32(14, 129);
|
||||
ExpectFatal([&] { (void)RefreshShaderSource(source, runtime); },
|
||||
"leaf clobbering its return pair was linked");
|
||||
// AND_B64 writes the high half despite the decoder's generic scalar width.
|
||||
// AND_B64 must replace the preceding high-half definition.
|
||||
const uint32_t high_writer[] = {
|
||||
EncodeSMovB32(15, 129), EncodeSop2(0x0f, 14, 128, 128),
|
||||
EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
|
||||
auto pair_source = PrepareShaderSource(high_writer, options);
|
||||
IR::SrtWalker pair_walker(pair_source.call_targets, runtime);
|
||||
uint32_t high = 1;
|
||||
Check(pair_walker.Evaluate(pair_source.call->target[1], high) && high == 0,
|
||||
Check(pair_walker.Evaluate(pair_source.calls.front().target[1], high) && high == 0,
|
||||
"scalar pair producer used a stale preceding high-half definition");
|
||||
const uint32_t joined[] = {
|
||||
EncodeSopp(0x04, 2), EncodeSMovB32(14, 129), EncodeSopp(0x02, 1),
|
||||
@@ -10915,7 +10921,7 @@ void TestMemoryFedScalarLeafCall() {
|
||||
"ambiguous reaching definitions were accepted for a call target");
|
||||
const uint32_t multiple[] = {EncodeSop1(0x21, 14, 14), EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
|
||||
ExpectFatal([&] { (void)PrepareShaderSource(multiple, options); },
|
||||
"call metadata bypassed the multiple-call rejection");
|
||||
"a native return pair was accepted as a later call target");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -10962,7 +10968,7 @@ void TestFusedShaderHandoffPreservesRegisters() {
|
||||
for (const auto handoff: {EncodeSop1(0x20, 0, 6), 0xbefd2106u}) {
|
||||
front[2] = handoff; // SETPC or captured SWAPPC with NULL destination
|
||||
auto source = PrepareShaderSource(front, options);
|
||||
Check(!source.decoded.has_swap_pc && !source.call,
|
||||
Check(!source.decoded.has_swap_pc && source.calls.empty(),
|
||||
"fused SETPC handoff entered scalar call preparation");
|
||||
auto translated = TranslateProgram(source.decoded, options);
|
||||
uint32_t allocations = 0;
|
||||
@@ -12964,6 +12970,54 @@ void TestLateConstantPropagation() {
|
||||
"an ungrounded Phi cycle acquired a constant");
|
||||
}
|
||||
|
||||
void TestRepeatedBitcastPropagation() {
|
||||
using namespace ShaderRecompiler::IR;
|
||||
Block entry, left, right;
|
||||
IREmitter e(&entry), l(&left), r(&right);
|
||||
const auto raw = e.GetUserData(static_cast<ScalarReg>(0));
|
||||
const auto other = e.GetUserData(static_cast<ScalarReg>(1));
|
||||
const auto fp = e.Emit(ValueOpcode::ConvertF32U32, {raw});
|
||||
for (const auto [opcode, source, distinct] : {
|
||||
std::tuple{ValueOpcode::BitCastF32U32, Value(raw), Value(other)},
|
||||
std::tuple{ValueOpcode::BitCastU32F32, fp,
|
||||
e.Emit(ValueOpcode::ConvertF32U32, {other})}}) {
|
||||
const auto first = e.Emit(opcode, {source});
|
||||
const auto duplicate = e.Emit(opcode, {source});
|
||||
const auto separate = e.Emit(opcode, {distinct});
|
||||
const auto left_cast = l.Emit(opcode, {source});
|
||||
const auto left_duplicate = l.Emit(opcode, {source});
|
||||
const auto right_cast = r.Emit(opcode, {source});
|
||||
const auto use = opcode == ValueOpcode::BitCastF32U32
|
||||
? e.Emit(ValueOpcode::FPIsNan32, {duplicate})
|
||||
: e.Emit(ValueOpcode::IEqual32, {duplicate, Value(0u)});
|
||||
ConstantPropagationPass({&entry, &left, &right});
|
||||
Check(duplicate.Resolve() == first && left_duplicate.Resolve() == left_cast &&
|
||||
use.Instruction()->Arg(0).Resolve() == first,
|
||||
"repeated same-block casts were not shared with their earlier definition");
|
||||
Check(separate.Resolve() == separate && left_cast.Resolve() == left_cast &&
|
||||
right_cast.Resolve() == right_cast && left_cast != first && right_cast != left_cast,
|
||||
"cast reuse merged distinct values or crossed block boundaries");
|
||||
}
|
||||
const auto as_float = e.Emit(ValueOpcode::BitCastF32U32, {raw});
|
||||
const auto raw_roundtrip = e.Emit(ValueOpcode::BitCastU32F32, {as_float});
|
||||
const auto as_bits = e.Emit(ValueOpcode::BitCastU32F32, {fp});
|
||||
const auto float_roundtrip = e.Emit(ValueOpcode::BitCastF32U32, {as_bits});
|
||||
ConstantPropagationPass({&entry});
|
||||
Check(raw_roundtrip.Resolve() == raw && float_roundtrip.Resolve() == fp,
|
||||
"sharing casts prevented inverse casts from folding");
|
||||
for (const uint32_t bits : {0u, 0x80000000u, 1u, 0x80000001u, 0x7f800000u,
|
||||
0xff800000u, 0x7fc12345u, 0x7f812345u}) {
|
||||
const auto constant_float = e.Emit(ValueOpcode::BitCastF32U32, {Value(bits)});
|
||||
const auto constant_bits = e.Emit(ValueOpcode::BitCastU32F32,
|
||||
{Value::F32(std::bit_cast<float>(bits))});
|
||||
ConstantPropagationPass({&entry});
|
||||
Check(constant_float.Resolve().IsImmediate() &&
|
||||
std::bit_cast<uint32_t>(constant_float.Resolve().F32Value()) == bits &&
|
||||
constant_bits.Resolve() == Value(bits),
|
||||
"constant bitcast folding changed signed zero, subnormal, infinity or NaN bits");
|
||||
}
|
||||
}
|
||||
|
||||
void TestMaskedShiftConstantPropagation() {
|
||||
using namespace ShaderRecompiler::IR;
|
||||
for (const uint32_t count : {0u, 1u, 8u, 31u, 32u, 33u, 40u, 63u, 64u, UINT32_MAX}) {
|
||||
@@ -15353,6 +15407,58 @@ void TestScalarMemoryLoadCrossesIntoVcc() {
|
||||
}
|
||||
}
|
||||
|
||||
void TestEmptyBranchScalarReadDce() {
|
||||
using namespace ShaderRecompiler::IR;
|
||||
using ShaderRecompiler::CFG::TerminatorKind;
|
||||
for (const bool reverse : {false, true}) {
|
||||
for (uint32_t mode = 0; mode < 4; ++mode) {
|
||||
Block entry, detour, merge;
|
||||
const BlockList blocks{&entry, &detour, &merge};
|
||||
entry.AddBranch(&detour);
|
||||
entry.AddBranch(&merge);
|
||||
detour.AddBranch(&merge);
|
||||
entry.terminator.kind = TerminatorKind::ConditionalBranch;
|
||||
entry.terminator.true_block = reverse ? &merge : &detour;
|
||||
entry.terminator.false_block = reverse ? &detour : &merge;
|
||||
entry.terminator.merge_block = &merge;
|
||||
detour.terminator.kind = TerminatorKind::Branch;
|
||||
detour.terminator.true_block = &merge;
|
||||
IREmitter ir(&entry);
|
||||
const auto address = ir.Emit(ValueOpcode::GetAddressResource,
|
||||
{Value(0x1000u), Value(0u)});
|
||||
const auto read = ir.Emit(ValueOpcode::LoadAddressU32,
|
||||
{address, Value(24u), Value(0u), Value(true)});
|
||||
const auto predicate = ir.INotEqual(U32(read), U32(Value(0u)));
|
||||
entry.condition = ir.Emit(ValueOpcode::ConditionRef, {predicate});
|
||||
ir.Emit(ValueOpcode::Reference, {entry.condition});
|
||||
ir.Emit(ValueOpcode::Barrier);
|
||||
detour.AppendNewInst(ValueOpcode::ControlNop, {});
|
||||
if (mode == 1) detour.AppendNewInst(ValueOpcode::Barrier, {});
|
||||
if (mode == 2) {
|
||||
auto& phi = merge.AppendNewInst(ValueOpcode::Phi, {});
|
||||
phi.SetFlags(Type::U32);
|
||||
phi.AddPhiOperand(&entry, Value(1u));
|
||||
phi.AddPhiOperand(&detour, Value(2u));
|
||||
merge.AppendNewInst(ValueOpcode::ReferenceU32, {Value(&phi)});
|
||||
}
|
||||
if (mode == 3) merge.AppendNewInst(ValueOpcode::ReferenceU32, {read});
|
||||
EliminateDeadCode(blocks);
|
||||
Check(entry.condition.IsImmediate() == (mode == 0 || mode == 3),
|
||||
"empty branch cleanup changed side effects or a live Phi");
|
||||
const auto contains = [&](ValueOpcode op) {
|
||||
return std::ranges::any_of(entry, [=](const Inst& inst) {
|
||||
return inst.GetOpcode() == op;
|
||||
});
|
||||
};
|
||||
Check(contains(ValueOpcode::LoadAddressU32) == (mode != 0),
|
||||
"empty branch cleanup lost a shared read or retained a dead read");
|
||||
Check(contains(ValueOpcode::Barrier) && entry.ImmSuccessors().size() == 2 &&
|
||||
merge.ImmPredecessors().size() == 2,
|
||||
"empty branch cleanup changed earlier side effects or CFG edges");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TestScalarMemoryUnusedTailDce() {
|
||||
const uint32_t shader[] = {
|
||||
EncodeSmem0(0x0c, 16, 0),
|
||||
@@ -16670,6 +16776,7 @@ int main() {
|
||||
TestValuePhiValidation();
|
||||
TestLaneMaskProjection();
|
||||
TestLateConstantPropagation();
|
||||
TestRepeatedBitcastPropagation();
|
||||
TestMaskedShiftConstantPropagation();
|
||||
TestU64ShiftConstantPropagation();
|
||||
TestU64RegisterPairRoundTrip();
|
||||
@@ -16701,6 +16808,7 @@ int main() {
|
||||
TestSrtWalkerRealSBufferTranslation();
|
||||
TestScalarMemorySourcesCapturedBeforeWrites();
|
||||
TestScalarMemoryLoadCrossesIntoVcc();
|
||||
TestEmptyBranchScalarReadDce();
|
||||
TestScalarMemoryUnusedTailDce();
|
||||
TestResourceTrackingRealDensePatching();
|
||||
TestDirectTranslationResetsAnalysisState();
|
||||
|
||||
Reference in New Issue
Block a user