#include "graphics/host_gpu/renderer/renderDraw.h" #include "common/assert.h" #include "common/common.h" #include "common/file.h" #include "common/logging/log.h" #include "common/profiler.h" #include "common/stringUtils.h" #include "common/threads.h" #include "graphics/guest_gpu/gpu_defs.h" #include "graphics/guest_gpu/graphicsRun.h" #include "graphics/guest_gpu/hardwareContext.h" #include "graphics/guest_gpu/tile.h" #include "graphics/host_gpu/graphicContext.h" #include "graphics/host_gpu/renderer/colorRenderTarget.h" #include "graphics/host_gpu/renderer/debug.h" #include "graphics/host_gpu/renderer/depthRenderTarget.h" #include "graphics/host_gpu/renderer/pipeline/pipelineCache.h" #include "graphics/host_gpu/renderer/pipeline/shaderResourceBarrier.h" #include "graphics/host_gpu/renderer/render.h" #include "graphics/host_gpu/renderer/renderContext.h" #include "graphics/host_gpu/vulkanCommon.h" #include "graphics/shader/recompiler/ir/ShaderIR.h" #include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h" #include "graphics/shader/shader.h" #include "kernel/eventQueue.h" #include "kernel/memory.h" #include "kernel/pthread.h" #include "libs/errno.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Libs::Graphics { int32_t ResolveVertexOffset(uint32_t index_offset, const ShaderVertexInputInfo& vs_input_info) { if (index_offset != 0 || !vs_input_info.fetch_embedded) { return static_cast(index_offset); } 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.vertex_offset_sgpr >= static_cast(program.user_data_base)) { const auto index = static_cast(program.info.vertex_offset_sgpr) - program.user_data_base; if (index < resources.user_data.size()) { return static_cast(resources.user_data[index]); } } 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; static std::atomic g_shader_stage_log_count = 0; static std::atomic g_framebuffer_skip_log_count = 0; static float ConvertPolygonOffsetConstantFactor(float guest_factor, const HW::PolyOffset& offset, vk::Format host_depth_format) { if (offset.db_is_float_fmt) { return guest_factor; } int host_depth_bits = 0; switch (host_depth_format) { case vk::Format::eD16Unorm: case vk::Format::eD16UnormS8Uint: host_depth_bits = 16; break; case vk::Format::eD24UnormS8Uint: host_depth_bits = 24; break; default: // A fixed-point guest bias cannot be represented exactly by a floating-point host // attachment without VK_EXT_depth_bias_control. return guest_factor; } return std::ldexp(guest_factor, host_depth_bits + offset.neg_num_db_bits); } static const char* RenderColorTypeName(RenderColorType type) { switch (type) { case RenderColorType::NoColorOutput: return "NoColorOutput"; case RenderColorType::RenderTexture: return "RenderTexture"; default: return "Unknown"; } } static void LogFramebufferSkip(const char* draw_name, const RenderColorInfo& color, const RenderDepthInfo& depth, const CommandBuffer& buffer, uint32_t index_count, uint32_t flags) { const auto& ctx = buffer.GetRegisters(); const auto& ucfg = buffer.GetUserConfig(); if (!graphics_debug_dump_enabled()) { return; } auto log_id = g_framebuffer_skip_log_count.fetch_add(1, std::memory_order_relaxed); if (log_id >= 128) { return; } LOGF( "DrawFramebufferSkip[%u]: %s color=%s color_addr=0x%010" PRIx64 " color_size=0x%016" PRIx64 " color_image=%s depth_format=%s depth_image=%s depth_vaddr_num=%d target_mask=0x%08" PRIx32 " prim=%u index_count=%u flags=0x%08" PRIx32 "\n", log_id, draw_name, RenderColorTypeName(color.type), color.base_addr, color.buffer_size, color.image_id ? "yes" : "no", VulkanToString(depth.format).c_str(), depth.image_id ? "yes" : "no", depth.vaddr_num, ctx.GetRenderTargetMask(), static_cast(ucfg.GetPrimType()), index_count, flags); } static void LogMrtState(const char* draw_name, const CommandBuffer& buffer, const ShaderPixelInputInfo& ps_input_info) { const auto& ctx = buffer.GetRegisters(); const auto& sh_regs = ctx.GetShaderRegisters(); const auto rt_mask = ctx.GetRenderTargetMask(); const auto cb_shader_mask = sh_regs.m_cbShaderMask; const auto& bc0 = ctx.GetBlendControl(0); auto log_id = g_mrt_state_log_count.fetch_add(1); if (log_id >= 32) { return; } LOGF("MrtState[%u]: %s rt_mask=0x%08" PRIx32 " cb_shader_mask=0x%08" PRIx32 " blend0=%s src=%u dst=%u alpha_src=%u alpha_dst=%u sep_alpha=%s\n", log_id, draw_name, rt_mask, cb_shader_mask, bc0.enable ? "true" : "false", bc0.color_srcblend, bc0.color_destblend, bc0.alpha_srcblend, bc0.alpha_destblend, bc0.separate_alpha_blend ? "true" : "false"); for (uint32_t i = 0; i < 8; i++) { const auto& rt = ctx.GetRenderTarget(i); const auto& bc = ctx.GetBlendControl(i); const auto ctm = (rt_mask >> (i * 4u)) & 0x0fu; const auto csm = (cb_shader_mask >> (i * 4u)) & 0x0fu; if (rt.base.addr == 0 && ps_input_info.target_output_mode[i] == 0 && ctm == 0 && csm == 0 && !bc.enable) { continue; } LOGF("MrtState[%u]: slot=%u addr=0x%010" PRIx64 " target_mask=0x%x shader_mask=0x%x out_mode=%u" " fmt=0x%08" PRIx32 " nfmt=0x%08" PRIx32 " order=0x%08" PRIx32 " width=%u height=%u tile=%u" " blend=%s src=%u dst=%u alpha_src=%u alpha_dst=%u\n", log_id, i, rt.base.addr, ctm, csm, ps_input_info.target_output_mode[i], static_cast(rt.info.format), static_cast(rt.info.channel_type), static_cast(rt.info.channel_order), rt.attrib2.width + 1, rt.attrib2.height + 1, static_cast(rt.attrib3.tile_mode), bc.enable ? "true" : "false", bc.color_srcblend, bc.color_destblend, bc.alpha_srcblend, bc.alpha_destblend); } } static void LogDrawTargetState(const char* draw_name, const RenderColorInfo& color, const RenderDepthInfo& depth, const CommandBuffer& buffer, const ShaderPixelInputInfo& ps_input_info, uint32_t index_count, uint32_t flags) { const auto& ctx = buffer.GetRegisters(); const auto& ucfg = buffer.GetUserConfig(); if (color.type == RenderColorType::NoColorOutput) { return; } auto log_id = g_draw_state_log_count.fetch_add(1); if (log_id >= 192) { return; } const auto& cc = ctx.GetColorControl(); const auto& bc = ctx.GetBlendControl(color.target_slot); const auto& dc = ctx.GetDepthControl(); const auto& vp = ctx.GetScreenViewport(); const auto& vp0 = vp.viewports[0]; const auto& ps_resources = ps_input_info.stage.program->info; const auto sampled_images = std::count_if( ps_resources.images.begin(), ps_resources.images.end(), [](const auto& image) { return image.resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled; }); vk::Extent2D extent = color.image_id ? color.extent : vk::Extent2D {}; auto sc = calc_final_scissor(vp, ctx.GetScanModeControl(), extent); LOGF( "DrawTargetState[%u]: frame=%d %s target=%s addr=0x%010" PRIx64 " extent=%ux%u prim=%u index_count=%u flags=0x%08" PRIx32 " color_mask=0x%08" PRIx32 " clear=%s clear_rgba=(%.3f,%.3f,%.3f,%.3f) cc_mode=%u cc_op=0x%02x" " blend=%s src=%u dst=%u comb=%u ps_tex=%d sampled=%d storage=%d ps_kill=%s target_mode0=%u" " depth_test=%s depth_write=%s depth_func=%u depth_clear=%s viewport=(%.1f,%.1f %.1fx%.1f) " "scissor=(%d,%d)-(%d,%d)\n", log_id, buffer.GetContext().GetGpu().GetFrameNum(), draw_name, RenderColorTypeName(color.type), color.base_addr, extent.width, extent.height, static_cast(ucfg.GetPrimType()), index_count, flags, ctx.GetRenderTargetMask(), color.color_clear_enable ? "true" : "false", color.color_clear_value.float32[0], color.color_clear_value.float32[1], color.color_clear_value.float32[2], color.color_clear_value.float32[3], cc.mode, cc.op, bc.enable ? "true" : "false", bc.color_srcblend, bc.color_destblend, bc.color_comb_fcn, static_cast(ps_resources.images.size()), static_cast(sampled_images), static_cast(ps_resources.images.size() - sampled_images), ps_input_info.ps_pixel_kill_enable ? "true" : "false", ps_input_info.target_output_mode[0], dc.z_enable ? "true" : "false", dc.z_write_enable ? "true" : "false", dc.zfunc, depth.depth_clear_enable ? "true" : "false", vp0.xoffset - vp0.xscale, vp0.yoffset - vp0.yscale, vp0.xscale * 2.0f, vp0.yscale * 2.0f, sc.left, sc.top, sc.right, sc.bottom); LogMrtState(draw_name, buffer, ps_input_info); } static void LogDrawInputState(const CommandBuffer& buffer, const RenderColorInfo& color, const ShaderVertexInputInfo& vs_input_info, uint32_t index_type_and_size, uint32_t index_count, const void* index_addr) { auto log_id = g_draw_input_log_count.fetch_add(1); if (log_id >= 64) { return; } LOGF("DrawInputState[%u]: frame=%d target=%s addr=0x%010" PRIx64 " index_type=%u index_count=%u index_addr=0x%016" PRIx64 " vs_resources=%d vs_buffers=%d\n", log_id, buffer.GetContext().GetGpu().GetFrameNum(), RenderColorTypeName(color.type), color.base_addr, index_type_and_size, index_count, reinterpret_cast(index_addr), vs_input_info.resources_num, vs_input_info.buffers_num); for (int bi = 0; bi < vs_input_info.buffers_num; bi++) { const auto& b = vs_input_info.buffers[bi]; LOGF("DrawInputState[%u]: vb[%d] addr=0x%010" PRIx64 " stride=%u records=%u fetch_index=%u attr_num=%d\n", log_id, bi, b.addr, b.stride, b.num_records, b.fetch_index, b.attr_num); const auto* bytes = reinterpret_cast(b.addr); if (bytes != nullptr && b.stride != 0) { const uint32_t records = std::min(b.num_records, 4u); for (uint32_t rec = 0; rec < records; rec++) { const auto* rec_bytes = bytes + static_cast(rec) * b.stride; const auto dword_num = std::min(b.stride / 4u, 12u); uint32_t raw[12] = {}; float flt[12] = {}; for (uint32_t i = 0; i < dword_num; i++) { std::memcpy(&raw[i], rec_bytes + i * 4u, sizeof(raw[i])); std::memcpy(&flt[i], rec_bytes + i * 4u, sizeof(flt[i])); } LOGF("DrawInputState[%u]: vb[%d].rec[%u] stride=%u dwords=%u raw=%08" PRIx32 " %08" PRIx32 " %08" PRIx32 " %08" PRIx32 " %08" PRIx32 " %08" PRIx32 " %08" PRIx32 " %08" PRIx32 " %08" PRIx32 " f=(%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f)\n", log_id, bi, rec, b.stride, dword_num, raw[0], raw[1], raw[2], raw[3], raw[4], raw[5], raw[6], raw[7], raw[8], flt[0], flt[1], flt[2], flt[3], flt[4], flt[5], flt[6], flt[7], flt[8]); for (int ai = 0; ai < b.attr_num; ai++) { const auto res_index = b.attr_indices[ai]; const auto& r = vs_input_info.resources[res_index]; const auto& rd = vs_input_info.resources_dst[res_index]; const auto offset = b.attr_offsets[ai]; if (offset + 4u <= b.stride && r.Format() == Prospero::BufferFormat::k8_8_8_8UNorm) { uint32_t packed = 0; std::memcpy(&packed, rec_bytes + offset, sizeof(packed)); const auto r8 = (packed >> 0u) & 0xffu; const auto g8 = (packed >> 8u) & 0xffu; const auto b8 = (packed >> 16u) & 0xffu; const auto a8 = (packed >> 24u) & 0xffu; LOGF("DrawInputState[%u]: vb[%d].rec[%u].attr[%d] dst=v%d fmt=56 " "rgba8=%02" PRIx32 "%02" PRIx32 "%02" PRIx32 "%02" PRIx32 " rgba=(%.3f,%.3f,%.3f,%.3f)\n", log_id, bi, rec, ai, rd.register_start, r8, g8, b8, a8, static_cast(r8) / 255.0, static_cast(g8) / 255.0, static_cast(b8) / 255.0, static_cast(a8) / 255.0); } } } } for (int ai = 0; ai < b.attr_num; ai++) { const auto res_index = b.attr_indices[ai]; const auto& r = vs_input_info.resources[res_index]; const auto& rd = vs_input_info.resources_dst[res_index]; LOGF("DrawInputState[%u]: attr[%d] res=%d offset=%u dst=v%d regs=%d fetch_index=%u " "sharp=%08" PRIx32 " %08" PRIx32 " %08" PRIx32 " %08" PRIx32 "\n", log_id, ai, res_index, b.attr_offsets[ai], rd.register_start, rd.registers_num, rd.fetch_index, r.fields[0], r.fields[1], r.fields[2], r.fields[3]); } } } static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuffer vk_buffer, const RenderColorInfo* colors, uint32_t color_count, const RenderDepthInfo& depth) { KYTY_PROFILER_FUNCTION(); EXIT_IF(colors == nullptr); const auto& ctx = buffer.GetRegisters(); const auto& vp = ctx.GetScreenViewport(); vk::Extent2D framebuffer_extent {}; if (color_count > 0 && colors[0].image_id) { framebuffer_extent = colors[0].extent; } else if (depth.image_id) { framebuffer_extent = {depth.width, depth.height}; } else { const auto& limits = buffer.GetGraphics().GetPhysicalDeviceProperties().limits; framebuffer_extent = {limits.maxFramebufferWidth, limits.maxFramebufferHeight}; } const auto final_scissor = calc_final_scissor(vp, ctx.GetScanModeControl(), framebuffer_extent); vk::Viewport viewport {}; if (ctx.GetClipControl().clip_disable) { const auto& limits = buffer.GetGraphics().GetPhysicalDeviceProperties().limits; viewport.x = 0.0f; viewport.y = 0.0f; viewport.width = static_cast(std::min(limits.maxViewportDimensions[0], 16384u)); viewport.height = static_cast(std::min(limits.maxViewportDimensions[1], 16384u)); } else { viewport.x = vp.viewports[0].xoffset - vp.viewports[0].xscale; viewport.y = vp.viewports[0].yoffset - vp.viewports[0].yscale; viewport.width = vp.viewports[0].xscale * 2.0f; viewport.height = vp.viewports[0].yscale * 2.0f; } viewport.minDepth = vp.viewports[0].zoffset; viewport.maxDepth = vp.viewports[0].zscale + vp.viewports[0].zoffset; vk_buffer.setViewport(0, 1, &viewport); vk::Rect2D scissor {}; scissor.offset = {final_scissor.left, final_scissor.top}; scissor.extent = {static_cast(final_scissor.right - final_scissor.left), static_cast(final_scissor.bottom - final_scissor.top)}; vk_buffer.setScissor(0, 1, &scissor); float line_width = ctx.GetLineWidth(); if (line_width != 1.0f) { static bool logged = false; if (!logged) { LOGF("Render: temporary: clamping Vulkan line width %f to 1.0 because wideLines is " "not enabled\n", line_width); logged = true; } line_width = 1.0f; } vk_buffer.setLineWidth(line_width); const auto& blend = ctx.GetBlendColor(); const std::array blend_constants {blend.red, blend.green, blend.blue, blend.alpha}; vk_buffer.setBlendConstants(blend_constants.data()); vk_buffer.setDepthTestEnable(depth.depth_test_enable ? VK_TRUE : VK_FALSE); vk_buffer.setDepthWriteEnable( depth.depth_write_enable && !depth.depth_clear_enable ? VK_TRUE : VK_FALSE); vk_buffer.setDepthCompareOp(depth.depth_compare_op); const auto& mode = ctx.GetModeControl(); const auto& poly_offset = ctx.GetPolyOffset(); const bool use_front = mode.poly_offset_front_enable && !mode.cull_front; const bool use_back = mode.poly_offset_back_enable && !mode.cull_back; const bool depth_bias_enable = use_front || use_back; vk_buffer.setDepthBiasEnable(depth_bias_enable ? VK_TRUE : VK_FALSE); if (depth_bias_enable) { // Vulkan has one bias for both faces. Prefer a visible front face when both are enabled. const float guest_constant_factor = use_front ? poly_offset.front_offset : poly_offset.back_offset; const float constant_factor = ConvertPolygonOffsetConstantFactor(guest_constant_factor, poly_offset, depth.format); const float slope_factor = (use_front ? poly_offset.front_scale : poly_offset.back_scale) / 16.0f; vk_buffer.setDepthBias(constant_factor, poly_offset.clamp, slope_factor); } if (depth.stencil_test_enable) { vk_buffer.setStencilCompareMask(vk::StencilFaceFlagBits::eFront, depth.stencil_dynamic_front.compareMask); vk_buffer.setStencilCompareMask(vk::StencilFaceFlagBits::eBack, depth.stencil_dynamic_back.compareMask); vk_buffer.setStencilWriteMask(vk::StencilFaceFlagBits::eFront, depth.stencil_dynamic_front.writeMask); vk_buffer.setStencilWriteMask(vk::StencilFaceFlagBits::eBack, depth.stencil_dynamic_back.writeMask); vk_buffer.setStencilReference(vk::StencilFaceFlagBits::eFront, depth.stencil_dynamic_front.reference); vk_buffer.setStencilReference(vk::StencilFaceFlagBits::eBack, depth.stencil_dynamic_back.reference); } #if defined(__APPLE__) // MoltenVK has no VK_EXT_color_write_enable; the pipeline is created without the // eColorWriteEnableEXT dynamic state and relies on the static colorWriteMask instead. #else vk::Bool32 enable[RENDER_COLOR_ATTACHMENTS_MAX] = {}; // Color-control operation selects special color-buffer paths, not the normal component write // mask. Attachment availability therefore follows the target write mask. for (uint32_t i = 0; i < color_count; i++) { enable[i] = render_target_mask_slot(ctx.GetRenderTargetMask(), colors[i].target_slot) != 0 ? VK_TRUE : VK_FALSE; } if (color_count != 0) { vk_buffer.setColorWriteEnableEXT(color_count, enable); } #endif } static bool DrawHasValidVertexShader(const HW::Shader& sh_ctx) { const auto& vs = sh_ctx.GetVs(); return ShaderAddressValid(vs.es_regs.data_addr); } static bool PixelShaderHasDepthOrCoverageSideEffects(const HW::ShaderRegisters& sh_regs) { const auto& db = sh_regs.db_shader_control; return sh_regs.shader_z_format != 0 || db.shader_kill_enable || db.shader_z_export_enable || db.shader_mask_export_enable || db.shader_dual_export_enable || db.shader_execute_on_noop; } static bool ShouldSkipGeShader(const CommandBuffer& buffer) { return false; const auto& ctx = buffer.GetRegisters(); const auto& ucfg = buffer.GetUserConfig(); const auto& sh_ctx = buffer.GetShaders(); const auto& sh_regs = ctx.GetShaderRegisters(); const auto& ge_cntl = ucfg.GetGeControl(); const auto& vertex_info = sh_ctx.GetVs(); const auto stages = ctx.GetShaderStages(); const auto is_known_gs_out_prim_type = [](uint32_t value) { switch (static_cast(value)) { case Prospero::GsOutputPrimitiveType::kPoints: case Prospero::GsOutputPrimitiveType::kLines: case Prospero::GsOutputPrimitiveType::kTriangles: case Prospero::GsOutputPrimitiveType::k2dRectangle: case Prospero::GsOutputPrimitiveType::kRectList: return true; } return false; }; const bool ps5_ngg_vertex_path = stages == 0x02002000 && vertex_info.es_regs.data_addr != 0 && sh_regs.m_vgtGsMaxVertOut == 0x00000000 && is_known_gs_out_prim_type(sh_regs.m_vgtGsOutPrimType); const bool unsupported_stage_mask = (stages != 0 && stages != 0x02002000); const bool unsupported_gs_stage = (vertex_info.es_regs.data_addr != 0 && vertex_info.gs_regs.data_addr != 0 && !ps5_ngg_vertex_path); // GE_CNTL group sizes control guest scheduling and do not constrain the host vertex path. const bool ge_shader_regs = (sh_regs.m_geNggSubgrpCntl != 0x00000000 && sh_regs.m_geNggSubgrpCntl != 0x00000001) || sh_regs.m_vgtGsMaxVertOut != 0x00000000 || !is_known_gs_out_prim_type(sh_regs.m_vgtGsOutPrimType) || sh_regs.m_geMaxOutputPerSubgroup > 0x00000040; if (unsupported_stage_mask || unsupported_gs_stage || ge_shader_regs) { static std::once_flag warning_once; std::call_once(warning_once, [] { std::printf("Warning: game uses unsupported graphics pipelines; some draw calls were " "skipped.\n"); }); const auto log_id = g_shader_stage_log_count.fetch_add(1); if (log_id < 32) { LOGF("Skipping unsupported GE shader draw: stages=0x%08" PRIx32 " prim_group=0x%04" PRIx16 " vert_group=0x%04" PRIx16 " ngg=0x%08" PRIx32 " max_out=0x%08" PRIx32 " gs_max_vert=0x%08" PRIx32 " gs_out_prim=0x%08" PRIx32 " es=0x%016" PRIx64 " gs=0x%016" PRIx64 "\n", stages, ge_cntl.primitive_group_size, ge_cntl.vertex_group_size, sh_regs.m_geNggSubgrpCntl, sh_regs.m_geMaxOutputPerSubgroup, sh_regs.m_vgtGsMaxVertOut, sh_regs.m_vgtGsOutPrimType, vertex_info.es_regs.data_addr, vertex_info.gs_regs.data_addr); } return true; } return false; } struct DrawRenderState { RenderDepthInfo depth_info; RenderColorInfo color_info[RENDER_COLOR_ATTACHMENTS_MAX] = {}; uint32_t color_count = 0; bool ps_active = true; RenderState rendering; ShaderVertexInputInfo vs_input_info; ShaderPixelInputInfo ps_input_info; PipelineCache::GraphicsPrograms programs; }; struct DrawCallInfo { const char* name = nullptr; CommandBufferDebugOp debug_op = CommandBufferDebugOp::DrawIndex; uint32_t index_count = 0; uint32_t instance_count = 0; uint32_t first_instance = 0; }; static bool ResolveDccAttachmentClear(TextureCache& cache, const RenderColorInfo& target, const ImageViewInfo& view, vk::ClearColorValue& clear_value) { if (target.desc.info.metadata.kind != ImageMetadataKind::Dcc) { return false; } // A DCC fast clear may update metadata only and leave the color allocation stale. Vulkan has // no guest DCC state, so materialize the deferred value when the surface is next bound. const auto metadata_address = target.desc.info.metadata.range.address; uint32_t metadata_value = 0xffffffffu; if (!cache.IsMetaCleared(metadata_address, view.base_layer, &metadata_value)) { return false; } // Translate recognized constant and register-backed DCC states into a host clear value. clear_value = {}; switch (static_cast(metadata_value)) { case 0x00: break; case 0x20: if (!target.metadata_clear_supported) { return false; } clear_value = target.color_clear_value; break; case 0x40: if (!target.metadata_fixed_clear_supported) { return false; } clear_value.float32[3] = 1.0f; break; case 0x80: if (!target.metadata_fixed_clear_supported) { return false; } clear_value.float32[0] = 1.0f; clear_value.float32[1] = 1.0f; clear_value.float32[2] = 1.0f; break; case 0xc0: if (!target.metadata_fixed_clear_supported) { return false; } clear_value.float32[0] = 1.0f; clear_value.float32[1] = 1.0f; clear_value.float32[2] = 1.0f; clear_value.float32[3] = 1.0f; break; default: return false; } for (uint32_t layer = 1; layer < view.layer_count; layer++) { if (!cache.IsMetaCleared(metadata_address, view.base_layer + layer)) { return false; } } // Consume only after every layer in this Vulkan view can be materialized together. for (uint32_t layer = 0; layer < view.layer_count; layer++) { if (!cache.TouchMeta(metadata_address, view.base_layer + layer, false)) { EXIT("failed to consume DCC clear state\n"); } } return true; } RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderColorInfo* colors, uint32_t color_count, RenderDepthInfo& depth) { EXIT_IF(colors == nullptr || color_count > RENDER_COLOR_ATTACHMENTS_MAX); auto& cache = m_context.GetTextureCache(); RenderState state {}; state.width = std::numeric_limits::max(); state.height = std::numeric_limits::max(); state.num_layers = std::numeric_limits::max(); state.num_color_attachments = color_count; uint32_t attachment_samples = 0; for (uint32_t i = 0; i < color_count; i++) { auto& target = colors[i]; EXIT_IF(!target.image_id); const auto old_image = cache.m_slot_images.try_get(target.image_id); if (old_image == nullptr || (!old_image->registered && !old_image->info.data.Empty()) || old_image->binding.needs_rebind) { if (old_image != nullptr) { old_image->binding = {}; } target.image_id = cache.FindImage(target.desc); BindRenderTarget(target.image_id); } target.image_view = cache.FindRenderTarget(target.image_id, target.desc); auto& image = cache.GetImage(target.image_id); SetVulkanObjectNameF(m_context.GetGraphics().device, image.backing.image, "Kyty.MRT{}.Image[guest=0x{:016x} size=0x{:x} format={}]", target.target_slot, image.info.data.address, image.info.data.size, static_cast(image.info.pixel_format)); SetVulkanObjectNameF(m_context.GetGraphics().device, target.image_view, "Kyty.MRT{}.View[guest=0x{:016x} mip={} layer={}+{}]", target.target_slot, image.info.data.address, target.desc.view_info.base_level, target.desc.view_info.base_layer, target.desc.view_info.layer_count); EXIT_IF(image.backing.samples != target.samples || target.image_view == nullptr); if (attachment_samples == 0) { attachment_samples = target.samples; } else if (attachment_samples != target.samples) { EXIT("mixed color attachment sample counts are unsupported: %u and %u\n", attachment_samples, target.samples); } const auto& view = target.desc.view_info; const auto layout = image.binding.is_bound ? vk::ImageLayout::eGeneral : vk::ImageLayout::eColorAttachmentOptimal; image.Transit(layout, vk::AccessFlagBits2::eColorAttachmentRead | vk::AccessFlagBits2::eColorAttachmentWrite, ImageSubresourceRange {view.base_level, view.level_count, view.base_layer, view.layer_count}, buffer.Handle()); state.width = std::min(state.width, target.extent.width); state.height = std::min(state.height, target.extent.height); state.num_layers = std::min(state.num_layers, view.layer_count); auto& attachment = state.color_attachments[i]; attachment.image_view = target.image_view; attachment.image_layout = layout; attachment.clear_value = target.color_clear_value.uint32; vk::ClearColorValue metadata_clear_value {}; const bool metadata_clear = ResolveDccAttachmentClear(cache, target, view, metadata_clear_value); if (metadata_clear) { attachment.clear_value = metadata_clear_value.uint32; } attachment.is_clear = target.color_clear_enable || metadata_clear; } if (depth.image_id) { const auto owner = cache.m_slot_images.try_get(depth.image_id); if (owner == nullptr || !owner->registered || owner->binding.needs_rebind) { EXIT("depth target changed after render-state discovery\n"); } depth.image_view = cache.FindDepthTarget(depth.image_id, depth.desc); if (depth.htile && depth.depth_clear_enable && !cache.ClearMeta(depth.htile_buffer_vaddr)) { EXIT("failed to acquire HTile metadata for a depth clear\n"); } depth.depth_meta_clear_enable = depth.htile && cache.IsMetaCleared(depth.htile_buffer_vaddr, depth.desc.view_info.base_layer); depth.depth_load_clear_enable = depth.depth_clear_enable || depth.depth_meta_clear_enable; if (depth.depth_meta_clear_enable && !cache.TouchMeta(depth.htile_buffer_vaddr, depth.desc.view_info.base_layer, false)) { EXIT("failed to consume HTile clear state\n"); } auto& image = cache.GetImage(depth.image_id); SetVulkanObjectNameF(m_context.GetGraphics().device, image.backing.image, "Kyty.DepthTarget.Image[guest=0x{:016x} size=0x{:x} format={}]", image.info.data.address, image.info.data.size, static_cast(image.info.pixel_format)); SetVulkanObjectNameF(m_context.GetGraphics().device, depth.image_view, "Kyty.DepthTarget.View[guest=0x{:016x} layer={}+{}]", image.info.data.address, depth.desc.view_info.base_layer, depth.desc.view_info.layer_count); EXIT_IF(depth.image_view == nullptr || image.backing.samples != depth.samples); if (attachment_samples == 0) { attachment_samples = depth.samples; } else if (attachment_samples != depth.samples) { EXIT("mixed color/depth sample counts are unsupported: %u and %u\n", attachment_samples, depth.samples); } const auto layout = depth_attachment_layout(depth); const auto writes = depth.AttachmentWriteAspects(); auto access = vk::AccessFlags2 {vk::AccessFlagBits2::eDepthStencilAttachmentRead}; if (writes) { access |= vk::AccessFlagBits2::eDepthStencilAttachmentWrite; } const auto& view = depth.desc.view_info; image.Transit(layout, access, ImageSubresourceRange {view.base_level, view.level_count, view.base_layer, view.layer_count}, buffer.Handle()); state.width = std::min(state.width, depth.width); state.height = std::min(state.height, depth.height); state.num_layers = std::min(state.num_layers, view.layer_count); const auto aspects = ImageViewOps::DepthAspectMask(depth.format); auto& attachment = state.depth_stencil_attachment; attachment.image_view = depth.image_view; attachment.image_layout = layout; attachment.clear_value[0] = std::bit_cast(depth.depth_clear_value); attachment.clear_value[1] = depth.stencil_clear_value; attachment.has_depth = static_cast(aspects & vk::ImageAspectFlagBits::eDepth); attachment.depth_clear = depth.depth_load_clear_enable; attachment.has_stencil = static_cast(aspects & vk::ImageAspectFlagBits::eStencil); attachment.stencil_clear = depth.stencil_clear_enable; } if (color_count == 0 && !depth.image_id) { const auto& limits = buffer.GetGraphics().GetPhysicalDeviceProperties().limits; state.width = limits.maxFramebufferWidth; state.height = limits.maxFramebufferHeight; } else if (attachment_samples == 0 || vulkan_sample_count(attachment_samples) == vk::SampleCountFlagBits {}) { EXIT("render state has no valid attachments\n"); } if (state.num_layers == std::numeric_limits::max()) { state.num_layers = 1; } EXIT_IF(state.width == 0 || state.height == 0 || state.num_layers == 0 || state.width == std::numeric_limits::max() || state.height == std::numeric_limits::max()); return state; } static bool DrawHasActivePixelShader(const CommandBuffer& buffer) { const auto& ctx = buffer.GetRegisters(); const auto& sh_regs = ctx.GetShaderRegisters(); const bool has_color_output = (ctx.GetRenderTargetMask() & sh_regs.m_cbShaderMask) != 0; return ShaderAddressValid(buffer.GetShaders().GetPs().ps_regs.data_addr) && (has_color_output || PixelShaderHasDepthOrCoverageSideEffects(sh_regs)); } enum class CbColorMode : uint8_t { Disable = 0, Normal = 1, EliminateFastClear = 2, Resolve = 3, FmaskDecompress = 5, DccDecompress = 6, }; static bool ConsumeMetadataColorOperation(const CommandBuffer& buffer) { const auto& ctx = buffer.GetRegisters(); const auto mode = ctx.GetColorControl().mode; // These special modes run color-buffer metadata or decompression operations. The shader is a // vehicle for that operation, and its exported color must not be applied as a normal draw. // Kyty stores expanded Vulkan images rather than compressed guest surfaces, so no equivalent // hardware pass is emitted. Tracked DCC clear state is materialized on attachment bind; // future CMask/FMask support can consume its state through the same TextureCache path. return mode == static_cast(CbColorMode::EliminateFastClear) || mode == static_cast(CbColorMode::FmaskDecompress) || mode == static_cast(CbColorMode::DccDecompress); } struct DrawEmitInfo { bool indexed = false; int32_t vertex_offset = 0; uint32_t first_vertex = 0; uint32_t first_instance = 0; }; struct DrawIndexBufferSource { bool enabled = false; uint64_t address = 0; const void* host_data = nullptr; uint64_t size = 0; vk::IndexType type = vk::IndexType::eUint16; }; struct PreparedIndexBuffer { vk::Buffer buffer = nullptr; uint64_t size = 0; vk::DeviceSize offset = 0; vk::IndexType type = vk::IndexType::eUint16; }; static uint64_t VertexBufferDescriptorSize(const ShaderVertexInputBuffer& buffer) { return (buffer.stride != 0 ? static_cast(buffer.stride) * buffer.num_records : buffer.num_records); } struct VertexBufferRange { uint64_t base_address = 0; uint64_t requested_end = 0; uint64_t acquired_end = 0; std::pair binding; [[nodiscard]] uint64_t RequestedSize() const { return requested_end - base_address; } }; struct PreparedVertexBuffers { static constexpr uint32_t MaxBuffers = ShaderVertexInputInfo::RES_MAX; std::array buffers {}; std::array offsets {}; uint32_t count = 0; }; static PreparedVertexBuffers AcquireVertexBuffers(CommandBuffer& buffer, const ShaderVertexInputInfo& vs_input_info) { EXIT_IF(vs_input_info.buffers_num < 0 || vs_input_info.buffers_num > ShaderVertexInputInfo::RES_MAX); // Collect the non-empty guest vertex ranges. std::array ranges {}; uint32_t range_count = 0; for (int i = 0; i < vs_input_info.buffers_num; i++) { const auto& vertex = vs_input_info.buffers[i]; const auto size = VertexBufferDescriptorSize(vertex); if (size == 0) { continue; } if (vertex.addr == 0 || size > UINT64_MAX - vertex.addr) { EXIT("invalid vertex buffer range: addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", vertex.addr, size); } ranges[range_count++] = {vertex.addr, vertex.addr + size}; } std::sort(ranges.begin(), ranges.begin() + range_count, [](const VertexBufferRange& left, const VertexBufferRange& right) { return left.base_address < right.base_address; }); // Merge overlapping or touching ranges before acquiring host buffers. std::array merged_ranges {}; uint32_t merged_count = 0; for (uint32_t i = 0; i < range_count; i++) { const auto& range = ranges[i]; if (merged_count != 0 && merged_ranges[merged_count - 1].requested_end >= range.base_address) { merged_ranges[merged_count - 1].requested_end = std::max(merged_ranges[merged_count - 1].requested_end, range.requested_end); continue; } merged_ranges[merged_count++] = {range.base_address, range.requested_end}; } auto& cache = buffer.GetContext().GetBufferCache(); for (uint32_t i = 0; i < merged_count; i++) { auto& range = merged_ranges[i]; // PPSA20298 const auto size = Libs::LibKernel::Memory::ClampRangeSize(range.base_address, range.RequestedSize()); range.acquired_end = range.base_address + size; range.binding = cache.ObtainBuffer(range.base_address, size, false); SetVulkanObjectNameF( buffer.GetContext().GetGraphics().device, range.binding.first->Handle(), "Kyty.VertexBufferRange[guest=0x{:016x} size=0x{:x}]", range.base_address, size); } // Rebuild slot bindings, offsetting non-empty slots into their acquired merged range. PreparedVertexBuffers prepared; prepared.count = static_cast(vs_input_info.buffers_num); vk::Buffer null_buffer = nullptr; for (int i = 0; i < vs_input_info.buffers_num; i++) { const auto& vertex = vs_input_info.buffers[i]; const auto size = VertexBufferDescriptorSize(vertex); if (size == 0) { if (null_buffer == nullptr) { null_buffer = cache.GetBuffer(NULL_BUFFER_ID).Handle(); } prepared.buffers[i] = null_buffer; prepared.offsets[i] = 0; continue; } const auto range = std::find_if(merged_ranges.begin(), merged_ranges.begin() + merged_count, [&](const VertexBufferRange& value) { return vertex.addr >= value.base_address && vertex.addr < value.acquired_end; }); if (range == merged_ranges.begin() + merged_count) { EXIT("vertex buffer address is outside the acquired range: addr=0x%016" PRIx64 "\n", vertex.addr); } prepared.buffers[i] = range->binding.first->Handle(); prepared.offsets[i] = range->binding.second + vertex.addr - range->base_address; SetVulkanObjectNameF( buffer.GetContext().GetGraphics().device, prepared.buffers[i], "Kyty.VertexBuffer[slot={} guest=0x{:016x} size=0x{:x} stride={} records={}]", i, vertex.addr, size, vertex.stride, vertex.num_records); } return prepared; } static void SetDrawDebugPhase(CommandBuffer& buffer, uint64_t submit_id, const DrawCallInfo& draw, uint32_t phase) { EXIT_IF(draw.name == nullptr); buffer.SetDebugInfo(static_cast(draw.debug_op), submit_id, phase, draw.index_count, 0, draw.instance_count, draw.first_instance); } static bool GetDrawTopology(const HW::UserConfig& ucfg, bool auto_draw, vk::PrimitiveTopology& topology) { topology = vk::PrimitiveTopology::ePointList; switch (ucfg.GetPrimType()) { case Prospero::PrimitiveType::kNone: return false; case Prospero::PrimitiveType::kPointList: topology = vk::PrimitiveTopology::ePointList; break; case Prospero::PrimitiveType::kLineList: topology = vk::PrimitiveTopology::eLineList; break; case Prospero::PrimitiveType::kLineStrip: topology = vk::PrimitiveTopology::eLineStrip; break; case Prospero::PrimitiveType::kTriList: topology = vk::PrimitiveTopology::eTriangleList; break; case Prospero::PrimitiveType::kTriFan: topology = vk::PrimitiveTopology::eTriangleFan; break; case Prospero::PrimitiveType::kTriStrip: topology = vk::PrimitiveTopology::eTriangleStrip; break; case Prospero::PrimitiveType::kRectList: topology = vk::PrimitiveTopology::ePatchList; break; case Prospero::PrimitiveType::kRectListLegacy: if (!auto_draw) { EXIT("unknown primitive type: %u\n", static_cast(ucfg.GetPrimType())); } topology = vk::PrimitiveTopology::eTriangleStrip; break; case Prospero::PrimitiveType::kQuadListLegacy: topology = vk::PrimitiveTopology::eTriangleFan; break; default: EXIT("unknown primitive type: %u\n", static_cast(ucfg.GetPrimType())); } return true; } static bool ResolvePrimitiveRestart(const CommandBuffer& buffer, vk::PrimitiveTopology topology, uint32_t index_type_and_size) { const auto control = buffer.GetUserConfig().GetPrimitiveResetControl(); EXIT_NOT_IMPLEMENTED((control & ~0x3u) != 0); if ((control & 0x1u) == 0) { return false; } switch (buffer.GetUserConfig().GetPrimType()) { case Prospero::PrimitiveType::kLineStrip: case Prospero::PrimitiveType::kTriFan: case Prospero::PrimitiveType::kTriStrip: break; default: return false; } if (topology != vk::PrimitiveTopology::eLineStrip && topology != vk::PrimitiveTopology::eTriangleStrip && topology != vk::PrimitiveTopology::eTriangleFan) { return false; } uint32_t index_mask = 0; switch (static_cast(index_type_and_size)) { case Prospero::IndexType::kIndex8: index_mask = 0xffu; break; case Prospero::IndexType::kIndex16: index_mask = 0xffffu; break; case Prospero::IndexType::kIndex32: index_mask = 0xffffffffu; break; default: EXIT("unknown index_type_and_size: %u\n", index_type_and_size); } const auto reset_index = buffer.GetRegisters().GetPrimitiveResetIndex(); if ((control & 0x2u) != 0 && (reset_index & ~index_mask) != 0) { return false; } EXIT_NOT_IMPLEMENTED((reset_index & index_mask) != index_mask); return true; } bool RenderExecutor::PrepareDrawRenderState(uint64_t submit_id, CommandBuffer& buffer, const DrawCallInfo& draw, uint32_t render_target_slice_offset, bool log_setup_phases, DrawRenderState& state) { EXIT_IF(draw.name == nullptr); auto& ctx = buffer.GetRegisters(); if (ResolveColorTargets(submit_id, buffer, render_target_slice_offset)) { return false; } if (log_setup_phases) { LogDrawPhase(draw.name, "ResolveRenderColorTarget"); } for (uint32_t slot = 0; slot < RENDER_COLOR_ATTACHMENTS_MAX; slot++) { if (slot == 0 || (render_target_mask_slot(ctx.GetRenderTargetMask(), slot) != 0 && ctx.GetRenderTarget(slot).base.addr != 0)) { ResolveRenderColorTarget(submit_id, buffer, state.color_info[state.color_count], render_target_slice_offset, slot); if (state.color_info[state.color_count].image_id) { state.color_count++; } } } if (log_setup_phases) { LogDrawPhase(draw.name, "ResolveRenderDepthTarget"); } ResolveRenderDepthTarget(submit_id, buffer, state.depth_info); state.ps_active = DrawHasActivePixelShader(buffer); const bool with_depth = (state.depth_info.format != vk::Format::eUndefined && static_cast(state.depth_info.image_id)); if (state.color_count == 0 && !with_depth && !state.ps_active) { LogFramebufferSkip(draw.name, state.color_info[0], state.depth_info, buffer, draw.index_count, 0); return false; } return true; } static void RefreshShaders(CommandBuffer& buffer, const DrawCallInfo& draw, bool log_phases, DrawRenderState& state) { EXIT_IF(draw.name == nullptr); auto& ctx = buffer.GetRegisters(); auto& sh_ctx = buffer.GetShaders(); const auto& vertex_shader_info = sh_ctx.GetVs(); const auto& pixel_shader_info = sh_ctx.GetPs(); const auto& shader_regs = ctx.GetShaderRegisters(); state.programs = {}; state.ps_input_info = {}; std::array target_export_mapping {}; for (uint32_t i = 0; i < state.color_count; i++) { target_export_mapping[state.color_info[i].target_slot] = state.color_info[i].export_mapping; } auto& pipeline_cache = buffer.GetContext().GetPipelineCache(); if (log_phases) { LogDrawPhase(draw.name, "GetGraphicsPrograms"); } state.programs = pipeline_cache.GetGraphicsPrograms( vertex_shader_info, pixel_shader_info, shader_regs, ctx, target_export_mapping, state.ps_active, state.vs_input_info, state.ps_input_info); } static PreparedVertexBuffers PrepareVertexBuffers(uint64_t submit_id, CommandBuffer& buffer, const DrawCallInfo& draw, const ShaderVertexInputInfo& vs_input_info) { EXIT_IF(draw.name == nullptr); (void)submit_id; LogDrawPhase(draw.name, "PrepareVertexBuffers"); return AcquireVertexBuffers(buffer, vs_input_info); } static PreparedIndexBuffer PrepareIndexBuffer(CommandBuffer& buffer, const DrawIndexBufferSource& source) { PreparedIndexBuffer prepared; if (!source.enabled) { return prepared; } EXIT_IF(source.size == 0); prepared.size = source.size; prepared.type = source.type; if (source.host_data != nullptr) { auto& stream = buffer.GetContext().GetBufferCache().GetUtilityBuffer(MemoryUsage::Stream); prepared.offset = stream.Copy(source.host_data, source.size, 16); prepared.buffer = stream.Handle(); } else { auto [buffer_ptr, offset] = buffer.GetContext().GetBufferCache().ObtainBuffer(source.address, source.size, false); prepared.buffer = buffer_ptr->Handle(); prepared.offset = offset; } if (source.host_data != nullptr) { SetVulkanObjectNameF(buffer.GetContext().GetGraphics().device, prepared.buffer, "Kyty.IndexBuffer[guest=transient size=0x{:x} type={}]", source.size, static_cast(source.type)); } else { SetVulkanObjectNameF(buffer.GetContext().GetGraphics().device, prepared.buffer, "Kyty.IndexBuffer[guest=0x{:016x} size=0x{:x} type={}]", source.address, source.size, static_cast(source.type)); } return prepared; } static void CommitVertexBuffers(vk::CommandBuffer vk_buffer, const PreparedVertexBuffers& prepared) { for (uint32_t i = 0; i < prepared.count; i++) { EXIT_IF(prepared.buffers[i] == nullptr); } if (prepared.count != 0) { vk_buffer.bindVertexBuffers(0, prepared.count, prepared.buffers.data(), prepared.offsets.data()); } } static void CommitIndexBuffer(vk::CommandBuffer vk_buffer, const PreparedIndexBuffer& prepared) { if (prepared.size == 0) { return; } EXIT_IF(prepared.buffer == nullptr); vk_buffer.bindIndexBuffer(prepared.buffer, prepared.offset, prepared.type); } static void LogDrawStateIfNeeded(const CommandBuffer& buffer, const DrawCallInfo& draw, const DrawRenderState& state, bool always_log, bool force_legacy_rect_log, uint32_t index_type_and_size, const void* index_addr) { EXIT_IF(draw.name == nullptr); if (!graphics_debug_dump_enabled()) { return; } if (!always_log && !force_legacy_rect_log) { return; } if (state.ps_active) { LogDrawTargetState(draw.name, state.color_info[0], state.depth_info, buffer, state.ps_input_info, draw.index_count, 0); } LogDrawInputState(buffer, state.color_info[0], state.vs_input_info, index_type_and_size, draw.index_count, index_addr); // LogDrawTextureState(draw.name, state.color_info[0], state.ps_input_info); } static void EmitDrawPrimitives(const HW::UserConfig& ucfg, vk::CommandBuffer vk_buffer, const ShaderVertexInputInfo& vs_input_info, const DrawCallInfo& draw, const DrawEmitInfo& emit) { EXIT_IF(draw.name == nullptr); switch (ucfg.GetPrimType()) { case Prospero::PrimitiveType::kPointList: case Prospero::PrimitiveType::kLineList: case Prospero::PrimitiveType::kLineStrip: case Prospero::PrimitiveType::kTriList: case Prospero::PrimitiveType::kTriFan: case Prospero::PrimitiveType::kTriStrip: case Prospero::PrimitiveType::kRectList: if (emit.indexed) { vk_buffer.drawIndexed(draw.index_count, draw.instance_count, 0, emit.vertex_offset, emit.first_instance); } else { vk_buffer.draw(draw.index_count, draw.instance_count, emit.first_vertex, emit.first_instance); } break; case Prospero::PrimitiveType::kRectListLegacy: if (emit.indexed) { EXIT("unknown primitive type: %u\n", static_cast(ucfg.GetPrimType())); } // Sarah EXIT_NOT_IMPLEMENTED(!(draw.index_count == 3 && vs_input_info.buffers_num == 0)); 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, emit.first_instance); } else { vk_buffer.draw(4, draw.instance_count, i + emit.first_vertex, emit.first_instance); } } break; default: EXIT("unknown primitive type: %u\n", static_cast(ucfg.GetPrimType())); } } void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buffer, const DrawCallInfo& draw, DrawRenderState& state, vk::PrimitiveTopology topology, const DrawEmitInfo& emit, const DrawIndexBufferSource& index_source, bool primitive_restart_enable, bool log_pipeline_phase, bool set_bind_debug, bool set_auto_debug) { EXIT_IF(draw.name == nullptr); auto& ucfg = buffer.GetUserConfig(); LogDrawPhase(draw.name, "PrepareBindings"); auto bindings = PrepareGraphicsBindings(state.vs_input_info.stage, state.ps_input_info.stage, state.ps_active); auto vertex_bindings = PrepareVertexBuffers(submit_id, buffer, draw, state.vs_input_info); auto index_binding = PrepareIndexBuffer(buffer, index_source); state.rendering = AcquireRenderTargets(buffer, state.color_info, state.color_count, state.depth_info); if (log_pipeline_phase) { LogDrawPhase(draw.name, "CreatePipeline"); } auto& pipeline = m_context.GetPipelineCache().CreateGraphicsPipeline( std::span {state.color_info, state.color_count}, state.depth_info, state.vs_input_info, buffer, state.ps_active ? &state.ps_input_info : nullptr, topology, primitive_restart_enable, state.programs.vertex, state.programs.pixel); // Resource preparation above may synchronously finish and restart the scheduler. From this // point onward, every operation targets the current command buffer and cannot touch guest // memory. auto vk_buffer = buffer.Handle(); if (set_bind_debug) { SetDrawDebugPhase(buffer, submit_id, draw, 0x100u); } if (set_auto_debug) { SetDrawDebugPhase(buffer, submit_id, draw, 0x200u); } CommitVertexBuffers(vk_buffer, vertex_bindings); if (bindings.pixel.has_value()) { if (set_auto_debug) { SetDrawDebugPhase(buffer, submit_id, draw, 0x300u); } } std::array descriptor_stages {&bindings.vertex, nullptr}; const size_t descriptor_stage_count = bindings.pixel.has_value() ? 2u : 1u; if (bindings.pixel) { descriptor_stages[1] = &*bindings.pixel; } CommitBindings(buffer, vk::PipelineBindPoint::eGraphics, pipeline, std::span {descriptor_stages.data(), descriptor_stage_count}); CommitIndexBuffer(vk_buffer, index_binding); SetGraphicsDynamicParams(buffer, vk_buffer, state.color_info, state.color_count, state.depth_info); LogDrawPhase(draw.name, "BeginRendering"); if (set_auto_debug) { SetDrawDebugPhase(buffer, submit_id, draw, 0x400u); } m_context.GetCommandScheduler().BeginRendering(state.rendering); vk_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline.pipeline); if (set_auto_debug) { SetDrawDebugPhase(buffer, submit_id, draw, 0x500u); } EmitDrawPrimitives(ucfg, vk_buffer, state.vs_input_info, draw, emit); if (set_auto_debug) { SetDrawDebugPhase(buffer, submit_id, draw, 0x600u); } vk::PipelineStageFlags shader_write_stages = {}; if (HasShaderBufferWrites(state.vs_input_info.stage)) { shader_write_stages |= vk::PipelineStageFlagBits::eVertexShader; } if (state.ps_active && HasShaderBufferWrites(state.ps_input_info.stage)) { shader_write_stages |= vk::PipelineStageFlagBits::eFragmentShader; } if (shader_write_stages) { m_context.GetCommandScheduler().EndRendering(); ShaderWriteBarrier(vk_buffer, shader_write_stages); } LogDrawPhase(draw.name, "DrawComplete"); if (set_auto_debug) { SetDrawDebugPhase(buffer, submit_id, draw, 0x700u); } } void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer, const DrawIndexArgs& args) { KYTY_PROFILER_FUNCTION(); EXIT_IF(buffer.IsInvalid()); EXIT_IF(args.offset_source == DrawOffsetSource::DrawState && args.first_instance != 0); m_context.GetCommandScheduler().PopPendingOperations(); auto& ucfg = buffer.GetUserConfig(); auto& sh_ctx = buffer.GetShaders(); buffer.SetDebugInfo(static_cast(CommandBufferDebugOp::DrawIndex), submit_id, args.index_count, 0, 1, args.instance_count, reinterpret_cast(args.index_addr)); Common::LockGuard lock(m_context.GetMutex()); if (args.index_count == 0 || args.instance_count == 0) { return; } if (ConsumeMetadataColorOperation(buffer)) { ResetBindings(); return; } if (!DrawHasValidVertexShader(sh_ctx)) { return; } if (ShouldSkipGeShader(buffer)) { return; } if (graphics_debug_dump_enabled()) { sh_print("GraphicsRenderDrawIndex():Shader:", sh_ctx); uc_print("GraphicsRenderDrawIndex():UserConfig:", ucfg); hw_print(buffer); LOGF("GraphicsRenderDrawIndex():Parameters:\n" "\t index_type_and_size = 0x%08" PRIx32 "\n" "\t index_count = 0x%08" PRIx32 "\n" "\t index_addr = 0x%016" PRIx64 "\n" "\t instance_count = 0x%08" PRIx32 "\n" "\t base_vertex = 0x%08" PRIx32 "\n" "\t first_instance = 0x%08" PRIx32 "\n", args.index_type_and_size, args.index_count, reinterpret_cast(args.index_addr), args.instance_count, static_cast(args.base_vertex), args.first_instance); } uc_check(ucfg); hw_check(buffer); vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList; if (!GetDrawTopology(ucfg, false, topology)) { return; } vk::IndexType index_type = vk::IndexType::eUint16; uint64_t index_size = 0; bool expand_index8_to_u16 = false; const bool primitive_restart = ResolvePrimitiveRestart(buffer, topology, args.index_type_and_size); switch (static_cast(args.index_type_and_size)) { case Prospero::IndexType::kIndex16: index_type = vk::IndexType::eUint16; index_size = 2 * static_cast(args.index_count); break; case Prospero::IndexType::kIndex32: index_type = vk::IndexType::eUint32; index_size = 4 * static_cast(args.index_count); break; // Some games use it - need vulkan extension case Prospero::IndexType::kIndex8: index_type = vk::IndexType::eUint16; index_size = static_cast(args.index_count); expand_index8_to_u16 = true; break; default: EXIT("unknown index_type_and_size: %u\n", args.index_type_and_size); } const DrawCallInfo draw {"DrawIndex", CommandBufferDebugOp::DrawIndex, args.index_count, args.instance_count, args.first_instance}; std::vector expanded_indices; if (expand_index8_to_u16) { EXIT_NOT_IMPLEMENTED(args.index_addr == nullptr); const auto* src = static_cast(args.index_addr); expanded_indices.resize(args.index_count); for (uint32_t i = 0; i < args.index_count; i++) { expanded_indices[i] = primitive_restart && src[i] == 0xffu ? 0xffffu : src[i]; } } DrawIndexBufferSource index_source {}; index_source.enabled = true; index_source.address = reinterpret_cast(args.index_addr); index_source.host_data = expanded_indices.empty() ? nullptr : static_cast(expanded_indices.data()); index_source.size = expanded_indices.empty() ? index_size : expanded_indices.size() * sizeof(uint16_t); index_source.type = index_type; DrawRenderState state {}; if (!PrepareDrawRenderState(submit_id, buffer, draw, args.render_target_slice_offset, true, state)) { ResetBindings(); return; } RefreshShaders(buffer, draw, true, state); LogDrawStateIfNeeded(buffer, draw, state, true, false, args.index_type_and_size, args.index_addr); const bool indirect = args.offset_source == DrawOffsetSource::IndirectArgs; const auto vertex_offset = indirect ? args.base_vertex : ResolveVertexOffset(ucfg.GetIndexOffset(), state.vs_input_info) + args.base_vertex; DrawEmitInfo emit {}; emit.indexed = true; emit.vertex_offset = vertex_offset; emit.first_instance = indirect ? args.first_instance : ResolveInstanceOffset(state.vs_input_info); ExecutePreparedDraw(submit_id, buffer, draw, state, topology, emit, index_source, primitive_restart, true, true, false); ResetBindings(); } // NOLINTNEXTLINE(readability-function-cognitive-complexity) void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const DrawAutoArgs& args) { KYTY_PROFILER_FUNCTION(); EXIT_IF(buffer.IsInvalid()); EXIT_IF(args.offset_source == DrawOffsetSource::DrawState && args.first_instance != 0); m_context.GetCommandScheduler().PopPendingOperations(); auto& ucfg = buffer.GetUserConfig(); auto& sh_ctx = buffer.GetShaders(); buffer.SetDebugInfo(static_cast(CommandBufferDebugOp::DrawIndexAuto), submit_id, args.vertex_count, 0, args.first_vertex, args.instance_count, args.first_instance); Common::LockGuard lock(m_context.GetMutex()); if (args.vertex_count == 0 || args.instance_count == 0) { return; } if (ConsumeMetadataColorOperation(buffer)) { ResetBindings(); return; } if (!DrawHasValidVertexShader(sh_ctx)) { return; } if (ShouldSkipGeShader(buffer)) { return; } if (graphics_debug_dump_enabled()) { sh_print("GraphicsRenderDrawIndexAuto():Shader:", sh_ctx); uc_print("GraphicsRenderDrawIndexAuto():UserConfig:", ucfg); hw_print(buffer); LOGF("GraphicsRenderDrawIndexAuto():Parameters:\n" "\t vertex_count = 0x%08" PRIx32 "\n" "\t instance_count = 0x%08" PRIx32 "\n" "\t first_vertex = 0x%08" PRIx32 "\n" "\t first_instance = 0x%08" PRIx32 "\n", args.vertex_count, args.instance_count, args.first_vertex, args.first_instance); } uc_check(ucfg); hw_check(buffer); const DrawCallInfo draw {"DrawIndexAuto", CommandBufferDebugOp::DrawIndexAuto, args.vertex_count, args.instance_count, args.first_instance}; DrawRenderState state {}; if (!PrepareDrawRenderState(submit_id, buffer, draw, args.render_target_slice_offset, false, state)) { ResetBindings(); return; } vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList; if (!GetDrawTopology(ucfg, true, topology)) { ResetBindings(); return; } RefreshShaders(buffer, draw, false, state); const bool rect_list = topology == vk::PrimitiveTopology::ePatchList; if (rect_list && state.vs_input_info.buffers_num == 0 && state.vs_input_info.stage.program->param_export_mask == 0 && state.ps_input_info.input_num != 0) { if (graphics_debug_dump_enabled()) { LOGF("DrawIndexAuto: skipping rect-list draw with no VS param exports and PS inputs: " "ps_inputs=%u ps=0x%016" PRIx64 " es=0x%016" PRIx64 " gs=0x%016" PRIx64 "\n", state.ps_input_info.input_num, sh_ctx.GetPs().ps_regs.data_addr, sh_ctx.GetVs().es_regs.data_addr, sh_ctx.GetVs().gs_regs.data_addr); } ResetBindings(); return; } LogDrawStateIfNeeded(buffer, draw, state, false, ucfg.GetPrimType() == Prospero::PrimitiveType::kRectListLegacy, 0, nullptr); const bool indirect = args.offset_source == DrawOffsetSource::IndirectArgs; const auto vertex_offset = indirect ? static_cast(args.first_vertex) : ResolveVertexOffset(ucfg.GetIndexOffset(), state.vs_input_info) + static_cast(args.first_vertex); DrawEmitInfo emit {}; emit.first_vertex = static_cast(vertex_offset); emit.first_instance = indirect ? args.first_instance : ResolveInstanceOffset(state.vs_input_info); DrawIndexBufferSource index_source {}; ExecutePreparedDraw(submit_id, buffer, draw, state, topology, emit, index_source, false, false, false, true); ResetBindings(); } bool RenderExecutor::ResolveColorTargets(uint64_t submit_id, CommandBuffer& buffer, uint32_t render_target_slice_offset) { const auto& hw = buffer.GetRegisters(); if (hw.GetColorControl().mode != 3) { return false; } const auto& src_rt = hw.GetRenderTarget(0); const auto& dst_rt = hw.GetRenderTarget(1); if (src_rt.base.addr == 0 || dst_rt.base.addr == 0) { return false; } RenderColorInfo src {}; RenderColorInfo dst {}; ResolveRenderColorTarget(submit_id, buffer, src, render_target_slice_offset, 0, true, true); ResolveRenderColorTarget(submit_id, buffer, dst, render_target_slice_offset, 1, true, true); if (!src.image_id || !dst.image_id || src.type == RenderColorType::NoColorOutput || dst.type == RenderColorType::NoColorOutput) { return false; } if (src.base_addr == dst.base_addr && src.base_mip_level == dst.base_mip_level && src.base_array_layer == dst.base_array_layer) { return true; } auto& cache = m_context.GetTextureCache(); cache.MarkGpuWritten(dst.image_id); auto& source = cache.GetImage(src.image_id); auto& destination = cache.GetImage(dst.image_id); destination.Resolve(source, {src.base_mip_level, 1, src.base_array_layer, 1}, {dst.base_mip_level, 1, dst.base_array_layer, 1}); return true; } } // namespace Libs::Graphics