mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 06:00:25 +02:00
Added Support for Multiplayer Player 1 Immersive View
This commit is contained in:
1 parent
9e37bb6447
commit
3e8bdcfd7b
22 files changed
+1013
-38
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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(÷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;
|
||||
}
|
||||
@@ -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},
|
||||
|
||||
Reference in new issue
Block a user