mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-10 17:16:47 -04:00
452 lines
16 KiB
C++
452 lines
16 KiB
C++
#include "efb_ram_copy.hpp"
|
|
|
|
#include "efb_ram_encoder.hpp"
|
|
#include "tex_copy_conv.hpp"
|
|
#include "../internal.hpp"
|
|
#include "../webgpu/gpu.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstring>
|
|
#include <mutex>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
#include <magic_enum.hpp>
|
|
|
|
namespace aurora::gfx::efb_ram {
|
|
namespace {
|
|
|
|
Module Log("aurora::gfx::efb_ram");
|
|
|
|
using webgpu::g_device;
|
|
using webgpu::g_instance;
|
|
|
|
// A CPU-consumed EFB copy this small is the lens-flare occlusion probe (4x4 Z24X8, 64 bytes).
|
|
// Resolving it synchronously stalls the producer and kills interpolation, so read it back async.
|
|
constexpr size_t kAsyncReadbackMaxBytes = 256;
|
|
// Each destination keeps its readback buffer forever. Only a handful are expected, and the cap
|
|
// stops an unexpected pattern of one-shot destinations from leaking GPU buffers.
|
|
constexpr size_t kMaxAsyncSlots = 32;
|
|
|
|
struct PendingCopy {
|
|
void* dest = nullptr;
|
|
uint32_t width = 0;
|
|
uint32_t height = 0;
|
|
GXTexFmt format = GX_TF_RGBA8;
|
|
TextureHandle texture;
|
|
TextureHandle nativeTexture;
|
|
Range nativeBlitUniform;
|
|
};
|
|
|
|
struct Download {
|
|
PendingCopy copy;
|
|
wgpu::Buffer buffer;
|
|
uint32_t bytesPerRow = 0;
|
|
uint64_t bufferSize = 0;
|
|
};
|
|
|
|
enum class AsyncState : uint8_t {
|
|
Idle,
|
|
CopySubmitted,
|
|
MapPending,
|
|
};
|
|
|
|
// One readback per destination address, reused for that destination's lifetime: the probe hits
|
|
// the same buffer every frame, so pooling avoids a per-frame allocation.
|
|
struct AsyncSlot {
|
|
wgpu::Buffer buffer;
|
|
TextureHandle nativeTexture;
|
|
uint64_t bufferSize = 0;
|
|
uint32_t bytesPerRow = 0;
|
|
// Latched at encode time and read by the map callback.
|
|
void* dest = nullptr;
|
|
GXTexFmt format = GX_TF_RGBA8;
|
|
uint32_t width = 0;
|
|
uint32_t height = 0;
|
|
uint32_t hostWidth = 0;
|
|
uint32_t hostHeight = 0;
|
|
HostPixelOrder order = HostPixelOrder::RGBA;
|
|
AsyncState state = AsyncState::Idle;
|
|
};
|
|
|
|
std::vector<PendingCopy> g_pending;
|
|
std::vector<Download> g_downloads;
|
|
|
|
// Recorded by the producer, moved to the worker-private sealed list while the
|
|
// producer is excluded.
|
|
std::vector<PendingCopy> g_asyncPending;
|
|
std::vector<PendingCopy> g_asyncSealed;
|
|
|
|
std::mutex g_asyncMutex;
|
|
std::unordered_map<void*, AsyncSlot> g_asyncSlots;
|
|
uint32_t g_asyncMapsInFlight = 0;
|
|
|
|
uint32_t align_to(uint32_t value, uint32_t alignment) noexcept { return (value + alignment - 1) & ~(alignment - 1); }
|
|
|
|
// An internally scaled copy cannot be read back directly, so blit it down to native-resolution
|
|
// RGBA8 first. Pushes a uniform, so it must run while the staging buffers are still mapped.
|
|
void ensure_native_texture(PendingCopy& pending, TextureHandle* cache = nullptr) noexcept {
|
|
if (pending.texture->size.width == pending.width && pending.texture->size.height == pending.height) {
|
|
return;
|
|
}
|
|
if (pending.nativeTexture) {
|
|
return;
|
|
}
|
|
if (cache != nullptr && *cache && (*cache)->size.width == pending.width &&
|
|
(*cache)->size.height == pending.height) {
|
|
pending.nativeTexture = *cache;
|
|
} else {
|
|
pending.nativeTexture = new_conv_texture(pending.width, pending.height, GX_TF_RGBA8, "GX EFB RAM native readback");
|
|
if (cache != nullptr) {
|
|
*cache = pending.nativeTexture;
|
|
}
|
|
}
|
|
const std::array nativeBlitUniform{
|
|
0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 64.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
|
|
};
|
|
pending.nativeBlitUniform = push_uniform(nativeBlitUniform);
|
|
}
|
|
|
|
void encode_native_blit(const wgpu::CommandEncoder& encoder, const PendingCopy& pending) noexcept {
|
|
const tex_copy_conv::ConvRequest blitRequest{
|
|
.fmt = GX_TF_RGBA8,
|
|
.srcView = pending.texture->sampleTextureView,
|
|
.uniformRange = pending.nativeBlitUniform,
|
|
.dst = pending.nativeTexture,
|
|
.sampleFilter = pending.format == GX_TF_Z16 || pending.format == GX_TF_Z24X8
|
|
? tex_copy_conv::SampleFilter::Nearest
|
|
: tex_copy_conv::SampleFilter::Linear,
|
|
};
|
|
tex_copy_conv::blit(encoder, blitRequest);
|
|
}
|
|
|
|
HostPixelOrder texture_pixel_order(const TextureHandle& texture) noexcept {
|
|
return texture->format == wgpu::TextureFormat::BGRA8Unorm ? HostPixelOrder::BGRA : HostPixelOrder::RGBA;
|
|
}
|
|
|
|
void complete_async_slot(void* dest, wgpu::MapAsyncStatus status, wgpu::StringView message) noexcept {
|
|
std::lock_guard lock{g_asyncMutex};
|
|
if (g_asyncMapsInFlight > 0) {
|
|
--g_asyncMapsInFlight;
|
|
}
|
|
const auto it = g_asyncSlots.find(dest);
|
|
if (it == g_asyncSlots.end()) {
|
|
return;
|
|
}
|
|
auto& slot = it->second;
|
|
if (status == wgpu::MapAsyncStatus::Success) {
|
|
const auto* pixels = static_cast<const uint8_t*>(slot.buffer.GetConstMappedRange(0, slot.bufferSize));
|
|
if (pixels != nullptr) {
|
|
// Writes guest RAM from the event-queue thread while the guest may be reading it. The only
|
|
// consumer min/maxes depth for a fade factor, so a torn tile just mixes two frames' depths.
|
|
const size_t outputSize = encoded_size(slot.format, slot.width, slot.height);
|
|
if (!encode(slot.dest, outputSize, slot.format, slot.width, slot.height, pixels, slot.hostWidth, slot.hostHeight,
|
|
slot.bytesPerRow, slot.order)) {
|
|
Log.error("Failed to encode async EFB RAM copy format=0x{:x} size={}x{}", static_cast<unsigned>(slot.format),
|
|
slot.width, slot.height);
|
|
}
|
|
// Guest RAM written from outside the embedder, so nothing bumps its write generation and a
|
|
// texture cached over this range would keep its digest.
|
|
notify_guest_write(slot.dest, outputSize);
|
|
}
|
|
slot.buffer.Unmap();
|
|
} else if (status != wgpu::MapAsyncStatus::CallbackCancelled && status != wgpu::MapAsyncStatus::Aborted) {
|
|
Log.warn("Async EFB RAM readback mapping failed {}: {}", magic_enum::enum_name(status), message);
|
|
}
|
|
slot.state = AsyncState::Idle;
|
|
}
|
|
|
|
// Runs the completion callbacks for maps whose GPU work has finished. Must not
|
|
// be called while g_asyncMutex is held: complete_async_slot takes it.
|
|
void drain_async_events() noexcept {
|
|
{
|
|
std::lock_guard lock{g_asyncMutex};
|
|
if (g_asyncMapsInFlight == 0) {
|
|
return;
|
|
}
|
|
}
|
|
if (g_instance) {
|
|
g_instance.ProcessEvents();
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void schedule(void* dest, uint32_t width, uint32_t height, GXTexFmt format, TextureHandle texture) noexcept {
|
|
if (dest == nullptr || width == 0 || height == 0 || !texture) {
|
|
return;
|
|
}
|
|
if (!supports_format(format)) {
|
|
Log.fatal("Unsupported CPU-visible EFB copy format 0x{:x}", static_cast<unsigned>(format));
|
|
}
|
|
|
|
PendingCopy request{
|
|
.dest = dest,
|
|
.width = width,
|
|
.height = height,
|
|
.format = format,
|
|
.texture = std::move(texture),
|
|
};
|
|
|
|
const size_t encodedSize = encoded_size(format, width, height);
|
|
// Offscreen copies resolve on a separate pass list that the sealed frame's
|
|
// native render does not replay, so they stay on the lazy path.
|
|
bool async = !is_offscreen() && encodedSize != 0 && encodedSize <= kAsyncReadbackMaxBytes;
|
|
if (async) {
|
|
std::lock_guard lock{g_asyncMutex};
|
|
const auto it = g_asyncSlots.find(dest);
|
|
if (it == g_asyncSlots.end()) {
|
|
if (g_asyncSlots.size() >= kMaxAsyncSlots) {
|
|
async = false;
|
|
} else {
|
|
g_asyncSlots.try_emplace(dest);
|
|
// No readback has landed here yet. 0xff decodes to far Z, which the probe reads as unobstructed
|
|
// so a new flare fades in; zero-filled RAM would decode as fully occluded.
|
|
std::memset(dest, 0xff, encodedSize);
|
|
notify_guest_write(dest, encodedSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto& list = async ? g_asyncPending : g_pending;
|
|
if (auto it = std::find_if(list.begin(), list.end(),
|
|
[dest](const PendingCopy& pending) { return pending.dest == dest; });
|
|
it != list.end()) {
|
|
*it = std::move(request);
|
|
} else {
|
|
list.push_back(std::move(request));
|
|
}
|
|
}
|
|
|
|
bool has_pending(void* dest) noexcept {
|
|
if (dest == nullptr) return !g_pending.empty() || !g_downloads.empty();
|
|
return std::any_of(g_pending.begin(), g_pending.end(),
|
|
[dest](const PendingCopy& pending) { return pending.dest == dest; }) ||
|
|
std::any_of(g_downloads.begin(), g_downloads.end(),
|
|
[dest](const Download& download) { return download.copy.dest == dest; });
|
|
}
|
|
|
|
bool prepare_downloads(void* dest) noexcept {
|
|
bool found = false;
|
|
for (auto& pending : g_pending) {
|
|
if (dest != nullptr && pending.dest != dest) continue;
|
|
found = true;
|
|
ensure_native_texture(pending);
|
|
}
|
|
return found;
|
|
}
|
|
|
|
void encode_downloads(const wgpu::CommandEncoder& encoder, void* dest) noexcept {
|
|
if (!g_downloads.empty()) {
|
|
Log.fatal("Attempted to encode new EFB RAM downloads before completing the previous batch");
|
|
}
|
|
|
|
g_downloads.reserve(g_pending.size());
|
|
for (auto it = g_pending.begin(); it != g_pending.end();) {
|
|
if (dest != nullptr && it->dest != dest) {
|
|
++it;
|
|
continue;
|
|
}
|
|
PendingCopy pending = std::move(*it);
|
|
it = g_pending.erase(it);
|
|
if (pending.nativeTexture) {
|
|
encode_native_blit(encoder, pending);
|
|
}
|
|
const auto& texture = pending.nativeTexture ? pending.nativeTexture : pending.texture;
|
|
const uint32_t bytesPerRow = align_to(texture->size.width * 4, 256);
|
|
const uint64_t bufferSize = static_cast<uint64_t>(bytesPerRow) * texture->size.height;
|
|
const wgpu::BufferDescriptor descriptor{
|
|
.label = "GX EFB RAM copy readback",
|
|
.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead,
|
|
.size = bufferSize,
|
|
};
|
|
auto buffer = g_device.CreateBuffer(&descriptor);
|
|
const wgpu::TexelCopyTextureInfo source{
|
|
.texture = texture->texture,
|
|
.mipLevel = 0,
|
|
.origin = {0, 0, 0},
|
|
.aspect = wgpu::TextureAspect::All,
|
|
};
|
|
const wgpu::TexelCopyBufferInfo destination{
|
|
.layout =
|
|
wgpu::TexelCopyBufferLayout{
|
|
.offset = 0,
|
|
.bytesPerRow = bytesPerRow,
|
|
.rowsPerImage = texture->size.height,
|
|
},
|
|
.buffer = buffer,
|
|
};
|
|
encoder.CopyTextureToBuffer(&source, &destination, &texture->size);
|
|
g_downloads.push_back({
|
|
.copy = std::move(pending),
|
|
.buffer = std::move(buffer),
|
|
.bytesPerRow = bytesPerRow,
|
|
.bufferSize = bufferSize,
|
|
});
|
|
}
|
|
}
|
|
|
|
bool complete_downloads() noexcept {
|
|
bool success = true;
|
|
for (auto& download : g_downloads) {
|
|
wgpu::MapAsyncStatus mapStatus = wgpu::MapAsyncStatus::CallbackCancelled;
|
|
wgpu::StringView mapMessage{};
|
|
const auto future =
|
|
download.buffer.MapAsync(wgpu::MapMode::Read, 0, download.bufferSize, wgpu::CallbackMode::WaitAnyOnly,
|
|
[&mapStatus, &mapMessage](wgpu::MapAsyncStatus status, wgpu::StringView message) {
|
|
mapStatus = status;
|
|
mapMessage = message;
|
|
});
|
|
const auto waitStatus = g_instance.WaitAny(future, 5000000000);
|
|
if (waitStatus != wgpu::WaitStatus::Success || mapStatus != wgpu::MapAsyncStatus::Success) {
|
|
Log.error("EFB RAM readback failed wait={} map={} message={}", magic_enum::enum_name(waitStatus),
|
|
magic_enum::enum_name(mapStatus), mapMessage);
|
|
success = false;
|
|
continue;
|
|
}
|
|
|
|
const auto* pixels = static_cast<const uint8_t*>(download.buffer.GetConstMappedRange(0, download.bufferSize));
|
|
const auto& readbackTexture = download.copy.nativeTexture ? download.copy.nativeTexture : download.copy.texture;
|
|
const auto order = texture_pixel_order(readbackTexture);
|
|
const size_t outputSize = encoded_size(download.copy.format, download.copy.width, download.copy.height);
|
|
if (!encode(download.copy.dest, outputSize, download.copy.format, download.copy.width, download.copy.height, pixels,
|
|
readbackTexture->size.width, readbackTexture->size.height, download.bytesPerRow, order)) {
|
|
Log.error("Failed to encode EFB RAM copy format=0x{:x} size={}x{}", static_cast<unsigned>(download.copy.format),
|
|
download.copy.width, download.copy.height);
|
|
success = false;
|
|
}
|
|
notify_guest_write(download.copy.dest, outputSize);
|
|
download.buffer.Unmap();
|
|
}
|
|
g_downloads.clear();
|
|
return success;
|
|
}
|
|
|
|
void cancel() noexcept {
|
|
g_pending.clear();
|
|
g_downloads.clear();
|
|
g_asyncPending.clear();
|
|
}
|
|
|
|
void seal_async_downloads() noexcept {
|
|
{
|
|
std::lock_guard lock{g_asyncMutex};
|
|
for (auto& pending : g_asyncPending) {
|
|
const auto it = g_asyncSlots.find(pending.dest);
|
|
ensure_native_texture(pending, it != g_asyncSlots.end() ? &it->second.nativeTexture : nullptr);
|
|
}
|
|
}
|
|
g_asyncSealed = std::move(g_asyncPending);
|
|
g_asyncPending.clear();
|
|
}
|
|
|
|
void encode_async_downloads(const wgpu::CommandEncoder& encoder) noexcept {
|
|
if (g_asyncSealed.empty()) {
|
|
return;
|
|
}
|
|
// Retire the previous frame's map first so its slot is free here, otherwise a single-slot
|
|
// destination could only be sampled every other frame.
|
|
drain_async_events();
|
|
std::lock_guard lock{g_asyncMutex};
|
|
for (auto& pending : g_asyncSealed) {
|
|
const auto it = g_asyncSlots.find(pending.dest);
|
|
if (it == g_asyncSlots.end()) {
|
|
continue;
|
|
}
|
|
auto& slot = it->second;
|
|
// Only one readback per destination may be outstanding and the previous map still owns the pooled
|
|
// buffer. Dropping this request just leaves the guest on a slightly older probe.
|
|
if (slot.state != AsyncState::Idle) {
|
|
continue;
|
|
}
|
|
if (pending.nativeTexture) {
|
|
encode_native_blit(encoder, pending);
|
|
}
|
|
const auto& texture = pending.nativeTexture ? pending.nativeTexture : pending.texture;
|
|
const uint32_t bytesPerRow = align_to(texture->size.width * 4, 256);
|
|
const uint64_t bufferSize = static_cast<uint64_t>(bytesPerRow) * texture->size.height;
|
|
if (!slot.buffer || slot.bufferSize != bufferSize) {
|
|
const wgpu::BufferDescriptor descriptor{
|
|
.label = "GX EFB RAM async readback",
|
|
.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead,
|
|
.size = bufferSize,
|
|
};
|
|
slot.buffer = g_device.CreateBuffer(&descriptor);
|
|
slot.bufferSize = bufferSize;
|
|
}
|
|
if (!slot.buffer) {
|
|
continue;
|
|
}
|
|
const wgpu::TexelCopyTextureInfo source{
|
|
.texture = texture->texture,
|
|
.mipLevel = 0,
|
|
.origin = {0, 0, 0},
|
|
.aspect = wgpu::TextureAspect::All,
|
|
};
|
|
const wgpu::TexelCopyBufferInfo destination{
|
|
.layout =
|
|
wgpu::TexelCopyBufferLayout{
|
|
.offset = 0,
|
|
.bytesPerRow = bytesPerRow,
|
|
.rowsPerImage = texture->size.height,
|
|
},
|
|
.buffer = slot.buffer,
|
|
};
|
|
encoder.CopyTextureToBuffer(&source, &destination, &texture->size);
|
|
slot.bytesPerRow = bytesPerRow;
|
|
slot.dest = pending.dest;
|
|
slot.format = pending.format;
|
|
slot.width = pending.width;
|
|
slot.height = pending.height;
|
|
slot.hostWidth = texture->size.width;
|
|
slot.hostHeight = texture->size.height;
|
|
slot.order = texture_pixel_order(texture);
|
|
slot.state = AsyncState::CopySubmitted;
|
|
}
|
|
g_asyncSealed.clear();
|
|
}
|
|
|
|
void after_submit() noexcept {
|
|
struct PendingMap {
|
|
void* dest;
|
|
wgpu::Buffer buffer;
|
|
uint64_t bufferSize;
|
|
};
|
|
std::vector<PendingMap> pendingMaps;
|
|
{
|
|
std::lock_guard lock{g_asyncMutex};
|
|
for (auto& [dest, slot] : g_asyncSlots) {
|
|
if (slot.state != AsyncState::CopySubmitted) {
|
|
continue;
|
|
}
|
|
slot.state = AsyncState::MapPending;
|
|
++g_asyncMapsInFlight;
|
|
pendingMaps.push_back({dest, slot.buffer, slot.bufferSize});
|
|
}
|
|
}
|
|
|
|
for (const auto& pending : pendingMaps) {
|
|
pending.buffer.MapAsync(wgpu::MapMode::Read, 0, pending.bufferSize, wgpu::CallbackMode::AllowSpontaneous,
|
|
[dest = pending.dest](wgpu::MapAsyncStatus status, wgpu::StringView message) {
|
|
complete_async_slot(dest, status, message);
|
|
});
|
|
}
|
|
|
|
// Nothing else pumps the WebGPU event queue on this thread, so a completed map would sit
|
|
// unharvested with its slot busy. ProcessEvents does not block.
|
|
drain_async_events();
|
|
}
|
|
|
|
void abort_async() noexcept { g_asyncSealed.clear(); }
|
|
|
|
void shutdown() noexcept {
|
|
cancel();
|
|
g_asyncSealed.clear();
|
|
std::lock_guard lock{g_asyncMutex};
|
|
g_asyncSlots.clear();
|
|
g_asyncMapsInFlight = 0;
|
|
}
|
|
|
|
} // namespace aurora::gfx::efb_ram
|