mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-10-05 13:42:00 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 355f9d84ef |
@@ -653,6 +653,9 @@ struct Values {
|
||||
SwitchableSetting<bool> emulate_bgr565{linkage, false, "emulate_bgr565",
|
||||
Category::RendererHacks};
|
||||
|
||||
SwitchableSetting<bool> rescale_hack{linkage, false, "rescale_hack",
|
||||
Category::RendererHacks};
|
||||
|
||||
SwitchableSetting<bool> use_asynchronous_shaders{linkage, false, "use_asynchronous_shaders",
|
||||
Category::RendererHacks};
|
||||
|
||||
|
||||
@@ -271,6 +271,12 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent) {
|
||||
"quality and performance consistency in some games."));
|
||||
INSERT(Settings, fix_bloom_effects, tr("Fix bloom effects"), tr("Removes bloom in Burnout."));
|
||||
|
||||
INSERT(Settings, rescale_hack, tr("Enable Legacy Rescale Pass"),
|
||||
tr("May fix rescale issues in some games by relying on behavior from the previous "
|
||||
"implementation.\n"
|
||||
"Legacy behavior workaround that fixes line artifacts on AMD and Intel GPUs, and "
|
||||
"grey texture flicker on Nvidia GPUs in Luigis Mansion 3."));
|
||||
|
||||
// Renderer (Extensions)
|
||||
INSERT(Settings, dyna_state, tr("Extended Dynamic State"),
|
||||
tr("Controls the number of features that can be used in Extended Dynamic State.\n"
|
||||
|
||||
@@ -301,7 +301,7 @@ IR::Program TranslateProgram(ObjectPool<IR::Inst>& inst_pool, ObjectPool<IR::Blo
|
||||
Optimization::GlobalMemoryToStorageBufferPass(program, normalized_host_info);
|
||||
Optimization::TexturePass(env, program, normalized_host_info);
|
||||
|
||||
if (Settings::values.resolution_info.active) {
|
||||
if (Settings::values.resolution_info.active || Settings::values.rescale_hack.GetValue()) {
|
||||
Optimization::RescalingPass(program);
|
||||
}
|
||||
Optimization::DeadCodeEliminationPass(program);
|
||||
|
||||
@@ -38,6 +38,8 @@ struct HostTranslateInfo {
|
||||
///< passthrough shaders
|
||||
bool support_conditional_barrier{}; ///< True when the device supports barriers in conditional
|
||||
///< control flow
|
||||
bool support_attribute_texture_handle{}; ///< True when the host binds texture handles that
|
||||
///< arrive as vertex attributes
|
||||
|
||||
void ApplyDescriptorLimitPolicy() noexcept {
|
||||
if (min_ssbo_alignment == 0) {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -31,7 +28,6 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
|
||||
void LayerPass(IR::Program& program, const HostTranslateInfo& host_info);
|
||||
void VendorWorkaroundPass(IR::Program& program);
|
||||
void VerificationPass(const IR::Program& program);
|
||||
bool HasBrokenPattern(const IR::Program& program);
|
||||
|
||||
// Dual Vertex
|
||||
void VertexATransformPass(IR::Program& program);
|
||||
|
||||
@@ -67,11 +67,18 @@ void VisitMark(IR::Block& block, IR::Inst& inst) {
|
||||
if (must_patch_outside) {
|
||||
const auto it{IR::Block::InstructionList::s_iterator_to(inst)};
|
||||
IR::IREmitter ir{block, it};
|
||||
IR::Inst* const new_inst{&*block.PrependNewInst(it, inst)};
|
||||
const IR::F32 new_bitcast{ir.ConvertUToF(32, 32, IR::Value{new_inst})};
|
||||
const IR::F32 up_factor{ir.FPRecip(ir.ResolutionDownFactor())};
|
||||
const IR::Value converted{ir.FPMul(new_bitcast, up_factor)};
|
||||
inst.ReplaceUsesWith(converted);
|
||||
if (Settings::values.rescale_hack.GetValue()) {
|
||||
const IR::F32 new_inst{&*block.PrependNewInst(it, inst)};
|
||||
const IR::F32 up_factor{ir.FPRecip(ir.ResolutionDownFactor())};
|
||||
const IR::Value converted{ir.FPMul(new_inst, up_factor)};
|
||||
inst.ReplaceUsesWith(converted);
|
||||
} else {
|
||||
IR::Inst* const new_inst{&*block.PrependNewInst(it, inst)};
|
||||
const IR::F32 new_bitcast{ir.ConvertUToF(32, 32, IR::Value{new_inst})};
|
||||
const IR::F32 up_factor{ir.FPRecip(ir.ResolutionDownFactor())};
|
||||
const IR::Value converted{ir.FPMul(new_bitcast, up_factor)};
|
||||
inst.ReplaceUsesWith(converted);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -268,6 +268,10 @@ static inline u32 ReadCbufCached(Environment& env, u32 index, u32 offset) {
|
||||
return v;
|
||||
}
|
||||
|
||||
static inline bool IsAttributeHandle(const ConstBufferAddr& cbuf) {
|
||||
return cbuf.index == ATTRIBUTE_HANDLE_CBUF_INDEX;
|
||||
}
|
||||
|
||||
static inline u32 GetTextureHandleCached(Environment& env, const ConstBufferAddr& cbuf) {
|
||||
// Must all be uniquely different variables
|
||||
// If has secondary, then it will be cbuf.secondary_{index|offset}, else its 0.
|
||||
@@ -287,14 +291,23 @@ static inline u32 GetTextureHandleCached(Environment& env, const ConstBufferAddr
|
||||
|
||||
// Cached variants of existing helpers
|
||||
static inline TextureType ReadTextureTypeCached(Environment& env, const ConstBufferAddr& cbuf) {
|
||||
if (IsAttributeHandle(cbuf)) {
|
||||
return TextureType::Color2D;
|
||||
}
|
||||
return env.ReadTextureType(GetTextureHandleCached(env, cbuf));
|
||||
}
|
||||
static inline TexturePixelFormat ReadTexturePixelFormatCached(Environment& env,
|
||||
const ConstBufferAddr& cbuf) {
|
||||
if (IsAttributeHandle(cbuf)) {
|
||||
return TexturePixelFormat::A8B8G8R8_UNORM;
|
||||
}
|
||||
return env.ReadTexturePixelFormat(GetTextureHandleCached(env, cbuf));
|
||||
}
|
||||
static inline bool IsTexturePixelFormatIntegerCached(Environment& env,
|
||||
const ConstBufferAddr& cbuf) {
|
||||
if (IsAttributeHandle(cbuf)) {
|
||||
return false;
|
||||
}
|
||||
return env.IsTexturePixelFormatInteger(GetTextureHandleCached(env, cbuf));
|
||||
}
|
||||
|
||||
@@ -466,15 +479,43 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
|
||||
};
|
||||
}
|
||||
|
||||
bool IsAttributeSourcedHandle(const IR::Value& handle) {
|
||||
if (handle.IsImmediate()) {
|
||||
return false;
|
||||
}
|
||||
const IR::Inst* inst{handle.InstRecursive()};
|
||||
if (inst->GetOpcode() == IR::Opcode::BitCastU32F32 && !inst->Arg(0).IsImmediate()) {
|
||||
inst = inst->Arg(0).InstRecursive();
|
||||
}
|
||||
return inst->GetOpcode() == IR::Opcode::GetAttribute ||
|
||||
inst->GetOpcode() == IR::Opcode::GetAttributeU32;
|
||||
}
|
||||
|
||||
TextureInst MakeInst(Environment& env, IR::Block* block, IR::Inst& inst, const HostTranslateInfo& host_info) {
|
||||
ConstBufferAddr addr;
|
||||
if (IsBindless(inst)) {
|
||||
const std::optional<ConstBufferAddr> track_addr{TrackCached(inst.Arg(0), env, host_info)};
|
||||
|
||||
if (!track_addr) {
|
||||
throw NotImplementedException("Failed to track bindless texture constant buffer");
|
||||
} else {
|
||||
if (track_addr) {
|
||||
addr = *track_addr;
|
||||
} else if (host_info.support_attribute_texture_handle &&
|
||||
IsAttributeSourcedHandle(inst.Arg(0))) {
|
||||
// The handle travels with the vertices (sprite batches pick the texture per quad), so
|
||||
// there is no constant buffer to read it from. The host binds the handle of each run
|
||||
// of vertices that share one.
|
||||
addr = ConstBufferAddr{
|
||||
.index = ATTRIBUTE_HANDLE_CBUF_INDEX,
|
||||
.offset = 0,
|
||||
.shift_left = 0,
|
||||
.secondary_index = 0,
|
||||
.secondary_offset = 0,
|
||||
.secondary_shift_left = 0,
|
||||
.dynamic_offset = {},
|
||||
.count = 1,
|
||||
.has_secondary = false,
|
||||
};
|
||||
} else {
|
||||
throw NotImplementedException("Failed to track bindless texture constant buffer");
|
||||
}
|
||||
} else {
|
||||
addr = ConstBufferAddr{
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -79,44 +76,4 @@ void VendorWorkaroundPass(IR::Program& program) {
|
||||
}
|
||||
}
|
||||
|
||||
bool HasBrokenPattern(const IR::Program& program) {
|
||||
if (program.stage != Stage::Fragment) {
|
||||
return false;
|
||||
}
|
||||
for (const IR::Block* block : program.post_order_blocks) {
|
||||
for (const IR::Inst& inst : block->Instructions()) {
|
||||
switch (inst.GetOpcode()) {
|
||||
case IR::Opcode::ShuffleIndex:
|
||||
case IR::Opcode::ShuffleUp:
|
||||
case IR::Opcode::ShuffleDown:
|
||||
case IR::Opcode::ShuffleButterfly:
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
const IR::Value shuffle_arg{inst.Arg(0)};
|
||||
if (shuffle_arg.IsImmediate()) {
|
||||
continue;
|
||||
}
|
||||
const IR::Inst* bitcast{shuffle_arg.InstRecursive()};
|
||||
if (!bitcast || bitcast->GetOpcode() != IR::Opcode::BitCastU32F32) {
|
||||
continue;
|
||||
}
|
||||
const IR::Value bitcast_arg{bitcast->Arg(0)};
|
||||
if (bitcast_arg.IsImmediate()) {
|
||||
continue;
|
||||
}
|
||||
const IR::Inst* attribute{bitcast_arg.InstRecursive()};
|
||||
if (!attribute || attribute->GetOpcode() != IR::Opcode::GetAttribute) {
|
||||
continue;
|
||||
}
|
||||
const IR::Attribute attr{attribute->Arg(0).Attribute()};
|
||||
if (attr == IR::Attribute::PositionX || attr == IR::Attribute::PositionY) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Shader::Optimization
|
||||
|
||||
@@ -38,6 +38,10 @@ enum class TextureType : u32 {
|
||||
};
|
||||
constexpr u32 NUM_TEXTURE_TYPES = 9;
|
||||
|
||||
/// Constant buffer index marking a texture handle that the shader receives as a vertex attribute
|
||||
/// instead of reading it from a constant buffer. The host supplies the handle of each draw run.
|
||||
constexpr u32 ATTRIBUTE_HANDLE_CBUF_INDEX = 0xFFFF;
|
||||
|
||||
enum class TexturePixelFormat {
|
||||
A8B8G8R8_UNORM,
|
||||
A8B8G8R8_SNORM,
|
||||
|
||||
@@ -276,6 +276,9 @@ GraphicsPipeline::GraphicsPipeline(
|
||||
num_textures += Shader::NumDescriptors(info->texture_descriptors);
|
||||
num_image_elements += Shader::NumDescriptors(info->texture_descriptors);
|
||||
num_image_elements += Shader::NumDescriptors(info->image_descriptors);
|
||||
for (const auto& desc : info->texture_descriptors) {
|
||||
uses_attribute_handle |= desc.cbuf_index == Shader::ATTRIBUTE_HANDLE_CBUF_INDEX;
|
||||
}
|
||||
num_descriptor_entries += NumDescriptorEntries(*info);
|
||||
}
|
||||
fragment_has_color0_output = stage_infos[NUM_STAGES - 1].stores_frag_color[0];
|
||||
@@ -375,6 +378,9 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) {
|
||||
}
|
||||
const auto& cbufs{maxwell3d->state.shader_stages[stage].const_buffers};
|
||||
const auto read_handle{[&](const auto& desc, u32 index) {
|
||||
if (desc.cbuf_index == Shader::ATTRIBUTE_HANDLE_CBUF_INDEX) {
|
||||
return TexturePair(attribute_handle, via_header_index);
|
||||
}
|
||||
ASSERT(cbufs[desc.cbuf_index].enabled);
|
||||
const u32 index_offset{index << desc.size_shift};
|
||||
const u32 offset{desc.cbuf_offset + index_offset};
|
||||
|
||||
@@ -110,6 +110,16 @@ public:
|
||||
return configure_func(this, is_indexed);
|
||||
}
|
||||
|
||||
/// True when a shader samples a texture whose handle arrives as a vertex attribute.
|
||||
[[nodiscard]] bool UsesAttributeHandle() const noexcept {
|
||||
return uses_attribute_handle;
|
||||
}
|
||||
|
||||
/// Handle bound to those textures by the next Configure.
|
||||
void SetAttributeHandle(u32 handle) noexcept {
|
||||
attribute_handle = handle;
|
||||
}
|
||||
|
||||
[[nodiscard]] GraphicsPipeline* Next(const GraphicsPipelineCacheKey& current_key) noexcept {
|
||||
if (key == current_key) {
|
||||
return this;
|
||||
@@ -168,6 +178,8 @@ private:
|
||||
u32 num_descriptor_entries{};
|
||||
size_t num_image_elements{};
|
||||
u32 num_textures{};
|
||||
u32 attribute_handle{};
|
||||
bool uses_attribute_handle{};
|
||||
bool fragment_has_color0_output{};
|
||||
|
||||
vk::DescriptorSetLayout descriptor_set_layout;
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#include "shader_recompiler/environment.h"
|
||||
#include "shader_recompiler/frontend/maxwell/control_flow.h"
|
||||
#include "shader_recompiler/frontend/maxwell/translate_program.h"
|
||||
#include "shader_recompiler/ir_opt/passes.h"
|
||||
#include "shader_recompiler/program_header.h"
|
||||
#include "video_core/engines/kepler_compute.h"
|
||||
#include "video_core/engines/maxwell_3d.h"
|
||||
@@ -480,6 +479,7 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_,
|
||||
.support_viewport_index_layer = device.IsExtShaderViewportIndexLayerSupported(),
|
||||
.support_geometry_shader_passthrough = device.IsNvGeometryShaderPassthroughSupported(),
|
||||
.support_conditional_barrier = device.SupportsConditionalBarriers(),
|
||||
.support_attribute_texture_handle = true,
|
||||
};
|
||||
host_info.ApplyDescriptorLimitPolicy();
|
||||
|
||||
@@ -827,11 +827,6 @@ std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline(
|
||||
programs[index] = MergeDualVertexPrograms(program_va, program_vb, env);
|
||||
}
|
||||
|
||||
if (device.GetDriverID() != VK_DRIVER_ID_NVIDIA_PROPRIETARY && Shader::Optimization::HasBrokenPattern(programs[index])) {
|
||||
LOG_WARNING(Render_Vulkan, "0x{:016x}: Skipping shader with known issues", key.unique_hashes[index]);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (Settings::values.dump_guest_shaders) {
|
||||
env.Dump(hash, key.unique_hashes[index]);
|
||||
}
|
||||
|
||||
@@ -248,6 +248,15 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) {
|
||||
std::scoped_lock lock{buffer_cache.mutex, texture_cache.mutex};
|
||||
// update engine as channel may be different.
|
||||
pipeline->SetEngine(maxwell3d, gpu_memory);
|
||||
attribute_handle_pipeline = nullptr;
|
||||
attribute_handle_runs.clear();
|
||||
if (split_attribute_handles && pipeline->UsesAttributeHandle()) {
|
||||
CollectAttributeHandleRuns(is_indexed);
|
||||
if (!attribute_handle_runs.empty()) {
|
||||
attribute_handle_pipeline = pipeline;
|
||||
pipeline->SetAttributeHandle(attribute_handle_runs.front().handle);
|
||||
}
|
||||
}
|
||||
if (!pipeline->Configure(is_indexed))
|
||||
return;
|
||||
|
||||
@@ -259,22 +268,110 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) {
|
||||
draw_func();
|
||||
}
|
||||
|
||||
void RasterizerVulkan::CollectAttributeHandleRuns(bool is_indexed) {
|
||||
using VertexAttribute = Maxwell::VertexAttribute;
|
||||
const auto& regs = maxwell3d->regs;
|
||||
const auto& draw_state = maxwell3d->draw_manager.draw_state;
|
||||
const u32 total = is_indexed ? draw_state.index_buffer.count : draw_state.vertex_buffer.count;
|
||||
if (total == 0) {
|
||||
return;
|
||||
}
|
||||
// The handle is an integer attribute; sprite batches repeat it on every vertex of a quad.
|
||||
const VertexAttribute* source{};
|
||||
for (const auto& attr : regs.vertex_attrib_format) {
|
||||
if (attr.constant || attr.size == VertexAttribute::Size::Invalid) {
|
||||
continue;
|
||||
}
|
||||
if (attr.type == VertexAttribute::Type::UInt || attr.type == VertexAttribute::Type::SInt) {
|
||||
source = &attr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const auto& stream = regs.vertex_streams[source ? source->buffer.Value() : 0];
|
||||
if (!source || !stream.IsEnabled()) {
|
||||
attribute_handle_runs.push_back({0, total, 0});
|
||||
return;
|
||||
}
|
||||
// Only plain triangle lists are split; anything else keeps the handle of its first vertex.
|
||||
const u32 primitive_size =
|
||||
draw_state.topology == Maxwell::PrimitiveTopology::Triangles ? 3 : total;
|
||||
const GPUVAddr base_address = stream.Address() + source->offset;
|
||||
const auto read_handle = [&](u32 position) {
|
||||
u32 vertex;
|
||||
if (is_indexed) {
|
||||
const auto& index_buffer = draw_state.index_buffer;
|
||||
const GPUVAddr address =
|
||||
index_buffer.IndexStart() + size_t{position} * index_buffer.FormatSizeInBytes();
|
||||
u32 index{};
|
||||
gpu_memory->ReadBlockUnsafe(address, &index, index_buffer.FormatSizeInBytes());
|
||||
vertex = static_cast<u32>(draw_state.base_index + index);
|
||||
} else {
|
||||
vertex = draw_state.vertex_buffer.first + position;
|
||||
}
|
||||
return gpu_memory->Read<u32>(base_address + static_cast<GPUVAddr>(vertex) * stream.stride);
|
||||
};
|
||||
for (u32 position = 0; position + primitive_size <= total; position += primitive_size) {
|
||||
const u32 handle = read_handle(position);
|
||||
if (!attribute_handle_runs.empty() && attribute_handle_runs.back().handle == handle) {
|
||||
attribute_handle_runs.back().count += primitive_size;
|
||||
} else {
|
||||
attribute_handle_runs.push_back({position, primitive_size, handle});
|
||||
}
|
||||
}
|
||||
if (attribute_handle_runs.empty()) {
|
||||
attribute_handle_runs.push_back({0, total, read_handle(0)});
|
||||
} else {
|
||||
const u32 covered = attribute_handle_runs.back().first + attribute_handle_runs.back().count;
|
||||
if (covered < total) {
|
||||
attribute_handle_runs.back().count += total - covered;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RasterizerVulkan::Draw(bool is_indexed, u32 instance_count) {
|
||||
split_attribute_handles = true;
|
||||
SCOPE_EXIT {
|
||||
split_attribute_handles = false;
|
||||
};
|
||||
PrepareDraw(is_indexed, [this, is_indexed, instance_count] {
|
||||
const auto& draw_state = maxwell3d->draw_manager.draw_state;
|
||||
const u32 num_instances{instance_count};
|
||||
const DrawParams draw_params{MakeDrawParams(draw_state, num_instances, is_indexed)};
|
||||
|
||||
scheduler.Record([draw_params](vk::CommandBuffer cmdbuf) {
|
||||
if (draw_params.is_indexed) {
|
||||
cmdbuf.DrawIndexed(draw_params.num_vertices, draw_params.num_instances,
|
||||
draw_params.first_index, draw_params.base_vertex,
|
||||
draw_params.base_instance);
|
||||
} else {
|
||||
cmdbuf.Draw(draw_params.num_vertices, draw_params.num_instances,
|
||||
draw_params.base_vertex, draw_params.base_instance);
|
||||
const auto record_draw = [this](const DrawParams& params) {
|
||||
scheduler.Record([params](vk::CommandBuffer cmdbuf) {
|
||||
if (params.is_indexed) {
|
||||
cmdbuf.DrawIndexed(params.num_vertices, params.num_instances,
|
||||
params.first_index, params.base_vertex,
|
||||
params.base_instance);
|
||||
} else {
|
||||
cmdbuf.Draw(params.num_vertices, params.num_instances, params.base_vertex,
|
||||
params.base_instance);
|
||||
}
|
||||
});
|
||||
};
|
||||
if (attribute_handle_pipeline && attribute_handle_runs.size() > 1) {
|
||||
// The first run is already configured, each further one binds its own texture.
|
||||
for (size_t i = 0; i < attribute_handle_runs.size(); ++i) {
|
||||
const AttributeHandleRun& run = attribute_handle_runs[i];
|
||||
if (i != 0) {
|
||||
attribute_handle_pipeline->SetAttributeHandle(run.handle);
|
||||
if (!attribute_handle_pipeline->Configure(is_indexed)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
DrawParams run_params = draw_params;
|
||||
run_params.num_vertices = run.count;
|
||||
if (is_indexed) {
|
||||
run_params.first_index += run.first;
|
||||
} else {
|
||||
run_params.base_vertex += run.first;
|
||||
}
|
||||
record_draw(run_params);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
record_draw(draw_params);
|
||||
}
|
||||
|
||||
// Log draw call
|
||||
if (GPU::Logging::IsActive() &&
|
||||
|
||||
@@ -157,6 +157,20 @@ private:
|
||||
template <typename Func>
|
||||
void PrepareDraw(bool is_indexed, Func&&);
|
||||
|
||||
/// A stretch of a draw whose vertices all carry the same texture handle.
|
||||
struct AttributeHandleRun {
|
||||
u32 first;
|
||||
u32 count;
|
||||
u32 handle;
|
||||
};
|
||||
|
||||
/// Splits the current draw wherever the texture handle carried in the vertex data changes.
|
||||
void CollectAttributeHandleRuns(bool is_indexed);
|
||||
|
||||
std::vector<AttributeHandleRun> attribute_handle_runs;
|
||||
GraphicsPipeline* attribute_handle_pipeline{};
|
||||
bool split_attribute_handles{};
|
||||
|
||||
void FlushWork();
|
||||
|
||||
void UpdateDynamicStates();
|
||||
|
||||
Reference in New Issue
Block a user