mirror of
https://github.com/simplysanatori/KartFrame.git
synced 2026-10-17 15:01:21 +02:00
266 lines
11 KiB
C++
266 lines
11 KiB
C++
// Opt-in D3D12 readback test: P1 is red, P2 green, P3 blue, P4 yellow.
|
|
// Both eyes must be entirely P1 while the original EFB keeps every pane.
|
|
#include <aurora/aurora.h>
|
|
#include <aurora/gfx.h>
|
|
#include <dolphin/gx.h>
|
|
#include <dolphin/mtx.h>
|
|
#include "stereo.hpp"
|
|
#include "gfx/common.hpp"
|
|
|
|
#include <array>
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
|
|
using namespace aurora::webgpu;
|
|
namespace {
|
|
// Eyes match the EFB width so a one-pixel-class divider on the virtual screen
|
|
// still covers eye pixels; a 160x120 eye never rasterized it.
|
|
constexpr uint32_t kWidth = 640, kHeight = 480, kEfbWidth = 640, kEfbHeight = 528, kPitch = 2560;
|
|
constexpr uint64_t kBytes = kPitch * kEfbHeight;
|
|
std::array<wgpu::Buffer, 3> readbacks;
|
|
std::array<wgpu::TextureFormat, 3> formats;
|
|
uint64_t token = 0;
|
|
uint32_t copies = 0;
|
|
bool Provide(uint32_t, AuroraStereoFrame* frame, void*) {
|
|
*frame = {};
|
|
frame->frameToken = ++token;
|
|
frame->contentTag = 42;
|
|
for (auto& eye : frame->eyes) {
|
|
eye.width = kWidth;
|
|
eye.height = kHeight;
|
|
eye.projection[0] = eye.projection[5] = 1;
|
|
eye.projection[10] = eye.projection[11] = eye.projection[14] = -1;
|
|
eye.viewFromCenter[0] = eye.viewFromCenter[5] = eye.viewFromCenter[10] = 1;
|
|
}
|
|
return true;
|
|
}
|
|
bool Encode(wgpu::CommandEncoder& encoder, const aurora::stereo::SinkFrame& frame, void*) noexcept {
|
|
for (uint32_t i = 0; i < 3; ++i) {
|
|
const auto texture = i < 2 ? *frame.eyes[i].texture : present_source().texture;
|
|
formats[i] = texture.GetFormat();
|
|
const wgpu::TexelCopyTextureInfo source{.texture = texture};
|
|
const wgpu::TexelCopyBufferInfo destination{.layout = {.bytesPerRow = kPitch, .rowsPerImage = kEfbHeight},
|
|
.buffer = readbacks[i]};
|
|
const wgpu::Extent3D size{i < 2 ? kWidth : kEfbWidth, i < 2 ? kHeight : kEfbHeight, 1};
|
|
encoder.CopyTextureToBuffer(&source, &destination, &size);
|
|
}
|
|
++copies;
|
|
return true;
|
|
}
|
|
void Draw(uint32_t players) {
|
|
// GX depth occupies [-w, 0], unlike OpenGL's [-w, +w].
|
|
Mtx44 projection{{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, -1}, {0, 0, -1, 0}};
|
|
Mtx transform{{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, -2}};
|
|
GXSetProjection(projection, GX_PERSPECTIVE);
|
|
GXSetCurrentMtx(GX_PNMTX0);
|
|
GXLoadPosMtxImm(transform, GX_PNMTX0);
|
|
GXClearVtxDesc();
|
|
GXSetVtxDesc(GX_VA_POS, GX_DIRECT);
|
|
GXSetVtxDesc(GX_VA_CLR0, GX_DIRECT);
|
|
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0);
|
|
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_CLR0, GX_CLR_RGBA, GX_RGBA8, 0);
|
|
GXSetNumTexGens(0);
|
|
GXSetNumChans(1);
|
|
GXSetChanCtrl(GX_COLOR0A0, GX_FALSE, GX_SRC_REG, GX_SRC_VTX, GX_LIGHT_NULL, GX_DF_NONE, GX_AF_NONE);
|
|
GXSetNumTevStages(1);
|
|
GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR0A0);
|
|
GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_RASC);
|
|
GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_RASA);
|
|
GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
|
|
GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
|
|
GXSetZMode(GX_FALSE, GX_ALWAYS, GX_FALSE);
|
|
GXSetAlphaCompare(GX_ALWAYS, 0, GX_AOP_AND, GX_ALWAYS, 0);
|
|
GXSetCullMode(GX_CULL_NONE);
|
|
GXSetBlendMode(GX_BM_NONE, GX_BL_ONE, GX_BL_ZERO, GX_LO_COPY);
|
|
GXSetColorUpdate(GX_TRUE);
|
|
GXSetAlphaUpdate(GX_TRUE);
|
|
const GXColor colors[]{{255, 0, 0, 255}, {0, 255, 0, 255}, {0, 0, 255, 255}, {255, 255, 0, 255}};
|
|
for (uint32_t p = 0; p < players; ++p) {
|
|
const uint32_t width = players >= 3 ? kEfbWidth / 2 : kEfbWidth;
|
|
const uint32_t height = players >= 2 ? kEfbHeight / 2 : kEfbHeight;
|
|
const uint32_t x = players >= 3 ? (p % 2) * width : 0;
|
|
const uint32_t y = players >= 3 ? (p / 2) * height : p * height;
|
|
GXSetViewport(float(x), float(y), float(width), float(height), 0, 1);
|
|
GXSetScissor(x, y, width, height);
|
|
GXBegin(GX_QUADS, GX_VTXFMT0, 4);
|
|
GXPosition3f32(-2, -2, 0);
|
|
GXColor4u8(colors[p].r, colors[p].g, colors[p].b, 255);
|
|
GXPosition3f32(2, -2, 0);
|
|
GXColor4u8(colors[p].r, colors[p].g, colors[p].b, 255);
|
|
GXPosition3f32(2, 2, 0);
|
|
GXColor4u8(colors[p].r, colors[p].g, colors[p].b, 255);
|
|
GXPosition3f32(-2, 2, 0);
|
|
GXColor4u8(colors[p].r, colors[p].g, colors[p].b, 255);
|
|
GXEnd();
|
|
}
|
|
if (players == 1)
|
|
return;
|
|
// MKW's separators and per-pane backing quads can share one full-screen
|
|
// orthographic viewport. Viewport filtering alone must not admit them in VR.
|
|
Mtx44 ortho{{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, -0.5f}, {0, 0, 0, 1}};
|
|
Mtx identity{{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}};
|
|
GXSetProjection(ortho, GX_ORTHOGRAPHIC);
|
|
GXLoadPosMtxImm(identity, GX_PNMTX0);
|
|
GXSetViewport(0, 0, kEfbWidth, kEfbHeight, 0, 1);
|
|
GXSetScissor(0, 0, kEfbWidth, kEfbHeight);
|
|
const auto rect = [](float left, float bottom, float right, float top) {
|
|
GXBegin(GX_QUADS, GX_VTXFMT0, 4);
|
|
for (const auto& p :
|
|
std::array<std::array<float, 2>, 4>{{{left, bottom}, {right, bottom}, {right, top}, {left, top}}}) {
|
|
GXPosition3f32(p[0], p[1], 0);
|
|
GXColor4u8(0, 0, 0, 255);
|
|
}
|
|
GXEnd();
|
|
};
|
|
if (players >= 3)
|
|
rect(0, 0, 1, 1); // The reported black quadrant.
|
|
else
|
|
rect(-1, -1, 1, 0);
|
|
if (players >= 3) {
|
|
GXSetLineWidth(6, GX_TO_ZERO);
|
|
GXBegin(GX_LINES, GX_VTXFMT0, 2);
|
|
GXPosition3f32(0, -1, 0);
|
|
GXColor4u8(0, 0, 0, 255);
|
|
GXPosition3f32(0, 1, 0);
|
|
GXColor4u8(0, 0, 0, 255);
|
|
GXEnd();
|
|
}
|
|
// MKW's partition_line layout draws the divider as one-pixel picture panes
|
|
// that sample a pattern texture, so the divider here is a textured quad too:
|
|
// texture use alone must not keep it out of the furniture filter.
|
|
alignas(32) static const uint8_t blackTexels[8 * 8 * 2]{}; // RGB565 zero: opaque black.
|
|
GXTexObj divider{};
|
|
GXInitTexObj(÷r, blackTexels, 8, 8, GX_TF_RGB565, GX_CLAMP, GX_CLAMP, GX_FALSE);
|
|
GXLoadTexObj(÷r, GX_TEXMAP0);
|
|
GXSetVtxDesc(GX_VA_TEX0, GX_DIRECT);
|
|
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_TEX_ST, GX_F32, 0);
|
|
GXSetNumTexGens(1);
|
|
GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY);
|
|
GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR0A0);
|
|
GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_TEXC);
|
|
GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_TEXA);
|
|
GXBegin(GX_QUADS, GX_VTXFMT0, 4);
|
|
for (const auto& p :
|
|
std::array<std::array<float, 2>, 4>{{{-1, -0.006f}, {1, -0.006f}, {1, 0.006f}, {-1, 0.006f}}}) {
|
|
GXPosition3f32(p[0], p[1], 0);
|
|
GXColor4u8(255, 255, 255, 255);
|
|
GXTexCoord2f32(p[0] * 0.5f + 0.5f, p[1] > 0 ? 1.f : 0.f);
|
|
}
|
|
GXEnd();
|
|
}
|
|
bool Check(uint32_t players) {
|
|
bool okay = copies != 0;
|
|
for (uint32_t i = 0; i < 3; ++i) {
|
|
wgpu::MapAsyncStatus status{};
|
|
const auto future = readbacks[i].MapAsync(wgpu::MapMode::Read, 0, kBytes, wgpu::CallbackMode::WaitAnyOnly,
|
|
[&](wgpu::MapAsyncStatus result, wgpu::StringView) { status = result; });
|
|
if (g_instance.WaitAny(future, 5'000'000'000) != wgpu::WaitStatus::Success ||
|
|
status != wgpu::MapAsyncStatus::Success) {
|
|
return false;
|
|
}
|
|
const auto* bytes = static_cast<const uint8_t*>(readbacks[i].GetConstMappedRange());
|
|
for (uint32_t quadrant = 0; quadrant < 4; ++quadrant) {
|
|
const uint32_t width = i < 2 ? kWidth : kEfbWidth;
|
|
const uint32_t height = i < 2 ? kHeight : kEfbHeight;
|
|
const uint32_t x = (quadrant % 2) * width / 2 + width / 4;
|
|
const uint32_t y = (quadrant / 2) * height / 2 + height / 4;
|
|
uint32_t player = i < 2 || players == 1 ? 0 : players == 2 ? quadrant / 2 : quadrant;
|
|
if (player >= players)
|
|
continue;
|
|
const auto* pixel = bytes + y * kPitch + x * 4;
|
|
const bool bgra = formats[i] == wgpu::TextureFormat::BGRA8Unorm;
|
|
const uint8_t r = pixel[bgra ? 2 : 0], g = pixel[1], b = pixel[bgra ? 0 : 2];
|
|
const bool mask = i == 2 && players > 1 && (players == 2 ? quadrant >= 2 : quadrant == 1);
|
|
const bool match = r == (!mask && (player == 0 || player == 3) ? 255 : 0) &&
|
|
g == (!mask && (player == 1 || player == 3) ? 255 : 0) &&
|
|
b == (!mask && player == 2 ? 255 : 0);
|
|
if (!match)
|
|
std::fprintf(stderr, "%uP target %u quadrant %u expected player %u, got %u,%u,%u\n", players, i, quadrant,
|
|
player + 1, r, g, b);
|
|
okay &= match;
|
|
}
|
|
if (i < 2) {
|
|
// Include the divider locations; quadrant-center samples alone miss them.
|
|
for (uint32_t y = 4; y < kHeight - 4; ++y) {
|
|
for (uint32_t x = 4; x < kWidth - 4; ++x) {
|
|
const auto* pixel = bytes + y * kPitch + x * 4;
|
|
const bool bgra = formats[i] == wgpu::TextureFormat::BGRA8Unorm;
|
|
if (pixel[bgra ? 2 : 0] != 255 || pixel[1] != 0 || pixel[bgra ? 0 : 2] != 0) {
|
|
std::fprintf(stderr, "%uP eye %u has split-screen overlay at %u,%u\n", players, i, x, y);
|
|
okay = false;
|
|
y = kHeight;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else if (players > 1) {
|
|
const auto* divider = bytes + (kEfbHeight / 2) * kPitch + (kEfbWidth / 4) * 4;
|
|
if (divider[0] != 0 || divider[1] != 0 || divider[2] != 0) {
|
|
std::fprintf(stderr, "Desktop divider was removed\n");
|
|
okay = false;
|
|
}
|
|
}
|
|
readbacks[i].Unmap();
|
|
}
|
|
return okay;
|
|
}
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv) {
|
|
std::filesystem::create_directories("stereo-multiplayer-cache");
|
|
AuroraConfig config{};
|
|
config.appName = "Aurora multiplayer VR validation";
|
|
config.userPath = ".";
|
|
config.cachePath = "stereo-multiplayer-cache";
|
|
config.desiredBackend = BACKEND_D3D12;
|
|
config.windowWidth = kWidth;
|
|
config.windowHeight = kHeight;
|
|
config.hasWindowPosition = true;
|
|
config.windowPosX = config.windowPosY = -30000;
|
|
config.xrInterop = true;
|
|
config.logLevel = LOG_WARNING;
|
|
config.logCallback = [](AuroraLogLevel, const char* module, const char* message, unsigned length) {
|
|
std::fprintf(stderr, "%s: %.*s\n", module, int(length), message);
|
|
};
|
|
aurora_initialize(argc, argv, &config);
|
|
aurora_set_skip_unready_pipelines(false);
|
|
aurora_begin_frame();
|
|
GXInit(nullptr, 0);
|
|
const wgpu::BufferDescriptor descriptor{.usage = wgpu::BufferUsage::MapRead | wgpu::BufferUsage::CopyDst,
|
|
.size = kBytes};
|
|
for (auto& buffer : readbacks)
|
|
buffer = g_device.CreateBuffer(&descriptor);
|
|
aurora_set_stereo_frame_provider(Provide, nullptr);
|
|
aurora::gfx::set_stereo_hud_screen(true, 2.f, 1.f);
|
|
aurora::stereo::set_sink(Encode, nullptr);
|
|
bool okay = true;
|
|
for (bool interpolate : {false, true}) {
|
|
aurora_set_stereo_frame_interpolation(interpolate);
|
|
for (uint32_t players : {1u, 2u, 3u, 4u, 1u}) {
|
|
for (uint32_t frame = 0; frame < 3; ++frame) {
|
|
aurora_update();
|
|
if (!aurora_begin_frame())
|
|
return 2;
|
|
Draw(players);
|
|
// Deliberately omit the setter for 1P to exercise per-frame reset.
|
|
if (players > 1)
|
|
aurora_set_stereo_local_player_count(players);
|
|
aurora_end_frame_tagged(42);
|
|
aurora_begin_frame();
|
|
aurora_wait_for_frame_worker();
|
|
}
|
|
const bool passed = Check(players);
|
|
okay &= passed;
|
|
std::printf("%u players, interpolation %s: %s\n", players, interpolate ? "on" : "off", passed ? "PASS" : "FAIL");
|
|
}
|
|
}
|
|
aurora_set_stereo_frame_interpolation(false);
|
|
aurora_quiesce_frame_worker();
|
|
aurora_set_stereo_frame_provider(nullptr, nullptr);
|
|
aurora::stereo::set_sink(nullptr, nullptr);
|
|
for (auto& buffer : readbacks)
|
|
buffer = nullptr;
|
|
aurora_shutdown();
|
|
return okay ? 0 : 1;
|
|
}
|