Added First Person VR item management

- Implemented DVDReadVrAsset function to read mapped disc paths for VR assets.
- Created HeldItem structure and ReadHeldItem function for managing held items in the game.
- Developed unit tests for ReadHeldItem to ensure correct functionality and edge case handling.
- Added cockpit item data tests to validate model indexing and parsing of archives.
This commit is contained in:
iChris4 committed 2026-09-29 23:55:52 +02:00
1 parent 75f075b234
commit db30945f4c
26 files changed
+1598 -19

No files matched your search

+7 -1
View File
@@ -1,6 +1,12 @@
include(FetchContent)
include(GoogleTest)
# The parser test uses synthetic malformed inputs by default. Passing the
# user's Race/Common.szs additionally audits every required model and texture.
add_executable(cockpit_item_data_test cockpit_item_data_test.cpp)
target_compile_features(cockpit_item_data_test PRIVATE cxx_std_20)
add_test(NAME cockpit_item_data_test COMMAND cockpit_item_data_test)
option(AURORA_GPU_SMOKE_TESTS "Build opt-in tests requiring a desktop GPU" OFF)
if (AURORA_GPU_SMOKE_TESTS AND AURORA_ENABLE_GX AND WIN32)
# Exercises the custom Dawn DLL's Aurora Vulkan ABI (patches/dawn). Run it with that
@@ -29,7 +35,7 @@ if (AURORA_GPU_SMOKE_TESTS AND AURORA_ENABLE_GX AND WIN32)
dawn::dawncpp_headers)
# VR cockpit overlay (hands, synthetic wheel) against real scene depth. Standalone: it
# defines the GPU globals itself and needs only the header.
add_executable(cockpit_gpu_smoke cockpit_gpu_smoke.cpp)
add_executable(cockpit_gpu_smoke cockpit_gpu_smoke.cpp ../lib/gfx/texture_convert.cpp)
target_include_directories(cockpit_gpu_smoke PRIVATE ../include ../lib)
target_compile_definitions(cockpit_gpu_smoke PRIVATE AURORA TARGET_PC WEBGPU_DAWN)
target_link_libraries(cockpit_gpu_smoke PRIVATE fmt::fmt xxhash absl::flat_hash_map absl::btree
+62 -2
View File
@@ -5,10 +5,16 @@
#include "../lib/gfx/cockpit.hpp"
#include <fstream>
#include <iostream>
#include <iterator>
#include <atomic>
namespace aurora::webgpu { wgpu::Device g_device; wgpu::Queue g_queue; GraphicsConfig g_graphicsConfig{}; }
namespace aurora {
AuroraConfig g_config{};
void log_internal(AuroraLogLevel,const char*,const char*,unsigned int) noexcept {}
void Module::show_fatal_dialog(const char*,std::string_view) noexcept {}
}
std::atomic<int> errors=0;
int main() {
int main(int argc,char** argv) {
using namespace aurora;
using namespace webgpu;
wgpu::InstanceDescriptor id{};
@@ -40,6 +46,50 @@ int main() {
if(!gfx::cockpit::geometry(native).empty()) return 1;
native.nativeWheel=false;
if(gfx::cockpit::geometry(native).empty()) return 1;
// The held item stays upright and faces the player whatever the hand's roll
// and pitch; only the hand's heading turns it.
const auto itemFrame=[](const AuroraCockpitHand& hand,float frontX,float frontZ) {
std::array<float,12> item{};
if(!gfx::cockpit_item::seat_from_item(hand,item)) return false;
const bool upright=std::abs(item[1])<1e-5f && std::abs(item[5]-1)<1e-5f && std::abs(item[9])<1e-5f;
return upright && std::abs(item[2]-frontX)<1e-4f && std::abs(item[10]-frontZ)<1e-4f;
};
for(float roll : {0.0f,1.0f,-2.0f}) for(float pitch : {0.0f,0.5f,-0.7f}) {
// Fingers ahead (grip -Y is seat -Z), little finger to thumb up (grip -Z is
// seat +Y), then rolled about the fingers and pitched about seat +X.
const float cr=std::cos(roll),sr=std::sin(roll),cp=std::cos(pitch),sp=std::sin(pitch);
const gfx::cockpit::M neutral{1,0,0,-0.18f, 0,0,-1,-0.30f, 0,1,0,-0.42f};
const gfx::cockpit::M rollZ{cr,-sr,0,0, sr,cr,0,0, 0,0,1,0}, pitchX{1,0,0,0, 0,cp,-sp,0, 0,sp,cp,0};
AuroraCockpitHand hand{};
const auto pose=gfx::cockpit::compose(pitchX,gfx::cockpit::compose(rollZ,neutral));
std::memcpy(hand.seatFromGrip,pose.data(),sizeof(hand.seatFromGrip));
if(!itemFrame(hand,0,1)) { std::cerr<<"Held item not upright or not facing the player\n"; return 1; }
}
{
AuroraCockpitHand hand{};
// A tracked palm joint with the fingers (-Z) ahead, then a grip turned to the right.
const auto palm=gfx::cockpit::identity();
std::memcpy(hand.seatFromJoint[0],palm.data(),sizeof(hand.seatFromJoint[0]));
hand.jointsValid=true;
if(!itemFrame(hand,0,1)) { std::cerr<<"Held item ignores the palm joint\n"; return 1; }
hand.jointsValid=false;
const gfx::cockpit::M right{0,-1,0,0, 0,0,-1,0, 1,0,0,0};
std::memcpy(hand.seatFromGrip,right.data(),sizeof(hand.seatFromGrip));
if(!itemFrame(hand,-1,0)) { std::cerr<<"Held item does not turn with the hand\n"; return 1; }
}
const bool itemEnabled=argc>1;
if(itemEnabled) {
std::ifstream file(argv[1],std::ios::binary);
if(!file) return 1;
const std::vector<uint8_t> bytes{std::istreambuf_iterator<char>(file),std::istreambuf_iterator<char>()};
gfx::cockpit_item::set_archive(bytes.data(),static_cast<uint32_t>(bytes.size()));
for(uint8_t id=0;id<19;++id) if(!gfx::cockpit_item::has_model(id)) return 1;
for(const auto& model:gfx::cockpit_item::archive->models)
for(const auto& texture:model.textures)
if(texture.rgba.size()!=gfx::cockpit_item::mip_bytes(texture,texture.mips)) {
std::cerr << "Cockpit item texture conversion failed: " << texture.name << '\n';return 1;
}
}
for(bool bike : {false,true}) for(bool original : {false,true}) for(uint32_t samples : {1u,4u})
for(bool hud : {false,true}) for(int coverage : {0,1,2}) for(bool reversed : {false,true}) for(uint32_t eyeIndex : {0u,1u}) {
const bool occluded=coverage==1;
@@ -48,6 +98,16 @@ int main() {
frame.cockpit.active=true;frame.cockpit.wheelAngle=0.35f;
frame.cockpit.nativeWheel=original;
frame.cockpit.bike=bike;frame.cockpit.handlebarRadius=0.25f;
if(itemEnabled) {
const uint32_t itemCase=((((uint32_t(bike)*2+uint32_t(original))*2+
uint32_t(samples==4))*3+uint32_t(coverage))*2+
uint32_t(reversed))*2+eyeIndex;
const bool preview=samples==4 && !bike && !original && coverage==0 &&
!reversed && eyeIndex==0;
const uint8_t id=preview?0:static_cast<uint8_t>(itemCase%19);
frame.cockpitItem={1,id,static_cast<uint8_t>(itemCase%3+1),
static_cast<uint8_t>(itemCase%2),true};
}
const float handlePose[12]{1,0,0,0, 0,0,1,-0.3f, 0,-1,0,-0.42f};
std::memcpy(frame.cockpit.seatFromHandlebar,handlePose,sizeof(handlePose));
for(int hand=0;hand<2;++hand) {
@@ -156,7 +216,7 @@ int main() {
}
if(left<500||right>8) { std::cerr<<"Partial wall occlusion failed for eye "<<eyeIndex<<'\n';++errors; }
}
if(samples==4 && !original && !occluded) {
if(samples==4 && !bike && !original && !hud && coverage==0 && !reversed && eyeIndex==0) {
std::ofstream image("cockpit-preview.ppm",std::ios::binary);image<<"P6\n512 512\n255\n";
for(size_t i=0;i<512*512;++i) image.write(reinterpret_cast<const char*>(bytes+i*4),3);
}
@@ -0,0 +1,75 @@
#include "../lib/gfx/cockpit_item_data.hpp"
#include <cmath>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <iterator>
static void Check(bool value,const char* what) {
if (!value) { std::cerr << "FAILED: " << what << '\n'; std::abort(); }
}
static bool Near(float a,float b) { return std::fabs(a-b)<1e-4f; }
int main(int argc, char** argv) {
using namespace aurora::gfx::cockpit_item::data;
for (uint8_t id=0; id<19; ++id) Check(model_index(id)>=0,"every inventory ID has a model");
Check(model_index(19)==-1 && model_index(20)==-1,"no model past the triple banana");
const uint8_t brokenYaz[]{'Y','a','z','0',0,0,0,4,0,0,0,0,0,0,0,0,0};
const uint8_t brokenU8[]{0x55,0xaa,0x38,0x2d,0,0,0,0};
Check(parse_archive(nullptr,0).loaded==0,"null archive");
Check(parse_archive(brokenYaz,sizeof(brokenYaz)).loaded==0,"truncated Yaz0");
Check(parse_archive(brokenU8,sizeof(brokenU8)).loaded==0,"truncated U8");
// Maya texture SRT: a 2x scale pivots t around 1, as G3D does.
const auto srt=texture_srt(2,2,0,0,0,0);
Check(Near(srt[0],2) && Near(srt[1],0) && Near(srt[2],0) &&
Near(srt[3],0) && Near(srt[4],2) && Near(srt[5],-1),"Maya scale");
const auto identity=texture_srt(1,1,0,0,0,0);
Check(Near(identity[0],1) && Near(identity[2],0) && Near(identity[4],1) && Near(identity[5],0),"identity SRT");
// KSEL: fixed fractions, whole konst colours, and single konst components.
const std::array<Color,4> konst{{{0.1f,0.2f,0.3f,0.4f},{0.5f,0.6f,0.7f,0.8f},{},{}}};
Check(Near(konst_value(konst,0,false)[0],1) && Near(konst_value(konst,4,true)[3],0.5f),"KSEL fractions");
Check(Near(konst_value(konst,0x0d,false)[1],0.6f),"KSEL konst colour");
Check(Near(konst_value(konst,0x1c,true)[3],0.4f) && Near(konst_value(konst,0x15,true)[3],0.6f),"KSEL components");
Check(Near(signed11(0x7ff),-1.f/255.f) && Near(signed11(0xff),1),"TEV register sign");
if (argc>1) {
std::ifstream file(argv[1],std::ios::binary);
Check(bool(file),"archive readable");
const std::vector<uint8_t> bytes{std::istreambuf_iterator<char>(file),std::istreambuf_iterator<char>()};
const Archive archive=parse_archive(bytes.data(),bytes.size());
Check(archive.loaded==15,"all 15 item models");
for (size_t i=0;i<archive.models.size();++i) {
const auto& model=archive.models[i];
Check(model.valid(),"model valid");
size_t vertices=0,billboards=0;
for (const auto& part:model.parts) {
Check(part.vertices.size()%3==0,"triangle list");
Check(part.material<model.materials.size(),"part material");
vertices+=part.vertices.size();
billboards+=part.billboard;
}
for (const auto& material:model.materials) {
Check(material.stageCount>=1 && material.stageCount<=4,"TEV stage count");
for (uint32_t s=0;s<material.stageCount;++s) {
const auto& stage=material.stages[s];
if (stage.textured) Check(material.maps[stage.texMap].texture>=0,"stage texture resolves");
}
}
Check(vertices>0,"geometry");
std::cout << names[i] << ": " << vertices << " vertices, " << model.textures.size() << " textures, "
<< model.materials.size() << " materials, " << billboards << " billboard parts\n";
}
// The properties whose absence corrupted the held items.
const auto& shell=archive.models[0].materials[0];
Check(shell.maps[0].wrapS==2 && Near(shell.texGens[0].matrix[0],2),"shell mirror wrap and 2x SRT");
Check(shell.texGens[1].normal,"shell specular is an env map");
Check(archive.models[2].materials[0].cull==0,"banana is double-sided");
Check(archive.models[3].parts.size()==3,"fake item box parts");
size_t thunderBillboards=0;
for (const auto& part:archive.models[7].parts) thunderBillboards+=part.billboard;
Check(thunderBillboards==1,"lightning glow is a billboard");
const auto& bolt=archive.models[7].materials[archive.models[7].parts[0].material];
Check(Near(bolt.registers[1][0],1) && Near(bolt.registers[1][1],1) && Near(bolt.registers[1][2],0),
"lightning C0 is yellow");
}
}