From fb4d97e8a94a9f2b38bab01c006718ba50f303af Mon Sep 17 00:00:00 2001 From: nmzik Date: Thu, 3 Sep 2026 21:52:02 +0200 Subject: [PATCH] graphics: fix PS5 NGG vertex and instance draw offsets Refs: https://github.com/KytyPS5/KytyPS5/issues/130 --- .../command_processor/commandProcessor.h | 4 +- src/graphics/guest_gpu/graphicsRun.cpp | 27 ++++---- src/graphics/host_gpu/renderer/render.h | 6 +- src/graphics/host_gpu/renderer/renderDraw.cpp | 65 ++++++++++++++----- src/graphics/host_gpu/renderer/renderDraw.h | 5 +- .../shader/recompiler/ShaderRecompiler.cpp | 42 ++++++++---- .../frontend/translate/Translate.cpp | 3 +- .../recompiler/frontend/translate/Translate.h | 3 +- src/graphics/shader/recompiler/ir/ShaderIR.h | 1 + tests/ShaderRecompilerComputeTests.cpp | 39 +++++++++++ 10 files changed, 147 insertions(+), 48 deletions(-) diff --git a/src/graphics/guest_gpu/command_processor/commandProcessor.h b/src/graphics/guest_gpu/command_processor/commandProcessor.h index 911eb4480..983bd2db6 100644 --- a/src/graphics/guest_gpu/command_processor/commandProcessor.h +++ b/src/graphics/guest_gpu/command_processor/commandProcessor.h @@ -76,7 +76,7 @@ public: void DrawIndex(uint32_t index_count, const void* index_addr, uint32_t flags, uint32_t type, uint32_t instance_count = 0, const void* object_ids = nullptr, uint32_t render_target_slice_offset = 0, int32_t vertex_offset_add = 0, - uint32_t first_instance = 0); + uint32_t first_instance = 0, bool offsets_from_indirect_args = false); void DrawIndexOffset(uint32_t index_offset, uint32_t index_count, uint32_t flags); void DrawIndexAuto(uint32_t index_count, uint32_t flags, uint32_t render_target_slice_offset = 0); @@ -151,7 +151,7 @@ private: void SuspendPm4(); void SubmitNonIndexedDraw(uint32_t vertex_count, uint32_t flags, uint32_t render_target_slice_offset, uint32_t first_vertex, - uint32_t first_instance); + uint32_t first_instance, bool offsets_from_indirect_args = false); CommandScheduler& GetScheduler() const { return m_renderer.GetCommandScheduler(); } CommandBuffer& CurrentBuffer() { return GetScheduler().Current(); } diff --git a/src/graphics/guest_gpu/graphicsRun.cpp b/src/graphics/guest_gpu/graphicsRun.cpp index 38ccd08c2..d57200056 100644 --- a/src/graphics/guest_gpu/graphicsRun.cpp +++ b/src/graphics/guest_gpu/graphicsRun.cpp @@ -850,7 +850,7 @@ void CommandProcessor::SetPredication(uint32_t condition, uint32_t op, uint32_t void CommandProcessor::DrawIndex(uint32_t index_count, const void* index_addr, uint32_t flags, uint32_t type, uint32_t instance_count, const void* object_ids, uint32_t render_target_slice_offset, int32_t vertex_offset_add, - uint32_t first_instance) { + uint32_t first_instance, bool offsets_from_indirect_args) { CheckBuffer(); if (instance_count == 0) { @@ -868,9 +868,10 @@ void CommandProcessor::DrawIndex(uint32_t index_count, const void* index_addr, u "\n", vertex_offset_add, first_instance); } - m_renderer.GetRenderExecutor().DrawIndex( - m_submit_id, CurrentBuffer(), m_index_type_and_size, index_count, index_addr, flags, type, - instance_count, render_target_slice_offset, vertex_offset_add, first_instance); + m_renderer.GetRenderExecutor().DrawIndex(m_submit_id, CurrentBuffer(), m_index_type_and_size, + index_count, index_addr, flags, type, instance_count, + render_target_slice_offset, vertex_offset_add, + first_instance, offsets_from_indirect_args); } void CommandProcessor::DrawIndexOffset(uint32_t index_offset, uint32_t index_count, @@ -929,7 +930,7 @@ void CommandProcessor::DrawIndirect(uint32_t data_offset, uint32_t draw_initiato } m_num_instances = args.instance_count; SubmitNonIndexedDraw(args.vertex_count_per_instance, 0, 0, args.start_vertex_location, - args.start_instance_location); + args.start_instance_location, true); return; } @@ -971,7 +972,7 @@ void CommandProcessor::DrawIndirect(uint32_t data_offset, uint32_t draw_initiato m_num_instances = args.instance_count; DrawIndex(index_count, index_addr, 0, 1, args.instance_count, nullptr, 0, - static_cast(args.base_vertex_location), args.start_instance_location); + static_cast(args.base_vertex_location), args.start_instance_location, true); } void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_count_or_count, @@ -1029,7 +1030,7 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun } m_num_instances = args->instance_count; SubmitNonIndexedDraw(args->vertex_count_per_instance, 0, 0, args->start_vertex_location, - args->start_instance_location); + args->start_instance_location, true); continue; } @@ -1072,7 +1073,8 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun m_num_instances = args->instance_count; DrawIndex(index_count, index_addr, 0, 1, args->instance_count, nullptr, 0, - static_cast(args->base_vertex_location), args->start_instance_location); + static_cast(args->base_vertex_location), args->start_instance_location, + true); } } @@ -1170,12 +1172,13 @@ void CommandProcessor::DrawIndexAuto(uint32_t index_count, uint32_t flags, void CommandProcessor::SubmitNonIndexedDraw(uint32_t vertex_count, uint32_t flags, uint32_t render_target_slice_offset, - uint32_t first_vertex, uint32_t first_instance) { + uint32_t first_vertex, uint32_t first_instance, + bool offsets_from_indirect_args) { CheckBuffer(); - m_renderer.GetRenderExecutor().DrawAuto(m_submit_id, CurrentBuffer(), vertex_count, flags, - render_target_slice_offset, m_num_instances, - first_vertex, first_instance); + m_renderer.GetRenderExecutor().DrawAuto( + m_submit_id, CurrentBuffer(), vertex_count, flags, render_target_slice_offset, + m_num_instances, first_vertex, first_instance, offsets_from_indirect_args); } void CommandProcessor::WaitFlipDone(uint32_t video_out_handle, uint32_t display_buffer_index) { diff --git a/src/graphics/host_gpu/renderer/render.h b/src/graphics/host_gpu/renderer/render.h index af0972b25..61747c34f 100644 --- a/src/graphics/host_gpu/renderer/render.h +++ b/src/graphics/host_gpu/renderer/render.h @@ -132,10 +132,12 @@ public: void DrawIndex(uint64_t submit_id, CommandBuffer& buffer, uint32_t index_type_and_size, uint32_t index_count, const void* index_addr, uint32_t flags, uint32_t type, uint32_t instance_count = 1, uint32_t render_target_slice_offset = 0, - int32_t vertex_offset_add = 0, uint32_t first_instance = 0); + int32_t vertex_offset_add = 0, uint32_t first_instance = 0, + bool offsets_from_indirect_args = false); void DrawAuto(uint64_t submit_id, CommandBuffer& buffer, uint32_t index_count, uint32_t flags, uint32_t render_target_slice_offset = 0, uint32_t instance_count = 1, - uint32_t first_vertex = 0, uint32_t first_instance = 0); + uint32_t first_vertex = 0, uint32_t first_instance = 0, + bool offsets_from_indirect_args = false); void DispatchDirect(uint64_t submit_id, CommandBuffer& buffer, uint32_t thread_group_x, uint32_t thread_group_y, uint32_t thread_group_z, uint32_t mode); diff --git a/src/graphics/host_gpu/renderer/renderDraw.cpp b/src/graphics/host_gpu/renderer/renderDraw.cpp index 0c63cc278..05946d945 100644 --- a/src/graphics/host_gpu/renderer/renderDraw.cpp +++ b/src/graphics/host_gpu/renderer/renderDraw.cpp @@ -64,6 +64,25 @@ int32_t ResolveVertexOffset(uint32_t index_offset, const ShaderVertexInputInfo& return 0; } +uint32_t ResolveInstanceOffset(const ShaderVertexInputInfo& vs_input_info) { + if (!vs_input_info.fetch_embedded) { + return 0; + } + + EXIT_IF(!vs_input_info.stage); + const auto& program = *vs_input_info.stage.program; + const auto& resources = vs_input_info.stage.resources; + if (program.info.instance_offset_sgpr >= static_cast(program.user_data_base)) { + const auto index = + static_cast(program.info.instance_offset_sgpr) - program.user_data_base; + if (index < resources.user_data.size()) { + return resources.user_data[index]; + } + } + + return 0; +} + static std::atomic g_draw_state_log_count = 0; static std::atomic g_draw_input_log_count = 0; static std::atomic g_mrt_state_log_count = 0; @@ -508,6 +527,7 @@ struct DrawCallInfo { uint32_t flags = 0; uint32_t instance_count = 0; uint32_t first_instance = 0; + bool offsets_from_indirect_args = false; }; static bool ResolveDccAttachmentClear(TextureCache& cache, const RenderColorInfo& target, @@ -747,6 +767,7 @@ struct DrawEmitInfo { bool indexed = false; int32_t vertex_offset = 0; uint32_t first_vertex = 0; + uint32_t first_instance = 0; }; struct DrawIndexBufferSource { @@ -1124,10 +1145,10 @@ static void EmitDrawPrimitives(const HW::UserConfig& ucfg, vk::CommandBuffer vk_ case Prospero::PrimitiveType::kRectList: if (emit.indexed) { vk_buffer.drawIndexed(draw.index_count, draw.instance_count, 0, emit.vertex_offset, - draw.first_instance); + emit.first_instance); } else { vk_buffer.draw(draw.index_count, draw.instance_count, emit.first_vertex, - draw.first_instance); + emit.first_instance); } break; case Prospero::PrimitiveType::kRectListLegacy: @@ -1136,17 +1157,17 @@ static void EmitDrawPrimitives(const HW::UserConfig& ucfg, vk::CommandBuffer vk_ } // Sarah EXIT_NOT_IMPLEMENTED(!(draw.index_count == 3 && vs_input_info.buffers_num == 0)); - vk_buffer.draw(4, draw.instance_count, emit.first_vertex, draw.first_instance); + vk_buffer.draw(4, draw.instance_count, emit.first_vertex, emit.first_instance); break; case Prospero::PrimitiveType::kQuadListLegacy: EXIT_NOT_IMPLEMENTED((draw.index_count & 0x3u) != 0); for (uint32_t i = 0; i < draw.index_count; i += 4) { if (emit.indexed) { vk_buffer.drawIndexed(4, draw.instance_count, i, emit.vertex_offset, - draw.first_instance); + emit.first_instance); } else { vk_buffer.draw(4, draw.instance_count, i + emit.first_vertex, - draw.first_instance); + emit.first_instance); } } break; @@ -1242,7 +1263,8 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer, uint32_t index_type_and_size, uint32_t index_count, const void* index_addr, uint32_t flags, uint32_t type, uint32_t instance_count, uint32_t render_target_slice_offset, - int32_t vertex_offset_add, uint32_t first_instance) { + int32_t vertex_offset_add, uint32_t first_instance, + bool offsets_from_indirect_args) { KYTY_PROFILER_FUNCTION(); EXIT_IF(buffer.IsInvalid()); @@ -1328,7 +1350,8 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer, EXIT_NOT_IMPLEMENTED(type != 1); const DrawCallInfo draw {"DrawIndex", CommandBufferDebugOp::DrawIndex, index_count, flags, - instance_count, first_instance}; + instance_count, first_instance, + offsets_from_indirect_args}; std::vector expanded_indices; if (expand_index8_to_u16) { EXIT_NOT_IMPLEMENTED(index_addr == nullptr); @@ -1359,11 +1382,16 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer, LogDrawStateIfNeeded(buffer, draw, state, true, false, index_type_and_size, index_addr); const auto vertex_offset = - ResolveVertexOffset(ucfg.GetIndexOffset(), state.vs_input_info) + vertex_offset_add; + draw.offsets_from_indirect_args + ? vertex_offset_add + : ResolveVertexOffset(ucfg.GetIndexOffset(), state.vs_input_info) + vertex_offset_add; DrawEmitInfo emit {}; - emit.indexed = true; - emit.vertex_offset = vertex_offset; + emit.indexed = true; + emit.vertex_offset = vertex_offset; + emit.first_instance = draw.offsets_from_indirect_args + ? draw.first_instance + : ResolveInstanceOffset(state.vs_input_info); ExecutePreparedDraw(submit_id, buffer, draw, state, topology, emit, index_source, primitive_restart, true, true, false); @@ -1374,7 +1402,7 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer, void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, uint32_t index_count, uint32_t flags, uint32_t render_target_slice_offset, uint32_t instance_count, uint32_t first_vertex, - uint32_t first_instance) { + uint32_t first_instance, bool offsets_from_indirect_args) { KYTY_PROFILER_FUNCTION(); EXIT_IF(buffer.IsInvalid()); @@ -1426,7 +1454,8 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, uint32_ EXIT_NOT_IMPLEMENTED(flags != 0); const DrawCallInfo draw {"DrawIndexAuto", CommandBufferDebugOp::DrawIndexAuto, index_count, flags, - instance_count, first_instance}; + instance_count, first_instance, + offsets_from_indirect_args}; DrawRenderState state {}; if (!PrepareDrawRenderState(submit_id, buffer, draw, render_target_slice_offset, false, @@ -1460,10 +1489,16 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, uint32_ ucfg.GetPrimType() == Prospero::PrimitiveType::kRectListLegacy, 0, nullptr); - const auto vertex_offset = ResolveVertexOffset(ucfg.GetIndexOffset(), state.vs_input_info) + - static_cast(first_vertex); + const auto vertex_offset = + draw.offsets_from_indirect_args + ? static_cast(first_vertex) + : ResolveVertexOffset(ucfg.GetIndexOffset(), state.vs_input_info) + + static_cast(first_vertex); DrawEmitInfo emit {}; - emit.first_vertex = static_cast(vertex_offset); + emit.first_vertex = static_cast(vertex_offset); + emit.first_instance = draw.offsets_from_indirect_args + ? draw.first_instance + : ResolveInstanceOffset(state.vs_input_info); DrawIndexBufferSource index_source {}; ExecutePreparedDraw(submit_id, buffer, draw, state, topology, emit, index_source, false, false, diff --git a/src/graphics/host_gpu/renderer/renderDraw.h b/src/graphics/host_gpu/renderer/renderDraw.h index fe1ea3b1f..d695354bc 100644 --- a/src/graphics/host_gpu/renderer/renderDraw.h +++ b/src/graphics/host_gpu/renderer/renderDraw.h @@ -7,8 +7,9 @@ namespace Libs::Graphics { struct ShaderVertexInputInfo; -[[nodiscard]] int32_t ResolveVertexOffset(uint32_t index_offset, - const ShaderVertexInputInfo& vs_input_info); +[[nodiscard]] int32_t ResolveVertexOffset(uint32_t index_offset, + const ShaderVertexInputInfo& vs_input_info); +[[nodiscard]] uint32_t ResolveInstanceOffset(const ShaderVertexInputInfo& vs_input_info); } // namespace Libs::Graphics diff --git a/src/graphics/shader/recompiler/ShaderRecompiler.cpp b/src/graphics/shader/recompiler/ShaderRecompiler.cpp index c15f89416..7f5d2c962 100644 --- a/src/graphics/shader/recompiler/ShaderRecompiler.cpp +++ b/src/graphics/shader/recompiler/ShaderRecompiler.cpp @@ -234,8 +234,10 @@ EmbeddedFetchData DetectEmbeddedVertexFetch(const Decoder::Program& decoded uint32_t wave_size) { EmbeddedFetchData data; data.loads.reserve(input_info->resources_num); - int32_t offset_candidate = -1; - bool offset_conflict = false; + int32_t vertex_offset_candidate = -1; + int32_t instance_offset_candidate = -1; + bool vertex_offset_conflict = false; + bool instance_offset_conflict = false; const int shift_regs = 8; const int attrib_reg = input_info->fetch_attrib_reg + shift_regs; @@ -263,26 +265,34 @@ EmbeddedFetchData DetectEmbeddedVertexFetch(const Decoder::Program& decoded for (const auto& inst: decoded.instructions) { // Fetch shaders accumulate the draw's vertex offset in v0. The PS5 NGG ABI - // seeds S_NGG_VERTEX_INDEX in v5 and applies the same offset there before fetching. + // seeds S_NGG_VERTEX_INDEX in v5 and S_NGG_INSTANCE_INDEX in v8, then applies the + // corresponding direct-draw offsets before fetching. const bool vertex_index_accumulator = IsDecodedVgpr(inst.dst) && (inst.dst.reg == 0 || (user_data_base == 8 && inst.dst.reg == 5)); + const bool instance_index_accumulator = + IsDecodedVgpr(inst.dst) && (inst.dst.reg == (user_data_base == 8 ? 8u : 3u)); uint32_t sad_zero = 0; - const bool vertex_offset_add = - vertex_index_accumulator && IsDecodedSgpr(inst.src0) && + const bool index_offset_add = + (vertex_index_accumulator || instance_index_accumulator) && + IsDecodedSgpr(inst.src0) && ((inst.opcode == Decoder::Opcode::V_ADD_I32 && IsDecodedVgpr(inst.src1) && inst.src1.reg == inst.dst.reg) || - (user_data_base == 8 && inst.dst.reg == 5 && + (user_data_base == 8 && (inst.dst.reg == 5 || inst.dst.reg == 8) && inst.opcode == Decoder::Opcode::V_SAD_U32 && IsDecodedVgpr(inst.src2) && inst.src2.reg == inst.dst.reg && TryDecodedOperandConstant(sgprs, inst.src1, sad_zero) && sad_zero == 0)); - if (data.loads.empty() && vertex_offset_add) { + if (data.loads.empty() && index_offset_add) { const auto reg = DecodedSgprReg(inst.src0); if (reg >= user_data_base && reg - user_data_base < user_data_count) { - if (offset_candidate >= 0 && offset_candidate != static_cast(reg)) { - offset_conflict = true; + auto& candidate = vertex_index_accumulator ? vertex_offset_candidate + : instance_offset_candidate; + auto& conflict = vertex_index_accumulator ? vertex_offset_conflict + : instance_offset_conflict; + if (candidate >= 0 && candidate != static_cast(reg)) { + conflict = true; } else { - offset_candidate = static_cast(reg); + candidate = static_cast(reg); } } } @@ -441,8 +451,13 @@ EmbeddedFetchData DetectEmbeddedVertexFetch(const Decoder::Program& decoded load.attrib_id = buffer.attrib_id; load.components = DecodedDstSize(inst); load.prolog_loads = buffer.prolog_loads; - if (data.loads.empty() && !offset_conflict) { - data.vertex_offset_sgpr = offset_candidate; + if (data.loads.empty()) { + if (!vertex_offset_conflict) { + data.vertex_offset_sgpr = vertex_offset_candidate; + } + if (!instance_offset_conflict) { + data.instance_offset_sgpr = instance_offset_candidate; + } } data.loads.push_back(load); } @@ -615,7 +630,8 @@ TranslateResult TranslateProgram(std::span code, const CompileOp IR::TrackResources(ir); IR::EliminateDeadCode(ir.blocks); if (options.stage == ShaderType::Vertex) { - ir.info.vertex_offset_sgpr = embedded_fetch.vertex_offset_sgpr; + ir.info.vertex_offset_sgpr = embedded_fetch.vertex_offset_sgpr; + ir.info.instance_offset_sgpr = embedded_fetch.instance_offset_sgpr; } TranslateResult result; diff --git a/src/graphics/shader/recompiler/frontend/translate/Translate.cpp b/src/graphics/shader/recompiler/frontend/translate/Translate.cpp index 27925fe5e..f42ebb41d 100644 --- a/src/graphics/shader/recompiler/frontend/translate/Translate.cpp +++ b/src/graphics/shader/recompiler/frontend/translate/Translate.cpp @@ -950,7 +950,8 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph& result.cfg_failure_kind = options.cfg_failure_kind; result.fallback_reason = options.fallback_reason; if (options.embedded_fetch != nullptr) { - result.info.vertex_offset_sgpr = options.embedded_fetch->vertex_offset_sgpr; + result.info.vertex_offset_sgpr = options.embedded_fetch->vertex_offset_sgpr; + result.info.instance_offset_sgpr = options.embedded_fetch->instance_offset_sgpr; } uint32_t vector_limit = 1u; diff --git a/src/graphics/shader/recompiler/frontend/translate/Translate.h b/src/graphics/shader/recompiler/frontend/translate/Translate.h index fa558ecf1..5d5bc3b78 100644 --- a/src/graphics/shader/recompiler/frontend/translate/Translate.h +++ b/src/graphics/shader/recompiler/frontend/translate/Translate.h @@ -19,7 +19,8 @@ struct EmbeddedFetchLoad { struct EmbeddedFetchPlan { std::vector loads; - int32_t vertex_offset_sgpr = -1; + int32_t vertex_offset_sgpr = -1; + int32_t instance_offset_sgpr = -1; }; struct TranslateOptions { diff --git a/src/graphics/shader/recompiler/ir/ShaderIR.h b/src/graphics/shader/recompiler/ir/ShaderIR.h index 7ce4595b3..0ddc30a51 100644 --- a/src/graphics/shader/recompiler/ir/ShaderIR.h +++ b/src/graphics/shader/recompiler/ir/ShaderIR.h @@ -418,6 +418,7 @@ struct ShaderInfo { std::vector outputs; std::array vertex_fetch_components {}; int32_t vertex_offset_sgpr = -1; + int32_t instance_offset_sgpr = -1; bool has_bitwise_xor = false; bool uses_dma = false; diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index ff76619b0..89a858e2d 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -21909,6 +21909,18 @@ void CheckEmbeddedFetchVertexOffset() { return ResolveVertexOffset(index_offset, vertex); }; + const auto ResolveInstance = [](const CompiledShader &result) { + ShaderVertexInputInfo vertex; + vertex.fetch_embedded = true; + ShaderRecompiler::IR::CompiledShaderInfo program{}; + program.user_data_base = result.program.user_data_base; + program.info.instance_offset_sgpr = + result.program.info.instance_offset_sgpr; + vertex.stage.program = &program; + vertex.stage.resources = result.resources; + return ResolveInstanceOffset(vertex); + }; + const auto valid = Compile("EmbeddedFetchVertexOffset", MakeFetch({{18, 0}}), 7); Require( @@ -21928,6 +21940,33 @@ void CheckEmbeddedFetchVertexOffset() { "PS5 NGG vertex-index offset or register index-offset precedence is " "wrong"); + const auto ngg_instance = + Compile("EmbeddedFetchNggInstanceOffset", + MakeFetch({{18, 8}}, {}, 8, true), 81); + Require("EmbeddedFetchNggInstanceOffset", "parse", + ngg_instance.program.info.vertex_offset_sgpr == -1 && + ngg_instance.program.info.instance_offset_sgpr == 18 && + ResolveInstance(ngg_instance) == 81, + "PS5 NGG instance-index offset was not detected or resolved"); + + const auto instance_conflict = Compile( + "EmbeddedFetchConflictingInstanceOffset", + MakeFetch({{17, 8}, {18, 8}}, {}, 8, true), 81); + const auto instance_malformed = + Compile("EmbeddedFetchMalformedInstanceOffset", + MakeFetch({{18, 7}}, {}, 7, true), 81); + const auto instance_outside = + Compile("EmbeddedFetchOutsideInstanceOffset", + MakeFetch({{19, 8}}, {}, 8, true), 81); + for (const auto *result : + {&instance_conflict, &instance_malformed, &instance_outside}) { + Require("EmbeddedFetchNggInstanceOffset", "fail closed", + result->program.info.instance_offset_sgpr == -1 && + ResolveInstance(*result) == 0, + "conflicting, malformed, or out-of-window instance add was " + "classified"); + } + const auto pointer = 0x5b7c5100u; const auto late = Compile("EmbeddedFetchLateOffset", MakeFetch({}, std::pair{18, 0}), pointer);