From c94b398f14c05697dc9cbb79aa31601b2ad0d70e Mon Sep 17 00:00:00 2001 From: ReinUsesLisp Date: Mon, 22 Jun 2020 03:03:27 -0300 Subject: [PATCH] vk_rasterizer: Use VK_EXT_extended_dynamic_state --- .../renderer_vulkan/vk_graphics_pipeline.cpp | 20 +- .../renderer_vulkan/vk_rasterizer.cpp | 254 +++++++++++++++--- .../renderer_vulkan/vk_rasterizer.h | 10 + .../renderer_vulkan/vk_state_tracker.cpp | 68 +++++ .../renderer_vulkan/vk_state_tracker.h | 50 ++++ 5 files changed, 356 insertions(+), 46 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index b892df412c..739801f5df 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp @@ -354,11 +354,27 @@ vk::Pipeline VKGraphicsPipeline::CreatePipeline(const RenderPassParams& renderpa color_blend_ci.pAttachments = cb_attachments.data(); std::memset(color_blend_ci.blendConstants, 0, sizeof(color_blend_ci.blendConstants)); - static constexpr std::array dynamic_states = { + std::vector dynamic_states = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR, VK_DYNAMIC_STATE_DEPTH_BIAS, VK_DYNAMIC_STATE_BLEND_CONSTANTS, VK_DYNAMIC_STATE_DEPTH_BOUNDS, VK_DYNAMIC_STATE_STENCIL_COMPARE_MASK, - VK_DYNAMIC_STATE_STENCIL_WRITE_MASK, VK_DYNAMIC_STATE_STENCIL_REFERENCE}; + VK_DYNAMIC_STATE_STENCIL_WRITE_MASK, VK_DYNAMIC_STATE_STENCIL_REFERENCE, + }; + if (device.IsExtExtendedDynamicStateSupported()) { + static constexpr std::array extended = { + VK_DYNAMIC_STATE_CULL_MODE_EXT, + VK_DYNAMIC_STATE_FRONT_FACE_EXT, + VK_DYNAMIC_STATE_PRIMITIVE_TOPOLOGY_EXT, + VK_DYNAMIC_STATE_VERTEX_INPUT_BINDING_STRIDE_EXT, + VK_DYNAMIC_STATE_DEPTH_TEST_ENABLE_EXT, + VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE_EXT, + VK_DYNAMIC_STATE_DEPTH_COMPARE_OP_EXT, + VK_DYNAMIC_STATE_DEPTH_BOUNDS_TEST_ENABLE_EXT, + VK_DYNAMIC_STATE_STENCIL_TEST_ENABLE_EXT, + VK_DYNAMIC_STATE_STENCIL_OP_EXT, + }; + dynamic_states.insert(dynamic_states.end(), extended.begin(), extended.end()); + } VkPipelineDynamicStateCreateInfo dynamic_state_ci; dynamic_state_ci.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index a42f8c5640..40d8df8c77 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -186,13 +186,22 @@ bool HasToPreserveDepthContents(bool is_clear, const Maxwell& regs) { scissor.max_y < regs.zeta_height; } +template +std::array ExpandStrides(const std::array& strides) { + std::array expanded; + std::copy(strides.begin(), strides.end(), expanded.begin()); + return expanded; +} + } // Anonymous namespace class BufferBindings final { public: - void AddVertexBinding(VkBuffer buffer, VkDeviceSize offset) { + void AddVertexBinding(VkBuffer buffer, VkDeviceSize offset, VkDeviceSize size, u32 stride) { vertex.buffers[vertex.num_buffers] = buffer; vertex.offsets[vertex.num_buffers] = offset; + vertex.sizes[vertex.num_buffers] = size; + vertex.strides[vertex.num_buffers] = static_cast(stride); ++vertex.num_buffers; } @@ -202,76 +211,76 @@ public: index.type = type; } - void Bind(VKScheduler& scheduler) const { + void Bind(const VKDevice& device, VKScheduler& scheduler) const { // Use this large switch case to avoid dispatching more memory in the record lambda than // what we need. It looks horrible, but it's the best we can do on standard C++. switch (vertex.num_buffers) { case 0: - return BindStatic<0>(scheduler); + return BindStatic<0>(device, scheduler); case 1: - return BindStatic<1>(scheduler); + return BindStatic<1>(device, scheduler); case 2: - return BindStatic<2>(scheduler); + return BindStatic<2>(device, scheduler); case 3: - return BindStatic<3>(scheduler); + return BindStatic<3>(device, scheduler); case 4: - return BindStatic<4>(scheduler); + return BindStatic<4>(device, scheduler); case 5: - return BindStatic<5>(scheduler); + return BindStatic<5>(device, scheduler); case 6: - return BindStatic<6>(scheduler); + return BindStatic<6>(device, scheduler); case 7: - return BindStatic<7>(scheduler); + return BindStatic<7>(device, scheduler); case 8: - return BindStatic<8>(scheduler); + return BindStatic<8>(device, scheduler); case 9: - return BindStatic<9>(scheduler); + return BindStatic<9>(device, scheduler); case 10: - return BindStatic<10>(scheduler); + return BindStatic<10>(device, scheduler); case 11: - return BindStatic<11>(scheduler); + return BindStatic<11>(device, scheduler); case 12: - return BindStatic<12>(scheduler); + return BindStatic<12>(device, scheduler); case 13: - return BindStatic<13>(scheduler); + return BindStatic<13>(device, scheduler); case 14: - return BindStatic<14>(scheduler); + return BindStatic<14>(device, scheduler); case 15: - return BindStatic<15>(scheduler); + return BindStatic<15>(device, scheduler); case 16: - return BindStatic<16>(scheduler); + return BindStatic<16>(device, scheduler); case 17: - return BindStatic<17>(scheduler); + return BindStatic<17>(device, scheduler); case 18: - return BindStatic<18>(scheduler); + return BindStatic<18>(device, scheduler); case 19: - return BindStatic<19>(scheduler); + return BindStatic<19>(device, scheduler); case 20: - return BindStatic<20>(scheduler); + return BindStatic<20>(device, scheduler); case 21: - return BindStatic<21>(scheduler); + return BindStatic<21>(device, scheduler); case 22: - return BindStatic<22>(scheduler); + return BindStatic<22>(device, scheduler); case 23: - return BindStatic<23>(scheduler); + return BindStatic<23>(device, scheduler); case 24: - return BindStatic<24>(scheduler); + return BindStatic<24>(device, scheduler); case 25: - return BindStatic<25>(scheduler); + return BindStatic<25>(device, scheduler); case 26: - return BindStatic<26>(scheduler); + return BindStatic<26>(device, scheduler); case 27: - return BindStatic<27>(scheduler); + return BindStatic<27>(device, scheduler); case 28: - return BindStatic<28>(scheduler); + return BindStatic<28>(device, scheduler); case 29: - return BindStatic<29>(scheduler); + return BindStatic<29>(device, scheduler); case 30: - return BindStatic<30>(scheduler); + return BindStatic<30>(device, scheduler); case 31: - return BindStatic<31>(scheduler); + return BindStatic<31>(device, scheduler); case 32: - return BindStatic<32>(scheduler); + return BindStatic<32>(device, scheduler); } UNREACHABLE(); } @@ -282,6 +291,8 @@ private: std::size_t num_buffers = 0; std::array buffers; std::array offsets; + std::array sizes; + std::array strides; } vertex; struct { @@ -291,15 +302,23 @@ private: } index; template - void BindStatic(VKScheduler& scheduler) const { - if (index.buffer) { - BindStatic(scheduler); + void BindStatic(const VKDevice& device, VKScheduler& scheduler) const { + if (device.IsExtExtendedDynamicStateSupported()) { + if (index.buffer) { + BindStatic(scheduler); + } else { + BindStatic(scheduler); + } } else { - BindStatic(scheduler); + if (index.buffer) { + BindStatic(scheduler); + } else { + BindStatic(scheduler); + } } } - template + template void BindStatic(VKScheduler& scheduler) const { static_assert(N <= Maxwell::NumVertexArrays); if constexpr (N == 0) { @@ -311,6 +330,31 @@ private: std::copy(vertex.buffers.begin(), vertex.buffers.begin() + N, buffers.begin()); std::copy(vertex.offsets.begin(), vertex.offsets.begin() + N, offsets.begin()); + if constexpr (has_extended_dynamic_state) { + // With extended dynamic states we can specify the length and stride of a vertex buffer + std::array sizes; + std::array strides; + std::copy(vertex.sizes.begin(), vertex.sizes.begin() + N, sizes.begin()); + std::copy(vertex.strides.begin(), vertex.strides.begin() + N, strides.begin()); + + if constexpr (is_indexed) { + scheduler.Record( + [buffers, offsets, sizes, strides, index = index](vk::CommandBuffer cmdbuf) { + cmdbuf.BindIndexBuffer(index.buffer, index.offset, index.type); + cmdbuf.BindVertexBuffers2EXT(0, static_cast(N), buffers.data(), + offsets.data(), sizes.data(), + ExpandStrides(strides).data()); + }); + } else { + scheduler.Record([buffers, offsets, sizes, strides](vk::CommandBuffer cmdbuf) { + cmdbuf.BindVertexBuffers2EXT(0, static_cast(N), buffers.data(), + offsets.data(), sizes.data(), + ExpandStrides(strides).data()); + }); + } + return; + } + if constexpr (is_indexed) { // Indexed draw scheduler.Record([buffers, offsets, index = index](vk::CommandBuffer cmdbuf) { @@ -402,7 +446,7 @@ void RasterizerVulkan::Draw(bool is_indexed, bool is_instanced) { UpdateDynamicStates(); - buffer_bindings.Bind(scheduler); + buffer_bindings.Bind(device, scheduler); BeginTransformFeedback(); @@ -893,6 +937,17 @@ void RasterizerVulkan::UpdateDynamicStates() { UpdateBlendConstants(regs); UpdateDepthBounds(regs); UpdateStencilFaces(regs); + if (device.IsExtExtendedDynamicStateSupported()) { + UpdateCullMode(regs); + UpdateDepthBoundsTestEnable(regs); + UpdateDepthTestEnable(regs); + UpdateDepthWriteEnable(regs); + UpdateDepthCompareOp(regs); + UpdateFrontFace(regs); + UpdatePrimitiveTopology(regs); + UpdateStencilOp(regs); + UpdateStencilTestEnable(regs); + } } void RasterizerVulkan::BeginTransformFeedback() { @@ -952,13 +1007,13 @@ void RasterizerVulkan::SetupVertexArrays(BufferBindings& buffer_bindings) { const GPUVAddr end{regs.vertex_array_limit[index].LimitAddress()}; ASSERT(end >= start); - const std::size_t size{end - start}; + const std::size_t size = end - start; if (size == 0) { - buffer_bindings.AddVertexBinding(DefaultBuffer(), 0); + buffer_bindings.AddVertexBinding(DefaultBuffer(), 0, DEFAULT_BUFFER_SIZE, 0); continue; } const auto info = buffer_cache.UploadMemory(start, size); - buffer_bindings.AddVertexBinding(info.handle, info.offset); + buffer_bindings.AddVertexBinding(info.handle, info.offset, size, vertex_array.stride); } } @@ -1310,6 +1365,117 @@ void RasterizerVulkan::UpdateStencilFaces(Tegra::Engines::Maxwell3D::Regs& regs) } } +void RasterizerVulkan::UpdateCullMode(Tegra::Engines::Maxwell3D::Regs& regs) { + if (!state_tracker.TouchCullMode()) { + return; + } + scheduler.Record( + [enabled = regs.cull_test_enabled, cull_face = regs.cull_face](vk::CommandBuffer cmdbuf) { + cmdbuf.SetCullModeEXT(enabled ? MaxwellToVK::CullFace(cull_face) : VK_CULL_MODE_NONE); + }); +} + +void RasterizerVulkan::UpdateDepthBoundsTestEnable(Tegra::Engines::Maxwell3D::Regs& regs) { + if (!state_tracker.TouchDepthBoundsTestEnable()) { + return; + } + scheduler.Record([enable = regs.depth_bounds_enable](vk::CommandBuffer cmdbuf) { + cmdbuf.SetDepthBoundsTestEnableEXT(enable); + }); +} + +void RasterizerVulkan::UpdateDepthTestEnable(Tegra::Engines::Maxwell3D::Regs& regs) { + if (!state_tracker.TouchDepthTestEnable()) { + return; + } + scheduler.Record([enable = regs.depth_test_enable](vk::CommandBuffer cmdbuf) { + cmdbuf.SetDepthTestEnableEXT(enable); + }); +} + +void RasterizerVulkan::UpdateDepthWriteEnable(Tegra::Engines::Maxwell3D::Regs& regs) { + if (!state_tracker.TouchDepthWriteEnable()) { + return; + } + scheduler.Record([enable = regs.depth_write_enabled](vk::CommandBuffer cmdbuf) { + cmdbuf.SetDepthWriteEnableEXT(enable); + }); +} + +void RasterizerVulkan::UpdateDepthCompareOp(Tegra::Engines::Maxwell3D::Regs& regs) { + if (!state_tracker.TouchDepthCompareOp()) { + return; + } + scheduler.Record([func = regs.depth_test_func](vk::CommandBuffer cmdbuf) { + cmdbuf.SetDepthCompareOpEXT(MaxwellToVK::ComparisonOp(func)); + }); +} + +void RasterizerVulkan::UpdateFrontFace(Tegra::Engines::Maxwell3D::Regs& regs) { + if (!state_tracker.TouchFrontFace()) { + return; + } + + VkFrontFace front_face = MaxwellToVK::FrontFace(regs.front_face); + if (regs.screen_y_control.triangle_rast_flip != 0) { + front_face = front_face == VK_FRONT_FACE_CLOCKWISE ? VK_FRONT_FACE_COUNTER_CLOCKWISE + : VK_FRONT_FACE_CLOCKWISE; + } + scheduler.Record( + [front_face](vk::CommandBuffer cmdbuf) { cmdbuf.SetFrontFaceEXT(front_face); }); +} + +void RasterizerVulkan::UpdatePrimitiveTopology(Tegra::Engines::Maxwell3D::Regs& regs) { + if (!state_tracker.TouchPrimitiveTopology()) { + return; + } + const Maxwell::PrimitiveTopology primitive_topology = regs.draw.topology.Value(); + scheduler.Record([this, primitive_topology](vk::CommandBuffer cmdbuf) { + cmdbuf.SetPrimitiveTopologyEXT(MaxwellToVK::PrimitiveTopology(device, primitive_topology)); + }); +} + +void RasterizerVulkan::UpdateStencilOp(Tegra::Engines::Maxwell3D::Regs& regs) { + if (!state_tracker.TouchStencilOp()) { + return; + } + const Maxwell::StencilOp fail = regs.stencil_front_op_fail; + const Maxwell::StencilOp zfail = regs.stencil_front_op_zfail; + const Maxwell::StencilOp zpass = regs.stencil_front_op_zpass; + const Maxwell::ComparisonOp compare = regs.stencil_front_func_func; + if (regs.stencil_two_side_enable) { + scheduler.Record([fail, zfail, zpass, compare](vk::CommandBuffer cmdbuf) { + cmdbuf.SetStencilOpEXT(VK_STENCIL_FACE_FRONT_AND_BACK, MaxwellToVK::StencilOp(fail), + MaxwellToVK::StencilOp(zpass), MaxwellToVK::StencilOp(zfail), + MaxwellToVK::ComparisonOp(compare)); + }); + } else { + const Maxwell::StencilOp back_fail = regs.stencil_back_op_fail; + const Maxwell::StencilOp back_zfail = regs.stencil_back_op_zfail; + const Maxwell::StencilOp back_zpass = regs.stencil_back_op_zpass; + const Maxwell::ComparisonOp back_compare = regs.stencil_back_func_func; + scheduler.Record([fail, zfail, zpass, compare, back_fail, back_zfail, back_zpass, + back_compare](vk::CommandBuffer cmdbuf) { + cmdbuf.SetStencilOpEXT(VK_STENCIL_FACE_FRONT_BIT, MaxwellToVK::StencilOp(fail), + MaxwellToVK::StencilOp(zpass), MaxwellToVK::StencilOp(zfail), + MaxwellToVK::ComparisonOp(compare)); + cmdbuf.SetStencilOpEXT(VK_STENCIL_FACE_BACK_BIT, MaxwellToVK::StencilOp(back_fail), + MaxwellToVK::StencilOp(back_zpass), + MaxwellToVK::StencilOp(back_zfail), + MaxwellToVK::ComparisonOp(back_compare)); + }); + } +} + +void RasterizerVulkan::UpdateStencilTestEnable(Tegra::Engines::Maxwell3D::Regs& regs) { + if (!state_tracker.TouchStencilTestEnable()) { + return; + } + scheduler.Record([enable = regs.stencil_enable](vk::CommandBuffer cmdbuf) { + cmdbuf.SetStencilTestEnableEXT(enable); + }); +} + std::size_t RasterizerVulkan::CalculateGraphicsStreamBufferSize(bool is_indexed) const { std::size_t size = CalculateVertexArraysSize(); if (is_indexed) { diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.h b/src/video_core/renderer_vulkan/vk_rasterizer.h index e1cd1e3929..923178b0bb 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.h +++ b/src/video_core/renderer_vulkan/vk_rasterizer.h @@ -245,6 +245,16 @@ private: void UpdateDepthBounds(Tegra::Engines::Maxwell3D::Regs& regs); void UpdateStencilFaces(Tegra::Engines::Maxwell3D::Regs& regs); + void UpdateCullMode(Tegra::Engines::Maxwell3D::Regs& regs); + void UpdateDepthBoundsTestEnable(Tegra::Engines::Maxwell3D::Regs& regs); + void UpdateDepthTestEnable(Tegra::Engines::Maxwell3D::Regs& regs); + void UpdateDepthWriteEnable(Tegra::Engines::Maxwell3D::Regs& regs); + void UpdateDepthCompareOp(Tegra::Engines::Maxwell3D::Regs& regs); + void UpdateFrontFace(Tegra::Engines::Maxwell3D::Regs& regs); + void UpdatePrimitiveTopology(Tegra::Engines::Maxwell3D::Regs& regs); + void UpdateStencilOp(Tegra::Engines::Maxwell3D::Regs& regs); + void UpdateStencilTestEnable(Tegra::Engines::Maxwell3D::Regs& regs); + std::size_t CalculateGraphicsStreamBufferSize(bool is_indexed) const; std::size_t CalculateComputeStreamBufferSize() const; diff --git a/src/video_core/renderer_vulkan/vk_state_tracker.cpp b/src/video_core/renderer_vulkan/vk_state_tracker.cpp index 94a89e3885..e5a583dd5b 100644 --- a/src/video_core/renderer_vulkan/vk_state_tracker.cpp +++ b/src/video_core/renderer_vulkan/vk_state_tracker.cpp @@ -36,6 +36,15 @@ Flags MakeInvalidationFlags() { flags[BlendConstants] = true; flags[DepthBounds] = true; flags[StencilProperties] = true; + flags[CullMode] = true; + flags[DepthBoundsEnable] = true; + flags[DepthTestEnable] = true; + flags[DepthWriteEnable] = true; + flags[DepthCompareOp] = true; + flags[FrontFace] = true; + flags[PrimitiveTopology] = true; + flags[StencilOp] = true; + flags[StencilTestEnable] = true; return flags; } @@ -75,6 +84,57 @@ void SetupDirtyStencilProperties(Tables& tables) { table[OFF(stencil_back_func_mask)] = StencilProperties; } +void SetupDirtyCullMode(Tables& tables) { + auto& table = tables[0]; + table[OFF(cull_face)] = CullMode; + table[OFF(cull_test_enabled)] = CullMode; +} + +void SetupDirtyDepthBoundsEnable(Tables& tables) { + tables[0][OFF(depth_bounds_enable)] = DepthBoundsEnable; +} + +void SetupDirtyDepthTestEnable(Tables& tables) { + tables[0][OFF(depth_test_enable)] = DepthTestEnable; +} + +void SetupDirtyDepthWriteEnable(Tables& tables) { + tables[0][OFF(depth_write_enabled)] = DepthWriteEnable; +} + +void SetupDirtyDepthCompareOp(Tables& tables) { + tables[0][OFF(depth_test_func)] = DepthCompareOp; +} + +void SetupDirtyFrontFace(Tables& tables) { + auto& table = tables[0]; + table[OFF(front_face)] = FrontFace; + table[OFF(screen_y_control)] = FrontFace; +} + +void SetupDirtyPrimitiveTopology(Tables& tables) { + tables[0][OFF(draw.topology)] = PrimitiveTopology; +} + +void SetupDirtyStencilOp(Tables& tables) { + auto& table = tables[0]; + table[OFF(stencil_front_op_fail)] = StencilOp; + table[OFF(stencil_front_op_zfail)] = StencilOp; + table[OFF(stencil_front_op_zpass)] = StencilOp; + table[OFF(stencil_front_func_func)] = StencilOp; + table[OFF(stencil_back_op_fail)] = StencilOp; + table[OFF(stencil_back_op_zfail)] = StencilOp; + table[OFF(stencil_back_op_zpass)] = StencilOp; + table[OFF(stencil_back_func_func)] = StencilOp; + + // Table 0 is used by StencilProperties + tables[1][OFF(stencil_two_side_enable)] = StencilOp; +} + +void SetupDirtyStencilTestEnable(Tables& tables) { + tables[0][OFF(stencil_enable)] = StencilTestEnable; +} + } // Anonymous namespace StateTracker::StateTracker(Core::System& system) @@ -90,6 +150,14 @@ void StateTracker::Initialize() { SetupDirtyBlendConstants(tables); SetupDirtyDepthBounds(tables); SetupDirtyStencilProperties(tables); + SetupDirtyCullMode(tables); + SetupDirtyDepthBoundsEnable(tables); + SetupDirtyDepthTestEnable(tables); + SetupDirtyDepthWriteEnable(tables); + SetupDirtyDepthCompareOp(tables); + SetupDirtyFrontFace(tables); + SetupDirtyPrimitiveTopology(tables); + SetupDirtyStencilOp(tables); } void StateTracker::InvalidateCommandBufferState() { diff --git a/src/video_core/renderer_vulkan/vk_state_tracker.h b/src/video_core/renderer_vulkan/vk_state_tracker.h index 03bc415b25..54ca0d6c68 100644 --- a/src/video_core/renderer_vulkan/vk_state_tracker.h +++ b/src/video_core/renderer_vulkan/vk_state_tracker.h @@ -26,6 +26,16 @@ enum : u8 { DepthBounds, StencilProperties, + CullMode, + DepthBoundsEnable, + DepthTestEnable, + DepthWriteEnable, + DepthCompareOp, + FrontFace, + PrimitiveTopology, + StencilOp, + StencilTestEnable, + Last }; static_assert(Last <= std::numeric_limits::max()); @@ -64,6 +74,46 @@ public: return Exchange(Dirty::StencilProperties, false); } + bool TouchCullMode() { + return Exchange(Dirty::CullMode, false); + } + + bool TouchDepthBoundsTestEnable() { + return Exchange(Dirty::DepthBoundsEnable, false); + } + + bool TouchDepthTestEnable() { + return Exchange(Dirty::DepthTestEnable, false); + } + + bool TouchDepthBoundsEnable() { + return Exchange(Dirty::DepthBoundsEnable, false); + } + + bool TouchDepthWriteEnable() { + return Exchange(Dirty::DepthWriteEnable, false); + } + + bool TouchDepthCompareOp() { + return Exchange(Dirty::DepthCompareOp, false); + } + + bool TouchFrontFace() { + return Exchange(Dirty::FrontFace, false); + } + + bool TouchPrimitiveTopology() { + return Exchange(Dirty::PrimitiveTopology, false); + } + + bool TouchStencilOp() { + return Exchange(Dirty::StencilOp, false); + } + + bool TouchStencilTestEnable() { + return Exchange(Dirty::StencilTestEnable, false); + } + private: bool Exchange(std::size_t id, bool new_value) const noexcept { auto& flags = system.GPU().Maxwell3D().dirty.flags;