Added Support for Multiplayer Player 1 Immersive View

This commit is contained in:
iChris4 committed 2026-09-16 17:06:03 +02:00
1 parent 9e37bb6447
commit 3e8bdcfd7b
22 files changed
+1013 -38

No files matched your search

+8
View File
@@ -7,6 +7,14 @@ if (AURORA_GPU_SMOKE_TESTS AND AURORA_ENABLE_GX AND WIN32)
target_include_directories(stereo_frame_worker_smoke PRIVATE ../lib)
target_link_libraries(stereo_frame_worker_smoke PRIVATE aurora::core aurora::gx aurora::main aurora::vi
dawn::dawncpp_headers)
add_executable(stereo_multiplayer_smoke stereo_multiplayer_smoke.cpp)
target_include_directories(stereo_multiplayer_smoke PRIVATE ../lib)
target_link_libraries(stereo_multiplayer_smoke PRIVATE aurora::core aurora::gx aurora::main aurora::vi
dawn::dawncpp_headers)
add_executable(efb_ram_lifetime_smoke efb_ram_lifetime_smoke.cpp)
target_include_directories(efb_ram_lifetime_smoke PRIVATE ../lib)
target_link_libraries(efb_ram_lifetime_smoke PRIVATE aurora::core aurora::gx aurora::main aurora::vi
dawn::dawncpp_headers)
endif ()
if (NOT TARGET gtest)
@@ -0,0 +1,146 @@
// Opt-in GPU regression for a depth-probe allocation reused during a scene
// restart. A late map callback must not overwrite the new camera allocation.
#include <aurora/aurora.h>
#include <dolphin/gx.h>
#include "gfx/efb_ram_copy.hpp"
#include "gfx/efb_ram_encoder.hpp"
#include "webgpu/gpu.hpp"
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdio>
#include <filesystem>
#include <thread>
using namespace aurora;
namespace {
constexpr uint8_t kCameraByte = 0x35;
std::array<uint8_t, 256> guest;
gfx::TextureHandle MakeTexture(uint32_t width, uint32_t height, std::array<uint8_t, 4> pixel) {
auto texture = gfx::new_render_texture(width, height, GX_TF_RGBA8, "Probe lifetime test");
if (texture->format == wgpu::TextureFormat::BGRA8Unorm)
std::swap(pixel[0], pixel[2]);
std::array<uint8_t, 256> pixels;
for (size_t i = 0; i < width * height; ++i)
std::copy(pixel.begin(), pixel.end(), pixels.begin() + i * 4);
const wgpu::TexelCopyTextureInfo destination{.texture = texture->texture};
const wgpu::TexelCopyBufferLayout layout{.bytesPerRow = width * 4, .rowsPerImage = height};
webgpu::g_queue.WriteTexture(&destination, pixels.data(), width * height * 4, &layout, &texture->size);
return texture;
}
void SubmitProbe(const gfx::TextureHandle& texture, GXTexFmt format, uint32_t width, uint32_t height) {
gfx::efb_ram::schedule(guest.data(), width, height, format, texture);
gfx::efb_ram::seal_async_downloads();
auto encoder = webgpu::g_device.CreateCommandEncoder();
gfx::efb_ram::encode_async_downloads(encoder);
auto commands = encoder.Finish();
webgpu::g_queue.Submit(1, &commands);
// The guest frees the probe and constructs a camera at the same address,
// before the readback callback is registered.
guest.fill(kCameraByte);
gfx::efb_ram::after_submit();
}
bool CameraIntact() {
return std::all_of(guest.begin(), guest.end(), [](uint8_t byte) { return byte == kCameraByte; });
}
bool AwaitProbe(const gfx::TextureHandle& texture, GXTexFmt format, uint32_t width, uint32_t height,
const std::array<uint8_t, 4>& pixel) {
const size_t size = gfx::efb_ram::encoded_size(format, width, height);
std::array<uint8_t, 256> pixels{}, expected{};
for (size_t i = 0; i < width * height; ++i)
std::copy(pixel.begin(), pixel.end(), pixels.begin() + i * 4);
if (!gfx::efb_ram::encode(expected.data(), size, format, width, height, pixels.data(), width, height, width * 4,
gfx::efb_ram::HostPixelOrder::RGBA))
return false;
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
do {
// Give the real GPU completion callback a chance to run while the old
// destination belongs to the camera, then verify it remains untouched.
webgpu::g_instance.ProcessEvents();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
if (!CameraIntact()) {
std::fprintf(stderr, "Late GPU readback overwrote the reused camera allocation\n");
return false;
}
// This explicit copy owns the destination again and may receive the result.
gfx::efb_ram::schedule(guest.data(), width, height, format, texture);
const bool arrived = std::equal(expected.begin(), expected.begin() + size, guest.begin());
const bool canary =
std::all_of(guest.begin() + size, guest.end(), [](uint8_t byte) { return byte == kCameraByte; });
gfx::efb_ram::cancel();
guest.fill(kCameraByte);
if (!canary)
return false;
if (arrived)
return true;
} while (std::chrono::steady_clock::now() < deadline);
std::fprintf(stderr, "Completed probe was not delivered by the next compatible copy\n");
return false;
}
bool Run() {
const std::array<uint8_t, 4> pixel{0xff, 0xfa, 0xb6, 0xff};
auto large = MakeTexture(8, 8, pixel);
auto small = MakeTexture(4, 4, pixel);
SubmitProbe(large, GX_TF_Z24X8, 8, 8);
if (!AwaitProbe(large, GX_TF_Z24X8, 8, 8, pixel))
return false;
std::puts("Reused camera allocation survives late 256-byte probe: PASS");
// Same address, smaller allocation and different encoding: the retained
// 256-byte result must not escape the new 32-byte destination.
gfx::efb_ram::schedule(guest.data(), 4, 4, GX_TF_Z16, small);
const bool fallback = std::all_of(guest.begin(), guest.begin() + 32, [](uint8_t b) { return b == 0xff; });
const bool canary = std::all_of(guest.begin() + 32, guest.end(), [](uint8_t b) { return b == kCameraByte; });
gfx::efb_ram::cancel();
if (!fallback || !canary)
return false;
std::puts("Reused address rejects incompatible retained format and size: PASS");
SubmitProbe(small, GX_TF_Z24X8, 4, 4);
if (!AwaitProbe(small, GX_TF_Z24X8, 4, 4, pixel))
return false;
std::puts("Crash-dump 64-byte depth pattern delivered only during a new copy: PASS");
SubmitProbe(small, GX_TF_Z16, 4, 4);
if (!AwaitProbe(small, GX_TF_Z16, 4, 4, pixel))
return false;
std::puts("Changed probe format delivers compatible pixels without touching canaries: PASS");
return true;
}
} // namespace
int main(int argc, char** argv) {
std::setvbuf(stdout, nullptr, _IONBF, 0);
std::filesystem::create_directories("efb-lifetime-cache");
AuroraConfig config{};
config.appName = "Aurora EFB lifetime validation";
config.userPath = ".";
config.cachePath = "efb-lifetime-cache";
config.desiredBackend = BACKEND_D3D12;
config.windowWidth = 160;
config.windowHeight = 120;
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);
std::puts("GPU initialized");
aurora_begin_frame();
GXInit(nullptr, 0);
std::puts("Starting readback lifetime checks");
const bool passed = Run();
std::puts("Readback lifetime checks finished");
aurora_end_frame();
aurora_quiesce_frame_worker();
aurora_shutdown();
return passed ? 0 : 1;
}
+65
View File
@@ -2269,6 +2269,71 @@ TEST_F(GXFifoTest, MergedDrawOffsetsCachedTopologyWithoutJoiningPrimitives) {
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{3, 4, 5}));
}
TEST_F(GXFifoTest, OrthographicQuadRecordsScreenRectForVrFurniture) {
// MKW draws its split-screen partition with the partition_line layout: a
// one-pixel picture pane sampling a pattern texture in a full-display
// orthographic viewport. VR replay drops it by its recorded screen
// rectangle. This covers the FIFO decode of that rectangle. The harness
// stubs populate_pipeline_config, so whether texture use disqualifies a
// rectangle is exercised by stereo_multiplayer_smoke instead.
aurora::gfx::testing::use_real_vertex_format_helpers(true);
aurora::gfx::testing::use_draw_command_tracking(true);
aurora::Mat4x4<float> proj{};
proj.m0[0] = 2.0f / 640.0f;
proj.m0[3] = -1.0f;
proj.m1[1] = 2.0f / 480.0f;
proj.m1[3] = -1.0f;
proj.m2[2] = -1.0f;
proj.m3[3] = 1.0f;
GXSetProjection(&proj, GX_ORTHOGRAPHIC);
GXSetViewport(0.0f, 0.0f, 640.0f, 480.0f, 0.0f, 1.0f);
GXSetScissor(0, 0, 640, 480);
aurora::Mat3x4<float> identity{};
identity.m0[0] = identity.m1[1] = identity.m2[2] = 1.0f;
GXLoadPosMtxImm(&identity, GX_PNMTX0);
GXSetCurrentMtx(GX_PNMTX0);
GXClearVtxDesc();
GXSetVtxDesc(GX_VA_POS, GX_DIRECT);
GXSetVtxDesc(GX_VA_CLR0, GX_DIRECT);
GXSetVtxDesc(GX_VA_TEX0, 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);
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_TEX_ST, GX_F32, 0);
GXSetNumChans(1);
GXSetNumTexGens(1);
GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY);
GXSetNumTevStages(1);
GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR0A0);
GXSetTevOp(GX_TEVSTAGE0, GX_MODULATE);
alignas(32) static const u8 pattern[8 * 8 * 2]{};
GXTexObj obj{};
GXInitTexObj(&obj, pattern, 8, 8, GX_TF_RGB565, GX_CLAMP, GX_CLAMP, GX_FALSE);
GXLoadTexObj(&obj, GX_TEXMAP0);
// yoko_line: full width, one pixel tall, centred vertically.
const float corners[4][2]{{0.0f, 239.5f}, {640.0f, 239.5f}, {640.0f, 240.5f}, {0.0f, 240.5f}};
GXBegin(GX_QUADS, GX_VTXFMT0, 4);
for (const auto& corner : corners) {
GXPosition3f32(corner[0], corner[1], 0.0f);
GXColor4u8(0, 0, 0, 255);
GXTexCoord2f32(corner[0] / 640.0f, corner[1] > 240.0f ? 1.0f : 0.0f);
}
GXEnd();
decode_fifo(flush_and_capture());
const auto* draw = aurora::gfx::get_last_draw_command<aurora::gx::DrawData>();
ASSERT_NE(draw, nullptr);
ASSERT_TRUE(draw->screenRect.has_value());
EXPECT_NEAR(draw->screenRect->left, 0.0f, 0.01f);
EXPECT_NEAR(draw->screenRect->top, 239.5f, 0.01f);
EXPECT_NEAR(draw->screenRect->width, 640.0f, 0.01f);
EXPECT_NEAR(draw->screenRect->height, 1.0f, 0.01f);
EXPECT_TRUE(
aurora::gfx::stereo_replay::is_split_screen_furniture(*draw->screenRect, {0.0f, 0.0f, 640.0f, 480.0f}, 2));
}
TEST_F(GXFifoTest, TexBufferSize_UsesExactLinearPcFormatSizes) {
EXPECT_EQ(GXGetTexBufferSize(8, 4, GX_TF_R8_PC, GX_FALSE, 0), 32u);
EXPECT_EQ(GXGetTexBufferSize(8, 4, GX_TF_RGBA8_PC, GX_FALSE, 0), 128u);
@@ -0,0 +1,265 @@
// 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(&divider, blackTexels, 8, 8, GX_TF_RGB565, GX_CLAMP, GX_CLAMP, GX_FALSE);
GXLoadTexObj(&divider, 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;
}
+83
View File
@@ -8,6 +8,89 @@
namespace aurora::gfx::stereo_replay {
namespace {
TEST(StereoReplayTest, SplitFurnitureIsRecognizedInFullDisplayCoordinates) {
const SubviewRect display{16.f, 8.f, 1280.f, 912.f};
for (uint32_t players : {2u, 3u, 4u}) {
EXPECT_TRUE(is_split_screen_furniture({16.f, 462.f, 1280.f, 4.f}, display, players));
EXPECT_FALSE(is_split_screen_furniture(display, display, players)); // Race fade.
EXPECT_FALSE(is_split_screen_furniture({80.f, 60.f, 100.f, 40.f}, display, players)); // HUD backing.
EXPECT_FALSE(is_split_screen_furniture(player_one_region(display, players), display, players));
}
EXPECT_TRUE(is_split_screen_furniture({16.f, 464.f, 1280.f, 456.f}, display, 2));
for (uint32_t players : {3u, 4u}) {
EXPECT_TRUE(is_split_screen_furniture({656.f, 8.f, 0.f, 912.f}, display, players));
EXPECT_TRUE(is_split_screen_furniture({658.f, 10.f, 636.f, 452.f}, display, players));
EXPECT_TRUE(is_split_screen_furniture({16.f, 464.f, 640.f, 456.f}, display, players));
EXPECT_TRUE(is_split_screen_furniture({656.f, 464.f, 640.f, 456.f}, display, players));
}
EXPECT_FALSE(is_split_screen_furniture({656.f, 8.f, 640.f, 456.f}, display, 1));
EXPECT_FALSE(is_split_screen_furniture({656.f, 8.f, 0.f, 912.f}, display, 2));
}
TEST(StereoReplayTest, PartitionLineLayoutPanesAreFurniture) {
// MKW's partition_line.brlyt draws yoko_line (800x1) and tate_line (1x800)
// picture panes centred on the display; both extend past a 4:3 root and are
// clipped by the display copy, so only the centre line and thickness matter.
const SubviewRect display{0.f, 0.f, 893.f, 456.f};
const SubviewRect yoko{46.5f, 227.5f, 800.f, 1.f};
const SubviewRect tate{446.f, -172.f, 1.f, 800.f};
EXPECT_TRUE(is_split_screen_furniture(yoko, display, 2));
EXPECT_FALSE(is_split_screen_furniture(tate, display, 2));
for (uint32_t players : {3u, 4u}) {
EXPECT_TRUE(is_split_screen_furniture(yoko, display, players));
EXPECT_TRUE(is_split_screen_furniture(tate, display, players));
}
// A textured pane of the same shape elsewhere is HUD art, not furniture.
EXPECT_FALSE(is_split_screen_furniture({46.5f, 100.f, 800.f, 1.f}, display, 2));
EXPECT_FALSE(is_split_screen_furniture({46.5f, 227.5f, 300.f, 1.f}, display, 2));
}
TEST(StereoReplayTest, MultiplayerSelectsOnlyPlayerOneWorld) {
const SubviewRect display{12.f, 8.f, 640.f, 456.f};
for (uint32_t count : {2u, 3u, 4u}) {
const auto player = player_one_region(display, count);
EXPECT_FLOAT_EQ(player.left, display.left);
EXPECT_FLOAT_EQ(player.top, display.top);
EXPECT_FLOAT_EQ(player.width, count == 2 ? 640.f : 320.f);
EXPECT_FLOAT_EQ(player.height, 228.f);
EXPECT_TRUE(replay_player_one_draw(player, player, true, false));
auto opponent = player;
opponent.top += player.height;
EXPECT_FALSE(replay_player_one_draw(opponent, player, true, false));
EXPECT_FALSE(replay_player_one_draw(opponent, player, false, false));
if (count >= 3) {
opponent = player;
opponent.left += player.width;
EXPECT_FALSE(replay_player_one_draw(opponent, player, true, false));
EXPECT_FALSE(replay_player_one_draw(opponent, player, false, false));
opponent.top += player.height; // P4 / unused fourth quadrant in 3P.
EXPECT_FALSE(replay_player_one_draw(opponent, player, true, false));
}
EXPECT_FALSE(replay_player_one_draw(display, player, true, false));
EXPECT_TRUE(replay_player_one_draw(display, player, false, false));
EXPECT_FALSE(replay_player_one_draw(player, player, false, true));
EXPECT_FALSE(replay_player_one_draw(display, player, false, true));
const auto remap = make_hud_ndc_remap(player.left, player.top, player.width, player.height, player.left, player.top,
player.width, player.height);
EXPECT_FLOAT_EQ(remap.scaleX, 1.f);
EXPECT_FLOAT_EQ(remap.scaleY, 1.f);
EXPECT_FLOAT_EQ(remap.offsetX, 0.f);
EXPECT_FLOAT_EQ(remap.offsetY, 0.f);
}
const auto single = player_one_region(display, 1);
EXPECT_FLOAT_EQ(single.width, display.width);
EXPECT_FLOAT_EQ(single.height, display.height);
}
TEST(StereoReplayTest, MultiplayerRejectsEmptyAndNonOverlappingScissors) {
const SubviewRect player{0.f, 0.f, 320.f, 228.f};
EXPECT_FALSE(subviews_overlap(player, {320.f, 0.f, 320.f, 228.f}));
EXPECT_FALSE(subviews_overlap(player, {0.f, 228.f, 640.f, 228.f}));
EXPECT_FALSE(subviews_overlap(player, {10.f, 10.f, 0.f, 10.f}));
EXPECT_TRUE(subviews_overlap(player, {10.f, 10.f, 20.f, 20.f}));
EXPECT_TRUE(subview_contains(player, {0.f, -0.5f, 320.f, 228.f}));
}
TEST(StereoReplayTest, EyeFrustumPreservesGameDepthMapping) {
const Mat4x4<float> game{
{10.0f, 11.0f, 12.0f, 13.0f},