/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2021 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ #include "xenia/gpu/primitive_processor.h" #include #include #include #include "xenia/base/assert.h" #include "xenia/base/byte_order.h" #include "xenia/base/cvar.h" #include "xenia/base/logging.h" #include "xenia/base/math.h" #include "xenia/base/profiling.h" #include "xenia/gpu/register_file.h" #include "xenia/gpu/registers.h" #include "xenia/gpu/shader.h" #include "xenia/gpu/trace_writer.h" #include "xenia/gpu/xenos.h" // All these overrides are always safe to use as all backends are expected to // support triangle lists and line strips. DEFINE_bool( force_convert_triangle_fans_to_lists, false, "For host graphics API downlevel support testing only, force CPU " "conversion of triangle fans to triangle lists even if the host supports " "triangle fan primitives natively.", "GPU"); DEFINE_bool( force_convert_line_loops_to_strips, false, "For host graphics API downlevel support testing only, force CPU " "conversion of line loops to line strips even if the host supports line " "loop primitives natively.", "GPU"); DEFINE_bool( force_convert_quad_lists_to_triangle_lists, false, "For host graphics API downlevel support testing only, force CPU " "conversion of quad lists to quad strips even if the host supports quad " "list primitives natively or via geometry shader emulation.\n" "May also be useful for graphics debugging when the debugger doesn't " "display the geometry generated by geometry shaders properly.", "GPU"); DEFINE_bool( ignore_32bit_vertex_index_support, false, "For host graphics API downlevel testing only (useful only for Qualcomm " "Adreno 4xx-level host GPU testing), force indirection or pre-masking and " "pre-swapping of 32-bit vertex indices as if the host only supports 24-bit " "indices.", "GPU"); // TODO(Triang3l): More investigation of the cache threshold as cache lookups // and insertions require global critical region locking, and insertions also // require protecting pages. At 1024, the cache only made the performance worse // (415607D4, 16-bit primitive reset index replacement). DEFINE_int32( primitive_processor_cache_min_indices, 4096, "Smallest number of guest indices to store in the cache to try reusing " "later in the same frame if processing (such as primitive type conversion " "or reset index replacement) is performed.\n" "Setting this to a very high value may result in excessive CPU processing, " "while a very low value may result in excessive locking and lookups.\n" "Negative values disable caching.", "GPU"); namespace xe { namespace gpu { // SIMD processing here assumes that alignment is not required (neither AVX nor // Neon requires it) and there's no punishment for using an unaligned access // instruction when the data is actually aligned (AVX has separate aligned / // unaligned movs, but they have the same performance nowadays; Neon dropped the // alignment specifier in AArch64), but truly unaligned access may result in two // hardware memory operations if some boundary that is >= vector size is // crossed. // // Therefore, to minimize unaligned access (primarily reads - since we depend on // the data immediately), SIMD usage here is performed according to the // following pattern (though we try to co-align the destination and the source // prior to calling, but still doing all the operations for more code // correctness and fewer unobvious conditions): // - Until the source pointer is vector-aligned, process the first indices // without SIMD. // - The best possible outcome of this is that both the source and the // destination will be vector-aligned (if they were co-aligned prior to the // call), in this case, neither load nor store instructions will be crossing // cache lines. // - The other possible outcome is that the source will be aligned (1 memory // read per load), while the destination will be unaligned (1-2 memory // writes per store). // - Process whole vectors with SIMD. // - If there are less elements than a vector can hold remaining, process them // without SIMD. // // We assume that indices are at least aligned to their natural alignment (2 or // 4 bytes depending on the format) - the R6xx documentation says that in // DRAW_INDEX, INDEX_BASE_LO is word-aligned, and that's required by host // graphics APIs. PrimitiveProcessor::~PrimitiveProcessor() { ShutdownCommon(); } bool PrimitiveProcessor::InitializeCommon( bool full_32bit_vertex_indices_supported, bool triangle_fans_supported, bool line_loops_supported, bool quad_lists_supported) { full_32bit_vertex_indices_used_ = full_32bit_vertex_indices_supported; convert_triangle_fans_to_lists_ = !triangle_fans_supported || cvars::force_convert_triangle_fans_to_lists; convert_line_loops_to_strips_ = !line_loops_supported || cvars::force_convert_line_loops_to_strips; convert_quad_lists_to_triangle_lists_ = !quad_lists_supported || cvars::force_convert_quad_lists_to_triangle_lists; // Initialize the index buffer for conversion of auto-indexed primitive types. uint32_t builtin_index_count = 0; if (convert_triangle_fans_to_lists_) { builtin_ib_offset_triangle_fans_to_lists_ = sizeof(uint16_t) * builtin_index_count; builtin_index_count += GetTriangleFanListIndexCount(UINT16_MAX); } else { builtin_ib_offset_triangle_fans_to_lists_ = SIZE_MAX; } if (convert_quad_lists_to_triangle_lists_) { builtin_ib_offset_quad_lists_to_triangle_lists_ = sizeof(uint16_t) * builtin_index_count; builtin_index_count += GetQuadListTriangleListIndexCount(UINT16_MAX); } else { builtin_ib_offset_quad_lists_to_triangle_lists_ = SIZE_MAX; } if (builtin_index_count) { if (!InitializeBuiltin16BitIndexBuffer( builtin_index_count, [this](uint16_t* mapping) { if (builtin_ib_offset_triangle_fans_to_lists_ != SIZE_MAX) { // Triangle fans as triangle lists. // Ordered as (v1, v2, v0), (v2, v3, v0) in Direct3D. // https://docs.microsoft.com/en-us/windows/desktop/direct3d9/triangle-fans uint16_t* triangle_list_ptr = mapping + builtin_ib_offset_triangle_fans_to_lists_ / sizeof(uint16_t); for (uint32_t i = 2; i < UINT16_MAX; ++i) { *(triangle_list_ptr++) = uint16_t(i - 1); *(triangle_list_ptr++) = uint16_t(i); *(triangle_list_ptr++) = 0; } } if (builtin_ib_offset_quad_lists_to_triangle_lists_ != SIZE_MAX) { uint16_t* triangle_list_ptr = mapping + builtin_ib_offset_quad_lists_to_triangle_lists_ / sizeof(uint16_t); // TODO(Triang3l): SIMD for faster initialization? for (uint32_t i = 0; i < UINT16_MAX / 4; ++i) { uint16_t quad_first_index = uint16_t(i * 4); // TODO(Triang3l): Find the correct order. // v0, v1, v2. *(triangle_list_ptr++) = quad_first_index; *(triangle_list_ptr++) = quad_first_index + 1; *(triangle_list_ptr++) = quad_first_index + 2; // v0, v2, v3. *(triangle_list_ptr++) = quad_first_index; *(triangle_list_ptr++) = quad_first_index + 2; *(triangle_list_ptr++) = quad_first_index + 3; } } })) { ShutdownCommon(); return false; } } return true; } void PrimitiveProcessor::ShutdownCommon() { if (memory_invalidation_callback_handle_) { // Clear the cache if it has ever been used and unregister the invalidation // callback. { auto global_lock = global_critical_region_.Acquire(); cache_map_.clear(); cache_bucket_free_first_entry_ = SIZE_MAX; std::memset(cache_buckets_non_empty_l1_, 0, sizeof(cache_buckets_non_empty_l1_)); std::memset(cache_buckets_non_empty_l2_, 0, sizeof(cache_buckets_non_empty_l2_)); } memory_.UnregisterPhysicalMemoryInvalidationCallback( memory_invalidation_callback_handle_); memory_invalidation_callback_handle_ = nullptr; cache_entry_pool_.clear(); } } void PrimitiveProcessor::ClearPerFrameCache() { if (!memory_invalidation_callback_handle_) { // Only do clearing if cache has ever been used. return; } auto global_lock = global_critical_region_.Acquire(); for (const std::pair& cache_map_entry : cache_map_) { cache_entry_pool_[cache_map_entry.second].free_next = cache_bucket_free_first_entry_; cache_bucket_free_first_entry_ = cache_map_entry.second; } cache_map_.clear(); std::memset(cache_buckets_non_empty_l1_, 0, sizeof(cache_buckets_non_empty_l1_)); std::memset(cache_buckets_non_empty_l2_, 0, sizeof(cache_buckets_non_empty_l2_)); } bool PrimitiveProcessor::Process(ProcessingResult& result_out) { SCOPE_profile_cpu_f("gpu"); const RegisterFile& regs = register_file_; auto vgt_draw_initiator = regs.Get(); // Parse the primitive type and the tessellation state (VGT_OUTPUT_PATH_CNTL // is only used in the explicit major mode) - there are cases in games when // this register is left over after usage of tessellation in draws that don't // need it. xenos::PrimitiveType guest_primitive_type = vgt_draw_initiator.prim_type; xenos::PrimitiveType host_primitive_type = guest_primitive_type; bool tessellation_enabled = xenos::IsMajorModeExplicit(vgt_draw_initiator.major_mode, vgt_draw_initiator.prim_type) && regs.Get().path_select == xenos::VGTOutputPath::kTessellationEnable; xenos::TessellationMode tessellation_mode = regs.Get().tess_mode; Shader::HostVertexShaderType host_vertex_shader_type; if (tessellation_enabled) { // Currently only supporting tessellation in known cases for safety, and not // yet converting patch strips / fans to patch lists until games using them // are found for easier debugging when it actually happens. // TODO(Triang3l): Conversion of patch strips / fans if found. host_vertex_shader_type = Shader::HostVertexShaderType(-1); switch (guest_primitive_type) { case xenos::PrimitiveType::kTriangleList: // Also supported by triangle strips and fans according to: // https://www.khronos.org/registry/OpenGL/extensions/AMD/AMD_vertex_shader_tessellator.txt // Would need to convert those to triangle lists, but haven't seen any // games using tessellated strips / fans so far. switch (tessellation_mode) { case xenos::TessellationMode::kDiscrete: // - 415607E1 - nets above barrels in the beginning of the first // mission (turn right after the end of the intro) - // kTriangleList. host_vertex_shader_type = Shader::HostVertexShaderType::kTriangleDomainCPIndexed; break; case xenos::TessellationMode::kContinuous: // - 4D5307F2 - tree building with a beehive in the beginning // (visible on the start screen behind the logo), waterfall in the // beginning - kTriangleList. host_vertex_shader_type = Shader::HostVertexShaderType::kTriangleDomainCPIndexed; break; default: break; } break; case xenos::PrimitiveType::kQuadList: switch (tessellation_mode) { // Also supported by quad strips according to: // https://www.khronos.org/registry/OpenGL/extensions/AMD/AMD_vertex_shader_tessellator.txt // Would need to convert those to quad lists, but haven't seen any // games using tessellated strips so far. case xenos::TessellationMode::kDiscrete: // Not seen in games so far. host_vertex_shader_type = Shader::HostVertexShaderType::kQuadDomainCPIndexed; break; case xenos::TessellationMode::kContinuous: // - 58410823 - retro screen and beams in the main menu - kQuadList. host_vertex_shader_type = Shader::HostVertexShaderType::kQuadDomainCPIndexed; break; default: break; } break; case xenos::PrimitiveType::kTrianglePatch: // - 4D5307E6 - water - adaptive. // - 4D5307ED - water - adaptive. host_vertex_shader_type = Shader::HostVertexShaderType::kTriangleDomainPatchIndexed; break; case xenos::PrimitiveType::kQuadPatch: // - 4D5307F1 - continuous. // - 4D5307F2 - garden ground - adaptive. host_vertex_shader_type = Shader::HostVertexShaderType::kQuadDomainPatchIndexed; break; default: // TODO(Triang3l): Support line patches. break; } if (host_vertex_shader_type == Shader::HostVertexShaderType(-1)) { XELOGE( "Primitive processor: Unsupported tessellation mode {} for primitive " "type {}. Report the game to Xenia developers!", uint32_t(tessellation_mode), uint32_t(guest_primitive_type)); assert_always(); return false; } } else { switch (guest_primitive_type) { case xenos::PrimitiveType::kPointList: case xenos::PrimitiveType::kLineList: case xenos::PrimitiveType::kLineStrip: case xenos::PrimitiveType::kTriangleList: case xenos::PrimitiveType::kTriangleStrip: case xenos::PrimitiveType::kRectangleList: // Supported natively or through geometry or compute shaders on all // backends. break; case xenos::PrimitiveType::kTriangleFan: if (convert_triangle_fans_to_lists_) { host_primitive_type = xenos::PrimitiveType::kTriangleList; } break; case xenos::PrimitiveType::kLineLoop: if (convert_line_loops_to_strips_) { host_primitive_type = xenos::PrimitiveType::kLineStrip; } break; case xenos::PrimitiveType::kQuadList: if (convert_quad_lists_to_triangle_lists_) { host_primitive_type = xenos::PrimitiveType::kQuadList; } break; default: XELOGE( "Primitive processor: Unsupported primitive type {}. Report the " "game to Xenia developers!", uint32_t(guest_primitive_type)); assert_always(); return false; } host_vertex_shader_type = Shader::HostVertexShaderType::kVertex; } // Process the indices. uint32_t guest_draw_vertex_count = vgt_draw_initiator.num_indices; uint32_t line_loop_closing_index = 0; uint32_t guest_index_base; CachedResult cacheable; cacheable.host_draw_vertex_count = guest_draw_vertex_count; cacheable.host_primitive_reset_enabled = false; cacheable.host_index_buffer_handle = SIZE_MAX; if (vgt_draw_initiator.source_select == xenos::SourceSelect::kAutoIndex) { // Auto-indexed - use a remapping index buffer if needed to change the // primitive type. if (tessellation_enabled && tessellation_mode == xenos::TessellationMode::kAdaptive) { XELOGE( "Primitive processor: Adaptive tessellation requires 32-bit " "floating-point edge tessellation factors in the index buffer, but " "no index buffer is provided by the guest."); assert_always(); return false; } guest_index_base = 0; cacheable.host_index_format = xenos::IndexFormat::kInt16; cacheable.host_index_endian = xenos::Endian::kNone; cacheable.host_primitive_reset_enabled = false; cacheable.index_buffer_type = ProcessedIndexBufferType::kNone; if (host_primitive_type != guest_primitive_type) { switch (guest_primitive_type) { case xenos::PrimitiveType::kTriangleFan: assert_true(host_primitive_type == xenos::PrimitiveType::kTriangleList); cacheable.host_draw_vertex_count = GetTriangleFanListIndexCount(cacheable.host_draw_vertex_count); cacheable.index_buffer_type = ProcessedIndexBufferType::kHostBuiltin; assert_true(builtin_ib_offset_triangle_fans_to_lists_ != SIZE_MAX); cacheable.host_index_buffer_handle = builtin_ib_offset_triangle_fans_to_lists_; break; case xenos::PrimitiveType::kLineLoop: // Plus 1 element (if there's anything to draw) in the strip, still // auto-indexed, but the added excess index should be treated as 0 by // the vertex shaders. assert_true(host_primitive_type == xenos::PrimitiveType::kLineStrip); cacheable.host_draw_vertex_count = GetLineLoopStripIndexCount(cacheable.host_draw_vertex_count); if (cacheable.host_draw_vertex_count) { line_loop_closing_index = cacheable.host_draw_vertex_count - 1; } break; case xenos::PrimitiveType::kQuadList: assert_true(host_primitive_type == xenos::PrimitiveType::kTriangleList); cacheable.host_draw_vertex_count = GetQuadListTriangleListIndexCount( cacheable.host_draw_vertex_count); cacheable.index_buffer_type = ProcessedIndexBufferType::kHostBuiltin; assert_true(builtin_ib_offset_quad_lists_to_triangle_lists_ != SIZE_MAX); cacheable.host_index_buffer_handle = builtin_ib_offset_quad_lists_to_triangle_lists_; break; default: assert_always(); return false; } } } else { // There is an index buffer. assert_true(vgt_draw_initiator.source_select == xenos::SourceSelect::kDMA); if (vgt_draw_initiator.source_select != xenos::SourceSelect::kDMA) { // TODO(Triang3l): Support immediate-indexed vertices. XELOGE( "Primitive processor: Unsupported vertex index source {}. Report the " "game to Xenia developers!", uint32_t(vgt_draw_initiator.source_select)); return false; } auto vgt_dma_size = regs.Get(); xenos::IndexFormat guest_index_format = vgt_draw_initiator.index_size; cacheable.host_index_format = guest_index_format; // Normalize the endian and the reset index. xenos::Endian guest_index_endian = vgt_dma_size.swap_mode; if (guest_index_format == xenos::IndexFormat::kInt16 && (guest_index_endian != xenos::Endian::kNone && guest_index_endian != xenos::Endian::k8in16)) { XELOGW( "Primitive processor: 32-bit endian swap mode {} is used for 16-bit " "indices. This shouldn't normally be happening, but report the game " "to Xenia developers for investigation of the intended behavior " "(ignore or actually swap across adjacent indices)! Currently " "disabling the swap for 16-and-32 and replacing 8-in-32 with " "8-in-16.", uint32_t(guest_index_endian)); guest_index_endian = guest_index_endian == xenos::Endian::k8in32 ? xenos::Endian::k8in16 : xenos::Endian::kNone; } bool guest_primitive_reset_enabled = false; uint32_t guest_primitive_reset_index_guest_endian = 0; if (tessellation_enabled && tessellation_mode == xenos::TessellationMode::kAdaptive) { // Adaptive tessellation uses the index buffer not for indices, but for // 32-bit floating-point edge factors - no primitive reset. if (guest_index_format != xenos::IndexFormat::kInt32) { XELOGE( "Primitive processor: Adaptive tessellation requires 32-bit " "floating-point edge tessellation factors in the index buffer, but " "16-bit index buffer is provided by the guest."); assert_always(); return false; } } else { if (regs.Get().multi_prim_ib_ena) { switch (guest_primitive_type) { case xenos::PrimitiveType::kLineStrip: case xenos::PrimitiveType::kTriangleFan: case xenos::PrimitiveType::kTriangleStrip: case xenos::PrimitiveType::kLineLoop: case xenos::PrimitiveType::kQuadStrip: case xenos::PrimitiveType::kPolygon: case xenos::PrimitiveType::k2DLineStrip: case xenos::PrimitiveType::k2DTriStrip: guest_primitive_reset_index_guest_endian = xenos::GpuSwap( regs.Get().reset_indx, guest_index_endian); // - VGT, what does the guest say about its primitive reset index? // - It's over 0xFFFF!!! // - What!? 0xFFFF!? There's no way that can be stored in 16 bits! guest_primitive_reset_enabled = guest_index_format == xenos::IndexFormat::kInt16 ? guest_primitive_reset_index_guest_endian <= UINT16_MAX : true; break; default: // Vulkan explicitly disallows primitive restart index for "list" // topologies. In Direct3D 12, it's valid for non-strips, but has // implementation-defined behavior. Make backend usage simpler by // explicitly filtering lists out, and hope the guest never uses // primitive reset for lists. break; } } } // Get the index buffer memory range. if (guest_draw_vertex_count > vgt_dma_size.num_words) { XELOGW( "Primitive processor: {} vertices attempted to be drawn with an " "index buffer only containing {}. Should be fetching zero indices " "instead of overflowing ones, but this is a rare situation, so not " "handled yet. Report the game to Xenia developers!", guest_draw_vertex_count, vgt_dma_size.num_words); guest_draw_vertex_count = vgt_dma_size.num_words; } uint32_t index_size_log2 = guest_index_format == xenos::IndexFormat::kInt16 ? 1 : 2; // The base should already be aligned, but aligning here too for safety. guest_index_base = regs[XE_GPU_REG_VGT_DMA_BASE].u32 & ~uint32_t((1 << index_size_log2) - 1); uint32_t guest_index_buffer_needed_bytes = guest_draw_vertex_count << index_size_log2; if (guest_index_base > SharedMemory::kBufferSize || SharedMemory::kBufferSize - guest_index_base < guest_index_buffer_needed_bytes) { XELOGE( "Primitive processor: Index buffer at 0x{:8X}, 0x{:X} bytes " "required, is out of the physical memory bounds", guest_index_base, guest_index_buffer_needed_bytes); assert_always(); return false; } cacheable.host_index_format = guest_index_format; cacheable.host_index_endian = guest_index_endian; uint32_t guest_index_mask_guest_endian = guest_index_format == xenos::IndexFormat::kInt16 ? UINT16_MAX : GpuSwap(xenos::kVertexIndexMask, guest_index_endian); if (host_primitive_type != guest_primitive_type) { // Already converting to a different index type - primitive reset is // performed during conversion here. Also doing the endian swap here for // hosts not supporting 32-bit indices because indirection is only used // for the shared memory buffer. // Writing to the trace irrespective of the cache lookup result because // cache behavior depends on runtime configuration and state. trace_writer_.WriteMemoryRead(guest_index_base, guest_index_buffer_needed_bytes); CacheTransaction cache_transaction( *this, CacheKey(guest_index_base, guest_draw_vertex_count, guest_index_format, guest_index_endian, guest_primitive_reset_enabled, guest_primitive_type)); if (cache_transaction.GetFoundResult()) { cacheable = *cache_transaction.GetFoundResult(); } else { const void* guest_indices_ptr = memory_.TranslatePhysical(guest_index_base); cacheable.index_buffer_type = ProcessedIndexBufferType::kHostConverted; cacheable.host_primitive_reset_enabled = false; std::function host_index_count_getter; switch (guest_primitive_type) { case xenos::PrimitiveType::kTriangleFan: host_index_count_getter = GetTriangleFanListIndexCount; break; case xenos::PrimitiveType::kLineLoop: host_index_count_getter = GetLineLoopStripIndexCount; break; case xenos::PrimitiveType::kQuadList: host_index_count_getter = GetQuadListTriangleListIndexCount; break; default: assert_unhandled_case(guest_primitive_type); return false; } single_primitive_ranges_.clear(); if (guest_index_format == xenos::IndexFormat::kInt16) { // 16-bit indices - just convert the primitive (or multiple // primitives) to the host topology. // TODO(Triang3l): 16-bit > 32-bit primitive type conversion for // Metal, where primitive reset is always enabled, if UINT16_MAX is // used as a real vertex index. auto guest_indices = reinterpret_cast(guest_indices_ptr); if (guest_primitive_reset_enabled && IsResetUsed(guest_indices, guest_draw_vertex_count, guest_primitive_reset_index_guest_endian)) { // Multiple primitives in the index buffer - gather all single // primitives. cacheable.host_draw_vertex_count = GetMultiPrimitiveHostIndexCountAndRanges( host_index_count_getter, guest_indices, guest_draw_vertex_count, guest_primitive_reset_index_guest_endian, single_primitive_ranges_); } else { cacheable.host_draw_vertex_count = host_index_count_getter(guest_draw_vertex_count); single_primitive_ranges_.emplace_back( 0, guest_draw_vertex_count, cacheable.host_draw_vertex_count); } auto host_indices = reinterpret_cast( RequestHostConvertedIndexBufferForCurrentFrame( xenos::IndexFormat::kInt16, cacheable.host_draw_vertex_count, false, guest_index_base, cacheable.host_index_buffer_handle)); if (!host_indices) { return false; } ConvertSinglePrimitiveRanges( host_indices, guest_indices, guest_primitive_type, PassthroughIndexTransform(), single_primitive_ranges_.cbegin(), single_primitive_ranges_.cend()); } else { // 32-bit indices - may need to pre-swap and pre-mask also if the host // doesn't support full 32-bit vertex indices. auto guest_indices = reinterpret_cast(guest_indices_ptr); if (guest_primitive_reset_enabled && IsResetUsed(guest_indices, guest_draw_vertex_count, guest_primitive_reset_index_guest_endian, guest_index_mask_guest_endian)) { // Multiple primitives in the index buffer - gather all single // primitives. cacheable.host_draw_vertex_count = GetMultiPrimitiveHostIndexCountAndRanges( host_index_count_getter, guest_indices, guest_draw_vertex_count, guest_primitive_reset_index_guest_endian, guest_index_mask_guest_endian, single_primitive_ranges_); } else { cacheable.host_draw_vertex_count = host_index_count_getter(guest_draw_vertex_count); single_primitive_ranges_.emplace_back( 0, guest_draw_vertex_count, cacheable.host_draw_vertex_count); } auto host_indices = reinterpret_cast( RequestHostConvertedIndexBufferForCurrentFrame( xenos::IndexFormat::kInt32, cacheable.host_draw_vertex_count, false, guest_index_base, cacheable.host_index_buffer_handle)); if (!host_indices) { return false; } auto single_primitive_ranges_beginning = single_primitive_ranges_.cbegin(); auto single_primitive_ranges_end = single_primitive_ranges_.cend(); if (full_32bit_vertex_indices_used_) { ConvertSinglePrimitiveRanges( host_indices, guest_indices, guest_primitive_type, PassthroughIndexTransform(), single_primitive_ranges_beginning, single_primitive_ranges_end); } else { switch (guest_index_endian) { case xenos::Endian::kNone: ConvertSinglePrimitiveRanges(host_indices, guest_indices, guest_primitive_type, To24NonSwappingIndexTransform(), single_primitive_ranges_beginning, single_primitive_ranges_end); break; case xenos::Endian::k8in16: ConvertSinglePrimitiveRanges(host_indices, guest_indices, guest_primitive_type, To24Swapping8In16IndexTransform(), single_primitive_ranges_beginning, single_primitive_ranges_end); break; case xenos::Endian::k8in32: ConvertSinglePrimitiveRanges(host_indices, guest_indices, guest_primitive_type, To24Swapping8In32IndexTransform(), single_primitive_ranges_beginning, single_primitive_ranges_end); break; case xenos::Endian::k16in32: ConvertSinglePrimitiveRanges(host_indices, guest_indices, guest_primitive_type, To24Swapping16In32IndexTransform(), single_primitive_ranges_beginning, single_primitive_ranges_end); break; default: assert_unhandled_case(guest_index_endian); return false; } cacheable.host_index_endian = xenos::Endian::kNone; } } cache_transaction.SetNewResult(cacheable); } } else { // Using the same indices on the host as on the guest, either directly or // (for backends not supporting full 32-bit indices, thus unable to // endian-swap, or even to safely drop the upper 8 bits if no swap is even // needed) indirectly. cacheable.host_draw_vertex_count = guest_draw_vertex_count; cacheable.index_buffer_type = ProcessedIndexBufferType::kGuest; cacheable.host_primitive_reset_enabled = guest_primitive_reset_enabled; if (guest_primitive_reset_enabled) { if (guest_index_format == xenos::IndexFormat::kInt16) { // The whole 16-bit index is compared to the primitive reset index. // Does not need indirection on backends not supporting full 32-bit // indices. if (guest_primitive_reset_index_guest_endian != UINT16_MAX) { // If primitive reset is with a non-0xFFFF index is used, replace // with 0xFFFF if 0xFFFF is not used as a real index, or with // 0xFFFFFFFF if it is. // Writing to the trace irrespective of the cache lookup result // because cache behavior depends on runtime configuration and // state. trace_writer_.WriteMemoryRead(guest_index_base, guest_index_buffer_needed_bytes); // Not specifying the primitive type in the cache key because not // replacing it, only the reset index in a type-independent way. CacheTransaction cache_transaction( *this, CacheKey(guest_index_base, guest_draw_vertex_count, guest_index_format, guest_index_endian, guest_primitive_reset_enabled)); if (cache_transaction.GetFoundResult()) { cacheable = *cache_transaction.GetFoundResult(); } else { auto guest_indices = memory_.TranslatePhysical(guest_index_base); bool is_reset_index_used, is_ffff_used_as_vertex_index; Get16BitResetIndexUsage(guest_indices, guest_draw_vertex_count, guest_primitive_reset_index_guest_endian, is_reset_index_used, is_ffff_used_as_vertex_index); if (is_reset_index_used) { cacheable.index_buffer_type = ProcessedIndexBufferType::kHostConverted; cacheable.host_index_format = is_ffff_used_as_vertex_index ? xenos::IndexFormat::kInt32 : xenos::IndexFormat::kInt16; void* host_indices_ptr = RequestHostConvertedIndexBufferForCurrentFrame( cacheable.host_index_format, guest_draw_vertex_count, true, guest_index_base, cacheable.host_index_buffer_handle); if (!host_indices_ptr) { return false; } if (is_ffff_used_as_vertex_index) { ReplaceResetIndex16To24( reinterpret_cast(host_indices_ptr), guest_indices, guest_draw_vertex_count, guest_primitive_reset_index_guest_endian); } else { ReplaceResetIndex16To16( reinterpret_cast(host_indices_ptr), guest_indices, guest_draw_vertex_count, guest_primitive_reset_index_guest_endian); } } cache_transaction.SetNewResult(cacheable); } } } else { // Low 24 bits of the guest index are compared to the primitive reset // index. If the backend doesn't support full 32-bit indices, for // ProcessedIndexBufferType::kGuest, the host needs to read the buffer // indirectly in the vertex shaders and swap, and for // ProcessedIndexBufferType::kHostConverted (if primitive reset is // actually used, thus exactly 0xFFFFFFFF must be sent to the host for // it in a true index buffer), no indirection is done, but // pre-swapping and pre-masking is done here. // Writing to the trace irrespective of the cache lookup result // because cache behavior depends on runtime configuration and state. trace_writer_.WriteMemoryRead(guest_index_base, guest_index_buffer_needed_bytes); // Not specifying the primitive type in the cache key because not // replacing it, only the reset index in a type-independent way. CacheTransaction cache_transaction( *this, CacheKey(guest_index_base, guest_draw_vertex_count, guest_index_format, guest_index_endian, guest_primitive_reset_enabled)); if (cache_transaction.GetFoundResult()) { cacheable = *cache_transaction.GetFoundResult(); } else { auto guest_indices = memory_.TranslatePhysical(guest_index_base); if (IsResetUsed(guest_indices, guest_draw_vertex_count, guest_primitive_reset_index_guest_endian, guest_index_mask_guest_endian)) { cacheable.index_buffer_type = ProcessedIndexBufferType::kHostConverted; auto host_indices = reinterpret_cast( RequestHostConvertedIndexBufferForCurrentFrame( xenos::IndexFormat::kInt32, guest_draw_vertex_count, true, guest_index_base, cacheable.host_index_buffer_handle)); if (!host_indices) { return false; } if (full_32bit_vertex_indices_used_ || guest_index_endian == xenos::Endian::kNone) { ReplaceResetIndex32To24( host_indices, guest_indices, guest_draw_vertex_count, guest_primitive_reset_index_guest_endian, guest_index_mask_guest_endian); } else if (guest_index_endian == xenos::Endian::k8in16) { ReplaceResetIndex32To24( host_indices, guest_indices, guest_draw_vertex_count, guest_primitive_reset_index_guest_endian, guest_index_mask_guest_endian); } else if (guest_index_endian == xenos::Endian::k8in32) { ReplaceResetIndex32To24( host_indices, guest_indices, guest_draw_vertex_count, guest_primitive_reset_index_guest_endian, guest_index_mask_guest_endian); } else if (guest_index_endian == xenos::Endian::k16in32) { ReplaceResetIndex32To24( host_indices, guest_indices, guest_draw_vertex_count, guest_primitive_reset_index_guest_endian, guest_index_mask_guest_endian); } else { assert_unhandled_case(guest_index_endian); return false; } cacheable.host_index_endian = full_32bit_vertex_indices_used_ ? guest_index_endian : xenos::Endian::kNone; } cache_transaction.SetNewResult(cacheable); } } } if (cacheable.index_buffer_type == ProcessedIndexBufferType::kGuest) { // Request the index buffer memory. // TODO(Triang3l): Shared memory request cache. if (!shared_memory_.RequestRange(guest_index_base, guest_index_buffer_needed_bytes)) { XELOGE( "PrimitiveProcessor: Failed to request index buffer 0x{:8X}, " "0x{:X} bytes needed, in the shared memory", guest_index_base, guest_index_buffer_needed_bytes); return false; } } } } result_out.guest_primitive_type = guest_primitive_type; result_out.host_primitive_type = host_primitive_type; result_out.host_vertex_shader_type = host_vertex_shader_type; result_out.tessellation_mode = tessellation_mode; result_out.host_draw_vertex_count = cacheable.host_draw_vertex_count; result_out.line_loop_closing_index = line_loop_closing_index; result_out.index_buffer_type = cacheable.index_buffer_type; result_out.guest_index_base = guest_index_base; result_out.host_index_format = cacheable.host_index_format; result_out.host_index_endian = cacheable.host_index_endian; result_out.host_primitive_reset_enabled = cacheable.host_primitive_reset_enabled; result_out.host_index_buffer_handle = cacheable.host_index_buffer_handle; return true; } bool PrimitiveProcessor::IsResetUsed(const uint16_t* source, uint32_t count, uint16_t reset_index_guest_endian) { #if XE_GPU_PRIMITIVE_PROCESSOR_SIMD_SIZE while (count && (reinterpret_cast(source) & (XE_GPU_PRIMITIVE_PROCESSOR_SIMD_SIZE - 1))) { --count; if (*(source++) == reset_index_guest_endian) { return true; } } if (count >= kSimdVectorU16Elements) { SimdVectorU16 reset_index_guest_endian_simd = ReplicateU16(reset_index_guest_endian); while (count >= kSimdVectorU16Elements) { count -= kSimdVectorU16Elements; SimdVectorU16 source_simd = LoadAlignedVectorU16(source); source += kSimdVectorU16Elements; #if XE_ARCH_AMD64 if (_mm_movemask_epi8( _mm_cmpeq_epi16(source_simd, reset_index_guest_endian_simd))) { return true; } #elif XE_ARCH_ARM64 uint64x1_t is_any = vreinterpret_u64_u32(vqmovn_u64(vreinterpretq_u64_u16( vceqq_u16(source_simd, reset_index_guest_endian_simd)))); if (*reinterpret_cast(&is_any)) { return true; } #else #error SIMD 16-bit IsResetUsed not implemented. #endif // XE_ARCH } } #endif // XE_GPU_PRIMITIVE_PROCESSOR_SIMD_SIZE while (count--) { if (*(source++) == reset_index_guest_endian) { return true; } } return false; } void PrimitiveProcessor::Get16BitResetIndexUsage( const uint16_t* source, uint32_t count, uint16_t reset_index_guest_endian, bool& is_reset_index_used_out, bool& is_ffff_used_as_vertex_index_out) { // Optimized for the more common case (reset index not used at all), therefore // not doing early-outs if both conditions are true for a simpler loop body. // Using the index 0xFFFF is likely not that common in general. // TODO(Triang3l): Revisit this - maybe the early-out will be free if this // function is bandwidth-bound. is_ffff_used_as_vertex_index_out = false; if (reset_index_guest_endian == UINT16_MAX) { is_reset_index_used_out = IsResetUsed(source, count, reset_index_guest_endian); return; } is_reset_index_used_out = false; #if XE_GPU_PRIMITIVE_PROCESSOR_SIMD_SIZE while (count && (reinterpret_cast(source) & (XE_GPU_PRIMITIVE_PROCESSOR_SIMD_SIZE - 1))) { --count; uint16_t index = *(source++); if (index == reset_index_guest_endian) { is_reset_index_used_out = true; } if (index == UINT16_MAX) { is_ffff_used_as_vertex_index_out = true; } } if (count >= kSimdVectorU16Elements) { SimdVectorU16 reset_index_guest_endian_simd = ReplicateU16(reset_index_guest_endian); SimdVectorU16 ffff_simd = ReplicateU16(UINT16_MAX); SimdVectorU16 is_reset_simd = ReplicateU16(0); SimdVectorU16 is_ffff_simd = ReplicateU16(0); while (count >= kSimdVectorU16Elements) { count -= kSimdVectorU16Elements; SimdVectorU16 source_simd = LoadAlignedVectorU16(source); source += kSimdVectorU16Elements; #if XE_ARCH_AMD64 is_reset_simd = _mm_or_si128( is_reset_simd, _mm_cmpeq_epi16(source_simd, reset_index_guest_endian_simd)); is_ffff_simd = _mm_or_si128(is_ffff_simd, _mm_cmpeq_epi16(source_simd, ffff_simd)); #elif XE_ARCH_ARM64 is_reset_simd = vcorrq_u16( is_reset_simd, vceqq_u16(source_simd, reset_index_guest_endian_simd)); is_ffff_simd = vmaxq_u16(is_ffff_simd, source_simd); #else #error SIMD Get16BitResetIndexUsage not implemented. #endif // XE_ARCH } #if XE_ARCH_AMD64 if (_mm_movemask_epi8(is_reset_simd)) { is_reset_index_used_out = true; } if (_mm_movemask_epi8(is_ffff_simd)) { is_ffff_used_as_vertex_index_out = true; } #elif XE_ARCH_ARM64 uint64x1_t is_reset_any = vreinterpret_u64_u32(vqmovn_u64(vreinterpretq_u64_u16(is_reset_simd))); if (*reinterpret_cast(&is_reset_any)) { is_reset_index_used_out = true; } uint64x1_t is_ffff_any = vreinterpret_u64_u32( vqmovn_u64(vreinterpretq_u64_u16(vceqq_u16(is_ffff_simd, ffff_simd)))); if (*reinterpret_cast(&is_ffff_any)) { is_ffff_used_as_vertex_index_out = true; } #else #error SIMD Get16BitResetIndexUsage not implemented. #endif // XE_ARCH } #endif // XE_GPU_PRIMITIVE_PROCESSOR_SIMD_SIZE while (count--) { uint16_t index = *(source++); if (index == reset_index_guest_endian) { is_reset_index_used_out = true; } if (index == UINT16_MAX) { is_ffff_used_as_vertex_index_out = true; } } } bool PrimitiveProcessor::IsResetUsed(const uint32_t* source, uint32_t count, uint32_t reset_index_guest_endian, uint32_t low_bits_mask_guest_endian) { // The Xbox 360's GPU only uses the low 24 bits of the index - masking before // comparing. #if XE_GPU_PRIMITIVE_PROCESSOR_SIMD_SIZE while (count && (reinterpret_cast(source) & (XE_GPU_PRIMITIVE_PROCESSOR_SIMD_SIZE - 1))) { --count; if ((*(source++) & low_bits_mask_guest_endian) == reset_index_guest_endian) { return true; } } if (count >= kSimdVectorU32Elements) { SimdVectorU32 reset_index_guest_endian_simd = ReplicateU32(reset_index_guest_endian); while (count >= kSimdVectorU32Elements) { count -= kSimdVectorU32Elements; SimdVectorU32 source_simd = LoadAlignedVectorU32(source); source += kSimdVectorU32Elements; SimdVectorU32 low_bits_mask_guest_endian_simd = ReplicateU32(low_bits_mask_guest_endian); #if XE_ARCH_AMD64 source_simd = _mm_and_si128(source_simd, low_bits_mask_guest_endian_simd); if (_mm_movemask_epi8( _mm_cmpeq_epi32(source_simd, reset_index_guest_endian_simd))) { return true; } #elif XE_ARCH_ARM64 source_simd = vandq_u32(source_simd, low_bits_mask_guest_endian_simd); uint64x1_t is_any = vreinterpret_u64_u32(vqmovn_u64(vreinterpretq_u64_u32( vceqq_u32(source_simd, reset_index_guest_endian_simd)))); if (*reinterpret_cast(&is_any)) { return true; } #else #error SIMD 32-bit IsResetUsed not implemented. #endif // XE_ARCH } } #endif // XE_GPU_PRIMITIVE_PROCESSOR_SIMD_SIZE while (count--) { if ((*(source++) & low_bits_mask_guest_endian) == reset_index_guest_endian) { return true; } } return false; } void PrimitiveProcessor::ReplaceResetIndex16To16( uint16_t* dest, const uint16_t* source, uint32_t count, uint16_t reset_index_guest_endian) { #if XE_GPU_PRIMITIVE_PROCESSOR_SIMD_SIZE while (count && (reinterpret_cast(source) & (XE_GPU_PRIMITIVE_PROCESSOR_SIMD_SIZE - 1))) { --count; uint16_t index = *(source++); *(dest++) = index != reset_index_guest_endian ? index : UINT16_MAX; } if (count >= kSimdVectorU16Elements) { SimdVectorU16 reset_index_guest_endian_simd = ReplicateU16(reset_index_guest_endian); while (count >= kSimdVectorU16Elements) { count -= kSimdVectorU16Elements; // Comparison produces 0 or 0xFFFF on AVX and Neon - we need 0xFFFF as the // result for the primitive reset indices, so the result is // `index | (index == reset_index)`. SimdVectorU16 source_simd = LoadAlignedVectorU16(source); source += kSimdVectorU16Elements; SimdVectorU16 result_simd; #if XE_ARCH_AMD64 result_simd = _mm_or_si128( source_simd, _mm_cmpeq_epi16(source_simd, reset_index_guest_endian_simd)); #elif XE_ARCH_ARM64 result_simd = vorrq_u16( source_simd, vceqq_u16(source_simd, reset_index_guest_endian_simd)); #else #error SIMD ReplaceResetIndex16To16 not implemented. #endif // XE_ARCH StoreUnalignedVectorU16(dest, result_simd); dest += kSimdVectorU16Elements; } } #endif // XE_GPU_PRIMITIVE_PROCESSOR_SIMD_SIZE while (count--) { uint16_t index = *(source++); *(dest++) = index != reset_index_guest_endian ? index : UINT16_MAX; } } void PrimitiveProcessor::ReplaceResetIndex16To24( uint32_t* dest, const uint16_t* source, uint32_t count, uint16_t reset_index_guest_endian) { #if XE_GPU_PRIMITIVE_PROCESSOR_SIMD_SIZE while (count && (reinterpret_cast(source) & (XE_GPU_PRIMITIVE_PROCESSOR_SIMD_SIZE - 1))) { --count; uint16_t index = *(source++); *(dest++) = index != reset_index_guest_endian ? index : UINT32_MAX; } if (count >= kSimdVectorU16Elements) { SimdVectorU16 reset_index_guest_endian_simd = ReplicateU16(reset_index_guest_endian); while (count >= kSimdVectorU16Elements) { count -= kSimdVectorU16Elements; SimdVectorU16 source_simd = LoadAlignedVectorU16(source); source += kSimdVectorU16Elements; // 1) Compare to the reset index as uint16, getting 0 or 0xFFFF. // 2) For primitive reset indices, replace the lower 16 bits with 0xFFFF // via OR with the comparison result. // 3) Expand to 32-bit, putting 0xFFFF in the upper 16 bits where // the comparison has passed, creating 0xFFFFFFFF for primitive reset // or 0x0000#### for non-primitive-reset indices (including // 0x0000FFFF if the original index buffer had 0xFFFF, but the // primitive reset index is different). // 4) Store. #if XE_ARCH_AMD64 __m128i are_reset = _mm_cmpeq_epi16(source_simd, reset_index_guest_endian_simd); __m128i result = _mm_or_si128(source_simd, are_reset); StoreUnalignedVectorU32(dest, _mm_unpacklo_epi16(result, are_reset)); // Expecting kSimdVectorU16Elements / 2 to be in the immediate offset // part of the address. StoreUnalignedVectorU32(dest + kSimdVectorU16Elements / 2, _mm_unpackhi_epi16(result, are_reset)); #elif XE_ARCH_ARM64 // Interleaving the indices and 0 / 0xFFFF via st2. uint16x8x2_t result; result.val[1] = vceqq_u16(source_simd, reset_index_guest_endian_simd); result.val[0] = vorrq_u16(source_simd, result.val[1]); vst2q_u16(reinterpret_cast(dest), result); #else #error SIMD ReplaceResetIndex16To24 not implemented. #endif // XE_ARCH dest += kSimdVectorU16Elements; } } #endif // XE_GPU_PRIMITIVE_PROCESSOR_SIMD_SIZE while (count--) { uint16_t index = *(source++); *(dest++) = index != reset_index_guest_endian ? index : UINT32_MAX; } } template void PrimitiveProcessor::ReplaceResetIndex32To24( uint32_t* dest, const uint32_t* source, uint32_t count, uint32_t reset_index_guest_endian, uint32_t low_bits_mask_guest_endian); template void PrimitiveProcessor::ReplaceResetIndex32To24( uint32_t* dest, const uint32_t* source, uint32_t count, uint32_t reset_index_guest_endian, uint32_t low_bits_mask_guest_endian); template void PrimitiveProcessor::ReplaceResetIndex32To24( uint32_t* dest, const uint32_t* source, uint32_t count, uint32_t reset_index_guest_endian, uint32_t low_bits_mask_guest_endian); template void PrimitiveProcessor::ReplaceResetIndex32To24( uint32_t* dest, const uint32_t* source, uint32_t count, uint32_t reset_index_guest_endian, uint32_t low_bits_mask_guest_endian); #define XE_GPU_PRIMITIVE_PROCESSOR_INSTANTIATE_CONVERSION_NO_PASSTHROUGH( \ ConverterName) \ template void PrimitiveProcessor::ConverterName( \ uint32_t* dest, const uint32_t* source, uint32_t source_index_count, \ const To24NonSwappingIndexTransform& index_transform); \ template void PrimitiveProcessor::ConverterName( \ uint32_t* dest, const uint32_t* source, uint32_t source_index_count, \ const To24Swapping8In16IndexTransform& index_transform); \ template void PrimitiveProcessor::ConverterName( \ uint32_t* dest, const uint32_t* source, uint32_t source_index_count, \ const To24Swapping8In32IndexTransform& index_transform); \ template void PrimitiveProcessor::ConverterName( \ uint32_t* dest, const uint32_t* source, uint32_t source_index_count, \ const To24Swapping16In32IndexTransform& index_transform); #define XE_GPU_PRIMITIVE_PROCESSOR_INSTANTIATE_CONVERSION(ConverterName) \ template void PrimitiveProcessor::ConverterName( \ uint16_t* dest, const uint16_t* source, uint32_t source_index_count, \ const PassthroughIndexTransform& index_transform); \ template void PrimitiveProcessor::ConverterName( \ uint32_t* dest, const uint32_t* source, uint32_t source_index_count, \ const PassthroughIndexTransform& index_transform); \ XE_GPU_PRIMITIVE_PROCESSOR_INSTANTIATE_CONVERSION_NO_PASSTHROUGH( \ ConverterName) XE_GPU_PRIMITIVE_PROCESSOR_INSTANTIATE_CONVERSION(TriangleFanToList) XE_GPU_PRIMITIVE_PROCESSOR_INSTANTIATE_CONVERSION_NO_PASSTHROUGH( LineLoopToStrip) // TODO(Triang3l): SIMD quad conversion maybe - 2 vectors to 3 vectors (though // multiple quads are rarely drawn anyway). XE_GPU_PRIMITIVE_PROCESSOR_INSTANTIATE_CONVERSION(QuadListToTriangleList) #undef XE_GPU_PRIMITIVE_PROCESSOR_INSTANTIATE_CONVERSION_NO_PASSTHROUGH #undef XE_GPU_PRIMITIVE_PROCESSOR_INSTANTIATE_CONVERSION void PrimitiveProcessor::LineLoopToStrip( uint16_t* dest, const uint16_t* source, uint32_t source_index_count, const PassthroughIndexTransform& index_transform) { if (source_index_count <= 1) { // To match GetLineLoopStripIndexCount. return; } std::memcpy(dest, source, sizeof(*source) * source_index_count); dest[source_index_count] = source[0]; } void PrimitiveProcessor::LineLoopToStrip( uint32_t* dest, const uint32_t* source, uint32_t source_index_count, const PassthroughIndexTransform& index_transform) { if (source_index_count <= 1) { // To match GetLineLoopStripIndexCount. return; } std::memcpy(dest, source, sizeof(*source) * source_index_count); dest[source_index_count] = source[0]; } uint32_t PrimitiveProcessor::GetMultiPrimitiveHostIndexCountAndRanges( std::function single_primitive_guest_to_host_count, const uint16_t* source, uint32_t source_index_count, uint16_t reset_index_guest_endian, std::deque& ranges_append_out) { uint32_t host_total_index_count = 0; uint32_t guest_index_offset = 0; for (;;) { uint32_t guest_primitive_offset = guest_index_offset; while (guest_index_offset < source_index_count) { if (source[guest_index_offset] == reset_index_guest_endian) { break; } ++guest_index_offset; } // Reset encountered or end of the index buffer - add the range. uint32_t guest_primitive_index_count = guest_index_offset - guest_primitive_offset; uint32_t host_primitive_index_count = single_primitive_guest_to_host_count(guest_primitive_index_count); if (host_primitive_index_count) { ranges_append_out.emplace_back(guest_primitive_offset, guest_primitive_index_count, host_primitive_index_count); host_total_index_count += host_primitive_index_count; } if (guest_index_offset >= source_index_count) { // End of the index buffer. break; } // Reset index encountered - skip. ++guest_index_offset; } return host_total_index_count; } uint32_t PrimitiveProcessor::GetMultiPrimitiveHostIndexCountAndRanges( std::function single_primitive_guest_to_host_count, const uint32_t* source, uint32_t source_index_count, uint32_t reset_index_guest_endian, uint32_t low_bits_mask_guest_endian, std::deque& ranges_append_out) { uint32_t host_total_index_count = 0; uint32_t guest_index_offset = 0; for (;;) { uint32_t guest_primitive_offset = guest_index_offset; while (guest_index_offset < source_index_count) { if ((source[guest_index_offset] & low_bits_mask_guest_endian) == reset_index_guest_endian) { break; } ++guest_index_offset; } // Reset encountered or end of the index buffer - add the range. uint32_t guest_primitive_index_count = guest_index_offset - guest_primitive_offset; uint32_t host_primitive_index_count = single_primitive_guest_to_host_count(guest_primitive_index_count); if (host_primitive_index_count) { ranges_append_out.emplace_back(guest_primitive_offset, guest_primitive_index_count, host_primitive_index_count); host_total_index_count += host_primitive_index_count; } if (guest_index_offset >= source_index_count) { // End of the index buffer. break; } // Reset index encountered - skip. ++guest_index_offset; } return host_total_index_count; } PrimitiveProcessor::CacheTransaction::CacheTransaction( PrimitiveProcessor& processor, CacheKey key) : processor_(processor), key_(key) { assert_zero(processor_.cache_currently_processing_size_bytes_); if (cvars::primitive_processor_cache_min_indices < 0 || key_.count < uint32_t(cvars::primitive_processor_cache_min_indices)) { // Don't cache if the vertex count is too small. key_.key = 0; } if (!key_.count) { return; } uint32_t size_bytes = (key_.format == xenos::IndexFormat::kInt16 ? sizeof(uint16_t) : sizeof(uint32_t)) * key_.count; { auto global_lock = processor_.global_critical_region_.Acquire(); auto cache_map_it = processor_.cache_map_.find(key_); if (cache_map_it != processor_.cache_map_.end()) { result_ = processor_.cache_entry_pool_[cache_map_it->second].result; result_type_ = ResultType::kExisting; } else { // Inhibit writing the new result if the range happens to be modified // during the processing outside the lock. processor_.cache_currently_processing_base_ = key_.base; processor_.cache_currently_processing_size_bytes_ = size_bytes; } } if (result_type_ != ResultType::kExisting) { // Enable the invalidation callback before reading the indices. // Also, only enable invalidation callbacks if anything needed processing at // all - don't waste time in the access violation handler doing nothing if // the guest doesn't use anything requiring host conversion. if (!processor_.memory_invalidation_callback_handle_) { processor_.memory_invalidation_callback_handle_ = processor_.memory_.RegisterPhysicalMemoryInvalidationCallback( MemoryInvalidationCallbackThunk, &processor_); } processor_.memory_.EnablePhysicalMemoryAccessCallbacks( key_.base, size_bytes, true, false); } } PrimitiveProcessor::CacheTransaction::~CacheTransaction() { if (!key_.count || result_type_ == ResultType::kExisting) { return; } auto global_lock = processor_.global_critical_region_.Acquire(); processor_.cache_currently_processing_base_ = 0; processor_.cache_currently_processing_size_bytes_ = 0; if (result_type_ == ResultType::kNewSet) { size_t new_entry_index; if (processor_.cache_bucket_free_first_entry_ != SIZE_MAX) { new_entry_index = processor_.cache_bucket_free_first_entry_; processor_.cache_bucket_free_first_entry_ = processor_.cache_entry_pool_[new_entry_index].free_next; } else { new_entry_index = processor_.cache_entry_pool_.size(); processor_.cache_entry_pool_.emplace_back(); } CacheEntry& new_entry = processor_.cache_entry_pool_[new_entry_index]; // Put the entry in 1 or 2 buckets. uint32_t bucket_start_index = key_.base >> kCacheBucketSizeBytesLog2; uint32_t bucket_count = CacheEntry::GetBucketCount(key_); for (uint32_t link_index = 0; link_index < bucket_count; ++link_index) { new_entry.buckets_prev[link_index] = SIZE_MAX; uint32_t bucket_index = bucket_start_index + link_index; uint64_t& bucket_non_empty_l1_ref = processor_.cache_buckets_non_empty_l1_[bucket_index >> 6]; uint64_t bucket_non_empty_l1_bit = uint64_t(1) << (bucket_index & 63); size_t& bucket_first_entry_ref = processor_.cache_bucket_first_entries_[bucket_index]; if (bucket_non_empty_l1_ref & bucket_non_empty_l1_bit) { // There is at least one entry already in the bucket - link to the // first. new_entry.buckets_next[link_index] = bucket_first_entry_ref; CacheEntry& bucket_first_entry = processor_.cache_entry_pool_[bucket_first_entry_ref]; // If the start ([0]) bucket of bucket_first_entry is bucket_index, // update its link [0]. Otherwise, since a cache entry may belong only // to at most 2 buckets, bucket_index must be its [1] bucket. bucket_first_entry .buckets_prev[size_t((bucket_first_entry.key.base >> kCacheBucketSizeBytesLog2) != bucket_index)] = new_entry_index; } else { new_entry.buckets_next[link_index] = SIZE_MAX; bucket_non_empty_l1_ref |= bucket_non_empty_l1_bit; processor_.UpdateCacheBucketsNonEmptyL2(bucket_index >> 6, global_lock); } bucket_first_entry_ref = new_entry_index; } new_entry.key = key_; new_entry.result = result_; processor_.cache_map_.emplace(key_, new_entry_index); } } std::pair PrimitiveProcessor::MemoryInvalidationCallback( uint32_t physical_address_start, uint32_t length, bool exact_range) { if (length == 0 || physical_address_start >= SharedMemory::kBufferSize) { return std::make_pair(uint32_t(0), UINT32_MAX); } length = std::min(length, SharedMemory::kBufferSize - physical_address_start); uint32_t physical_address_end = physical_address_start + length; if (!exact_range) { // Invalidate entire buckets if this is an access callback rather than // something like a file read to disable access violation handling for a // bigger range for higher performance. physical_address_start &= ~(kCacheBucketSizeBytes - 1); physical_address_end = xe::align(physical_address_end, kCacheBucketSizeBytes); } bool any_invalidated = false; uint32_t bucket_index_first = physical_address_start >> kCacheBucketSizeBytesLog2; uint32_t bucket_index_last = (physical_address_end - 1) >> kCacheBucketSizeBytesLog2; uint32_t bucket_l1_bits_index_first = bucket_index_first >> 6; uint32_t bucket_l1_bits_index_last = bucket_index_last >> 6; uint32_t bucket_l2_bits_index_first = bucket_index_first >> 12; uint32_t bucket_l2_bits_index_last = bucket_index_last >> 12; auto global_lock = global_critical_region_.Acquire(); for (uint32_t bucket_l2_bits_index = bucket_l2_bits_index_first; bucket_l2_bits_index <= bucket_l2_bits_index_last; ++bucket_l2_bits_index) { uint64_t bucket_l2_bits_mask = UINT64_MAX; if (bucket_l2_bits_index == bucket_l2_bits_index_first) { bucket_l2_bits_mask &= ~((uint64_t(1) << (bucket_l1_bits_index_first & 63)) - 1); } if (bucket_l2_bits_index == bucket_l2_bits_index_last && (bucket_l1_bits_index_last & 63) != 63) { bucket_l2_bits_mask &= (uint64_t(1) << ((bucket_l1_bits_index_last & 63) + 1)) - 1; } // Not caching L2 bits because they may be modified by unlinking. // Loop until any bits in the 64-bit portion of the L2 bit set are left. while (bucket_l2_bits_mask) { uint32_t bucket_l2_bit_shift; if (!xe::bit_scan_forward( cache_buckets_non_empty_l2_[bucket_l2_bits_index] & bucket_l2_bits_mask, &bucket_l2_bit_shift)) { break; } bucket_l2_bits_mask &= ~(uint64_t(1) << bucket_l2_bit_shift); uint32_t bucket_l1_bits_index = (bucket_l2_bits_index << 6) | bucket_l2_bit_shift; uint64_t bucket_l1_bits_mask = UINT64_MAX; if (bucket_l1_bits_index == bucket_l1_bits_index_first) { bucket_l1_bits_mask &= ~((uint64_t(1) << (bucket_index_first & 63)) - 1); } if (bucket_l1_bits_index == bucket_l1_bits_index_last && (bucket_index_last & 63) != 63) { bucket_l1_bits_mask &= (uint64_t(1) << ((bucket_index_last & 63) + 1)) - 1; } // Not caching L1 bits because they may be modified by unlinking. // Loop over buckets until any bits in the 64-bit portion of the L1 bit // set are left. while (bucket_l1_bits_mask) { uint32_t bucket_l1_bit_shift; if (!xe::bit_scan_forward( cache_buckets_non_empty_l1_[bucket_l1_bits_index] & bucket_l1_bits_mask, &bucket_l1_bit_shift)) { break; } bucket_l1_bits_mask &= ~(uint64_t(1) << bucket_l1_bit_shift); uint32_t bucket_index = (bucket_l1_bits_index << 6) | bucket_l1_bit_shift; // Invalidate the entries in the bucket, fully or partially. size_t entry_index = cache_bucket_first_entries_[bucket_index]; do { CacheEntry& entry = cache_entry_pool_[entry_index]; CacheKey entry_key = entry.key; // If the start ([0]) bucket of the entry is bucket_index, the link // within this bucket is its link [0]. Otherwise, since a cache entry // may belong only to at most 2 buckets, bucket_index must be its [1] // bucket. uint32_t entry_bucket_index_first = entry_key.base >> kCacheBucketSizeBytesLog2; assert_true((bucket_index - entry_bucket_index_first) <= 1, "Cache entries only store list links within two buckets"); size_t next_entry_index = entry.buckets_next[bucket_index - entry_bucket_index_first]; // For exact_range, don't invalidate bucket entries that are outside // the specified range. if (entry_key.base < physical_address_end) { uint32_t entry_end = entry_key.base + entry_key.GetSizeBytes(); if (entry_end > physical_address_end) { // Invalidate the entry. any_invalidated = true; // Remove the entry from the cache map. auto entry_map_it = cache_map_.find(entry_key); assert_true(entry_map_it != cache_map_.end()); if (entry_map_it != cache_map_.end()) { cache_map_.erase(entry_map_it); } // Unlink the entry from the bucket's list. uint32_t entry_link_index_last = ((entry_end - 1) >> kCacheBucketSizeBytesLog2) - entry_bucket_index_first; assert_true( entry_link_index_last <= 1, "Cache entries only store list links within two buckets"); for (uint32_t entry_link_index = 0; entry_link_index <= entry_link_index_last; ++entry_link_index) { uint32_t entry_bucket_index = entry_bucket_index_first + entry_link_index; size_t entry_link_prev = entry.buckets_prev[entry_link_index]; size_t entry_link_next = entry.buckets_next[entry_link_index]; if (entry_link_prev != SIZE_MAX) { CacheEntry& entry_prev = cache_entry_pool_[entry_link_prev]; entry_prev.buckets_next[size_t( (entry_prev.key.base >> kCacheBucketSizeBytesLog2) != entry_bucket_index)] = entry_link_next; } else { if (entry_link_next != SIZE_MAX) { cache_bucket_first_entries_[entry_bucket_index] = entry_link_next; } else { // The only entry that was remaining in the bucket - it's // empty now. cache_buckets_non_empty_l1_[entry_bucket_index >> 6] &= ~(uint64_t(1) << (entry_bucket_index & 63)); UpdateCacheBucketsNonEmptyL2(entry_bucket_index >> 6, global_lock); } } if (entry_link_next != SIZE_MAX) { CacheEntry& entry_next = cache_entry_pool_[entry_link_next]; entry_next.buckets_prev[size_t( (entry_next.key.base >> kCacheBucketSizeBytesLog2) != entry_bucket_index)] = entry_link_prev; } } // Make the entry free for reuse. entry.free_next = cache_bucket_free_first_entry_; cache_bucket_free_first_entry_ = entry_index; } } entry_index = next_entry_index; } while (entry_index != SIZE_MAX); } } } return any_invalidated ? std::make_pair(physical_address_start, physical_address_end - physical_address_start) : std::make_pair(uint32_t(0), UINT32_MAX); } std::pair PrimitiveProcessor::MemoryInvalidationCallbackThunk( void* context_ptr, uint32_t physical_address_start, uint32_t length, bool exact_range) { return reinterpret_cast(context_ptr) ->MemoryInvalidationCallback(physical_address_start, length, exact_range); } } // namespace gpu } // namespace xe