Compare commits

..

5 Commits

Author SHA1 Message Date
PavelBARABANOV 269c876cf1 Fixed Sacred 2 Remaster - added indexed texture handles support 2026-10-22 00:03:30 +03:00
PavelBARABANOV 720c7d227c Fixed Civilization VII - added TIR mixed samples support 2026-10-22 00:01:15 +03:00
PavelBARABANOV aee3785897 lic 2026-10-21 23:58:54 +03:00
PavelBARABANOV 2cba0ecf52 Fixed Disco Elysium - bug in swapped abs/neg bits in F2I decoding 2026-10-21 23:57:13 +03:00
John 989768aa46 Fix [core, gpu, threads] Multithreading refactor (#4254) (#4548)
Performance loss regression fixed in Link's Awakening, Castlevania, Super Mario RPG, etc

Tested by Mr. Knobs on Android and myself on Linux

Multicore ON may be fixed for some users using Intel CPUs.
Games include: FF Tactics, Shredder's Revenge, Unity Engine Games.

- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md).
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

-------------------

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4548
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Reviewed-by: lizzie <lizzie@eden-emu.dev>
2026-10-19 15:58:04 +02:00
23 changed files with 755 additions and 142 deletions
@@ -57,8 +57,21 @@ u32 HardwareComposer::ComposeLocked(f32* out_speed_scale, Display& display,
// Set default speed limit to 100%.
*out_speed_scale = 1.0f;
// If no layers are visible, skip the logic. Acquiring and holding buffers here would
// consume the producer's queue without ever presenting.
bool any_visible = false;
for (auto& layer : display.stack.layers) {
if (layer->visible) {
any_visible = true;
break;
}
}
if (!any_visible) {
return 1;
}
// Wait for the previous composite, bounding the number of in-flight composites.
nvdisp.WaitForComposite();
this->ReleaseFramebuffersLocked(display);
// Determine the number of vsync periods to wait before composing again.
std::optional<s32> swap_interval{};
@@ -152,6 +165,13 @@ u32 HardwareComposer::ComposeLocked(f32* out_speed_scale, Display& display,
// Advance by 1 frame (60 FPS compositing)
m_frame_number += 1;
// Release framebuffers once the frame has been handed to the compositor. This happens
// within the same vsync period as the acquire (release_frame_number is checked against
// the advanced counter), so the producer gets its buffers back immediately instead of
// losing a queue slot for a whole frame.
// TODO: support release fences; releasing before the composite has finished can tear.
this->ReleaseFramebuffersLocked(display);
return 1;
}
+12 -2
View File
@@ -40,9 +40,10 @@ struct HandleKey {
u32 sec_index, sec_offset, sec_shift_left;
u32 count;
bool has_secondary;
u32 mask, sec_mask;
constexpr bool operator==(const HandleKey& o) const noexcept {
return std::tie(index, offset, shift_left, sec_index, sec_offset, sec_shift_left, count, has_secondary)
== std::tie(o.index, o.offset, o.shift_left, o.sec_index, o.sec_offset, o.sec_shift_left, o.count, o.has_secondary);
return std::tie(index, offset, shift_left, sec_index, sec_offset, sec_shift_left, count, has_secondary, mask, sec_mask)
== std::tie(o.index, o.offset, o.shift_left, o.sec_index, o.sec_offset, o.sec_shift_left, o.count, o.has_secondary, o.mask, o.sec_mask);
}
};
struct HandleKeyHash {
@@ -52,6 +53,7 @@ struct HandleKeyHash {
h ^= (size_t(k.sec_index) << 33) ^ (size_t(k.sec_offset) << 2);
h ^= (size_t(k.sec_shift_left) << 3);
h ^= (size_t(k.count) << 7);
h ^= (size_t(k.mask) << 11) ^ (size_t(k.sec_mask) << 13);
return h ^ (k.has_secondary ? 0x9e3779b97f4a7c15ULL : 0ULL);
}
};
@@ -66,6 +68,14 @@ struct ConstBufferAddr {
IR::U32 dynamic_offset;
u32 count;
bool has_secondary;
/// The dynamic offset is a constant buffer value shifted left, the same for the whole draw
bool has_index_cbuf;
u32 index_cbuf_index;
u32 index_cbuf_offset;
u32 index_shift;
/// Applied after the shifts
u32 mask = 0xffffffff;
u32 secondary_mask = 0xffffffff;
};
class Environment {
@@ -1,12 +1,8 @@
// 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
#pragma once
#include <functional>
#include <optional>
#include <type_traits>
#include <queue>
@@ -17,10 +13,9 @@
namespace Shader::IR {
template <typename Pred, typename Resolve = std::identity>
auto BreadthFirstSearch(Value value, Pred&& pred, Resolve&& resolve = {})
template <typename Pred>
auto BreadthFirstSearch(const Value& value, Pred&& pred)
-> std::invoke_result_t<Pred, const Inst*> {
value = resolve(value);
if (value.IsImmediate()) {
// Nothing to do with immediates
return std::nullopt;
@@ -41,7 +36,7 @@ auto BreadthFirstSearch(Value value, Pred&& pred, Resolve&& resolve = {})
}
// Visit the right most arguments first
for (size_t arg = inst->NumArgs(); arg--;) {
const Value arg_value{resolve(inst->Arg(arg))};
const Value arg_value{inst->Arg(arg)};
if (arg_value.IsImmediate()) {
continue;
}
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
@@ -41,9 +41,9 @@ union F2I {
BitField<39, 2, Rounding> rounding;
BitField<41, 1, u64> half;
BitField<44, 1, u64> ftz;
BitField<45, 1, u64> abs;
BitField<45, 1, u64> neg;
BitField<47, 1, u64> cc;
BitField<49, 1, u64> neg;
BitField<49, 1, u64> abs;
};
size_t BitSize(DestFormat dest_format) {
+115 -96
View File
@@ -31,60 +31,6 @@ struct TextureInst {
using TextureInstVector = boost::container::small_vector<TextureInst, 24>;
const IR::Block* ConditionalHeader(const IR::Block* body) {
const auto predecessors{body->ImmPredecessors()};
const auto successors{body->ImmSuccessors()};
if (predecessors.size() != 1 || successors.size() != 1) return nullptr;
const IR::Block* const header{predecessors[0]};
const auto targets{header->ImmSuccessors()};
if (targets.size() != 2 || targets[0] != body || targets[1] != successors[0] ||
targets[1]->ImmPredecessors().size() != 2 || header->empty() ||
header->back().GetOpcode() != IR::Opcode::ConditionRef) {
return nullptr;
}
return header;
}
std::pair<IR::Value, bool> Condition(IR::Value value) {
bool negated{};
while (const IR::Inst* inst = value.TryInstRecursive()) {
if (inst->GetOpcode() == IR::Opcode::LogicalNot) negated = !negated;
else if (inst->GetOpcode() != IR::Opcode::ConditionRef) break;
value = inst->Arg(0);
}
return {value.Resolve(), negated};
}
IR::Value ResolveConditionalPhi(IR::Value value, const IR::Block* block) {
if (!value.Resolve().IsPhi() || !(block = ConditionalHeader(block))) return value;
const auto [condition, negated]{Condition(block->back().Arg(0))};
boost::container::small_vector<const IR::Block*, 16> visited;
while (value.Resolve().IsPhi() && std::ranges::find(visited, block) == visited.end()) {
visited.push_back(block);
const IR::Inst* const phi{value.InstRecursive()};
const auto predecessors{block->ImmPredecessors()};
if (predecessors.size() == 1) {
block = predecessors[0];
continue;
}
if (predecessors.size() != 2 || phi->NumArgs() != 2) break;
const IR::Block* body{predecessors[0]};
const IR::Block* header{ConditionalHeader(body)};
if (header != predecessors[1]) {
body = predecessors[1];
header = ConditionalHeader(body);
if (header != predecessors[0]) break;
}
const auto [other_condition, other_negated]{Condition(header->back().Arg(0))};
if (condition == other_condition && (phi->PhiBlock(0) == header || phi->PhiBlock(0) == body)) {
const IR::Block* const incoming{negated == other_negated ? body : header};
value = phi->Arg(phi->PhiBlock(0) == incoming ? 0 : 1);
}
block = header;
}
return value;
}
constexpr u32 DESCRIPTOR_SIZE = 8;
constexpr u32 DESCRIPTOR_SIZE_SHIFT = u32(std::countr_zero(DESCRIPTOR_SIZE));
constexpr u32 DESCRIPTOR_MAX_COUNT = 1024;
@@ -97,7 +43,8 @@ u32 DynamicDescriptorSizeShift(const IR::U32& dynamic_offset) {
if (!shift.IsImmediate())
return DESCRIPTOR_SIZE_SHIFT;
const u32 size_shift = shift.U32();
return size_shift >= DESCRIPTOR_SIZE_SHIFT && size_shift < 31 ? size_shift : DESCRIPTOR_SIZE_SHIFT;
// Separate texture and sampler handles are 4 bytes apart
return size_shift >= 2 && size_shift < 31 ? size_shift : DESCRIPTOR_SIZE_SHIFT;
}
u32 DynamicDescriptorCount(u32 base_offset, u32 size_shift, u32 max_descriptors) {
@@ -113,6 +60,37 @@ u32 DynamicDescriptorCount(u32 base_offset, u32 size_shift, u32 max_descriptors)
return std::min(descriptor_limit, available_count);
}
struct UniformIndex {
u32 cbuf_index;
u32 cbuf_offset;
u32 shift;
};
/// Matches a dynamic handle offset that is a constant buffer value, optionally shifted left
std::optional<UniformIndex> TrackUniformIndex(const IR::U32& dynamic_offset) {
if (dynamic_offset.IsImmediate()) {
return std::nullopt;
}
const IR::Inst* inst = dynamic_offset.InstRecursive();
u32 shift = 0;
if (inst->GetOpcode() == IR::Opcode::ShiftLeftLogical32) {
if (!inst->Arg(1).IsImmediate() || inst->Arg(0).IsImmediate()) {
return std::nullopt;
}
shift = inst->Arg(1).U32();
inst = inst->Arg(0).InstRecursive();
}
if (inst->GetOpcode() != IR::Opcode::GetCbufU32 || !inst->Arg(0).IsImmediate() ||
!inst->Arg(1).IsImmediate() || shift >= 32) {
return std::nullopt;
}
return UniformIndex{
.cbuf_index = inst->Arg(0).U32(),
.cbuf_offset = inst->Arg(1).U32(),
.shift = shift,
};
}
u32 SaturatingSub(u32 lhs, u32 rhs) {
return lhs > rhs ? lhs - rhs : 0;
}
@@ -327,14 +305,15 @@ static inline u32 GetTextureHandleCached(Environment& env, const ConstBufferAddr
// If has secondary, then it will be cbuf.secondary_{index|offset}, else its 0.
// So we can just hand it out the raw variable without using sec_idx or sec_off
// because comparing 0 against 0 will yield true.
const HandleKey hk{cbuf.index, cbuf.offset, cbuf.shift_left, cbuf.secondary_index, cbuf.secondary_offset, cbuf.secondary_shift_left, cbuf.count, cbuf.has_secondary};
const HandleKey hk{cbuf.index, cbuf.offset, cbuf.shift_left, cbuf.secondary_index, cbuf.secondary_offset, cbuf.secondary_shift_left, cbuf.count, cbuf.has_secondary, cbuf.mask, cbuf.secondary_mask};
if (auto it = env.handle_cache.find(hk); it != env.handle_cache.end())
return it->second;
const u32 sec_idx = cbuf.has_secondary ? cbuf.secondary_index : cbuf.index;
const u32 sec_off = cbuf.has_secondary ? cbuf.secondary_offset : cbuf.offset;
const u32 lhs = ReadCbufCached(env, cbuf.index, cbuf.offset) << cbuf.shift_left;
const u32 rhs = ReadCbufCached(env, sec_idx, sec_off) << cbuf.secondary_shift_left;
const u32 handle = lhs | rhs;
u32 handle = (ReadCbufCached(env, cbuf.index, cbuf.offset) << cbuf.shift_left) & cbuf.mask;
if (cbuf.has_secondary) {
handle |= (ReadCbufCached(env, cbuf.secondary_index, cbuf.secondary_offset)
<< cbuf.secondary_shift_left) &
cbuf.secondary_mask;
}
env.handle_cache.emplace(hk, handle);
return handle;
}
@@ -353,23 +332,23 @@ static inline bool IsTexturePixelFormatIntegerCached(Environment& env,
}
std::optional<ConstBufferAddr> Track(const IR::Value& value, Environment& env, const HostTranslateInfo& host_info, const IR::Block* block);
static inline std::optional<ConstBufferAddr> TrackCached(const IR::Value& v, Environment& env, const HostTranslateInfo& host_info, const IR::Block* block) {
if (const IR::Inst* key = v.TryInstRecursive()) {
std::optional<ConstBufferAddr> Track(const IR::Value& value, Environment& env, const HostTranslateInfo& host_info);
static inline std::optional<ConstBufferAddr> TrackCached(const IR::Value& v, Environment& env, const HostTranslateInfo& host_info) {
if (const IR::Inst* key = v.InstRecursive()) {
if (auto it = env.track_cache.find(key); it != env.track_cache.end()) return it->second;
auto found = Track(v, env, host_info, block);
auto found = Track(v, env, host_info);
if (found) env.track_cache.emplace(key, *found);
return found;
}
return Track(v, env, host_info, block);
return Track(v, env, host_info);
}
std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environment& env, const HostTranslateInfo& host_info, const IR::Block* block);
std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environment& env, const HostTranslateInfo& host_info);
std::optional<ConstBufferAddr> Track(const IR::Value& value, Environment& env, const HostTranslateInfo& host_info, const IR::Block* block) {
return IR::BreadthFirstSearch(value, [&env, &host_info, block](const IR::Inst* inst) {
return TryGetConstBuffer(inst, env, host_info, block);
}, [&](IR::Value operand) { return ResolveConditionalPhi(operand, block); });
std::optional<ConstBufferAddr> Track(const IR::Value& value, Environment& env, const HostTranslateInfo& host_info) {
return IR::BreadthFirstSearch(value, [&env, &host_info](const IR::Inst* inst) {
return TryGetConstBuffer(inst, env, host_info);
});
}
std::optional<u32> TryGetConstant(IR::Value& value, Environment& env) {
@@ -393,23 +372,35 @@ std::optional<u32> TryGetConstant(IR::Value& value, Environment& env) {
return ReadCbufCached(env, index_number, offset_number);
}
std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environment& env, const HostTranslateInfo& host_info, const IR::Block* block) {
std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environment& env, const HostTranslateInfo& host_info) {
switch (inst->GetOpcode()) {
default:
return std::nullopt;
case IR::Opcode::BitwiseOr32: {
std::optional lhs{TrackCached(inst->Arg(0), env, host_info, block)};
std::optional rhs{TrackCached(inst->Arg(1), env, host_info, block)};
std::optional lhs{TrackCached(inst->Arg(0), env, host_info)};
std::optional rhs{TrackCached(inst->Arg(1), env, host_info)};
if (!lhs || !rhs) {
return std::nullopt;
}
if (lhs->has_secondary || rhs->has_secondary) {
return std::nullopt;
}
if (lhs->count > 1 || rhs->count > 1) {
if (lhs->count > 1 && rhs->count > 1) {
return std::nullopt;
}
if (lhs->shift_left > 0 || lhs->index > rhs->index || lhs->offset > rhs->offset) {
if (rhs->has_index_cbuf || lhs->has_index_cbuf) {
if (rhs->has_index_cbuf && lhs->has_index_cbuf) {
return std::nullopt;
}
if (rhs->has_index_cbuf) {
std::swap(lhs, rhs);
}
} else if (rhs->count > 1) {
// An indexed handle array combined with a single handle (e.g. texture indices with
// a shared sampler), only the primary handle is indexed
std::swap(lhs, rhs);
} else if (lhs->count == 1 &&
(lhs->shift_left > 0 || lhs->index > rhs->index || lhs->offset > rhs->offset)) {
std::swap(lhs, rhs);
}
return ConstBufferAddr{
@@ -419,9 +410,15 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
.secondary_index = rhs->index,
.secondary_offset = rhs->offset,
.secondary_shift_left = rhs->shift_left,
.dynamic_offset = {},
.count = 1,
.dynamic_offset = lhs->dynamic_offset,
.count = lhs->count,
.has_secondary = true,
.has_index_cbuf = lhs->has_index_cbuf,
.index_cbuf_index = lhs->index_cbuf_index,
.index_cbuf_offset = lhs->index_cbuf_offset,
.index_shift = lhs->index_shift,
.mask = lhs->mask,
.secondary_mask = rhs->mask,
};
}
case IR::Opcode::ShiftLeftLogical32: {
@@ -429,9 +426,11 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
if (!shift.IsImmediate()) {
return std::nullopt;
}
std::optional lhs{TrackCached(inst->Arg(0), env, host_info, block)};
std::optional lhs{TrackCached(inst->Arg(0), env, host_info)};
if (lhs) {
lhs->shift_left = shift.U32();
// A mask applied before the shift moves with the value
lhs->mask <<= shift.U32();
}
return lhs;
}
@@ -457,9 +456,9 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
return std::nullopt;
} while (false);
}
std::optional lhs{TrackCached(op1, env, host_info, block)};
std::optional lhs{TrackCached(op1, env, host_info)};
if (lhs) {
lhs->shift_left = static_cast<u32>(std::countr_zero(op2.U32()));
lhs->mask &= op2.U32();
}
return lhs;
break;
@@ -489,12 +488,12 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
};
}
IR::Inst* const offset_inst{offset.InstRecursive()};
if (offset_inst->GetOpcode() != IR::Opcode::IAdd32) {
return std::nullopt;
}
u32 base_offset{};
IR::U32 dynamic_offset;
if (offset_inst->Arg(0).IsImmediate()) {
if (offset_inst->GetOpcode() != IR::Opcode::IAdd32) {
// The handle array starts at the beginning of the constant buffer
dynamic_offset = IR::U32{offset};
} else if (offset_inst->Arg(0).IsImmediate()) {
base_offset = offset_inst->Arg(0).U32();
dynamic_offset = IR::U32{offset_inst->Arg(1)};
} else if (offset_inst->Arg(1).IsImmediate()) {
@@ -504,6 +503,7 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
return std::nullopt;
}
auto const size_shift = DynamicDescriptorSizeShift(dynamic_offset);
const std::optional<UniformIndex> uniform_index{TrackUniformIndex(dynamic_offset)};
auto const sampled_limit = (std::max)(1U, (std::min)(host_info.max_per_stage_descriptor_sampled_images,
host_info.max_descriptor_set_sampled_images));
auto const resource_limit = (std::max)(1U, host_info.max_per_stage_resources);
@@ -517,13 +517,17 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
.dynamic_offset = dynamic_offset,
.count = DynamicDescriptorCount(base_offset, size_shift, (std::min)({DESCRIPTOR_MAX_COUNT, sampled_limit, resource_limit})),
.has_secondary = false,
.has_index_cbuf = uniform_index.has_value(),
.index_cbuf_index = uniform_index ? uniform_index->cbuf_index : 0,
.index_cbuf_offset = uniform_index ? uniform_index->cbuf_offset : 0,
.index_shift = uniform_index ? uniform_index->shift : 0,
};
}
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, block)};
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");
@@ -551,12 +555,13 @@ TextureInst MakeInst(Environment& env, IR::Block* block, IR::Inst& inst, const H
}
u32 GetTextureHandle(Environment& env, const ConstBufferAddr& cbuf) {
const u32 secondary_index{cbuf.has_secondary ? cbuf.secondary_index : cbuf.index};
const u32 secondary_offset{cbuf.has_secondary ? cbuf.secondary_offset : cbuf.offset};
const u32 lhs_raw{env.ReadCbufValue(cbuf.index, cbuf.offset) << cbuf.shift_left};
const u32 rhs_raw{env.ReadCbufValue(secondary_index, secondary_offset)
<< cbuf.secondary_shift_left};
return lhs_raw | rhs_raw;
u32 handle{(env.ReadCbufValue(cbuf.index, cbuf.offset) << cbuf.shift_left) & cbuf.mask};
if (cbuf.has_secondary) {
handle |= (env.ReadCbufValue(cbuf.secondary_index, cbuf.secondary_offset)
<< cbuf.secondary_shift_left) &
cbuf.secondary_mask;
}
return handle;
}
[[maybe_unused]] TextureType ReadTextureType(Environment& env, const ConstBufferAddr& cbuf) {
@@ -590,7 +595,8 @@ public:
desc.secondary_cbuf_offset == existing.secondary_cbuf_offset &&
desc.secondary_shift_left == existing.secondary_shift_left &&
desc.count == existing.count && desc.size_shift == existing.size_shift &&
desc.has_secondary == existing.has_secondary;
desc.has_secondary == existing.has_secondary && desc.mask == existing.mask &&
desc.secondary_mask == existing.secondary_mask;
});
}
@@ -616,7 +622,12 @@ public:
desc.secondary_cbuf_index == existing.secondary_cbuf_index &&
desc.secondary_cbuf_offset == existing.secondary_cbuf_offset &&
desc.secondary_shift_left == existing.secondary_shift_left &&
desc.count == existing.count && desc.size_shift == existing.size_shift;
desc.count == existing.count && desc.size_shift == existing.size_shift &&
desc.has_index_cbuf == existing.has_index_cbuf &&
desc.index_cbuf_index == existing.index_cbuf_index &&
desc.index_cbuf_offset == existing.index_cbuf_offset &&
desc.index_shift == existing.index_shift && desc.mask == existing.mask &&
desc.secondary_mask == existing.secondary_mask;
})};
// TODO: Read this from TIC
texture_descriptors[index].is_multisample |= desc.is_multisample;
@@ -725,7 +736,6 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
env.track_cache.clear();
TextureInstVector to_replace;
for (IR::Block* const block : program.post_order_blocks) {
env.track_cache.clear();
for (IR::Inst& inst : block->Instructions()) {
if (!IsTextureInstruction(inst)) {
continue;
@@ -863,9 +873,12 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
.secondary_shift_left = cbuf.secondary_shift_left,
.count = count,
.size_shift = size_shift,
.mask = cbuf.mask,
.secondary_mask = cbuf.secondary_mask,
});
} else {
count = std::min(count, sampled_dynamic_cap);
// A uniform index is resolved when drawing, so a single descriptor is enough
count = cbuf.has_index_cbuf ? 1 : std::min(count, sampled_dynamic_cap);
const bool is_integer{is_texture_pixel_format_integer(cbuf)};
index = descriptors.Add(TextureDescriptor{
.type = flags.type,
@@ -881,6 +894,12 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
.secondary_shift_left = cbuf.secondary_shift_left,
.count = count,
.size_shift = size_shift,
.has_index_cbuf = cbuf.has_index_cbuf,
.index_cbuf_index = cbuf.index_cbuf_index,
.index_cbuf_offset = cbuf.index_cbuf_offset,
.index_shift = cbuf.index_shift,
.mask = cbuf.mask,
.secondary_mask = cbuf.secondary_mask,
});
flags.is_integer.Assign(is_integer ? 1 : 0);
}
+9
View File
@@ -186,6 +186,8 @@ struct TextureBufferDescriptor {
u32 secondary_shift_left;
u32 count;
u32 size_shift;
u32 mask = 0xffffffff;
u32 secondary_mask = 0xffffffff;
auto operator<=>(const TextureBufferDescriptor&) const = default;
};
@@ -219,6 +221,13 @@ struct TextureDescriptor {
u32 secondary_shift_left;
u32 count;
u32 size_shift;
/// The handle offset is moved by a constant buffer value read when drawing
bool has_index_cbuf;
u32 index_cbuf_index;
u32 index_cbuf_offset;
u32 index_shift;
u32 mask = 0xffffffff;
u32 secondary_mask = 0xffffffff;
auto operator<=>(const TextureDescriptor&) const = default;
};
+7 -2
View File
@@ -948,9 +948,14 @@ void BufferCache<P>::BindHostGraphicsUniformBuffer(size_t stage, u32 index, u32
return alignment > 1 && (offset % alignment) != 0;
}
}();
// Uniform data rewritten by the CPU between draws would need a transfer copy into the cached
// buffer, which ends the current render pass. Read it from the stream buffer instead.
static constexpr u32 MAX_STREAMED_UNIFORM_BYTES = static_cast<u32>(16_KiB);
const bool use_fast_buffer = needs_alignment_stream
|| (has_host_buffer && size <= channel_state->uniform_buffer_skip_cache_size
&& !memory_tracker.IsRegionGpuModified(device_addr, size));
|| (has_host_buffer && !memory_tracker.IsRegionGpuModified(device_addr, size)
&& (size <= channel_state->uniform_buffer_skip_cache_size
|| (size <= MAX_STREAMED_UNIFORM_BYTES
&& memory_tracker.IsRegionCpuModified(device_addr, size))));
if (use_fast_buffer) {
if constexpr (IS_OPENGL) {
if (runtime.HasFastBufferSubData()) {
@@ -259,6 +259,23 @@ public:
void BindHostComputeBuffers();
/// Calls func(buffer, offset, device_addr, size) for each storage buffer the current compute
/// dispatch writes through a single host buffer
template <typename Func>
void ForEachWrittenComputeStorageBuffer(Func&& func) {
const u32 written = channel_state->enabled_compute_storage_buffers &
channel_state->written_compute_storage_buffers;
ForEachEnabledBit(written, [&](u32 index) {
const Binding& binding = channel_state->compute_storage_buffers[index];
if (binding.buffer_id == NULL_BUFFER_ID || binding.segment_count != 0 ||
binding.size == 0) {
return;
}
Buffer& buffer = slot_buffers[binding.buffer_id];
func(buffer, buffer.Offset(binding.device_addr), binding.device_addr, binding.size);
});
}
void SetUniformBuffersState(const std::array<u32, NUM_STAGES>& mask,
const UniformBufferSizes* sizes);
@@ -83,6 +83,7 @@ set(SHADER_FILES
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_present.vert
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_present_scaleforce_fp16.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_present_scaleforce_fp32.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_block_linear_unswizzle_2d_buffer.comp
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_quad_indexed.comp
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_turbo_mode.comp
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_uint8.comp
@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#version 460 core
// Copies a block linear 2D image from a buffer into a tightly packed linear buffer, 4 bytes at a
// time. 16 byte chunks of a row are contiguous in a GOB, so any texel size works as long as the
// row size is a multiple of 4 bytes.
layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in;
layout(std430, set = 0, binding = 0) readonly buffer InputBuffer {
uint swizzled[];
};
layout(std430, set = 0, binding = 1) writeonly buffer OutputBuffer {
uint linear[];
};
layout(push_constant) uniform PushConstants {
uint src_word_offset;
uint row_words;
uint height;
uint layer_stride;
uint block_size;
uint x_shift;
uint block_height;
uint block_height_mask;
};
const uint GOB_SIZE_X_SHIFT = 6;
const uint GOB_SIZE_Y_SHIFT = 3;
const uint GOB_SIZE_SHIFT = GOB_SIZE_X_SHIFT + GOB_SIZE_Y_SHIFT;
uint SwizzleTable(uint pos) {
const uint t[8] = uint[](
0x12100200, 0x13110301, 0x16140604, 0x17150705,
0x1a180a08, 0x1b190b09, 0x1e1c0e0c, 0x1f1d0f0d
);
const uint i = pos >> 4;
const uint h = (t[i / 4] >> ((i % 4) * 8)) & 0xff;
return (h << 4) | (pos & 0xf);
}
void main() {
const uint word = gl_GlobalInvocationID.x;
const uint y = gl_GlobalInvocationID.y;
const uint layer = gl_GlobalInvocationID.z;
if (word >= row_words || y >= height) {
return;
}
const uint x = word * 4;
const uint block_y = y >> GOB_SIZE_Y_SHIFT;
uint offset = layer * layer_stride;
offset += (block_y >> block_height) * block_size;
offset += (block_y & block_height_mask) << GOB_SIZE_SHIFT;
offset += (x >> GOB_SIZE_X_SHIFT) << x_shift;
offset += SwizzleTable((y & 7) * 64 + (x & 63));
linear[(layer * height + y) * row_words + word] = swizzled[src_word_offset + offset / 4];
}
@@ -107,22 +107,36 @@ void ComputePipeline::Configure() {
const auto read_handle{[&](const auto& desc, u32 index) {
ASSERT(((qmd.const_buffer_enable_mask >> desc.cbuf_index) & 1) != 0);
const u32 index_offset{index << desc.size_shift};
const u32 offset{desc.cbuf_offset + index_offset};
u32 offset{desc.cbuf_offset + index_offset};
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) {
if (desc.has_index_cbuf) {
ASSERT(((qmd.const_buffer_enable_mask >> desc.index_cbuf_index) & 1) != 0);
offset += gpu_memory->Read<u32>(cbufs[desc.index_cbuf_index].Address() +
desc.index_cbuf_offset)
<< desc.index_shift;
}
}
const GPUVAddr addr{cbufs[desc.cbuf_index].Address() + offset};
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
if (desc.has_secondary) {
ASSERT(((qmd.const_buffer_enable_mask >> desc.secondary_cbuf_index) & 1) != 0);
const u32 secondary_offset{desc.secondary_cbuf_offset + index_offset};
const u32 secondary_offset{desc.secondary_cbuf_offset};
const GPUVAddr separate_addr{cbufs[desc.secondary_cbuf_index].Address() +
secondary_offset};
const u32 lhs_raw{gpu_memory->Read<u32>(addr) << desc.shift_left};
const u32 rhs_raw{gpu_memory->Read<u32>(separate_addr)
<< desc.secondary_shift_left};
const u32 lhs_raw{(gpu_memory->Read<u32>(addr) << desc.shift_left) & desc.mask};
const u32 rhs_raw{(gpu_memory->Read<u32>(separate_addr)
<< desc.secondary_shift_left) &
desc.secondary_mask};
return TexturePair(lhs_raw | rhs_raw, via_header_index);
}
}
return TexturePair(gpu_memory->Read<u32>(addr), via_header_index);
u32 raw{gpu_memory->Read<u32>(addr)};
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
raw &= desc.mask;
}
return TexturePair(raw, via_header_index);
}};
const auto add_image{[&](const auto& desc, bool blacklist) {
for (u32 index = 0; index < desc.count; ++index) {
@@ -308,23 +308,37 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) {
const auto read_handle{[&](const auto& desc, u32 index) {
ASSERT(cbufs[desc.cbuf_index].enabled);
const u32 index_offset{index << desc.size_shift};
const u32 offset{desc.cbuf_offset + index_offset};
u32 offset{desc.cbuf_offset + index_offset};
if constexpr (std::is_same_v<decltype(desc), const TextureDescriptor&>) {
if (desc.has_index_cbuf) {
ASSERT(cbufs[desc.index_cbuf_index].enabled);
offset += gpu_memory->Read<u32>(cbufs[desc.index_cbuf_index].address +
desc.index_cbuf_offset)
<< desc.index_shift;
}
}
const GPUVAddr addr{cbufs[desc.cbuf_index].address + offset};
if constexpr (std::is_same_v<decltype(desc), const TextureDescriptor&> ||
std::is_same_v<decltype(desc), const TextureBufferDescriptor&>) {
if (desc.has_secondary) {
ASSERT(cbufs[desc.secondary_cbuf_index].enabled);
const u32 second_offset{desc.secondary_cbuf_offset + index_offset};
const u32 second_offset{desc.secondary_cbuf_offset};
const GPUVAddr separate_addr{cbufs[desc.secondary_cbuf_index].address +
second_offset};
const u32 lhs_raw{gpu_memory->Read<u32>(addr) << desc.shift_left};
const u32 rhs_raw{gpu_memory->Read<u32>(separate_addr)
<< desc.secondary_shift_left};
const u32 lhs_raw{(gpu_memory->Read<u32>(addr) << desc.shift_left) & desc.mask};
const u32 rhs_raw{(gpu_memory->Read<u32>(separate_addr)
<< desc.secondary_shift_left) &
desc.secondary_mask};
const u32 raw{lhs_raw | rhs_raw};
return TexturePair(raw, via_header_index);
}
}
return TexturePair(gpu_memory->Read<u32>(addr), via_header_index);
u32 raw{gpu_memory->Read<u32>(addr)};
if constexpr (std::is_same_v<decltype(desc), const TextureDescriptor&> ||
std::is_same_v<decltype(desc), const TextureBufferDescriptor&>) {
raw &= desc.mask;
}
return TexturePair(raw, via_header_index);
}};
const auto add_image{[&](const auto& desc, bool blacklist) LAMBDA_FORCEINLINE {
for (u32 index = 0; index < desc.count; ++index) {
@@ -129,7 +129,13 @@ void FixedPipelineState::Refresh(Tegra::Engines::Maxwell3D& maxwell3d, DynamicFe
topology.Assign(can_collapse_topology_class
? TOPOLOGY_CLASS_REPRESENTATIVE_LUT[static_cast<size_t>(topology_)]
: topology_);
msaa_mode.Assign(regs.anti_alias_samples_mode);
// Target-independent rasterization rasterizes (and depth tests) at the raster sample count
if (regs.tir_mode == Maxwell::TIRMode::RasterNTargetM &&
regs.anti_alias_raster != Maxwell::AntiAliasRaster::Mode1x1) {
msaa_mode.Assign(static_cast<Tegra::Texture::MsaaMode>(regs.anti_alias_raster));
} else {
msaa_mode.Assign(regs.anti_alias_samples_mode);
}
attachment0_dual_source_blend.Assign(ComputeAttachment0DualSourceBlend(regs) ? 1 : 0);
raw2 = 0;
@@ -20,6 +20,7 @@
#include "video_core/host_shaders/queries_prefix_scan_sum_comp_spv.h"
#include "video_core/host_shaders/queries_prefix_scan_sum_nosubgroups_comp_spv.h"
#include "video_core/host_shaders/resolve_conditional_render_comp_spv.h"
#include "video_core/host_shaders/vulkan_block_linear_unswizzle_2d_buffer_comp_spv.h"
#include "video_core/host_shaders/vulkan_quad_indexed_comp_spv.h"
#include "video_core/host_shaders/vulkan_uint8_comp_spv.h"
#include "video_core/host_shaders/block_linear_unswizzle_3d_bcn_comp_spv.h"
@@ -31,6 +32,7 @@
#include "video_core/renderer_vulkan/vk_update_descriptor.h"
#include "video_core/texture_cache/accelerated_swizzle.h"
#include "video_core/texture_cache/types.h"
#include "video_core/texture_cache/util.h"
#include "video_core/textures/decoders.h"
#include "video_core/vulkan_common/vulkan_device.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
@@ -640,6 +642,143 @@ struct alignas(16) BlockLinearUnswizzle3DPushConstants {
};
static_assert(sizeof(BlockLinearUnswizzle3DPushConstants) <= 128);
struct BlockLinearUnswizzle2DBufferPushConstants {
u32 src_word_offset;
u32 row_words;
u32 height;
u32 layer_stride;
u32 block_size;
u32 x_shift;
u32 block_height;
u32 block_height_mask;
};
BlockLinearUnswizzle2DBufferPass::BlockLinearUnswizzle2DBufferPass(
const Device& device_, Scheduler& scheduler_, DescriptorPool& descriptor_pool_,
StagingBufferPool& staging_buffer_pool_,
ComputePassDescriptorQueue& compute_pass_descriptor_queue_)
: ComputePass(device_, scheduler_, descriptor_pool_, INPUT_OUTPUT_DESCRIPTOR_SET_BINDINGS,
INPUT_OUTPUT_DESCRIPTOR_UPDATE_TEMPLATE, INPUT_OUTPUT_BANK_INFO,
COMPUTE_PUSH_CONSTANT_RANGE<sizeof(BlockLinearUnswizzle2DBufferPushConstants)>,
VULKAN_BLOCK_LINEAR_UNSWIZZLE_2D_BUFFER_COMP_SPV),
scheduler{scheduler_}, staging_buffer_pool{staging_buffer_pool_},
compute_pass_descriptor_queue{compute_pass_descriptor_queue_} {}
BlockLinearUnswizzle2DBufferPass::~BlockLinearUnswizzle2DBufferPass() = default;
bool BlockLinearUnswizzle2DBufferPass::IsSupported(const Image& image) {
const VideoCommon::ImageInfo& info = image.info;
if (info.type != VideoCommon::ImageType::e2D || info.num_samples != 1 ||
VideoCore::Surface::GetFormatType(info.format) != VideoCore::Surface::SurfaceType::ColorTexture ||
VideoCore::Surface::DefaultBlockWidth(info.format) != 1 ||
VideoCore::Surface::DefaultBlockHeight(info.format) != 1 ||
True(image.flags & VideoCommon::ImageFlagBits::Converted) ||
True(image.flags & VideoCommon::ImageFlagBits::Rescaled)) {
return false;
}
const u32 bytes_per_block = VideoCore::Surface::BytesPerBlock(info.format);
for (const VideoCommon::SwizzleParameters& swizzle : VideoCommon::FullUploadSwizzles(info)) {
if ((swizzle.num_tiles.width * bytes_per_block) % 4 != 0 || swizzle.num_tiles.depth != 1) {
return false;
}
}
return true;
}
std::pair<VkBuffer, boost::container::small_vector<VkBufferImageCopy, 16>>
BlockLinearUnswizzle2DBufferPass::Unswizzle(Image& image, VkBuffer src_buffer,
VkDeviceSize src_offset) {
const VideoCommon::ImageInfo& info = image.info;
const auto swizzles = VideoCommon::FullUploadSwizzles(info);
const u32 bytes_per_block = VideoCore::Surface::BytesPerBlock(info.format);
const u32 layers = static_cast<u32>(info.resources.layers);
const VkDeviceSize alignment = (std::max<VkDeviceSize>)(device.GetStorageBufferAlignment(), 4);
VkDeviceSize total_size = 0;
for (const VideoCommon::SwizzleParameters& swizzle : swizzles) {
total_size = Common::AlignUp(total_size, alignment);
total_size += VkDeviceSize{swizzle.num_tiles.width} * bytes_per_block *
swizzle.num_tiles.height * layers;
}
const StagingBufferRef linear = staging_buffer_pool.Request(total_size, MemoryUsage::DeviceLocal);
scheduler.RequestOutsideRenderPassOperationContext();
scheduler.Record([](vk::CommandBuffer cmdbuf) {
static constexpr VkMemoryBarrier READ_BARRIER{
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
.pNext = nullptr,
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT,
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
};
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, READ_BARRIER);
});
boost::container::small_vector<VkBufferImageCopy, 16> copies;
VkDeviceSize linear_offset = 0;
for (const VideoCommon::SwizzleParameters& swizzle : swizzles) {
linear_offset = Common::AlignUp(linear_offset, alignment);
const auto params = VideoCommon::Accelerated::MakeBlockLinearSwizzle2DParams(swizzle, info);
const u32 row_words = swizzle.num_tiles.width * bytes_per_block / 4;
const u32 height = swizzle.num_tiles.height;
const VkDeviceSize level_size = VkDeviceSize{row_words} * 4 * height * layers;
const VkDeviceSize level_src = src_offset + swizzle.buffer_offset;
const VkDeviceSize aligned_src = level_src & ~(alignment - 1);
const VkDeviceSize src_size = image.guest_size_bytes - swizzle.buffer_offset +
(level_src - aligned_src);
compute_pass_descriptor_queue.Acquire(scheduler, 2);
compute_pass_descriptor_queue.AddBuffer(src_buffer, aligned_src, src_size);
compute_pass_descriptor_queue.AddBuffer(linear.buffer, linear.offset + linear_offset,
level_size);
const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()};
const BlockLinearUnswizzle2DBufferPushConstants push{
.src_word_offset = static_cast<u32>((level_src - aligned_src) / 4),
.row_words = row_words,
.height = height,
.layer_stride = params.layer_stride,
.block_size = params.block_size,
.x_shift = params.x_shift,
.block_height = params.block_height,
.block_height_mask = params.block_height_mask,
};
scheduler.Record([this, descriptor_data, push, layers](vk::CommandBuffer cmdbuf) {
static constexpr u32 WORKGROUP_SIZE_X = 64;
const VkDescriptorSet set = descriptor_allocator.Commit();
device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, descriptor_data);
cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *pipeline);
cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {});
cmdbuf.PushConstants(*layout, VK_SHADER_STAGE_COMPUTE_BIT, push);
cmdbuf.Dispatch(Common::DivCeil(push.row_words, WORKGROUP_SIZE_X), push.height, layers);
});
copies.push_back(VkBufferImageCopy{
.bufferOffset = linear.offset + linear_offset,
.bufferRowLength = 0,
.bufferImageHeight = 0,
.imageSubresource{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.mipLevel = static_cast<u32>(swizzle.level),
.baseArrayLayer = 0,
.layerCount = layers,
},
.imageOffset{0, 0, 0},
.imageExtent{swizzle.num_tiles.width, height, 1},
});
linear_offset += level_size;
}
scheduler.Record([](vk::CommandBuffer cmdbuf) {
static constexpr VkMemoryBarrier WRITE_BARRIER{
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
.pNext = nullptr,
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT,
};
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, WRITE_BARRIER);
});
return {linear.buffer, std::move(copies)};
}
BlockLinearUnswizzle3DPass::BlockLinearUnswizzle3DPass(
const Device& device_, Scheduler& scheduler_,
DescriptorPool& descriptor_pool_,
@@ -10,6 +10,8 @@
#include <span>
#include <utility>
#include <boost/container/small_vector.hpp>
#include "common/common_types.h"
#include "video_core/engines/maxwell_3d.h"
#include "video_core/renderer_vulkan/vk_descriptor_pool.h"
@@ -137,6 +139,28 @@ private:
MemoryAllocator& memory_allocator;
};
class BlockLinearUnswizzle2DBufferPass final : public ComputePass {
public:
explicit BlockLinearUnswizzle2DBufferPass(
const Device& device_, Scheduler& scheduler_, DescriptorPool& descriptor_pool_,
StagingBufferPool& staging_buffer_pool_,
ComputePassDescriptorQueue& compute_pass_descriptor_queue_);
~BlockLinearUnswizzle2DBufferPass();
/// Returns true when the guest layout of the image can be unswizzled by this pass
[[nodiscard]] static bool IsSupported(const Image& image);
/// Unswizzles the block linear guest data of the image, read from a GPU buffer, into a linear
/// buffer. Returns the linear buffer and the copies that upload it into the image.
std::pair<VkBuffer, boost::container::small_vector<VkBufferImageCopy, 16>> Unswizzle(
Image& image, VkBuffer src_buffer, VkDeviceSize src_offset);
private:
Scheduler& scheduler;
StagingBufferPool& staging_buffer_pool;
ComputePassDescriptorQueue& compute_pass_descriptor_queue;
};
class BlockLinearUnswizzle3DPass final : public ComputePass {
public:
explicit BlockLinearUnswizzle3DPass(const Device& device_, Scheduler& scheduler_,
@@ -174,21 +174,36 @@ bool ComputePipeline::Configure(Tegra::Engines::KeplerCompute& kepler_compute,
const auto read_handle{[&](const auto& desc, u32 index) {
ASSERT(((qmd.const_buffer_enable_mask >> desc.cbuf_index) & 1) != 0);
const u32 index_offset{index << desc.size_shift};
const u32 offset{desc.cbuf_offset + index_offset};
u32 offset{desc.cbuf_offset + index_offset};
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) {
if (desc.has_index_cbuf) {
ASSERT(((qmd.const_buffer_enable_mask >> desc.index_cbuf_index) & 1) != 0);
offset += gpu_memory.Read<u32>(cbufs[desc.index_cbuf_index].Address() +
desc.index_cbuf_offset)
<< desc.index_shift;
}
}
const GPUVAddr addr{cbufs[desc.cbuf_index].Address() + offset};
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
if (desc.has_secondary) {
ASSERT(((qmd.const_buffer_enable_mask >> desc.secondary_cbuf_index) & 1) != 0);
const u32 secondary_offset{desc.secondary_cbuf_offset + index_offset};
const u32 secondary_offset{desc.secondary_cbuf_offset};
const GPUVAddr separate_addr{cbufs[desc.secondary_cbuf_index].Address() +
secondary_offset};
const u32 lhs_raw{gpu_memory.Read<u32>(addr) << desc.shift_left};
const u32 rhs_raw{gpu_memory.Read<u32>(separate_addr) << desc.secondary_shift_left};
const u32 lhs_raw{(gpu_memory.Read<u32>(addr) << desc.shift_left) & desc.mask};
const u32 rhs_raw{(gpu_memory.Read<u32>(separate_addr)
<< desc.secondary_shift_left) &
desc.secondary_mask};
return TexturePair(lhs_raw | rhs_raw, via_header_index);
}
}
return TexturePair(gpu_memory.Read<u32>(addr), via_header_index);
u32 raw{gpu_memory.Read<u32>(addr)};
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
raw &= desc.mask;
}
return TexturePair(raw, via_header_index);
}};
const auto add_image{[&](const auto& desc, bool blacklist) {
for (u32 index = 0; index < desc.count; ++index) {
@@ -377,23 +377,37 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) {
const auto read_handle{[&](const auto& desc, u32 index) {
ASSERT(cbufs[desc.cbuf_index].enabled);
const u32 index_offset{index << desc.size_shift};
const u32 offset{desc.cbuf_offset + index_offset};
u32 offset{desc.cbuf_offset + index_offset};
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) {
if (desc.has_index_cbuf) {
ASSERT(cbufs[desc.index_cbuf_index].enabled);
offset += gpu_memory->Read<u32>(cbufs[desc.index_cbuf_index].address +
desc.index_cbuf_offset)
<< desc.index_shift;
}
}
const GPUVAddr addr{cbufs[desc.cbuf_index].address + offset};
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
if (desc.has_secondary) {
ASSERT(cbufs[desc.secondary_cbuf_index].enabled);
const u32 second_offset{desc.secondary_cbuf_offset + index_offset};
const u32 second_offset{desc.secondary_cbuf_offset};
const GPUVAddr separate_addr{cbufs[desc.secondary_cbuf_index].address +
second_offset};
const u32 lhs_raw{gpu_memory->Read<u32>(addr) << desc.shift_left};
const u32 rhs_raw{gpu_memory->Read<u32>(separate_addr)
<< desc.secondary_shift_left};
const u32 lhs_raw{(gpu_memory->Read<u32>(addr) << desc.shift_left) & desc.mask};
const u32 rhs_raw{(gpu_memory->Read<u32>(separate_addr)
<< desc.secondary_shift_left) &
desc.secondary_mask};
const u32 raw{lhs_raw | rhs_raw};
return TexturePair(raw, via_header_index);
}
}
return TexturePair(gpu_memory->Read<u32>(addr), via_header_index);
u32 raw{gpu_memory->Read<u32>(addr)};
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
raw &= desc.mask;
}
return TexturePair(raw, via_header_index);
}};
const auto add_image{[&](const auto& desc, bool blacklist) LAMBDA_FORCEINLINE {
for (u32 index = 0; index < desc.count; ++index) {
@@ -596,6 +596,12 @@ void RasterizerVulkan::DispatchCompute() {
return;
}
const auto update_images_written_by_buffers = [this] {
buffer_cache.ForEachWrittenComputeStorageBuffer(
[this](auto& buffer, u32 offset, DAddr device_addr, u32 size) {
texture_cache.UpdateImagesFromBuffer(device_addr, size, buffer.Handle(), offset);
});
};
const auto& qmd{kepler_compute->launch_description};
auto indirect_address = kepler_compute->GetIndirectComputeAddress();
if (indirect_address) {
@@ -612,6 +618,7 @@ void RasterizerVulkan::DispatchCompute() {
}
cmdbuf.DispatchIndirect(indirect_buffer, indirect_offset);
});
update_images_written_by_buffers();
return;
}
const std::array<u32, 3> dim{qmd.grid_dim_x, qmd.grid_dim_y, qmd.grid_dim_z};
@@ -634,6 +641,7 @@ void RasterizerVulkan::DispatchCompute() {
}
cmdbuf.Dispatch(dim[0], dim[1], dim[2]);
});
update_images_written_by_buffers();
// Log compute dispatch
if (GPU::Logging::IsActive() &&
@@ -946,6 +946,8 @@ TextureCacheRuntime::TextureCacheRuntime(const Device& device_, Scheduler& sched
astc_decoder_pass.emplace(device, scheduler, descriptor_pool, staging_buffer_pool,
compute_pass_descriptor_queue, memory_allocator);
}
bl2d_buffer_unswizzle_pass.emplace(device, scheduler, descriptor_pool, staging_buffer_pool,
compute_pass_descriptor_queue);
if (!device.IsKhrImageFormatListSupported()) {
return;
}
@@ -1089,6 +1091,58 @@ VkImageView TextureCacheRuntime::GetOrCreateResolveShadow(VkImage msaa_image, Vk
return *shadow.view;
}
VkImageView TextureCacheRuntime::GetOrCreateTirColorScratch(VkImage color_image, VkFormat format,
VkExtent2D extent, u32 layers,
VkSampleCountFlagBits samples) {
TirColorScratch& scratch = tir_color_scratch[color_image];
if (scratch.image && scratch.format == format && scratch.extent.width == extent.width &&
scratch.extent.height == extent.height && scratch.layers == layers &&
scratch.samples == samples) {
return *scratch.view;
}
if (scratch.image) {
scheduler.Finish();
}
scratch.image = memory_allocator.CreateImage(VkImageCreateInfo{
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.imageType = VK_IMAGE_TYPE_2D,
.format = format,
.extent = {extent.width, extent.height, 1},
.mipLevels = 1,
.arrayLayers = layers,
.samples = samples,
.tiling = VK_IMAGE_TILING_OPTIMAL,
.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
.queueFamilyIndexCount = 0,
.pQueueFamilyIndices = nullptr,
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
});
scratch.view = device.GetLogical().CreateImageView(VkImageViewCreateInfo{
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.image = *scratch.image,
.viewType = layers > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D,
.format = format,
.components{},
.subresourceRange{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = layers,
},
});
scratch.format = format;
scratch.extent = extent;
scratch.layers = layers;
scratch.samples = samples;
return *scratch.view;
}
const TextureCacheRuntime::ResolveShadow* TextureCacheRuntime::GetValidResolveShadow(
VkImage msaa_image) const {
const auto it = resolve_shadows.find(msaa_image);
@@ -2068,6 +2122,27 @@ void Image::UploadMemory(VkBuffer buffer, VkDeviceSize offset,
}
}
bool Image::UploadSwizzledFromBuffer(VkBuffer buffer, VkDeviceSize offset) {
if (runtime == nullptr || !runtime->bl2d_buffer_unswizzle_pass || offset % 4 != 0 ||
guest_size_bytes > runtime->device.GetMaxStorageBufferRange() ||
!BlockLinearUnswizzle2DBufferPass::IsSupported(*this)) {
return false;
}
if (ENABLE_MSAA_RESOLVE_CONSUME) {
runtime->InvalidateResolveShadow(Handle());
}
auto [linear_buffer, copies] =
runtime->bl2d_buffer_unswizzle_pass->Unswizzle(*this, buffer, offset);
const VkImage vk_image = *original_image;
const VkImageAspectFlags vk_aspect_mask = aspect_mask;
const bool was_initialized = std::exchange(initialized, true);
scheduler->Record([linear_buffer, vk_image, vk_aspect_mask, was_initialized,
copies = std::move(copies)](vk::CommandBuffer cmdbuf) {
CopyBufferToImage(cmdbuf, linear_buffer, vk_image, vk_aspect_mask, was_initialized, copies);
});
return true;
}
void Image::UploadMemory(const StagingBufferRef& map, std::span<const BufferImageCopy> copies) {
UploadMemory(map.buffer, map.offset, copies);
}
@@ -2970,6 +3045,26 @@ Framebuffer::Framebuffer(TextureCacheRuntime& runtime, ImageView* color_buffer,
Framebuffer::~Framebuffer() = default;
/// Single-sampled color targets with a multisampled depth buffer, as used by target-independent
/// rasterization. Vulkan requires matching sample counts for these attachments.
static bool IsMixedSamplesTarget(std::span<ImageView*, NUM_RT> color_buffers,
const ImageView* depth_buffer) {
if (depth_buffer == nullptr || depth_buffer->Samples() == VK_SAMPLE_COUNT_1_BIT) {
return false;
}
bool has_color = false;
for (const ImageView* const color_buffer : color_buffers) {
if (color_buffer == nullptr) {
continue;
}
if (color_buffer->Samples() != VK_SAMPLE_COUNT_1_BIT) {
return false;
}
has_color = true;
}
return has_color;
}
void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime,
std::span<ImageView*, NUM_RT> color_buffers,
ImageView* depth_buffer, bool is_rescaled_) {
@@ -3024,6 +3119,13 @@ void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime,
} else {
renderpass_key.depth_format = PixelFormat::Invalid;
}
if (IsMixedSamplesTarget(color_buffers, depth_buffer)) {
const VkExtent2D extent{(std::min)(render_area.width, width),
(std::min)(render_area.height, height)};
CreateMixedSamplesFramebuffer(runtime, color_buffers, attachments, renderpass_key,
static_cast<u32>((std::max)(num_layers, 1)), extent);
return;
}
renderpass_key.samples = samples;
const bool do_resolve_color = ENABLE_MSAA_TILER_RESOLVE &&
samples != VK_SAMPLE_COUNT_1_BIT && num_colors > 0 && runtime.device.IsTiler();
@@ -3040,10 +3142,7 @@ void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime,
discard_msaa_depth_stencil = ENABLE_MSAA_RESOLVE_CONSUME &&
ENABLE_MSAA_DEPTH_STENCIL_DISCARD && do_resolve_depth_stencil;
renderpass = runtime.render_pass_cache.Get(renderpass_key);
render_pass_key = renderpass_key;
render_pass_cache = &runtime.render_pass_cache;
runtime_ptr = &runtime;
UseRenderPass(runtime, renderpass_key);
render_area.width = (std::min)(render_area.width, width);
render_area.height = (std::min)(render_area.height, height);
@@ -3075,6 +3174,53 @@ void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime,
}
num_color_buffers = static_cast<u32>(num_colors);
CreateVkFramebuffer(runtime, attachments, static_cast<u32>((std::max)(num_layers, 1)));
}
void Framebuffer::CreateMixedSamplesFramebuffer(TextureCacheRuntime& runtime,
std::span<ImageView*, NUM_RT> color_buffers,
std::span<const VkImageView> attachments,
RenderPassKey renderpass_key, u32 layers,
VkExtent2D extent) {
// attachments holds the real color targets followed by the depth buffer
const size_t num_colors = attachments.size() - 1;
boost::container::small_vector<VkImageView, NUM_RT * 2 + 2> mixed_attachments;
for (const ImageView* const color_buffer : color_buffers) {
if (!color_buffer) {
continue;
}
const VkFormat vk_format = MaxwellToVK::SurfaceFormat(runtime.device, FormatType::Optimal,
true, color_buffer->format)
.format;
mixed_attachments.push_back(runtime.GetOrCreateTirColorScratch(
color_buffer->ImageHandle(), vk_format, extent, layers, samples));
}
mixed_attachments.push_back(attachments[num_colors]);
for (size_t index = 0; index < num_colors; ++index) {
mixed_attachments.push_back(attachments[index]);
}
// Multisampled scratch color + depth, resolving color into the real single-sampled targets
renderpass_key.samples = samples;
renderpass_key.resolve_color = true;
renderpass_key.resolve_depth_stencil = false;
discard_msaa_color = false;
discard_msaa_depth_stencil = false;
UseRenderPass(runtime, renderpass_key);
render_area = extent;
num_color_buffers = static_cast<u32>(num_colors);
CreateVkFramebuffer(runtime, mixed_attachments, layers);
}
void Framebuffer::UseRenderPass(TextureCacheRuntime& runtime,
const RenderPassKey& renderpass_key) {
renderpass = runtime.render_pass_cache.Get(renderpass_key);
render_pass_key = renderpass_key;
render_pass_cache = &runtime.render_pass_cache;
runtime_ptr = &runtime;
}
void Framebuffer::CreateVkFramebuffer(TextureCacheRuntime& runtime,
std::span<const VkImageView> attachments, u32 layers) {
framebuffer = runtime.device.GetLogical().CreateFramebuffer({
.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
.pNext = nullptr,
@@ -3084,7 +3230,7 @@ void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime,
.pAttachments = attachments.data(),
.width = render_area.width,
.height = render_area.height,
.layers = static_cast<u32>((std::max)(num_layers, 1)),
.layers = layers,
});
}
@@ -133,6 +133,11 @@ public:
[[nodiscard]] const ResolveShadow* GetValidResolveShadow(VkImage msaa_image) const;
/// Multisampled stand-in for a single-sampled color target rendered with a TIR depth buffer
[[nodiscard]] VkImageView GetOrCreateTirColorScratch(VkImage color_image, VkFormat format,
VkExtent2D extent, u32 layers,
VkSampleCountFlagBits samples);
void InvalidateResolveShadow(VkImage msaa_image);
void MarkResolveShadowUpToDate(VkImage msaa_image);
@@ -159,6 +164,7 @@ public:
std::optional<ASTCDecoderPass> astc_decoder_pass;
std::optional<BlockLinearUnswizzle3DPass> bl3d_unswizzle_pass;
std::optional<BlockLinearUnswizzle2DBufferPass> bl2d_buffer_unswizzle_pass;
const Settings::ResolutionScalingInfo& resolution;
std::array<std::vector<VkFormat>, VideoCore::Surface::MaxPixelFormat> view_formats;
@@ -187,6 +193,15 @@ public:
std::vector<MsaaScratchImage> msaa_scratch_images;
::Common::unordered_map<VkImage, ResolveShadow> resolve_shadows;
struct TirColorScratch {
vk::Image image;
vk::ImageView view;
VkFormat format = VK_FORMAT_UNDEFINED;
VkExtent2D extent{};
u32 layers = 0;
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
};
::Common::unordered_map<VkImage, TirColorScratch> tir_color_scratch;
std::vector<std::pair<u64, ResolveShadow>> pending_resolve_shadows;
};
@@ -279,6 +294,22 @@ public:
void MarkResolveShadowsUpToDate() const;
private:
/// Builds a framebuffer for single-sampled color targets drawn with a multisampled depth
/// buffer (target-independent rasterization). Color is rendered into multisampled scratch
/// images that resolve into the real targets at the end of the pass.
void CreateMixedSamplesFramebuffer(TextureCacheRuntime& runtime,
std::span<ImageView*, NUM_RT> color_buffers,
std::span<const VkImageView> attachments,
RenderPassKey renderpass_key, u32 layers,
VkExtent2D extent);
/// Fetches the render pass for the key from the cache and remembers it for variants.
void UseRenderPass(TextureCacheRuntime& runtime, const RenderPassKey& renderpass_key);
/// Creates the Vulkan framebuffer for the current render pass and render area.
void CreateVkFramebuffer(TextureCacheRuntime& runtime,
std::span<const VkImageView> attachments, u32 layers);
static constexpr size_t NUM_MEMOIZED_RENDER_PASS_VARIANTS = 8;
vk::Framebuffer framebuffer;
@@ -325,6 +356,10 @@ public:
void UploadMemory(const StagingBufferRef& map,
std::span<const VideoCommon::BufferImageCopy> copies);
/// Replaces the image contents with its block linear guest data held in a GPU buffer.
/// Returns false when the image layout is not supported.
bool UploadSwizzledFromBuffer(VkBuffer buffer, VkDeviceSize offset);
void DownloadMemory(VkBuffer buffer, size_t offset,
std::span<const VideoCommon::BufferImageCopy> copies);
@@ -1,3 +1,6 @@
// 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
@@ -6,4 +9,6 @@
namespace VideoCommon {
template class VideoCommon::TextureCache<Vulkan::TextureCacheParams>;
template void VideoCommon::TextureCache<Vulkan::TextureCacheParams>::UpdateImagesFromBuffer<VkBuffer>(
DAddr, size_t, VkBuffer, u64);
}
+50 -1
View File
@@ -595,6 +595,36 @@ void TextureCache<P>::WriteMemory(DAddr cpu_addr, size_t size) {
});
}
template <class P>
template <typename BufferHandle>
void TextureCache<P>::UpdateImagesFromBuffer(DAddr cpu_addr, size_t size, BufferHandle buffer,
u64 buffer_offset) {
if constexpr (requires(Image& image) { image.UploadSwizzledFromBuffer(buffer, buffer_offset); }) {
// The guest can write a render target through a storage buffer bound over its memory
// (e.g. a compute clear). Only images the binding matches exactly are replaced, a larger
// binding may hold unrelated live images in the parts the shader does not write.
static constexpr size_t MAX_BINDING_PADDING = 64 * 1024;
ForEachImageInRegion(cpu_addr, size, [&](ImageId image_id, Image& image) {
if (image.cpu_addr != cpu_addr || size < image.guest_size_bytes ||
size - image.guest_size_bytes > MAX_BINDING_PADDING ||
True(image.flags & ImageFlagBits::Remapped) ||
True(image.flags & ImageFlagBits::Sparse)) {
return;
}
if (!image.UploadSwizzledFromBuffer(buffer, buffer_offset)) {
return;
}
if (True(image.flags & ImageFlagBits::CpuModified)) {
image.flags &= ~ImageFlagBits::CpuModified;
if (False(image.flags & ImageFlagBits::Tracked)) {
TrackImage(image, image_id);
}
}
MarkModification(image);
});
}
}
template <class P>
bool TextureCache<P>::IsDownloadable(const ImageBase& image) const noexcept {
if (!image.IsSafeGpuCopy()) {
@@ -1928,7 +1958,26 @@ ImageViewId TextureCache<P>::FindDepthBuffer() {
if (gpu_addr == 0) {
return ImageViewId{};
}
const ImageInfo info(regs.zeta, regs.zeta_size, regs.anti_alias_samples_mode);
// Target-independent rasterization: the depth buffer keeps the raster sample count while
// the color targets use anti_alias_samples_mode.
Tegra::Texture::MsaaMode zeta_msaa_mode = regs.anti_alias_samples_mode;
if (regs.anti_alias_raster != Tegra::Engines::Maxwell3D::Regs::AntiAliasRaster::Mode1x1) {
const auto raster_mode = static_cast<Tegra::Texture::MsaaMode>(regs.anti_alias_raster);
bool use_raster = regs.tir_mode == Tegra::Engines::Maxwell3D::Regs::TIRMode::RasterNTargetM;
if (!use_raster && regs.anti_alias_samples_mode == Tegra::Texture::MsaaMode::Msaa1x1) {
// Clears can be issued before tir_mode is enabled. Only a depth buffer sized for the
// raster samples relative to the color target is treated as multisampled, otherwise
// it ping-pongs between a 1x and a multisampled image every frame.
const auto& rt0 = regs.rt[0];
use_raster = rt0.Address() != 0 && rt0.format != Tegra::RenderTargetFormat::NONE &&
regs.zeta_size.width == rt0.width * NumSamplesX(raster_mode) &&
regs.zeta_size.height == rt0.height * NumSamplesY(raster_mode);
}
if (use_raster) {
zeta_msaa_mode = raster_mode;
}
}
const ImageInfo info(regs.zeta, regs.zeta_size, zeta_msaa_mode);
return FindRenderTargetView(info, gpu_addr);
}
@@ -203,6 +203,12 @@ public:
/// Mark images in a range as modified from the CPU
void WriteMemory(DAddr cpu_addr, size_t size);
/// Replaces the contents of the images fully inside a range that a GPU buffer wrote, with the
/// data of that buffer
template <typename BufferHandle>
void UpdateImagesFromBuffer(DAddr cpu_addr, size_t size, BufferHandle buffer,
u64 buffer_offset);
/// Download contents of host images to guest memory in a region
void DownloadMemory(DAddr cpu_addr, size_t size);