mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 00:00:17 +02:00
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:
1 parent
75f075b234
commit
db30945f4c
26 files changed
+1598
-19
No files matched your search
@@ -62,6 +62,7 @@ steering_wheel = true
|
||||
native_steering_wheel = true
|
||||
object_culling = false
|
||||
hand_steering = true
|
||||
cockpit_item_hand = "left"
|
||||
performance_level = "boost"
|
||||
```
|
||||
|
||||
@@ -300,6 +301,19 @@ see [Steering wheel and hand
|
||||
steering](#steering-wheel-and-hand-steering). Turning the wheel moves the controllers, and the game's
|
||||
own motion detection still reads them, so a sharp enough turn can read as a shake.
|
||||
|
||||
**Held item.** `cockpit_item_hand` accepts `"left"` (default), `"right"`, or `"off"` and is also
|
||||
available in F10 > VR. In cockpit view, the selected tracked hand holds one item model from the
|
||||
game's `Race/Common.szs` after the roulette settles. The item stands upright just above the palm
|
||||
with its front toward the player. It turns only with the hand's heading, so rolling or tilting the
|
||||
hand never tips it over. Triple items show their remaining inventory count beside the model, facing
|
||||
the player. The display follows player 1's inventory: using, losing, or deploying the
|
||||
item removes it from the hand even if a deployed object remains near the kart. Stick steering and
|
||||
the existing item buttons still work. The imported models use their static bind pose; item effects
|
||||
and animations are not reproduced in the hand. Each material is drawn from its own data: texture
|
||||
layers with their wrap modes, SRT and environment mapping, vertex colours, culling, blending and up
|
||||
to four TEV stages. Only the lighting is approximated, by a fixed cockpit light in place of the
|
||||
course's light set.
|
||||
|
||||
**Bare hands.** On the Quest, with `hand_tracking` on and the controllers put down, the hands drive
|
||||
`khr/simple_controller`: a right pinch is A with the pointer on the hand's aim ray, the left
|
||||
palm-up pinch is + (pause), and in the cockpit, while a hand holds the wheel, that hand holds A and
|
||||
|
||||
@@ -189,13 +189,20 @@ class SettingsPage(
|
||||
write = { c, value -> c.setBool("vr", "hand_steering", value) },
|
||||
enabledIf = cockpit,
|
||||
)
|
||||
// The hands follow the headset's hand tracking; kVrHandTrackingDefault is off. The
|
||||
// hands are only drawn while they can steer, so it goes with hand steering.
|
||||
choice(
|
||||
R.string.vr_cockpit_item_hand, R.string.vr_cockpit_item_hand_helper,
|
||||
listOf(R.string.vr_item_left, R.string.vr_item_right, R.string.vr_item_off),
|
||||
read = { stringIndex(it, "vr", "cockpit_item_hand", ITEM_HANDS) },
|
||||
write = { c, index -> c.setString("vr", "cockpit_item_hand", ITEM_HANDS[index]) },
|
||||
enabledIf = cockpit,
|
||||
)
|
||||
// Tracked hands can show the item with stick steering as well.
|
||||
toggle(
|
||||
R.string.vr_hand_tracking, R.string.vr_hand_tracking_helper,
|
||||
read = { it.bool("vr", "hand_tracking") ?: false },
|
||||
write = { c, value -> c.setBool("vr", "hand_tracking", value) },
|
||||
enabledIf = { c -> cockpit(c) && (c.bool("vr", "hand_steering") ?: true) },
|
||||
enabledIf = { c -> cockpit(c) && ((c.bool("vr", "hand_steering") ?: true) ||
|
||||
stringIndex(c, "vr", "cockpit_item_hand", ITEM_HANDS) != 2) },
|
||||
)
|
||||
slider(
|
||||
R.string.vr_lean_back, R.string.vr_lean_back_helper, -45.0, 45.0, 1.0,
|
||||
@@ -755,6 +762,7 @@ class SettingsPage(
|
||||
val ROTATION_DEFAULT = ROTATIONS.indexOf("yaw_pitch")
|
||||
// The runtime's default ("cockpit") first.
|
||||
val SEATS = listOf("cockpit", "custom")
|
||||
val ITEM_HANDS = listOf("left", "right", "off")
|
||||
// The runtime's default ("boost") first: an absent key reads as index 0.
|
||||
val PERFORMANCE_LEVELS = listOf("boost", "sustained_high", "sustained_low", "power_savings", "default")
|
||||
// runtime_config.h's kVrFoveationLevels, and its Quest default.
|
||||
|
||||
@@ -535,6 +535,11 @@
|
||||
<string name="vr_seat_custom">Custom</string>
|
||||
<string name="vr_hand_steering">Hand steering</string>
|
||||
<string name="vr_hand_steering_helper">In the cockpit, squeeze a grip near the steering wheel or handlebar to grab it, and turn it to steer. Releasing both grips gives steering back to the stick. Hand steering by heurazy.</string>
|
||||
<string name="vr_cockpit_item_hand">Item in cockpit hand</string>
|
||||
<string name="vr_cockpit_item_hand_helper">Show Player 1\'s settled inventory item above the selected palm. Triple items show the remaining count. Using or losing the item hides it.</string>
|
||||
<string name="vr_item_left">Left</string>
|
||||
<string name="vr_item_right">Right</string>
|
||||
<string name="vr_item_off">Off</string>
|
||||
<string name="vr_hand_tracking">Tracked hands</string>
|
||||
<string name="vr_hand_tracking_helper">The cockpit hands follow your own. Holding the controllers, the fingers follow their touch sensors; put one down and the cameras track that hand at once. Put the controllers down to drive with bare hands: close a hand on the wheel to hold it, which also holds the gas; pinch with a free hand to use an item; flick your hands up for a trick; pinch with your left palm facing you to pause. In menus, a right pinch is A. Choose Automatic drift. Needs hand tracking on in the headset\'s settings.</string>
|
||||
<string name="vr_lean_back">Lean back angle</string>
|
||||
|
||||
@@ -153,6 +153,16 @@ typedef struct {
|
||||
AuroraCockpitHand hands[2];
|
||||
} AuroraCockpit;
|
||||
|
||||
// Inventory from the same guest frame as the scene. hand: 0 left, 1 right,
|
||||
// 2 off. A zero-initialised value has no item.
|
||||
typedef struct {
|
||||
uint64_t raceGeneration;
|
||||
uint8_t id;
|
||||
uint8_t count;
|
||||
uint8_t hand;
|
||||
bool valid;
|
||||
} AuroraCockpitItem;
|
||||
|
||||
typedef struct {
|
||||
float position[3];
|
||||
int16_t joints[4];
|
||||
@@ -324,6 +334,11 @@ void aurora_set_stereo_scene_anchor(const float anchorFromScene[12]);
|
||||
// The sealed frame then owns that scale: each eye's head/IPD translation is
|
||||
// rescaled from the packet's AuroraCockpit::unitsPerMeter to it.
|
||||
void aurora_set_stereo_scene_anchor_scaled(const float anchorFromScene[12], float unitsPerMeter);
|
||||
// GX producer thread, after the scene anchor and before sealing that frame.
|
||||
void aurora_set_stereo_cockpit_item(const AuroraCockpitItem* item);
|
||||
// Copies a user-supplied Race/Common.szs archive. May be called on the guest
|
||||
// thread; the renderer owns decoded assets and never refers back to guest RAM.
|
||||
void aurora_set_cockpit_item_archive(const void* bytes, uint32_t size);
|
||||
// Select Player 1's subview for immersive replay of 2-4 local screens.
|
||||
// Producer-thread, per-frame metadata, consumed by the next end_frame call.
|
||||
// One (the default) keeps full-frame replay. Desktop rendering is unaffected.
|
||||
|
||||
@@ -118,6 +118,7 @@ struct StereoSceneAnchor {
|
||||
// World units per metre the anchor was built with, or zero when the packet's
|
||||
// own scale applies (aurora_set_stereo_scene_anchor_scaled).
|
||||
float unitsPerMeter = 0.f;
|
||||
AuroraCockpitItem cockpitItem{};
|
||||
};
|
||||
// Producer thread only, between aurora_set_stereo_scene_anchor() and the seal
|
||||
// that consumes it. Cleared at every seal so a producer that stops publishing
|
||||
@@ -829,6 +830,7 @@ gfx::StereoReplayFrame make_stereo_replay_frame(const AuroraStereoFrame& input,
|
||||
std::memcpy(&anchorFromScene, sceneAnchor.anchorFromScene.data(), sizeof(anchorFromScene));
|
||||
gfx::StereoReplayFrame replay{};
|
||||
replay.cockpit = input.cockpit;
|
||||
replay.cockpitItem = sceneAnchor.cockpitItem;
|
||||
replay.window = input.mode == AURORA_STEREO_FRAME_IMMERSIVE_REPLAY && input.window;
|
||||
// The sealed guest frame owns its scale. The packet may have been sampled
|
||||
// just before a change of scale (a character swap, a lightning strike), so
|
||||
@@ -2703,6 +2705,10 @@ void set_stereo_scene_anchor(const float anchorFromScene[12]) noexcept {
|
||||
g_pendingSceneAnchor = anchor;
|
||||
}
|
||||
|
||||
void set_stereo_cockpit_item(const AuroraCockpitItem* item) noexcept {
|
||||
g_pendingSceneAnchor.cockpitItem = item != nullptr ? *item : AuroraCockpitItem{};
|
||||
}
|
||||
|
||||
#ifdef AURORA_ENABLE_GX
|
||||
namespace stereo {
|
||||
void set_sink(SinkCallback callback, SubmitCallback submitted, void* userdata) noexcept {
|
||||
@@ -2763,6 +2769,10 @@ void aurora_set_stereo_scene_anchor_scaled(const float anchorFromScene[12], floa
|
||||
aurora::g_pendingSceneAnchor.unitsPerMeter = unitsPerMeter;
|
||||
}
|
||||
}
|
||||
|
||||
void aurora_set_stereo_cockpit_item(const AuroraCockpitItem* item) {
|
||||
aurora::set_stereo_cockpit_item(item);
|
||||
}
|
||||
void aurora_set_stereo_local_player_count(uint32_t count) {
|
||||
aurora::g_pendingStereoLocalPlayerCount = count >= 1 && count <= 4 ? count : 1;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// and hand steering".
|
||||
#pragma once
|
||||
#include "common.hpp"
|
||||
#include "cockpit_item.hpp"
|
||||
#include "../webgpu/gpu.hpp"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
@@ -251,12 +252,44 @@ inline void build_geometry(const AuroraCockpit& cockpit, std::vector<Vertex>& ve
|
||||
else glove(vertices,hand,side);
|
||||
}
|
||||
}
|
||||
inline void append_item_badge(const AuroraCockpit& cockpit,const AuroraCockpitItem& item,
|
||||
std::vector<Vertex>& vertices) {
|
||||
if(!item.valid || item.hand>1 || item.count<1 || item.count>3 ||
|
||||
(item.id!=5 && item.id!=16 && item.id!=17 && item.id!=18) ||
|
||||
!cockpit.hands[item.hand].tracked || !cockpit_item::has_model(item.id)) return;
|
||||
const auto& hand=cockpit.hands[item.hand];
|
||||
if(hand.jointsValid && !joints_finite(hand)) return;
|
||||
M frame;
|
||||
if(!cockpit_item::seat_from_item(hand,frame)) return;
|
||||
// In the item's upright frame, beside the widest model on the hand's outer
|
||||
// side, facing the player like the item.
|
||||
const float side=item.hand==0?-1.0f:1.0f;
|
||||
const size_t first=vertices.size();
|
||||
const V base{side*0.095f,0.035f,0.0f};
|
||||
ellipsoid(vertices,base,{0.016f,0.020f,0.004f},{0.05f,0.08f,0.13f});
|
||||
// A tiny raised seven-segment digit stays legible without creating another
|
||||
// textured game asset. One model is held for every triple inventory ID.
|
||||
const uint8_t digit=item.count==1?0x06:item.count==2?0x5b:0x4f;
|
||||
const V white{0.95f,0.98f,0.85f};
|
||||
const auto segment=[&](int bit,float x0,float y0,float x1,float y1) {
|
||||
if(digit&(1u<<bit)) tube(vertices,add(base,{x0,y0,0.005f}),add(base,{x1,y1,0.005f}),0.0016f,white,5);
|
||||
};
|
||||
segment(0,-0.007f, 0.010f, 0.007f, 0.010f);
|
||||
segment(1, 0.008f, 0.009f, 0.008f, 0.001f);
|
||||
segment(2, 0.008f,-0.001f, 0.008f,-0.009f);
|
||||
segment(3,-0.007f,-0.010f, 0.007f,-0.010f);
|
||||
segment(4,-0.008f,-0.009f,-0.008f,-0.001f);
|
||||
segment(5,-0.008f, 0.001f,-0.008f, 0.009f);
|
||||
segment(6,-0.007f, 0.0f, 0.007f, 0.0f);
|
||||
for(size_t i=first;i<vertices.size();++i) vertices[i].position=point(frame.data(),vertices[i].position);
|
||||
}
|
||||
inline std::vector<Vertex> geometry(const AuroraCockpit& cockpit) {
|
||||
std::vector<Vertex> result;build_geometry(cockpit,result);return result;
|
||||
}
|
||||
inline std::atomic<uint64_t> meshRevision{1};
|
||||
inline std::vector<Vertex> frameVertices;
|
||||
inline AuroraCockpit cachedCockpit{};
|
||||
inline AuroraCockpitItem cachedCockpitItem{};
|
||||
inline uint64_t cachedMeshRevision=0;
|
||||
inline wgpu::RenderPipeline pipeline;
|
||||
struct SceneDepth {
|
||||
@@ -268,7 +301,7 @@ inline bool pipelineReversedDepth=false;
|
||||
inline wgpu::TextureFormat pipelineFormat{}, pipelineDepthFormat{};
|
||||
inline std::array<wgpu::Buffer,2> vertexBuffers;
|
||||
inline std::array<uint64_t,2> vertexCapacity{};
|
||||
inline void shutdown() { pipeline=nullptr;pipelineSamples=0;vertexBuffers={};vertexCapacity={};cachedMeshRevision=0;frameVertices.clear(); }
|
||||
inline void shutdown() { pipeline=nullptr;pipelineSamples=0;vertexBuffers={};vertexCapacity={};cachedMeshRevision=0;cachedCockpitItem={};frameVertices.clear();cockpit_item::shutdown(); }
|
||||
inline void render(wgpu::CommandEncoder& cmd,const StereoReplayFrame& frame,uint32_t eye,SceneDepth sceneDepth={},
|
||||
const wgpu::RenderPassEncoder* existingPass=nullptr) {
|
||||
if(!frame.cockpit.active || !sceneDepth.valid) return;
|
||||
@@ -308,9 +341,11 @@ inline void render(wgpu::CommandEncoder& cmd,const StereoReplayFrame& frame,uint
|
||||
pipelineReversedDepth=reversedDepth;pipelineDepthFormat=target.depthFormat;
|
||||
}
|
||||
const auto revision=meshRevision.load();
|
||||
if(cachedMeshRevision!=revision || std::memcmp(&cachedCockpit,&frame.cockpit,sizeof(AuroraCockpit))!=0) {
|
||||
if(cachedMeshRevision!=revision || std::memcmp(&cachedCockpit,&frame.cockpit,sizeof(AuroraCockpit))!=0 ||
|
||||
std::memcmp(&cachedCockpitItem,&frame.cockpitItem,sizeof(AuroraCockpitItem))!=0) {
|
||||
build_geometry(frame.cockpit,frameVertices);
|
||||
cachedCockpit=frame.cockpit;cachedMeshRevision=revision;
|
||||
append_item_badge(frame.cockpit,frame.cockpitItem,frameVertices);
|
||||
cachedCockpit=frame.cockpit;cachedCockpitItem=frame.cockpitItem;cachedMeshRevision=revision;
|
||||
}
|
||||
const auto& vertices=frameVertices;
|
||||
if(vertices.empty()) return;
|
||||
@@ -348,6 +383,7 @@ inline void render(wgpu::CommandEncoder& cmd,const StereoReplayFrame& frame,uint
|
||||
// Mark only depth-visible samples; later virtual-screen draws test for zero.
|
||||
pass.SetStencilReference(1);
|
||||
pass.SetPipeline(pipeline);pass.SetVertexBuffer(0,buffer);pass.Draw(clip.size());
|
||||
cockpit_item::render(pass,frame,eye,sceneDepth.z,sceneDepth.constant);
|
||||
pass.SetStencilReference(0);
|
||||
if(!existingPass) pass.End();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,528 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "cockpit_item_data.hpp"
|
||||
#include "texture_convert.hpp"
|
||||
#include "../webgpu/gpu.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
namespace aurora::gfx::cockpit_item {
|
||||
|
||||
inline std::mutex archiveMutex;
|
||||
inline std::shared_ptr<const data::Archive> archive;
|
||||
inline std::atomic<uint64_t> archiveRevision{0};
|
||||
|
||||
inline size_t mip_bytes(const data::Texture& texture,uint32_t mips) {
|
||||
size_t total=0;
|
||||
for(uint32_t level=0;level<mips;++level)
|
||||
total+=size_t(std::max(texture.width>>level,1))*std::max(texture.height>>level,1)*4;
|
||||
return total;
|
||||
}
|
||||
inline void set_archive(const void* bytes,uint32_t size) {
|
||||
{ std::lock_guard lock(archiveMutex); if(archive) return; }
|
||||
auto parsed=std::make_shared<data::Archive>(data::parse_archive(bytes,size));
|
||||
for(auto& model:parsed->models) for(auto& texture:model.textures) {
|
||||
for(uint32_t mips:{texture.mips,1u}) {
|
||||
auto converted=convert_texture(texture.format,texture.width,texture.height,mips,
|
||||
ArrayRef<uint8_t>(texture.bytes));
|
||||
const size_t length=mip_bytes(texture,mips);
|
||||
if(converted.format!=wgpu::TextureFormat::RGBA8Unorm || converted.data.size()<length) continue;
|
||||
texture.rgba.assign(converted.data.data(),converted.data.data()+length);
|
||||
texture.mips=mips;
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::lock_guard lock(archiveMutex);
|
||||
if(archive) return;
|
||||
archive=parsed->loaded?std::move(parsed):nullptr;
|
||||
++archiveRevision;
|
||||
}
|
||||
inline bool has_model(uint8_t id) {
|
||||
const int index=data::model_index(id);
|
||||
if(index<0) return false;
|
||||
std::lock_guard lock(archiveMutex);
|
||||
return archive && archive->models[index].valid();
|
||||
}
|
||||
|
||||
// Uniform images. WGSL: struct Material and struct Frame below.
|
||||
struct GpuStage { uint32_t color[4],colorOp[4],alpha[4],alphaOp[4],misc[4];float konst[4],texGen[2][4]; };
|
||||
struct GpuMaterial { GpuStage stages[4];float registers[4][4],materialColor[4];uint32_t info[4]; };
|
||||
static_assert(sizeof(GpuMaterial)==608);
|
||||
struct GpuFrame { float eyeFromModel[12],seatFromModel[12],projection[4],depth[4]; };
|
||||
static_assert(sizeof(GpuFrame)==128);
|
||||
// Static per-model vertices: billboards keep their origin in position (w=1)
|
||||
// and their bone-local offset, which the vertex shader turns to face the eye.
|
||||
struct GpuVertex { float position[4],offset[4],normal[4],color[4],uv[4]; };
|
||||
static_assert(sizeof(GpuVertex)==80);
|
||||
|
||||
inline GpuMaterial gpu_material(const data::Material& material) {
|
||||
GpuMaterial out{};
|
||||
for(uint32_t i=0;i<material.stageCount;++i) {
|
||||
const auto& s=material.stages[i];
|
||||
auto& g=out.stages[i];
|
||||
const uint32_t c=s.color,a=s.alpha;
|
||||
g.color[0]=(c>>12)&15u;g.color[1]=(c>>8)&15u;g.color[2]=(c>>4)&15u;g.color[3]=c&15u;
|
||||
g.colorOp[0]=(c>>16)&3u;g.colorOp[1]=(c>>18)&1u;g.colorOp[2]=(c>>19)&1u;g.colorOp[3]=(c>>20)&3u;
|
||||
g.alpha[0]=(a>>13)&7u;g.alpha[1]=(a>>10)&7u;g.alpha[2]=(a>>7)&7u;g.alpha[3]=(a>>4)&7u;
|
||||
g.alphaOp[0]=(a>>16)&3u;g.alphaOp[1]=(a>>18)&1u;g.alphaOp[2]=(a>>19)&1u;g.alphaOp[3]=(a>>20)&3u;
|
||||
g.misc[0]=(c>>22)&3u;g.misc[1]=(a>>22)&3u;g.misc[2]=s.textured;g.misc[3]=s.rasterized;
|
||||
std::memcpy(g.konst,s.konst.data(),sizeof(g.konst));
|
||||
const auto& gen=material.texGens[s.texCoord];
|
||||
const auto& m=gen.matrix;
|
||||
const float row0[4]{m[0],m[1],m[2],gen.normal?1.f:0.f},row1[4]{m[3],m[4],m[5],float(gen.uvSet)};
|
||||
std::memcpy(g.texGen[0],row0,sizeof(row0));std::memcpy(g.texGen[1],row1,sizeof(row1));
|
||||
}
|
||||
for(int i=0;i<4;++i) std::memcpy(out.registers[i],material.registers[i].data(),sizeof(out.registers[i]));
|
||||
std::memcpy(out.materialColor,material.materialColor.data(),sizeof(out.materialColor));
|
||||
out.info[0]=material.stageCount;out.info[1]=material.alphaCompare;
|
||||
out.info[2]=material.colorControl;out.info[3]=material.alphaControl;
|
||||
return out;
|
||||
}
|
||||
|
||||
struct GpuModel {
|
||||
wgpu::Buffer vertices;
|
||||
std::vector<wgpu::Texture> textures;
|
||||
std::vector<wgpu::Buffer> uniforms;
|
||||
std::vector<wgpu::BindGroup> materials;
|
||||
};
|
||||
inline std::array<GpuModel,15> gpuModels;
|
||||
inline std::array<bool,15> gpuReady{};
|
||||
inline std::shared_ptr<const data::Archive> gpuArchive;
|
||||
inline uint64_t gpuRevision=0;
|
||||
inline wgpu::Texture whiteTexture;
|
||||
inline std::array<wgpu::Sampler,18> samplers;
|
||||
inline wgpu::BindGroupLayout materialLayout,frameLayout;
|
||||
inline wgpu::PipelineLayout pipelineLayout;
|
||||
inline wgpu::ShaderModule shader;
|
||||
// One frame uniform per eye: both eyes may be encoded before one submit.
|
||||
inline std::array<wgpu::Buffer,2> frameBuffers;
|
||||
inline std::array<wgpu::BindGroup,2> frameGroups;
|
||||
struct PipelineKey {
|
||||
uint8_t cull=2,blendSrc=0,blendDst=0;
|
||||
bool blend=false,subtract=false,depthWrite=true;
|
||||
bool operator==(const PipelineKey&) const = default;
|
||||
};
|
||||
inline std::vector<std::pair<PipelineKey,wgpu::RenderPipeline>> pipelines;
|
||||
inline uint32_t pipelineSamples=0;
|
||||
inline bool pipelineReversed=false;
|
||||
inline wgpu::TextureFormat pipelineColor{},pipelineDepth{};
|
||||
|
||||
inline void shutdown() {
|
||||
gpuModels={};gpuReady={};gpuArchive.reset();gpuRevision=0;
|
||||
pipelines.clear();samplers={};whiteTexture=nullptr;shader=nullptr;
|
||||
frameBuffers={};frameGroups={};materialLayout=nullptr;frameLayout=nullptr;pipelineLayout=nullptr;
|
||||
pipelineSamples=0;
|
||||
}
|
||||
|
||||
inline void refresh_archive() {
|
||||
const uint64_t revision=archiveRevision.load(std::memory_order_acquire);
|
||||
if(revision==gpuRevision) return;
|
||||
std::lock_guard lock(archiveMutex);
|
||||
gpuArchive=archive;
|
||||
gpuModels={};gpuReady={};
|
||||
gpuRevision=revision;
|
||||
}
|
||||
|
||||
// GX TEV, four stages at most (item materials use three). Konst selections are
|
||||
// resolved on the CPU; each stage samples its own binding with its own texgen.
|
||||
inline constexpr const char* tevShader=R"(
|
||||
struct Stage { color: vec4u, colorOp: vec4u, alpha: vec4u, alphaOp: vec4u, misc: vec4u, konst: vec4f,
|
||||
texGen0: vec4f, texGen1: vec4f };
|
||||
struct Material { stages: array<Stage, 4>, registers: array<vec4f, 4>, materialColor: vec4f, info: vec4u };
|
||||
struct Frame { eye0: vec4f, eye1: vec4f, eye2: vec4f, seat0: vec4f, seat1: vec4f, seat2: vec4f,
|
||||
projection: vec4f, depth: vec4f };
|
||||
@group(0) @binding(0) var<uniform> material: Material;
|
||||
@group(0) @binding(1) var sampler0: sampler;
|
||||
@group(0) @binding(2) var texture0: texture_2d<f32>;
|
||||
@group(0) @binding(3) var sampler1: sampler;
|
||||
@group(0) @binding(4) var texture1: texture_2d<f32>;
|
||||
@group(0) @binding(5) var sampler2: sampler;
|
||||
@group(0) @binding(6) var texture2: texture_2d<f32>;
|
||||
@group(0) @binding(7) var sampler3: sampler;
|
||||
@group(0) @binding(8) var texture3: texture_2d<f32>;
|
||||
@group(1) @binding(0) var<uniform> frame: Frame;
|
||||
struct Out { @builtin(position) position: vec4f, @location(0) color: vec4f,
|
||||
@location(1) uv01: vec4f, @location(2) uv23: vec4f };
|
||||
fn unit(v: vec3f) -> vec3f { return v / max(length(v), 1e-4); }
|
||||
// G3D texgen: a UV set or, for env maps, the view-space normal, then the SRT.
|
||||
fn texCoord(s: u32, normal: vec3f, uv: vec4f) -> vec2f {
|
||||
let g0 = material.stages[s].texGen0;
|
||||
let g1 = material.stages[s].texGen1;
|
||||
var base = select(uv.xy, uv.zw, g1.w > 0.5);
|
||||
if (g0.w > 0.5) { base = vec2f(0.5 * normal.x + 0.5, -0.5 * normal.y + 0.5); }
|
||||
return vec2f(dot(g0.xyz, vec3f(base, 1.0)), dot(g1.xyz, vec3f(base, 1.0)));
|
||||
}
|
||||
@vertex fn vs(@location(0) position: vec4f, @location(1) offset: vec4f, @location(2) normal: vec4f,
|
||||
@location(3) color: vec4f, @location(4) uv: vec4f) -> Out {
|
||||
let p4 = vec4f(position.xyz, 1.0);
|
||||
let p = vec3f(dot(frame.eye0, p4), dot(frame.eye1, p4), dot(frame.eye2, p4)) + offset.xyz * frame.depth.z;
|
||||
let billboard = position.w > 0.5;
|
||||
let n = normal.xyz;
|
||||
let eyeNormal = select(unit(vec3f(dot(frame.eye0.xyz, n), dot(frame.eye1.xyz, n), dot(frame.eye2.xyz, n))),
|
||||
vec3f(0.0, 0.0, 1.0), billboard);
|
||||
var lit = 1.0;
|
||||
if (!billboard && (material.info.z & 2u) != 0u) {
|
||||
let seatNormal = unit(vec3f(dot(frame.seat0.xyz, n), dot(frame.seat1.xyz, n), dot(frame.seat2.xyz, n)));
|
||||
lit = 0.55 + 0.45 * abs(dot(seatNormal, vec3f(0.3, 0.8, 0.5)));
|
||||
}
|
||||
var o: Out;
|
||||
let z = frame.depth.x * p.z + frame.depth.y;
|
||||
o.position = vec4f(frame.projection.x * p.x + frame.projection.y * p.z,
|
||||
frame.projection.z * p.y + frame.projection.w * p.z,
|
||||
clamp(z, 0.0, max(-p.z, 0.0)), -p.z);
|
||||
o.color = vec4f(select(material.materialColor.rgb, color.rgb, (material.info.z & 1u) != 0u) * lit,
|
||||
select(material.materialColor.a, color.a, (material.info.w & 1u) != 0u));
|
||||
o.uv01 = vec4f(texCoord(0u, eyeNormal, uv), texCoord(1u, eyeNormal, uv));
|
||||
o.uv23 = vec4f(texCoord(2u, eyeNormal, uv), texCoord(3u, eyeNormal, uv));
|
||||
return o;
|
||||
}
|
||||
fn colorIn(sel: u32, prev: vec4f, c0: vec4f, c1: vec4f, c2: vec4f, tex: vec4f, ras: vec4f, k: vec4f) -> vec3f {
|
||||
switch sel {
|
||||
case 0u: { return prev.rgb; } case 1u: { return vec3f(prev.a); }
|
||||
case 2u: { return c0.rgb; } case 3u: { return vec3f(c0.a); }
|
||||
case 4u: { return c1.rgb; } case 5u: { return vec3f(c1.a); }
|
||||
case 6u: { return c2.rgb; } case 7u: { return vec3f(c2.a); }
|
||||
case 8u: { return tex.rgb; } case 9u: { return vec3f(tex.a); }
|
||||
case 10u: { return ras.rgb; } case 11u: { return vec3f(ras.a); }
|
||||
case 12u: { return vec3f(1.0); } case 13u: { return vec3f(0.5); }
|
||||
case 14u: { return k.rgb; }
|
||||
default: { return vec3f(0.0); }
|
||||
}
|
||||
}
|
||||
fn alphaIn(sel: u32, prev: vec4f, c0: vec4f, c1: vec4f, c2: vec4f, tex: vec4f, ras: vec4f, k: vec4f) -> f32 {
|
||||
switch sel {
|
||||
case 0u: { return prev.a; } case 1u: { return c0.a; } case 2u: { return c1.a; } case 3u: { return c2.a; }
|
||||
case 4u: { return tex.a; } case 5u: { return ras.a; } case 6u: { return k.a; }
|
||||
default: { return 0.0; }
|
||||
}
|
||||
}
|
||||
fn tevBias(b: u32) -> f32 { if (b == 1u) { return 0.5; } if (b == 2u) { return -0.5; } return 0.0; }
|
||||
fn tevScale(s: u32) -> f32 { if (s == 1u) { return 2.0; } if (s == 2u) { return 4.0; } if (s == 3u) { return 0.5; } return 1.0; }
|
||||
fn alphaTest(f: u32, value: f32, reference: f32) -> bool {
|
||||
switch f {
|
||||
case 0u: { return false; } case 1u: { return value < reference; } case 2u: { return value == reference; }
|
||||
case 3u: { return value <= reference; } case 4u: { return value > reference; }
|
||||
case 5u: { return value != reference; } case 6u: { return value >= reference; }
|
||||
default: { return true; }
|
||||
}
|
||||
}
|
||||
@fragment fn fs(i: Out) -> @location(0) vec4f {
|
||||
var samples = array<vec4f, 4>(textureSample(texture0, sampler0, i.uv01.xy), textureSample(texture1, sampler1, i.uv01.zw),
|
||||
textureSample(texture2, sampler2, i.uv23.xy), textureSample(texture3, sampler3, i.uv23.zw));
|
||||
var prev = material.registers[0];
|
||||
var c0 = material.registers[1];
|
||||
var c1 = material.registers[2];
|
||||
var c2 = material.registers[3];
|
||||
var result = prev;
|
||||
for (var s = 0u; s < min(material.info.x, 4u); s++) {
|
||||
let st = material.stages[s];
|
||||
let tex = select(vec4f(1.0), samples[s], st.misc.z != 0u);
|
||||
let ras = select(vec4f(0.0), i.color, st.misc.w != 0u);
|
||||
let ca = colorIn(st.color.x, prev, c0, c1, c2, tex, ras, st.konst);
|
||||
let cb = colorIn(st.color.y, prev, c0, c1, c2, tex, ras, st.konst);
|
||||
let cc = colorIn(st.color.z, prev, c0, c1, c2, tex, ras, st.konst);
|
||||
let cd = colorIn(st.color.w, prev, c0, c1, c2, tex, ras, st.konst);
|
||||
var color = (cd + select(1.0, -1.0, st.colorOp.y != 0u) * mix(ca, cb, cc) + tevBias(st.colorOp.x)) * tevScale(st.colorOp.w);
|
||||
color = select(clamp(color, vec3f(-4.0), vec3f(4.0)), clamp(color, vec3f(0.0), vec3f(1.0)), st.colorOp.z != 0u);
|
||||
let aa = alphaIn(st.alpha.x, prev, c0, c1, c2, tex, ras, st.konst);
|
||||
let ab = alphaIn(st.alpha.y, prev, c0, c1, c2, tex, ras, st.konst);
|
||||
let ac = alphaIn(st.alpha.z, prev, c0, c1, c2, tex, ras, st.konst);
|
||||
let ad = alphaIn(st.alpha.w, prev, c0, c1, c2, tex, ras, st.konst);
|
||||
var alpha = (ad + select(1.0, -1.0, st.alphaOp.y != 0u) * mix(aa, ab, ac) + tevBias(st.alphaOp.x)) * tevScale(st.alphaOp.w);
|
||||
alpha = select(clamp(alpha, -4.0, 4.0), clamp(alpha, 0.0, 1.0), st.alphaOp.z != 0u);
|
||||
switch st.misc.x {
|
||||
case 1u: { c0 = vec4f(color, c0.a); } case 2u: { c1 = vec4f(color, c1.a); }
|
||||
case 3u: { c2 = vec4f(color, c2.a); } default: { prev = vec4f(color, prev.a); }
|
||||
}
|
||||
switch st.misc.y {
|
||||
case 1u: { c0.a = alpha; } case 2u: { c1.a = alpha; }
|
||||
case 3u: { c2.a = alpha; } default: { prev.a = alpha; }
|
||||
}
|
||||
result = vec4f(color, alpha);
|
||||
}
|
||||
result = clamp(result, vec4f(0.0), vec4f(1.0));
|
||||
let word = material.info.y;
|
||||
let a8 = round(result.a * 255.0);
|
||||
let pass0 = alphaTest((word >> 16u) & 7u, a8, f32(word & 255u));
|
||||
let pass1 = alphaTest((word >> 19u) & 7u, a8, f32((word >> 8u) & 255u));
|
||||
let logic = (word >> 22u) & 3u;
|
||||
var passed = pass0 && pass1;
|
||||
if (logic == 1u) { passed = pass0 || pass1; } else if (logic == 2u) { passed = pass0 != pass1; }
|
||||
else if (logic == 3u) { passed = pass0 == pass1; }
|
||||
if (!passed) { discard; }
|
||||
return result;
|
||||
}
|
||||
)";
|
||||
|
||||
inline const wgpu::Sampler& sampler(uint8_t wrapS,uint8_t wrapT,bool mipmapped) {
|
||||
auto& slot=samplers[(wrapS*3+wrapT)*2+mipmapped];
|
||||
if(!slot) {
|
||||
constexpr wgpu::AddressMode modes[3]{wgpu::AddressMode::ClampToEdge,wgpu::AddressMode::Repeat,
|
||||
wgpu::AddressMode::MirrorRepeat};
|
||||
const wgpu::SamplerDescriptor desc{.label="Cockpit item sampler",
|
||||
.addressModeU=modes[wrapS],.addressModeV=modes[wrapT],
|
||||
.magFilter=wgpu::FilterMode::Linear,.minFilter=wgpu::FilterMode::Linear,
|
||||
.mipmapFilter=mipmapped?wgpu::MipmapFilterMode::Linear:wgpu::MipmapFilterMode::Nearest};
|
||||
slot=webgpu::g_device.CreateSampler(&desc);
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
inline void prepare_layout() {
|
||||
using namespace webgpu;
|
||||
if(materialLayout) return;
|
||||
std::array<wgpu::BindGroupLayoutEntry,9> entries{};
|
||||
entries[0]={.binding=0,.visibility=wgpu::ShaderStage::Vertex|wgpu::ShaderStage::Fragment,
|
||||
.buffer=wgpu::BufferBindingLayout{.type=wgpu::BufferBindingType::Uniform,.minBindingSize=sizeof(GpuMaterial)}};
|
||||
for(uint32_t i=0;i<4;++i) {
|
||||
entries[1+i*2]={.binding=1+i*2,.visibility=wgpu::ShaderStage::Fragment,
|
||||
.sampler=wgpu::SamplerBindingLayout{.type=wgpu::SamplerBindingType::Filtering}};
|
||||
entries[2+i*2]={.binding=2+i*2,.visibility=wgpu::ShaderStage::Fragment,
|
||||
.texture=wgpu::TextureBindingLayout{.sampleType=wgpu::TextureSampleType::Float,
|
||||
.viewDimension=wgpu::TextureViewDimension::e2D}};
|
||||
}
|
||||
const wgpu::BindGroupLayoutDescriptor materialDesc{.entryCount=entries.size(),.entries=entries.data()};
|
||||
materialLayout=g_device.CreateBindGroupLayout(&materialDesc);
|
||||
const wgpu::BindGroupLayoutEntry frameEntry{.binding=0,.visibility=wgpu::ShaderStage::Vertex,
|
||||
.buffer=wgpu::BufferBindingLayout{.type=wgpu::BufferBindingType::Uniform,.minBindingSize=sizeof(GpuFrame)}};
|
||||
const wgpu::BindGroupLayoutDescriptor frameDesc{.entryCount=1,.entries=&frameEntry};
|
||||
frameLayout=g_device.CreateBindGroupLayout(&frameDesc);
|
||||
const std::array layouts{materialLayout,frameLayout};
|
||||
const wgpu::PipelineLayoutDescriptor layoutDesc{.bindGroupLayoutCount=layouts.size(),.bindGroupLayouts=layouts.data()};
|
||||
pipelineLayout=g_device.CreatePipelineLayout(&layoutDesc);
|
||||
wgpu::ShaderSourceWGSL source{};
|
||||
source.code=tevShader;
|
||||
wgpu::ShaderModuleDescriptor md{};md.nextInChain=&source;md.label="Cockpit item TEV";
|
||||
shader=g_device.CreateShaderModule(&md);
|
||||
for(uint32_t eye=0;eye<2;++eye) {
|
||||
const wgpu::BufferDescriptor bufferDesc{.label="Cockpit item frame",
|
||||
.usage=wgpu::BufferUsage::Uniform|wgpu::BufferUsage::CopyDst,.size=sizeof(GpuFrame)};
|
||||
frameBuffers[eye]=g_device.CreateBuffer(&bufferDesc);
|
||||
const wgpu::BindGroupEntry entry{.binding=0,.buffer=frameBuffers[eye],.size=sizeof(GpuFrame)};
|
||||
const wgpu::BindGroupDescriptor group{.layout=frameLayout,.entryCount=1,.entries=&entry};
|
||||
frameGroups[eye]=g_device.CreateBindGroup(&group);
|
||||
}
|
||||
const wgpu::TextureDescriptor desc{.label="Cockpit item white",
|
||||
.usage=wgpu::TextureUsage::TextureBinding|wgpu::TextureUsage::CopyDst,
|
||||
.dimension=wgpu::TextureDimension::e2D,.size={1,1,1},.format=wgpu::TextureFormat::RGBA8Unorm,
|
||||
.mipLevelCount=1,.sampleCount=1};
|
||||
whiteTexture=g_device.CreateTexture(&desc);
|
||||
const uint8_t white[4]{255,255,255,255};
|
||||
const wgpu::TexelCopyTextureInfo destination{.texture=whiteTexture};
|
||||
const wgpu::TexelCopyBufferLayout layout{.bytesPerRow=4,.rowsPerImage=1};
|
||||
const wgpu::Extent3D extent{1,1,1};
|
||||
g_queue.WriteTexture(&destination,white,4,&layout,&extent);
|
||||
}
|
||||
|
||||
inline void prepare_model(size_t index) {
|
||||
using namespace webgpu;
|
||||
const auto& model=gpuArchive->models[index];
|
||||
auto& gpu=gpuModels[index];
|
||||
std::vector<GpuVertex> vertices;
|
||||
for(const auto& part:model.parts) for(const auto& v:part.vertices) {
|
||||
const data::V3 at=part.billboard?part.origin:v.position,offset=part.billboard?v.position:data::V3{};
|
||||
vertices.push_back({{at.x,at.y,at.z,part.billboard?1.f:0.f},{offset.x,offset.y,offset.z,0},
|
||||
{v.normal.x,v.normal.y,v.normal.z,0},{v.color[0],v.color[1],v.color[2],v.color[3]},
|
||||
{v.uv[0].x,v.uv[0].y,v.uv[1].x,v.uv[1].y}});
|
||||
}
|
||||
const wgpu::BufferDescriptor vertexDesc{.label="Cockpit item vertices",
|
||||
.usage=wgpu::BufferUsage::Vertex|wgpu::BufferUsage::CopyDst,.size=vertices.size()*sizeof(GpuVertex)};
|
||||
gpu.vertices=g_device.CreateBuffer(&vertexDesc);
|
||||
g_queue.WriteBuffer(gpu.vertices,0,vertices.data(),vertices.size()*sizeof(GpuVertex));
|
||||
for(const auto& texture:model.textures) {
|
||||
const bool usable=!texture.rgba.empty() && texture.rgba.size()>=mip_bytes(texture,texture.mips);
|
||||
if(!usable) { gpu.textures.push_back(nullptr);continue; }
|
||||
const wgpu::TextureDescriptor desc{.label="Cockpit item texture",
|
||||
.usage=wgpu::TextureUsage::TextureBinding|wgpu::TextureUsage::CopyDst,
|
||||
.dimension=wgpu::TextureDimension::e2D,.size={texture.width,texture.height,1},
|
||||
.format=wgpu::TextureFormat::RGBA8Unorm,.mipLevelCount=texture.mips,.sampleCount=1};
|
||||
auto gpuTexture=g_device.CreateTexture(&desc);
|
||||
size_t offset=0;
|
||||
for(uint32_t level=0;level<texture.mips;++level) {
|
||||
const uint32_t w=std::max(texture.width>>level,1),h=std::max(texture.height>>level,1);
|
||||
const wgpu::TexelCopyTextureInfo destination{.texture=gpuTexture,.mipLevel=level};
|
||||
const wgpu::TexelCopyBufferLayout layout{.bytesPerRow=w*4,.rowsPerImage=h};
|
||||
const wgpu::Extent3D extent{w,h,1};
|
||||
g_queue.WriteTexture(&destination,texture.rgba.data()+offset,size_t(w)*h*4,&layout,&extent);
|
||||
offset+=size_t(w)*h*4;
|
||||
}
|
||||
gpu.textures.push_back(std::move(gpuTexture));
|
||||
}
|
||||
for(const auto& material:model.materials) {
|
||||
const auto uniform=gpu_material(material);
|
||||
const wgpu::BufferDescriptor bufferDesc{.label="Cockpit item material",
|
||||
.usage=wgpu::BufferUsage::Uniform|wgpu::BufferUsage::CopyDst,.size=sizeof(GpuMaterial)};
|
||||
auto buffer=g_device.CreateBuffer(&bufferDesc);
|
||||
g_queue.WriteBuffer(buffer,0,&uniform,sizeof(uniform));
|
||||
std::array<wgpu::BindGroupEntry,9> entries{};
|
||||
entries[0]={.binding=0,.buffer=buffer,.size=sizeof(GpuMaterial)};
|
||||
for(uint32_t i=0;i<4;++i) {
|
||||
const auto& stage=material.stages[i];
|
||||
const data::Map* map=i<material.stageCount && stage.textured?&material.maps[stage.texMap]:nullptr;
|
||||
const wgpu::Texture* texture=map && gpu.textures[map->texture]?&gpu.textures[map->texture]:nullptr;
|
||||
const bool mipmapped=texture && model.textures[map->texture].mips>1;
|
||||
entries[1+i*2]={.binding=1+i*2,.sampler=texture?sampler(map->wrapS,map->wrapT,mipmapped):sampler(1,1,false)};
|
||||
entries[2+i*2]={.binding=2+i*2,.textureView=(texture?*texture:whiteTexture).CreateView()};
|
||||
}
|
||||
const wgpu::BindGroupDescriptor group{.layout=materialLayout,.entryCount=entries.size(),.entries=entries.data()};
|
||||
gpu.materials.push_back(g_device.CreateBindGroup(&group));
|
||||
gpu.uniforms.push_back(std::move(buffer));
|
||||
}
|
||||
gpuReady[index]=true;
|
||||
}
|
||||
|
||||
inline wgpu::BlendFactor blend_factor(uint8_t factor,bool source) {
|
||||
switch(factor) {
|
||||
case 0: return wgpu::BlendFactor::Zero;
|
||||
case 1: return wgpu::BlendFactor::One;
|
||||
case 2: return source?wgpu::BlendFactor::Dst:wgpu::BlendFactor::Src;
|
||||
case 3: return source?wgpu::BlendFactor::OneMinusDst:wgpu::BlendFactor::OneMinusSrc;
|
||||
case 4: return wgpu::BlendFactor::SrcAlpha;
|
||||
case 5: return wgpu::BlendFactor::OneMinusSrcAlpha;
|
||||
case 6: return wgpu::BlendFactor::One; // The eye target's alpha is not the EFB's.
|
||||
default: return wgpu::BlendFactor::Zero;
|
||||
}
|
||||
}
|
||||
inline const wgpu::RenderPipeline& pipeline(const PipelineKey& key,const StereoReplayFrame& frame,uint32_t eye,bool reversed) {
|
||||
using namespace webgpu;
|
||||
const auto& target=frame.eyes[eye].target;
|
||||
const auto format=g_graphicsConfig.surfaceConfiguration.format;
|
||||
if(pipelineSamples!=target.msaaSamples || pipelineColor!=format ||
|
||||
pipelineDepth!=target.depthFormat || pipelineReversed!=reversed) {
|
||||
pipelines.clear();
|
||||
pipelineSamples=target.msaaSamples;pipelineColor=format;pipelineDepth=target.depthFormat;pipelineReversed=reversed;
|
||||
}
|
||||
for(const auto& [cached,value]:pipelines) if(cached==key) return value;
|
||||
const wgpu::VertexAttribute attrs[]{
|
||||
{.format=wgpu::VertexFormat::Float32x4,.offset=0,.shaderLocation=0},
|
||||
{.format=wgpu::VertexFormat::Float32x4,.offset=16,.shaderLocation=1},
|
||||
{.format=wgpu::VertexFormat::Float32x4,.offset=32,.shaderLocation=2},
|
||||
{.format=wgpu::VertexFormat::Float32x4,.offset=48,.shaderLocation=3},
|
||||
{.format=wgpu::VertexFormat::Float32x4,.offset=64,.shaderLocation=4},
|
||||
};
|
||||
const wgpu::VertexBufferLayout vertices{.arrayStride=sizeof(GpuVertex),.attributeCount=5,.attributes=attrs};
|
||||
const wgpu::BlendState blend{
|
||||
.color=key.subtract?wgpu::BlendComponent{.operation=wgpu::BlendOperation::ReverseSubtract,
|
||||
.srcFactor=wgpu::BlendFactor::One,.dstFactor=wgpu::BlendFactor::One}
|
||||
:wgpu::BlendComponent{.operation=wgpu::BlendOperation::Add,
|
||||
.srcFactor=blend_factor(key.blendSrc,true),
|
||||
.dstFactor=blend_factor(key.blendDst,false)},
|
||||
.alpha={.operation=wgpu::BlendOperation::Add,.srcFactor=wgpu::BlendFactor::One,
|
||||
.dstFactor=wgpu::BlendFactor::OneMinusSrcAlpha},
|
||||
};
|
||||
const wgpu::ColorTargetState color{.format=format,.blend=key.blend?&blend:nullptr};
|
||||
const wgpu::FragmentState fragment{.module=shader,.entryPoint="fs",.targetCount=1,.targets=&color};
|
||||
const bool stencil=target.depthFormat==wgpu::TextureFormat::Depth24PlusStencil8;
|
||||
const wgpu::StencilFaceState mark{.compare=wgpu::CompareFunction::Always,
|
||||
.passOp=stencil?wgpu::StencilOperation::Replace:wgpu::StencilOperation::Keep};
|
||||
const wgpu::DepthStencilState depth{.format=target.depthFormat,.depthWriteEnabled=key.depthWrite,
|
||||
.depthCompare=reversed?wgpu::CompareFunction::GreaterEqual:wgpu::CompareFunction::LessEqual,
|
||||
.stencilFront=mark,.stencilBack=mark,.stencilReadMask=1,.stencilWriteMask=stencil?1u:0u};
|
||||
wgpu::RenderPipelineDescriptor desc{};desc.label="Cockpit item";desc.layout=pipelineLayout;
|
||||
desc.vertex={.module=shader,.entryPoint="vs",.bufferCount=1,.buffers=&vertices};
|
||||
desc.fragment=&fragment;desc.depthStencil=&depth;desc.multisample.count=target.msaaSamples;
|
||||
desc.primitive.topology=wgpu::PrimitiveTopology::TriangleList;
|
||||
// Same winding as Aurora's GX pipelines: GX front faces are clockwise.
|
||||
desc.primitive.frontFace=wgpu::FrontFace::CW;
|
||||
desc.primitive.cullMode=key.cull==1?wgpu::CullMode::Front:key.cull==2?wgpu::CullMode::Back:wgpu::CullMode::None;
|
||||
pipelines.emplace_back(key,g_device.CreateRenderPipeline(&desc));
|
||||
return pipelines.back().second;
|
||||
}
|
||||
|
||||
inline bool finite_matrix(const float* m) {
|
||||
for(int i=0;i<12;++i) {uint32_t bits;std::memcpy(&bits,m+i,4);if((bits&0x7f800000u)==0x7f800000u) return false;}
|
||||
return true;
|
||||
}
|
||||
inline void compose(const float* a,const float* b,float* out) {
|
||||
for(int r=0;r<3;++r) for(int c=0;c<4;++c) {
|
||||
out[r*4+c]=c==3?a[r*4+3]:0;
|
||||
for(int k=0;k<3;++k) out[r*4+c]+=a[r*4+k]*b[k*4+c];
|
||||
}
|
||||
}
|
||||
|
||||
// The held item's frame in the seated frame (+X right, +Y up, -Z forward). It
|
||||
// stays upright whatever the hand's roll and pitch, sits just above the palm and
|
||||
// turns only with the hand's heading, facing back along it: the item's front
|
||||
// (+Z) faces the player while the fingers point ahead. The fingers run along
|
||||
// grip -Y and along the palm joint's -Z.
|
||||
inline bool seat_from_item(const AuroraCockpitHand& hand,std::array<float,12>& out) {
|
||||
const float* pose=hand.jointsValid?hand.seatFromJoint[0]:hand.seatFromGrip;
|
||||
if(!finite_matrix(pose)) return false;
|
||||
const int fingers=hand.jointsValid?2:1;
|
||||
// The heading only. Within ~9 degrees of pointing straight up or down it
|
||||
// fades to straight ahead instead of spinning the item.
|
||||
const float x=-pose[fingers],z=-pose[8+fingers];
|
||||
const float length=std::sqrt(x*x+z*z),weight=std::clamp(length/0.15f,0.0f,1.0f);
|
||||
float headingX=weight*x/std::max(length,1e-6f),headingZ=weight*z/std::max(length,1e-6f)-(1-weight);
|
||||
float heading=std::sqrt(headingX*headingX+headingZ*headingZ);
|
||||
if(!(heading>1e-4f)) { headingX=0;headingZ=-1;heading=1; }
|
||||
const float frontX=-headingX/heading,frontZ=-headingZ/heading;
|
||||
constexpr float lift=0.05f;
|
||||
out={frontZ,0,frontX,pose[3], 0,1,0,pose[7]+lift, -frontX,0,frontZ,pose[11]};
|
||||
return true;
|
||||
}
|
||||
// Scales a model to 14 cm across its largest side and stands it on the item
|
||||
// frame's origin, centred.
|
||||
inline std::array<float,12> item_from_model(const data::Model& model) {
|
||||
const float span=std::max({model.maximum.x-model.minimum.x,model.maximum.y-model.minimum.y,
|
||||
model.maximum.z-model.minimum.z});
|
||||
if(!(span>0.001f) || !std::isfinite(span)) return {};
|
||||
const float k=0.14f/span;
|
||||
const float cx=(model.minimum.x+model.maximum.x)*0.5f,cz=(model.minimum.z+model.maximum.z)*0.5f;
|
||||
return {k,0,0,-k*cx, 0,k,0,-k*model.minimum.y, 0,0,k,-k*cz};
|
||||
}
|
||||
|
||||
inline void render(const wgpu::RenderPassEncoder& pass,const StereoReplayFrame& frame,uint32_t eye,
|
||||
float sceneZ,float sceneConstant) {
|
||||
if(!frame.cockpit.active || !(frame.cockpit.unitsPerMeter>0) || eye>1) return;
|
||||
refresh_archive();
|
||||
if(!gpuArchive) return;
|
||||
prepare_layout();
|
||||
// Upload on registration, before the first roulette settles, so receiving an
|
||||
// item does not pause the frame on texture uploads.
|
||||
for(size_t i=0;i<gpuArchive->models.size();++i)
|
||||
if(!gpuReady[i] && gpuArchive->models[i].valid()) prepare_model(i);
|
||||
const auto& item=frame.cockpitItem;
|
||||
if(!item.valid || item.hand>1 || item.count==0 || item.count>3) return;
|
||||
const int index=data::model_index(item.id);
|
||||
if(index<0 || !gpuArchive->models[index].valid()) return;
|
||||
const auto& hand=frame.cockpit.hands[item.hand];
|
||||
std::array<float,12> seatFromItem;
|
||||
if(!hand.tracked || !seat_from_item(hand,seatFromItem)) return;
|
||||
const auto& model=gpuArchive->models[index];
|
||||
const auto itemFromModel=item_from_model(model);
|
||||
if(!(itemFromModel[0]>0)) return;
|
||||
GpuFrame uniform{};
|
||||
compose(seatFromItem.data(),itemFromModel.data(),uniform.seatFromModel);
|
||||
compose(frame.cockpit.eyeFromSeat[eye],uniform.seatFromModel,uniform.eyeFromModel);
|
||||
const auto& projection=frame.eyes[eye].projection;
|
||||
const float projectionRow[4]{projection.m0[0],projection.m0[2],projection.m1[1],projection.m1[2]};
|
||||
const float depth[4]{sceneZ,sceneConstant/std::max(frame.cockpit.unitsPerMeter,0.001f),itemFromModel[0],0};
|
||||
std::memcpy(uniform.projection,projectionRow,sizeof(projectionRow));
|
||||
std::memcpy(uniform.depth,depth,sizeof(depth));
|
||||
webgpu::g_queue.WriteBuffer(frameBuffers[eye],0,&uniform,sizeof(uniform));
|
||||
const auto& gpu=gpuModels[index];
|
||||
pass.SetVertexBuffer(0,gpu.vertices);
|
||||
pass.SetBindGroup(1,frameGroups[eye],0,nullptr);
|
||||
const bool reversed=sceneConstant>0;
|
||||
uint32_t start=0;
|
||||
for(const auto& part:model.parts) {
|
||||
const auto count=static_cast<uint32_t>(part.vertices.size());
|
||||
const auto& material=model.materials[part.material];
|
||||
if(material.cull!=3) {
|
||||
const PipelineKey key{material.cull,material.blendSrc,material.blendDst,material.blend,material.subtract,
|
||||
material.depthWrite};
|
||||
pass.SetPipeline(pipeline(key,frame,eye,reversed));
|
||||
pass.SetBindGroup(0,gpu.materials[part.material],0,nullptr);
|
||||
pass.Draw(count,1,start,0);
|
||||
}
|
||||
start+=count;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace aurora::gfx::cockpit_item
|
||||
@@ -0,0 +1,577 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
|
||||
// Small, bounded reader for MKW's item BRRES models: bind-pose geometry plus
|
||||
// each material's texture layers, texgens and TEV stages. All input is copied
|
||||
// from the user's mapped Common.szs. No game data is shipped.
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace aurora::gfx::cockpit_item::data {
|
||||
|
||||
struct Reader {
|
||||
const uint8_t* bytes = nullptr;
|
||||
size_t size = 0;
|
||||
bool has(size_t at, size_t count) const { return at <= size && count <= size - at; }
|
||||
uint8_t u8(size_t at) const { return has(at,1) ? bytes[at] : 0; }
|
||||
uint16_t u16(size_t at) const { return has(at,2) ? (uint16_t(bytes[at])<<8)|bytes[at+1] : 0; }
|
||||
uint32_t u32(size_t at) const {
|
||||
return has(at,4) ? (uint32_t(bytes[at])<<24)|(uint32_t(bytes[at+1])<<16)|
|
||||
(uint32_t(bytes[at+2])<<8)|bytes[at+3] : 0;
|
||||
}
|
||||
float f32(size_t at) const { uint32_t bits=u32(at); float out; std::memcpy(&out,&bits,4); return out; }
|
||||
// A section-relative offset: returns 0 (never valid here) when it leaves the file.
|
||||
size_t rel(size_t base,size_t at) const {
|
||||
const int64_t target=int64_t(base)+int32_t(u32(at));
|
||||
return target>0 && size_t(target)<size ? size_t(target) : 0;
|
||||
}
|
||||
std::string str(size_t at) const {
|
||||
if(at>=size) return {};
|
||||
size_t end=at;
|
||||
while(end<size && end-at<128 && bytes[end]) ++end;
|
||||
return end<size && end-at<128 ? std::string(reinterpret_cast<const char*>(bytes+at),end-at) : std::string{};
|
||||
}
|
||||
};
|
||||
|
||||
struct Entry { std::string name; size_t at; };
|
||||
inline std::vector<Entry> dict(Reader r,size_t at) {
|
||||
if(!r.has(at,8)) return {};
|
||||
const uint32_t count=r.u32(at+4);
|
||||
if(count>4096 || !r.has(at+8,size_t(count+1)*16)) return {};
|
||||
std::vector<Entry> out;
|
||||
out.reserve(count);
|
||||
for(uint32_t i=1;i<=count;++i) {
|
||||
const size_t e=at+8+size_t(i)*16;
|
||||
const size_t target=at+r.u32(e+12);
|
||||
const auto name=r.str(at+r.u32(e+8));
|
||||
if(name.empty() || target>=r.size) return {};
|
||||
out.push_back({name,target});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
inline size_t find(const std::vector<Entry>& entries,const std::string& name) {
|
||||
for(const auto& e:entries) if(e.name==name) return e.at;
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline std::vector<uint8_t> yaz0(Reader input) {
|
||||
if(input.size>32u*1024u*1024u || !input.has(0,16)) return {};
|
||||
if(input.u32(0)!=0x59617a30u) return std::vector<uint8_t>(input.bytes,input.bytes+input.size);
|
||||
const size_t length=input.u32(4);
|
||||
if(length==0 || length>32u*1024u*1024u) return {};
|
||||
std::vector<uint8_t> out;
|
||||
out.reserve(length);
|
||||
size_t at=16;
|
||||
while(out.size()<length) {
|
||||
if(!input.has(at,1)) return {};
|
||||
const uint8_t control=input.u8(at++);
|
||||
for(int bit=7;bit>=0 && out.size()<length;--bit) {
|
||||
if(control & (1u<<bit)) {
|
||||
if(!input.has(at,1)) return {};
|
||||
out.push_back(input.u8(at++));
|
||||
} else {
|
||||
if(!input.has(at,2)) return {};
|
||||
const uint8_t a=input.u8(at++), b=input.u8(at++);
|
||||
size_t count=a>>4;
|
||||
if(count) count+=2;
|
||||
else { if(!input.has(at,1)) return {}; count=size_t(input.u8(at++))+18; }
|
||||
const size_t distance=((size_t(a&15)<<8)|b)+1;
|
||||
if(distance>out.size() || count>length-out.size()) return {};
|
||||
for(size_t j=0;j<count;++j) out.push_back(out[out.size()-distance]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
struct V3 { float x=0,y=0,z=0; };
|
||||
struct V2 { float x=0,y=0; };
|
||||
using Color = std::array<float,4>;
|
||||
struct Matrix {
|
||||
std::array<float,12> v{1,0,0,0,0,1,0,0,0,0,1,0};
|
||||
};
|
||||
inline V3 point(const Matrix& m,V3 p) {
|
||||
const auto& a=m.v;
|
||||
return {a[0]*p.x+a[1]*p.y+a[2]*p.z+a[3],a[4]*p.x+a[5]*p.y+a[6]*p.z+a[7],
|
||||
a[8]*p.x+a[9]*p.y+a[10]*p.z+a[11]};
|
||||
}
|
||||
inline V3 direction(const Matrix& m,V3 p) {
|
||||
const auto& a=m.v;
|
||||
return {a[0]*p.x+a[1]*p.y+a[2]*p.z,a[4]*p.x+a[5]*p.y+a[6]*p.z,
|
||||
a[8]*p.x+a[9]*p.y+a[10]*p.z};
|
||||
}
|
||||
|
||||
inline float component(Reader r,size_t at,uint32_t type,uint8_t shift) {
|
||||
if(type==4) return r.f32(at);
|
||||
const float scale=std::ldexp(1.0f,-int(shift));
|
||||
if(type==0) return r.u8(at)*scale;
|
||||
if(type==1) return int8_t(r.u8(at))*scale;
|
||||
if(type==2) return r.u16(at)*scale;
|
||||
if(type==3) return int16_t(r.u16(at))*scale;
|
||||
return 0;
|
||||
}
|
||||
struct Array {
|
||||
uint32_t id=0;
|
||||
std::vector<V3> values;
|
||||
};
|
||||
enum class ArrayKind { Position, Normal, UV };
|
||||
inline std::vector<Array> arrays(Reader r,size_t model,uint32_t dictionary_offset,ArrayKind kind) {
|
||||
std::vector<Array> result;
|
||||
if(!dictionary_offset) return result;
|
||||
for(const auto& e:dict(r,model+dictionary_offset)) {
|
||||
const size_t h=e.at;
|
||||
if(!r.has(h,0x20)) return {};
|
||||
const uint32_t type=r.u32(h+0x18), comps=r.u32(h+0x14);
|
||||
const uint8_t shift=r.u8(h+0x1c), stride=r.u8(h+0x1d);
|
||||
const uint16_t count=r.u16(h+0x1e);
|
||||
const size_t data=h+r.u32(h+8);
|
||||
const size_t elem=type==4?4:(type==2||type==3?2:1);
|
||||
const size_t n=kind==ArrayKind::UV?(comps?2:1):
|
||||
kind==ArrayKind::Normal?(comps?9:3):(comps?3:2);
|
||||
if(type>4 || stride<n*elem || !r.has(data,size_t(count)*stride)) return {};
|
||||
Array a; a.id=r.u32(h+0x10);a.values.reserve(count);
|
||||
for(uint32_t i=0;i<count;++i) {
|
||||
const size_t p=data+size_t(i)*stride;
|
||||
a.values.push_back({component(r,p,type,shift),n>1?component(r,p+elem,type,shift):0,
|
||||
n>2?component(r,p+2*elem,type,shift):0});
|
||||
}
|
||||
result.push_back(std::move(a));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
template <typename T> inline const T* array_id(const std::vector<T>& entries,uint16_t id) {
|
||||
for(const auto& a:entries) if(a.id==id) return &a;
|
||||
return nullptr;
|
||||
}
|
||||
struct ColorArray {
|
||||
uint32_t id=0;
|
||||
std::vector<Color> values;
|
||||
};
|
||||
// GX colour array formats: RGB565, RGB8, RGBX8, RGBA4, RGBA6, RGBA8.
|
||||
inline std::vector<ColorArray> color_arrays(Reader r,size_t model,uint32_t dictionary_offset) {
|
||||
std::vector<ColorArray> result;
|
||||
if(!dictionary_offset) return result;
|
||||
constexpr uint8_t sizes[6]{2,3,4,2,3,4};
|
||||
for(const auto& e:dict(r,model+dictionary_offset)) {
|
||||
const size_t h=e.at;
|
||||
if(!r.has(h,0x20)) return {};
|
||||
const uint32_t format=r.u32(h+0x18);
|
||||
const uint8_t stride=r.u8(h+0x1c);
|
||||
const uint16_t count=r.u16(h+0x1e);
|
||||
const size_t data=h+r.u32(h+8);
|
||||
if(format>5 || stride<sizes[format] || !r.has(data,size_t(count)*stride)) return {};
|
||||
ColorArray a; a.id=r.u32(h+0x10);a.values.reserve(count);
|
||||
for(uint32_t i=0;i<count;++i) {
|
||||
const size_t p=data+size_t(i)*stride;
|
||||
const uint32_t v16=r.u16(p),v24=(uint32_t(r.u16(p))<<8)|r.u8(p+2);
|
||||
Color c{1,1,1,1};
|
||||
switch(format) {
|
||||
case 0: c={((v16>>11)&31)/31.f,((v16>>5)&63)/63.f,(v16&31)/31.f,1}; break;
|
||||
case 1: case 2: c={r.u8(p)/255.f,r.u8(p+1)/255.f,r.u8(p+2)/255.f,1}; break;
|
||||
case 3: c={(v16>>12)/15.f,((v16>>8)&15)/15.f,((v16>>4)&15)/15.f,(v16&15)/15.f}; break;
|
||||
case 4: c={(v24>>18)/63.f,((v24>>12)&63)/63.f,((v24>>6)&63)/63.f,(v24&63)/63.f}; break;
|
||||
default: c={r.u8(p)/255.f,r.u8(p+1)/255.f,r.u8(p+2)/255.f,r.u8(p+3)/255.f}; break;
|
||||
}
|
||||
a.values.push_back(c);
|
||||
}
|
||||
result.push_back(std::move(a));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// G3D texture SRT in Maya mode (every item material uses it), as a 2x3 matrix
|
||||
// applied to (s,t,1). Other modes fall back to a plain scale-rotate-translate.
|
||||
inline std::array<float,6> texture_srt(float sx,float sy,float degrees,float tx,float ty,uint32_t mode) {
|
||||
const float r=degrees*0.017453292519943295f,c=std::cos(r),s=std::sin(r);
|
||||
if(mode!=0) return {sx*c,-sy*s,tx,sx*s,sy*c,ty};
|
||||
return {sx*c,sy*-s,sx*(-0.5f*c-(0.5f*s-0.5f)-tx),
|
||||
sx*s,sy*c,sy*(-0.5f*c+(0.5f*s-0.5f)+ty)+1.0f};
|
||||
}
|
||||
|
||||
struct Vertex { V3 position;V3 normal;Color color{1,1,1,1};std::array<V2,2> uv{}; };
|
||||
struct Texture {
|
||||
std::string name;
|
||||
uint16_t width=0,height=0;
|
||||
uint32_t format=0,mips=1;
|
||||
std::vector<uint8_t> bytes;
|
||||
std::vector<uint8_t> rgba; // every mip level, level 0 first
|
||||
};
|
||||
struct TexGen {
|
||||
bool normal=false; // env map from the view-space normal; else a UV set
|
||||
uint8_t uvSet=0;
|
||||
std::array<float,6> matrix{1,0,0,0,1,0};
|
||||
};
|
||||
struct Stage {
|
||||
uint8_t texMap=0,texCoord=0;
|
||||
bool textured=false,rasterized=true;
|
||||
uint32_t color=0x8fff0,alpha=0; // BP 0xC0/0xC1 combiner words
|
||||
Color konst{1,1,1,1}; // the stage's resolved KSEL constant
|
||||
};
|
||||
struct Map { int texture=-1;uint8_t wrapS=1,wrapT=1; };
|
||||
struct Material {
|
||||
uint8_t cull=2; // GX: 0 none, 1 front, 2 back, 3 all
|
||||
bool blend=false,subtract=false,depthWrite=true;
|
||||
uint8_t blendSrc=4,blendDst=5;
|
||||
uint32_t alphaCompare=0x3f0000; // BP 0xF3
|
||||
uint32_t colorControl=0x700,alphaControl=0x700;
|
||||
Color materialColor{1,1,1,1};
|
||||
uint8_t stageCount=0;
|
||||
std::array<Stage,4> stages{};
|
||||
std::array<Color,4> registers{}; // PREV, C0, C1, C2
|
||||
std::array<TexGen,8> texGens{};
|
||||
std::array<Map,8> maps{};
|
||||
};
|
||||
struct Part {
|
||||
std::vector<Vertex> vertices;
|
||||
uint16_t material=0;
|
||||
bool translucent=false;
|
||||
// Billboards keep bone-local positions around origin and face the eye.
|
||||
bool billboard=false;
|
||||
V3 origin;
|
||||
};
|
||||
struct Model {
|
||||
std::vector<Part> parts;
|
||||
std::vector<Material> materials;
|
||||
std::vector<Texture> textures;
|
||||
V3 minimum{std::numeric_limits<float>::max(),std::numeric_limits<float>::max(),std::numeric_limits<float>::max()};
|
||||
V3 maximum{-std::numeric_limits<float>::max(),-std::numeric_limits<float>::max(),-std::numeric_limits<float>::max()};
|
||||
bool valid() const { return !parts.empty() && maximum.x>=minimum.x; }
|
||||
};
|
||||
inline void bounds(Model& model,V3 p) {
|
||||
model.minimum={std::min(model.minimum.x,p.x),std::min(model.minimum.y,p.y),std::min(model.minimum.z,p.z)};
|
||||
model.maximum={std::max(model.maximum.x,p.x),std::max(model.maximum.y,p.y),std::max(model.maximum.z,p.z)};
|
||||
}
|
||||
|
||||
inline float signed11(uint32_t v) { int32_t x=int32_t(v&0x7ffu);if(x&0x400) x-=0x800;return float(x)/255.f; }
|
||||
inline Color konst_value(const std::array<Color,4>& konst,uint32_t sel,bool alpha) {
|
||||
if(sel<8) { const float v=float(8-sel)/8.f;return {v,v,v,v}; }
|
||||
if(!alpha && sel>=0x0c && sel<=0x0f) { const auto& k=konst[sel-0x0c];return {k[0],k[1],k[2],k[3]}; }
|
||||
if(sel>=0x10 && sel<=0x1f) { const float v=konst[sel&3][(sel-0x10)>>2];return {v,v,v,v}; }
|
||||
return {0,0,0,0};
|
||||
}
|
||||
// Walks a G3D display list of BP (0x61), XF (0x10) and CP (0x08) loads.
|
||||
template <typename Bp,typename Xf>
|
||||
inline void walk_dl(Reader r,size_t at,size_t end,Bp&& bp,Xf&& xf) {
|
||||
end=std::min(end,r.size);
|
||||
while(at<end) {
|
||||
const uint8_t op=r.u8(at++);
|
||||
if(op==0) continue;
|
||||
if(op==0x61 && at+4<=end) { bp(r.u8(at),r.u32(at)&0xffffffu);at+=4; }
|
||||
else if(op==0x10 && at+4<=end) {
|
||||
const size_t count=size_t(r.u16(at))+1;const uint16_t address=r.u16(at+2);at+=4;
|
||||
for(size_t i=0;i<count && at+4<=end;++i,at+=4) xf(uint32_t(address+i),r.u32(at));
|
||||
} else if(op==0x08 && at+5<=end) at+=5;
|
||||
else return;
|
||||
}
|
||||
}
|
||||
inline int texture_index(Reader r,const std::vector<Entry>& fileTextures,const std::string& name,Model& model) {
|
||||
for(size_t i=0;i<model.textures.size();++i) if(model.textures[i].name==name) return int(i);
|
||||
const size_t tex=find(fileTextures,name);
|
||||
if(!tex || !r.has(tex,0x40) || r.u32(tex)!=0x54455830u) return -1;
|
||||
const size_t end=tex+r.u32(tex+4),start=tex+r.u32(tex+0x10);
|
||||
const uint16_t width=r.u16(tex+0x1c),height=r.u16(tex+0x1e);
|
||||
const uint32_t format=r.u32(tex+0x20),mips=std::clamp(r.u32(tex+0x24),1u,11u);
|
||||
if(end>r.size || start>=end || !width || !height || width>1024 || height>1024 ||
|
||||
format>14 || format==7 || (format>=8 && format<=13)) return -1;
|
||||
model.textures.push_back({name,width,height,format,mips,std::vector<uint8_t>(r.bytes+start,r.bytes+end),{}});
|
||||
return int(model.textures.size()-1);
|
||||
}
|
||||
inline bool parse_material(Reader r,size_t mat,const std::vector<Entry>& fileTextures,Model& model,Material& out) {
|
||||
if(!r.has(mat,0x418)) return false;
|
||||
const uint8_t genCount=std::min<uint8_t>(r.u8(mat+0x14),8);
|
||||
out.cull=uint8_t(r.u32(mat+0x18)&3u);
|
||||
const uint32_t layers=r.u32(mat+0x2c);
|
||||
const size_t layerAt=r.rel(mat,mat+0x30);
|
||||
if(layers>8 || (layers && (!layerAt || !r.has(layerAt,size_t(layers)*0x34)))) return false;
|
||||
for(uint32_t i=0;i<layers;++i) {
|
||||
const size_t layer=layerAt+size_t(i)*0x34;
|
||||
const uint32_t map=r.u32(layer+0x10);
|
||||
if(map>=8) return false;
|
||||
out.maps[map]={texture_index(r,fileTextures,r.str(r.rel(layer,layer)),model),
|
||||
uint8_t(std::min(r.u32(layer+0x18),2u)),uint8_t(std::min(r.u32(layer+0x1c),2u))};
|
||||
}
|
||||
const uint32_t srtMode=r.u32(mat+0x1ac);
|
||||
for(uint8_t i=0;i<genCount;++i) {
|
||||
const size_t srt=mat+0x1b0+size_t(i)*20;
|
||||
out.texGens[i].matrix=texture_srt(r.f32(srt),r.f32(srt+4),r.f32(srt+8),r.f32(srt+12),r.f32(srt+16),srtMode);
|
||||
const uint8_t mapMode=r.u8(mat+0x250+size_t(i)*0x34+2);
|
||||
out.texGens[i].normal=mapMode!=0;
|
||||
}
|
||||
const size_t channel=mat+0x3f0;
|
||||
out.materialColor={r.u8(channel+4)/255.f,r.u8(channel+5)/255.f,r.u8(channel+6)/255.f,r.u8(channel+7)/255.f};
|
||||
out.colorControl=r.u32(channel+0xc);out.alphaControl=r.u32(channel+0x10);
|
||||
std::array<Color,4> konst{};
|
||||
std::array<uint8_t,4> kc{},ka{};
|
||||
kc.fill(0x0c);ka.fill(0x1c);
|
||||
const auto bp=[&](uint8_t reg,uint32_t v) {
|
||||
if(reg==0xf3) out.alphaCompare=v;
|
||||
else if(reg==0x40) out.depthWrite=(v>>4)&1u;
|
||||
else if(reg==0x41) {
|
||||
out.blend=v&1u;out.blendDst=(v>>5)&7u;out.blendSrc=(v>>8)&7u;out.subtract=(v>>11)&1u;
|
||||
} else if(reg>=0xe0 && reg<=0xe7) {
|
||||
const bool hi=reg&1u;
|
||||
auto& c=(v>>23)?konst[(reg-0xe0)>>1]:out.registers[(reg-0xe0)>>1];
|
||||
const float low=(v>>23)?float(v&0xffu)/255.f:signed11(v),high=(v>>23)?float((v>>12)&0xffu)/255.f:signed11(v>>12);
|
||||
if(hi) { c[2]=low;c[1]=high; } else { c[0]=low;c[3]=high; }
|
||||
} else if(reg>=0x28 && reg<=0x29) {
|
||||
for(uint32_t half=0;half<2;++half) {
|
||||
auto& s=out.stages[(reg-0x28)*2+half];
|
||||
const uint32_t x=v>>(12*half);
|
||||
s.texMap=x&7u;s.texCoord=(x>>3)&7u;s.textured=(x>>6)&1u;s.rasterized=((x>>7)&7u)==0;
|
||||
}
|
||||
} else if(reg>=0xc0 && reg<=0xc7) {
|
||||
auto& s=out.stages[(reg-0xc0)>>1];
|
||||
(reg&1u?s.alpha:s.color)=v;
|
||||
} else if(reg>=0xf6 && reg<=0xf7) {
|
||||
for(uint32_t half=0;half<2;++half) {
|
||||
const size_t stage=(reg-0xf6)*2+half;
|
||||
kc[stage]=(v>>(4+10*half))&31u;ka[stage]=(v>>(9+10*half))&31u;
|
||||
}
|
||||
}
|
||||
};
|
||||
const auto xf=[&](uint32_t address,uint32_t v) {
|
||||
if(address<0x1040 || address>=0x1040u+genCount) return;
|
||||
// TEXMTXINFO source row: 1 is the normal, 5..12 are UV sets.
|
||||
const uint32_t row=(v>>7)&31u;
|
||||
auto& gen=out.texGens[address-0x1040];
|
||||
if(row>=5 && row<=12 && !gen.normal) gen.uvSet=uint8_t(std::min(row-5,1u));
|
||||
else gen.normal=true;
|
||||
};
|
||||
const size_t tev=r.rel(mat,mat+0x28),dl=r.rel(mat,mat+0x3c);
|
||||
if(!tev || !dl || !r.has(tev,0x20)) return false;
|
||||
walk_dl(r,dl,dl+0x180,bp,xf);
|
||||
walk_dl(r,tev+0x20,tev+std::min<size_t>(r.u32(tev),0x400),bp,xf);
|
||||
out.stageCount=std::min<uint8_t>(r.u8(tev+0xc),4);
|
||||
for(uint8_t i=0;i<out.stageCount;++i) {
|
||||
auto& s=out.stages[i];
|
||||
const Color c=konst_value(konst,kc[i],false),a=konst_value(konst,ka[i],true);
|
||||
s.konst={c[0],c[1],c[2],a[3]};
|
||||
s.textured=s.textured && out.maps[s.texMap].texture>=0;
|
||||
if(s.texCoord>=genCount) s.texCoord=0;
|
||||
}
|
||||
return out.stageCount>0;
|
||||
}
|
||||
|
||||
struct Bone { Matrix matrix;uint32_t billboard=0;bool valid=false; };
|
||||
inline bool decode_shape(Reader r,size_t shape,const std::vector<Array>& positions,
|
||||
const std::vector<Array>& normals,const std::vector<ColorArray>& colors,
|
||||
const std::vector<Array>& uvs,const std::vector<Bone>& bones,
|
||||
Part& part,Model& model) {
|
||||
if(!r.has(shape,0x68)) return false;
|
||||
const uint32_t lo=r.u32(shape+0xc),hi=r.u32(shape+0x10);
|
||||
const int desc[12]{int((lo>>9)&3),int((lo>>11)&3),int((lo>>13)&3),int((lo>>15)&3),
|
||||
int(hi&3),int((hi>>2)&3),int((hi>>4)&3),int((hi>>6)&3),int((hi>>8)&3),
|
||||
int((hi>>10)&3),int((hi>>12)&3),int((hi>>14)&3)};
|
||||
if(desc[0]<2) return false;
|
||||
const auto* pos=array_id(positions,r.u16(shape+0x48));
|
||||
const auto* nrm=desc[1]?array_id(normals,r.u16(shape+0x4a)):nullptr;
|
||||
const auto* clr=desc[2]?array_id(colors,r.u16(shape+0x4c)):nullptr;
|
||||
const Array* uv[2]{desc[4]?array_id(uvs,r.u16(shape+0x50)):nullptr,
|
||||
desc[5]?array_id(uvs,r.u16(shape+0x52)):nullptr};
|
||||
if(!pos || (desc[1] && !nrm) || (desc[2] && !clr) || (desc[4] && !uv[0]) || (desc[5] && !uv[1])) return false;
|
||||
size_t matrix_bytes=0;
|
||||
for(uint32_t mask=lo&511;mask;mask>>=1) matrix_bytes+=mask&1u;
|
||||
size_t stride=matrix_bytes;
|
||||
for(int d:desc) {
|
||||
if(d==1) return false;
|
||||
stride+=d==2?1:d==3?2:0;
|
||||
}
|
||||
const size_t begin=shape+0x24+r.u32(shape+0x2c),length=r.u32(shape+0x28);
|
||||
if(stride<2 || length>65536 || !r.has(begin,length)) return false;
|
||||
const size_t end=begin+length;
|
||||
// Matrix IDs, not bone indices: a single-bound shape names its own matrix;
|
||||
// one with PNMTXIDX loads a palette of them. Envelope IDs have no bone and are
|
||||
// identity in the bind pose (their vertices are already in model space).
|
||||
const int32_t single=int32_t(r.u32(shape+8));
|
||||
const auto resolve=[&](uint32_t id) -> const Bone* { return id<bones.size() && bones[id].valid?&bones[id]:nullptr; };
|
||||
const Bone identity{};
|
||||
const Bone* singleBone=single>=0?resolve(uint32_t(single)):nullptr;
|
||||
if(!(lo&1u) && singleBone && singleBone->billboard) {
|
||||
part.billboard=true;
|
||||
const auto& m=singleBone->matrix.v;
|
||||
part.origin={m[3],m[7],m[11]};
|
||||
}
|
||||
std::array<uint16_t,10> palette{};
|
||||
palette.fill(uint16_t(std::max(single,0)));
|
||||
size_t at=begin;
|
||||
while(at<end) {
|
||||
const uint8_t op=r.u8(at++);
|
||||
if(op==0) continue;
|
||||
if(op==0x20 || op==0x28 || op==0x30 || op==0x38) {
|
||||
if(at+4>end) return false;
|
||||
const uint32_t slot=(r.u16(at+2)&0xfffu)/12u;
|
||||
if(op==0x20 && slot<palette.size()) palette[slot]=r.u16(at);
|
||||
at+=4;continue;
|
||||
}
|
||||
const uint8_t primitive=op&0xf8;
|
||||
if((primitive!=0x80 && primitive!=0x90 && primitive!=0x98 && primitive!=0xa0) || at+2>end) return false;
|
||||
const uint16_t count=r.u16(at);at+=2;
|
||||
if(count>8192 || size_t(count)*stride>end-at) return false;
|
||||
std::vector<Vertex> source;
|
||||
source.reserve(count);
|
||||
for(uint16_t i=0;i<count;++i) {
|
||||
const Bone* bone=singleBone;
|
||||
if(lo&1u) {
|
||||
const uint32_t slot=r.u8(at)/3u;
|
||||
bone=slot<palette.size()?resolve(palette[slot]):nullptr;
|
||||
}
|
||||
at+=matrix_bytes;
|
||||
uint16_t indices[12]{};
|
||||
for(int a=0;a<12;++a) {
|
||||
if(desc[a]==2) indices[a]=r.u8(at++);
|
||||
else if(desc[a]==3) { indices[a]=r.u16(at);at+=2; }
|
||||
}
|
||||
if(indices[0]>=pos->values.size() || (nrm && indices[1]>=nrm->values.size()) ||
|
||||
(clr && indices[2]>=clr->values.size()) || (uv[0] && indices[4]>=uv[0]->values.size()) ||
|
||||
(uv[1] && indices[5]>=uv[1]->values.size())) return false;
|
||||
const Matrix& transform=(bone?bone:&identity)->matrix;
|
||||
Vertex v;
|
||||
v.normal=nrm?nrm->values[indices[1]]:V3{0,1,0};
|
||||
if(part.billboard) {
|
||||
// Keep the bone's scale; the renderer replaces its rotation.
|
||||
const auto& m=transform.v;
|
||||
const V3 p=pos->values[indices[0]];
|
||||
v.position={p.x*std::hypot(m[0],m[4],m[8]),p.y*std::hypot(m[1],m[5],m[9]),p.z*std::hypot(m[2],m[6],m[10])};
|
||||
} else {
|
||||
v.position=point(transform,pos->values[indices[0]]);
|
||||
v.normal=direction(transform,v.normal);
|
||||
}
|
||||
if(clr) v.color=clr->values[indices[2]];
|
||||
for(int set=0;set<2;++set) if(uv[set]) {
|
||||
const V3 t=uv[set]->values[indices[4+set]];
|
||||
v.uv[set]={t.x,t.y};
|
||||
}
|
||||
source.push_back(v);
|
||||
}
|
||||
const auto tri=[&](uint16_t a,uint16_t b,uint16_t c) {
|
||||
if(a==b || b==c || a==c) return;
|
||||
for(uint16_t i:{a,b,c}) part.vertices.push_back(source[i]);
|
||||
};
|
||||
if(primitive==0x90) { for(uint16_t i=0;i+2<count;i+=3) tri(i,i+1,i+2); }
|
||||
else if(primitive==0x80) { for(uint16_t i=0;i+3<count;i+=4) {tri(i,i+1,i+2);tri(i,i+2,i+3);} }
|
||||
else if(primitive==0x98) { for(uint16_t i=2;i<count;++i) {
|
||||
if(i&1) tri(i-1,i-2,i);else tri(i-2,i-1,i);
|
||||
} }
|
||||
else { for(uint16_t i=2;i<count;++i) tri(0,i-1,i); }
|
||||
}
|
||||
for(const auto& v:part.vertices) {
|
||||
if(!part.billboard) { bounds(model,v.position);continue; }
|
||||
const float radius=std::sqrt(v.position.x*v.position.x+v.position.y*v.position.y+v.position.z*v.position.z);
|
||||
bounds(model,{part.origin.x-radius,part.origin.y-radius,part.origin.z-radius});
|
||||
bounds(model,{part.origin.x+radius,part.origin.y+radius,part.origin.z+radius});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline Model parse_model(Reader r,const std::string& model_name) {
|
||||
Model result;
|
||||
if(!r.has(0,16) || r.u32(0)!=0x62726573u) return result;
|
||||
const auto groups=dict(r,r.u16(12)+8);
|
||||
const size_t model_dict=find(groups,"3DModels(NW4R)");
|
||||
const size_t texture_dict=find(groups,"Textures(NW4R)");
|
||||
if(!model_dict || !texture_dict) return result;
|
||||
const size_t m=find(dict(r,model_dict),model_name);
|
||||
if(!m || !r.has(m,0x40) || r.u32(m)!=0x4d444c30u || r.u32(m+8)!=11) return result;
|
||||
const size_t model_end=m+r.u32(m+4);
|
||||
if(model_end>r.size || model_end<=m) return result;
|
||||
const auto joint_entries=dict(r,m+r.u32(m+0x14));
|
||||
const auto material_entries=dict(r,m+r.u32(m+0x30));
|
||||
const auto shape_entries=dict(r,m+r.u32(m+0x38));
|
||||
const auto file_textures=dict(r,texture_dict);
|
||||
const auto draw_entries=dict(r,m+r.u32(m+0x10));
|
||||
if(joint_entries.empty() || material_entries.empty() || shape_entries.empty() ||
|
||||
joint_entries.size()>256 || shape_entries.size()>256 || material_entries.size()>64) return result;
|
||||
// Each bone stores its bind-pose model matrix; index it by matrix ID, the
|
||||
// number shapes and palettes use (NodeTree parents are matrix IDs too).
|
||||
std::vector<Bone> bones;
|
||||
for(const auto& e:joint_entries) {
|
||||
if(!r.has(e.at,0xa0)) return {};
|
||||
const uint32_t id=r.u32(e.at+0x10);
|
||||
if(id>=1024) return {};
|
||||
if(id>=bones.size()) bones.resize(id+1);
|
||||
auto& bone=bones[id];
|
||||
for(int i=0;i<12;++i) bone.matrix.v[i]=r.f32(e.at+0x70+size_t(i)*4);
|
||||
for(float f:bone.matrix.v) if(!std::isfinite(f)) return {};
|
||||
bone.billboard=r.u32(e.at+0x18);bone.valid=true;
|
||||
}
|
||||
const auto positions=arrays(r,m,r.u32(m+0x18),ArrayKind::Position);
|
||||
const auto normals=arrays(r,m,r.u32(m+0x1c),ArrayKind::Normal);
|
||||
const auto colors=color_arrays(r,m,r.u32(m+0x20));
|
||||
const auto uvs=arrays(r,m,r.u32(m+0x24),ArrayKind::UV);
|
||||
if(positions.empty()) return {};
|
||||
struct Draw { uint16_t mat,shape;bool xlu; };
|
||||
std::vector<Draw> draws;
|
||||
for(const char* list:{"DrawOpa","DrawXlu"}) {
|
||||
const size_t start=find(draw_entries,list);
|
||||
if(!start) continue;
|
||||
size_t at=start;
|
||||
for(size_t guard=0;guard<4096 && at<model_end;++guard) {
|
||||
const uint8_t op=r.u8(at);
|
||||
if(op==1) break;
|
||||
if(op!=4 || !r.has(at,8)) return {};
|
||||
draws.push_back({r.u16(at+1),r.u16(at+3),std::strcmp(list,"DrawXlu")==0});
|
||||
at+=8;
|
||||
}
|
||||
}
|
||||
if(draws.empty() || draws.size()>512) return {};
|
||||
std::vector<int> materialSlot(material_entries.size(),-1);
|
||||
for(const auto& draw:draws) {
|
||||
if(draw.mat>=material_entries.size() || draw.shape>=shape_entries.size()) return {};
|
||||
if(materialSlot[draw.mat]<0) {
|
||||
Material material;
|
||||
if(!parse_material(r,material_entries[draw.mat].at,file_textures,result,material)) return {};
|
||||
materialSlot[draw.mat]=int(result.materials.size());
|
||||
result.materials.push_back(material);
|
||||
}
|
||||
Part part;part.translucent=draw.xlu;part.material=uint16_t(materialSlot[draw.mat]);
|
||||
if(!decode_shape(r,shape_entries[draw.shape].at,positions,normals,colors,uvs,bones,part,result)) return {};
|
||||
if(!part.vertices.empty()) result.parts.push_back(std::move(part));
|
||||
}
|
||||
return result.valid()?result:Model{};
|
||||
}
|
||||
|
||||
inline constexpr std::array<const char*,15> names{"koura_green","koura_red","banana","itemBoxNiseRtpa",
|
||||
"kinoko","bomb","togezo_koura","thunder","star","kinoko_p","big_kinoko","gesso",
|
||||
"pow_bloc","kumo","item_killer"};
|
||||
inline int model_index(uint8_t id) {
|
||||
constexpr int map[19]{0,1,2,3,4,4,5,6,7,8,9,10,11,12,13,14,0,1,2};
|
||||
return id<19?map[id]:-1;
|
||||
}
|
||||
struct Archive { std::array<Model,15> models;uint32_t loaded=0; };
|
||||
inline Archive parse_archive(const void* bytes,size_t size) {
|
||||
Archive archive;
|
||||
if(!bytes || !size || size>32u*1024u*1024u) return archive;
|
||||
const auto unpacked=yaz0({static_cast<const uint8_t*>(bytes),size});
|
||||
Reader r{unpacked.data(),unpacked.size()};
|
||||
if(!r.has(0,0x20) || r.u32(0)!=0x55aa382du) return archive;
|
||||
const size_t root=r.u32(4);
|
||||
if(!r.has(root,12)) return archive;
|
||||
const uint32_t count=r.u32(root+8);
|
||||
if(count>8192 || count<2 || !r.has(root,size_t(count)*12)) return archive;
|
||||
const size_t names_base=root+size_t(count)*12;
|
||||
for(uint32_t i=1;i<count;++i) {
|
||||
const size_t e=root+size_t(i)*12,tag=r.u32(e);
|
||||
if(tag>>24) continue;
|
||||
const std::string filename=r.str(names_base+(tag&0xffffff));
|
||||
for(size_t item=0;item<names.size();++item) {
|
||||
if(filename!=std::string(names[item])+".brres") continue;
|
||||
const size_t at=r.u32(e+4),length=r.u32(e+8);
|
||||
if(!r.has(at,length) || length>4u*1024u*1024u) break;
|
||||
const Reader file{r.bytes+at,length};
|
||||
const std::string model_name=item==3?"itemBoxNise":names[item];
|
||||
archive.models[item]=parse_model(file,model_name);
|
||||
if(archive.models[item].valid()) ++archive.loaded;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return archive;
|
||||
}
|
||||
|
||||
} // namespace aurora::gfx::cockpit_item::data
|
||||
@@ -3122,3 +3122,7 @@ void aurora_set_vr_hand_mesh(uint32_t hand, const AuroraVRHandVertex* vertices,
|
||||
meshes[hand] = std::move(mesh);
|
||||
++meshRevision;
|
||||
}
|
||||
|
||||
void aurora_set_cockpit_item_archive(const void* bytes, uint32_t size) {
|
||||
aurora::gfx::cockpit_item::set_archive(bytes, size);
|
||||
}
|
||||
@@ -321,6 +321,7 @@ struct StereoReplayFrame {
|
||||
std::array<StereoReplayEye, AURORA_STEREO_EYE_COUNT> eyes;
|
||||
// VR hands and synthetic wheel, drawn per eye after the world (gfx/cockpit.hpp).
|
||||
AuroraCockpit cockpit{};
|
||||
AuroraCockpitItem cockpitItem{};
|
||||
// The immersive window (AuroraStereoFrame::window): each eye is masked to the
|
||||
// 2D layer's screen after its last draw (gfx/window_mask.hpp).
|
||||
bool window = false;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -456,6 +456,11 @@ target_include_directories(mkw_vr_player_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR
|
||||
target_compile_features(mkw_vr_player_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_vr_player_tests COMMAND mkw_vr_player_tests)
|
||||
|
||||
add_executable(mkw_vr_item_tests "${CMAKE_CURRENT_LIST_DIR}/tests/vr_item_tests.cpp")
|
||||
target_include_directories(mkw_vr_item_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_vr_item_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_vr_item_tests COMMAND mkw_vr_item_tests)
|
||||
|
||||
add_executable(mkw_vr_policy_tests tests/vr_policy_tests.cpp src/vr/mkw_vr_policy.cpp)
|
||||
target_include_directories(mkw_vr_policy_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_vr_policy_tests PRIVATE cxx_std_17)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
// Reads the same mapped disc path that the guest sees, including active file
|
||||
// replacements. Called on the guest thread after DVD initialization.
|
||||
std::vector<uint8_t> DVDReadVrAsset(const char* dvdPath);
|
||||
@@ -78,6 +78,7 @@ struct RuntimeUserConfig {
|
||||
std::optional<bool> vrNativeSteeringWheel;
|
||||
std::optional<bool> vrObjectCulling;
|
||||
std::optional<bool> vrHandSteering;
|
||||
std::optional<std::string> vrCockpitItemHand;
|
||||
std::optional<bool> vrHandTracking;
|
||||
std::optional<float> vrWheelKartDegrees;
|
||||
std::optional<float> vrWheelBikeDegrees;
|
||||
@@ -237,6 +238,7 @@ inline constexpr float kVrCockpitUnitsPerMeterMax = 400.0f;
|
||||
inline constexpr bool kVrSteeringWheelDefault = true;
|
||||
inline constexpr bool kVrNativeSteeringWheelDefault = true;
|
||||
inline constexpr bool kVrHandSteeringDefault = true;
|
||||
inline constexpr const char* kVrCockpitItemHandDefault = "left";
|
||||
// The game hides karts and objects its own camera cannot see, which a head
|
||||
// turn in VR reveals. object_culling false draws them anyway (see
|
||||
// vr/mkw_vr_culling.h); it only takes effect while VR is enabled. The PC
|
||||
@@ -605,6 +607,8 @@ inline void EnsureConfigFile() {
|
||||
"# (seconds) a hand that loses tracking keeps hold, and a short\n"
|
||||
"# pulse on grab and release. All changeable live from the F10 menu.\n"
|
||||
"hand_steering = true\n"
|
||||
"# Show the settled inventory item in one cockpit hand: left, right, or off.\n"
|
||||
"cockpit_item_hand = \"left\"\n"
|
||||
"wheel_kart_degrees = 90.0\n"
|
||||
"wheel_bike_degrees = 45.0\n"
|
||||
"wheel_grab_distance = 0.35\n"
|
||||
@@ -880,6 +884,7 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
config.vrNativeSteeringWheel = FindConfigValue<bool>(document, "vr", "native_steering_wheel");
|
||||
config.vrObjectCulling = FindConfigValue<bool>(document, "vr", "object_culling");
|
||||
config.vrHandSteering = FindConfigValue<bool>(document, "vr", "hand_steering");
|
||||
config.vrCockpitItemHand = FindConfigValue<std::string>(document, "vr", "cockpit_item_hand");
|
||||
config.vrHandTracking = FindConfigValue<bool>(document, "vr", "hand_tracking");
|
||||
config.vrWheelKartDegrees = readRangedFloat("wheel_kart_degrees", kVrWheelDegreesMin, kVrWheelDegreesMax);
|
||||
config.vrWheelBikeDegrees = readRangedFloat("wheel_bike_degrees", kVrWheelDegreesMin, kVrWheelDegreesMax);
|
||||
@@ -1282,6 +1287,12 @@ inline bool SetVrHandSteering(bool value) {
|
||||
return WriteSetting("vr", "hand_steering", value ? "true" : "false");
|
||||
}
|
||||
|
||||
inline bool SetVrCockpitItemHand(const std::string& value) {
|
||||
if (value != "left" && value != "right" && value != "off") return false;
|
||||
Mutable().vrCockpitItemHand = value;
|
||||
return WriteSetting("vr", "cockpit_item_hand", "\"" + value + "\"");
|
||||
}
|
||||
|
||||
inline bool SetVrHandTracking(bool value) {
|
||||
Mutable().vrHandTracking = value;
|
||||
return WriteSetting("vr", "hand_tracking", value ? "true" : "false");
|
||||
@@ -1773,6 +1784,11 @@ inline bool VrHandSteering(bool fallback = kVrHandSteeringDefault) {
|
||||
return Get().vrHandSteering.value_or(fallback);
|
||||
}
|
||||
|
||||
inline std::string VrCockpitItemHand() {
|
||||
const std::string value = Get().vrCockpitItemHand.value_or(kVrCockpitItemHandDefault);
|
||||
return value == "left" || value == "right" || value == "off" ? value : kVrCockpitItemHandDefault;
|
||||
}
|
||||
|
||||
inline bool VrHandTracking(bool fallback = kVrHandTrackingDefault) {
|
||||
return Get().vrHandTracking.value_or(fallback);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "vr/steering_wheel.h"
|
||||
#include "vr/mkw_vr_item.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -572,4 +573,8 @@ void MkwVRFirstPersonRecenter() noexcept;
|
||||
// anchor has been missing long enough to give up holding the last one.
|
||||
FirstPersonAnchor MkwVRFirstPersonGetAnchor() noexcept;
|
||||
|
||||
// Guest-frame inventory snapshot, sampled at the race draw boundary. A
|
||||
// generation change invalidates any item retained by a prior race.
|
||||
HeldItem MkwVRFirstPersonGetHeldItem() noexcept;
|
||||
|
||||
} // namespace mkw::vr
|
||||
@@ -0,0 +1,47 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace mkw::vr {
|
||||
|
||||
struct HeldItem {
|
||||
uint8_t id = 0x14;
|
||||
uint8_t count = 0;
|
||||
bool valid = false;
|
||||
uint64_t race_generation = 0;
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
|
||||
// PAL RMCP01 Item::Manager and Item::Player. Item::PlayerInventory is at
|
||||
// Player+0x88. Read the inventory, never PlayerRoulette::nextItemId: roulette
|
||||
// teardown clears the latter, and an interrupted roulette may predict an item
|
||||
// the player never receives.
|
||||
template <typename GuestMemory>
|
||||
HeldItem ReadHeldItem(uint32_t local_racer, uint64_t race_generation) noexcept {
|
||||
HeldItem result{};
|
||||
result.race_generation = race_generation;
|
||||
if (local_racer >= 12) return result;
|
||||
uint32_t manager = 0, players = 0;
|
||||
if (!GuestMemory::TryRead32(0x809C3618u, manager) || !manager ||
|
||||
!GuestMemory::TryRead32(manager + 0x14u, players) || !players) return result;
|
||||
const uint32_t player = players + local_racer * 0x248u;
|
||||
if (player < players || !GuestMemory::Contains(player, 0x94u)) return result;
|
||||
try {
|
||||
if (GuestMemory::Read8(player + 0x18u) != local_racer ||
|
||||
GuestMemory::Read32(player + 0x58u) != 0) return result;
|
||||
const uint32_t id = GuestMemory::Read32(player + 0x8Cu);
|
||||
const uint32_t count = GuestMemory::Read32(player + 0x90u);
|
||||
if (id > 0x12u || count == 0 || count > 3) return result;
|
||||
result.id = static_cast<uint8_t>(id);
|
||||
result.count = static_cast<uint8_t>(count);
|
||||
result.valid = true;
|
||||
} catch (const typename GuestMemory::AccessViolation&) {
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
} // namespace mkw::vr
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "hle_stubs.h"
|
||||
#include "isa/big_endian.h"
|
||||
#include "hle/dvd_contract.h"
|
||||
#include "hle/dvd_vr_asset.h"
|
||||
#include "hle/runtime_parse_helpers.h"
|
||||
#include "memory.h"
|
||||
|
||||
@@ -743,6 +744,20 @@ extern "C" const char* DVDResolveHostPathForTest(const char* dvdPath)
|
||||
return resolved.c_str();
|
||||
}
|
||||
|
||||
std::vector<uint8_t> DVDReadVrAsset(const char* dvdPath) {
|
||||
if (dvdPath == nullptr || dvdPath[0] == '\0') return {};
|
||||
DVDInit_8015EA1C();
|
||||
const auto it = g_pathToEntry.find(NormalizePath(dvdPath));
|
||||
if (it == g_pathToEntry.end() || it->second < 0 ||
|
||||
it->second >= static_cast<int32_t>(g_fileEntries.size())) return {};
|
||||
const DVDFileEntry& entry = g_fileEntries[it->second];
|
||||
if (entry.isDirectory || entry.size == 0 || entry.size > 32u * 1024u * 1024u) return {};
|
||||
std::vector<uint8_t> bytes(entry.size);
|
||||
std::ifstream file(entry.hostPath, std::ios::binary);
|
||||
if (!file.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) return {};
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// High-Level DVD API
|
||||
// ============================================================================
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "fiber_manager.h"
|
||||
#include "platform/host_platform.h"
|
||||
#include "runtime_log.h"
|
||||
#include "runtime_config.h"
|
||||
#include "vr/mkw_vr_first_person.h"
|
||||
#include "vr/mkw_vr_policy.h"
|
||||
#include "vr/openxr_integration.h"
|
||||
@@ -592,6 +593,7 @@ struct GxPresentRecord {
|
||||
std::array<float, 12> anchor{};
|
||||
bool anchorValid = false;
|
||||
float anchorUnitsPerMeter = 0.0f;
|
||||
AuroraCockpitItem cockpitItem{};
|
||||
bool reportPaced = false;
|
||||
bool paced = false;
|
||||
uint32_t localPlayerCount = 1;
|
||||
@@ -609,6 +611,7 @@ void GxPresent_gx(GxPresentRecord record) {
|
||||
} else {
|
||||
aurora_set_stereo_scene_anchor(record.anchorValid ? record.anchor.data() : nullptr);
|
||||
}
|
||||
aurora_set_stereo_cockpit_item(&record.cockpitItem);
|
||||
aurora_set_stereo_local_player_count(record.localPlayerCount);
|
||||
aurora_end_frame_ex(record.contentTag, record.imguiFrame);
|
||||
g_auroraFrameActive.store(false, std::memory_order_release);
|
||||
@@ -693,6 +696,13 @@ void VI_HLE_PresentFrame(bool presentedXfb, bool paceToRetrace) {
|
||||
record.anchor = anchor.anchor;
|
||||
record.anchorValid = anchor.valid;
|
||||
record.anchorUnitsPerMeter = anchor.unitsPerMeter;
|
||||
if (anchor.valid) {
|
||||
const auto item = mkw::vr::MkwVRFirstPersonGetHeldItem();
|
||||
const auto hand = RuntimeConfigFile::VrCockpitItemHand();
|
||||
record.cockpitItem = {item.race_generation, item.id, item.count,
|
||||
static_cast<uint8_t>(hand == "right" ? 1 : hand == "off" ? 2 : 0),
|
||||
item.valid && hand != "off"};
|
||||
}
|
||||
// Latch the current policy safety state into this exact Aurora job. The
|
||||
// asynchronous worker may ask for an XR packet after the guest has already
|
||||
// begun the next frame, so immersive replay is accepted only when both
|
||||
|
||||
@@ -170,6 +170,9 @@ bool g_vrSteeringWheel = RuntimeConfigFile::VrSteeringWheel();
|
||||
bool g_vrNativeSteeringWheel = RuntimeConfigFile::VrNativeSteeringWheel();
|
||||
bool g_vrObjectCulling = RuntimeConfigFile::VrObjectCulling();
|
||||
bool g_vrHandSteering = RuntimeConfigFile::VrHandSteering();
|
||||
constexpr std::array<const char*, 3> kVrCockpitItemHands{"Left", "Right", "Off"};
|
||||
int g_vrCockpitItemHand = RuntimeConfigFile::VrCockpitItemHand() == "right" ? 1 :
|
||||
RuntimeConfigFile::VrCockpitItemHand() == "off" ? 2 : 0;
|
||||
mkw::vr::WheelTuning g_vrWheelTuning = RuntimeConfigFile::VrWheelTuning();
|
||||
float g_vrFirstPersonHeadUp = RuntimeConfigFile::VrFirstPersonHeadUpMeters();
|
||||
float g_vrFirstPersonHeadForward = RuntimeConfigFile::VrFirstPersonHeadForwardMeters();
|
||||
@@ -1221,11 +1224,19 @@ void DrawVrSteeringWheelSettings() {
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Squeeze a grip near the wheel or handlebar to take hold of it, and turn "
|
||||
"it to steer, with one hand or both. Releasing both grips gives steering "
|
||||
"back to the stick, which still aims items. The runtime's hand mesh is "
|
||||
"used when hand steering was on at launch.");
|
||||
"back to the stick, which still aims items.");
|
||||
}
|
||||
if (ImGui::Combo("Item in cockpit hand", &g_vrCockpitItemHand,
|
||||
kVrCockpitItemHands.data(), static_cast<int>(kVrCockpitItemHands.size()))) {
|
||||
RuntimeConfigFile::SetVrCockpitItemHand(
|
||||
g_vrCockpitItemHand == 1 ? "right" : g_vrCockpitItemHand == 2 ? "off" : "left");
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Show Player 1's settled inventory item above this palm. "
|
||||
"Triple items show their remaining count. Using or losing the item hides it.");
|
||||
}
|
||||
#if defined(__ANDROID__)
|
||||
ImGui::BeginDisabled(!g_vrHandSteering);
|
||||
ImGui::BeginDisabled(!g_vrHandSteering && g_vrCockpitItemHand == 2);
|
||||
if (ImGui::Checkbox("Tracked hands", &g_vrHandTracking)) {
|
||||
RuntimeConfigFile::SetVrHandTracking(g_vrHandTracking);
|
||||
}
|
||||
@@ -1747,11 +1758,13 @@ void DrawVrCameraSettings() {
|
||||
g_vrSteeringWheel = RuntimeConfigFile::kVrSteeringWheelDefault;
|
||||
g_vrNativeSteeringWheel = RuntimeConfigFile::kVrNativeSteeringWheelDefault;
|
||||
g_vrHandSteering = RuntimeConfigFile::kVrHandSteeringDefault;
|
||||
g_vrCockpitItemHand = 0;
|
||||
RuntimeConfigFile::SetVrFirstPersonSeat(RuntimeConfigFile::kVrFirstPersonSeatDefault);
|
||||
RuntimeConfigFile::SetVrCockpitUnitsPerMeter(g_vrCockpitUnitsPerMeter);
|
||||
RuntimeConfigFile::SetVrSteeringWheel(g_vrSteeringWheel);
|
||||
RuntimeConfigFile::SetVrNativeSteeringWheel(g_vrNativeSteeringWheel);
|
||||
RuntimeConfigFile::SetVrHandSteering(g_vrHandSteering);
|
||||
RuntimeConfigFile::SetVrCockpitItemHand(RuntimeConfigFile::kVrCockpitItemHandDefault);
|
||||
#if defined(__ANDROID__)
|
||||
g_vrHandTracking = RuntimeConfigFile::kVrHandTrackingDefault;
|
||||
RuntimeConfigFile::SetVrHandTracking(g_vrHandTracking);
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
#include "vr/mkw_vr_first_person.h"
|
||||
|
||||
#include "aurora/aurora.h"
|
||||
|
||||
#include "gx_native_wheel.h"
|
||||
#include "hle/dvd_vr_asset.h"
|
||||
#include "gx_model_visibility.h"
|
||||
#include "memory.h"
|
||||
#include "runtime_config.h"
|
||||
@@ -317,6 +320,8 @@ struct FirstPersonState {
|
||||
|
||||
uint32_t camera_address = 0;
|
||||
detail::LocalPlayerKartRead player_kart{};
|
||||
HeldItem held_item{};
|
||||
uint64_t race_generation = 0;
|
||||
// Armed by the draw boundary, consumed by the frame seal.
|
||||
bool armed = false;
|
||||
uint64_t armed_frame = 0;
|
||||
@@ -1386,6 +1391,9 @@ void MkwVRFirstPersonReset() noexcept {
|
||||
g_state.armed_view_valid = false;
|
||||
g_state.camera_address = 0;
|
||||
g_state.player_kart = {};
|
||||
g_state.held_item = {};
|
||||
++g_state.race_generation;
|
||||
g_state.held_item.race_generation = g_state.race_generation;
|
||||
g_state.anchor = {};
|
||||
g_state.hold_frames = 0;
|
||||
g_state.ever_valid_this_race = false;
|
||||
@@ -1412,6 +1420,8 @@ void MkwVRFirstPersonRecenter() noexcept {
|
||||
|
||||
void MkwVRFirstPersonUpdate(uint64_t guest_frame_index, uint32_t race_camera_address) noexcept {
|
||||
std::lock_guard lock(g_mutex);
|
||||
g_state.held_item = {};
|
||||
g_state.held_item.race_generation = g_state.race_generation;
|
||||
DropHiddenModelsLocked();
|
||||
if (g_recenter_requested.exchange(false, std::memory_order_acq_rel)) {
|
||||
g_state.seated_eye.Recalibrate();
|
||||
@@ -1435,6 +1445,15 @@ void MkwVRFirstPersonUpdate(uint64_t guest_frame_index, uint32_t race_camera_add
|
||||
g_state.hold_frames = 0;
|
||||
}
|
||||
g_state.player_kart = player;
|
||||
if (player.failed_step == nullptr && g_state.seat == FirstPersonSeat::Cockpit) {
|
||||
static bool item_archive_attempted = false;
|
||||
if (!item_archive_attempted && RuntimeConfigFile::VrCockpitItemHand() != "off") {
|
||||
item_archive_attempted = true;
|
||||
const auto archive = DVDReadVrAsset("/Race/Common.szs");
|
||||
if (!archive.empty()) aurora_set_cockpit_item_archive(archive.data(), static_cast<uint32_t>(archive.size()));
|
||||
}
|
||||
g_state.held_item = detail::ReadHeldItem<Memory>(player.player_index, g_state.race_generation);
|
||||
}
|
||||
g_state.armed = true;
|
||||
g_state.armed_frame = guest_frame_index;
|
||||
g_state.armed_view_valid = ReadSceneViewMatrix(g_state.armed_view);
|
||||
@@ -1538,4 +1557,9 @@ FirstPersonAnchor MkwVRFirstPersonGetAnchor() noexcept {
|
||||
return g_state.anchor;
|
||||
}
|
||||
|
||||
HeldItem MkwVRFirstPersonGetHeldItem() noexcept {
|
||||
std::lock_guard lock(g_mutex);
|
||||
return g_state.held_item;
|
||||
}
|
||||
|
||||
} // namespace mkw::vr
|
||||
@@ -656,12 +656,13 @@ void OpenXRInput::UpdateSimultaneousHandsAndControllers(bool wanted) {
|
||||
"side to the cameras");
|
||||
}
|
||||
|
||||
// Trackers only exist while tracked hands and hand steering are both on: the
|
||||
// hands are only drawn while they can steer. They live as long as the session
|
||||
// Tracked hands can carry a cockpit item even while stick steering is used.
|
||||
// They live as long as the session
|
||||
// otherwise (Idle and the cockpit's reset keep them); a runtime that refuses
|
||||
// them is not asked again until the option is turned off and on.
|
||||
void OpenXRInput::UpdateHandTrackers() {
|
||||
const bool wanted = RuntimeConfigFile::VrHandTracking() && RuntimeConfigFile::VrHandSteering();
|
||||
const bool wanted = RuntimeConfigFile::VrHandTracking() &&
|
||||
(RuntimeConfigFile::VrHandSteering() || RuntimeConfigFile::VrCockpitItemHand() != "off");
|
||||
UpdateSimultaneousHandsAndControllers(wanted);
|
||||
if (!wanted) {
|
||||
DestroyHandTrackers();
|
||||
@@ -1417,8 +1418,10 @@ void OpenXRInput::UpdateDriving(XrTime display_time, const driving::SeatFrame& s
|
||||
tracked = false;
|
||||
}
|
||||
DrivingHand& out = snapshot.hands[hand];
|
||||
// Hands are shown only while they can steer.
|
||||
out.tracked = hand_steering && (tracked || joints);
|
||||
// The selected item hand is visible with stick steering too.
|
||||
const auto item_hand = RuntimeConfigFile::VrCockpitItemHand();
|
||||
const bool displays_item = item_hand == (hand == 0 ? "left" : "right");
|
||||
out.tracked = (hand_steering || displays_item) && (tracked || joints);
|
||||
out.held = false;
|
||||
out.squeeze = squeeze;
|
||||
out.seat_from_grip = seat_from_grip;
|
||||
|
||||
@@ -1386,7 +1386,8 @@ private:
|
||||
float lean_back_radians, AuroraCockpit& cockpit) noexcept {
|
||||
cockpit.unitsPerMeter = units_per_meter;
|
||||
const DrivingSnapshot driving = input_ != nullptr ? input_->Driving() : DrivingSnapshot{};
|
||||
if (driving.hand_steering && !hand_meshes_loaded_ && runtime_ != nullptr) {
|
||||
if ((driving.hand_steering || RuntimeConfigFile::VrCockpitItemHand() != "off") &&
|
||||
!hand_meshes_loaded_ && runtime_ != nullptr) {
|
||||
hand_meshes_loaded_ = true;
|
||||
// Tracked hands' trackers, when they exist, serve the mesh too.
|
||||
const XrHandTrackerEXT trackers[2]{input_ != nullptr ? input_->HandTracker(0) : XR_NULL_HANDLE,
|
||||
@@ -1397,7 +1398,8 @@ private:
|
||||
<< std::endl;
|
||||
}
|
||||
cockpit.active = driving.cockpit_active && position_valid && base_position_valid_ &&
|
||||
(driving.synthetic_control || driving.hand_steering);
|
||||
(driving.synthetic_control || driving.hand_steering ||
|
||||
RuntimeConfigFile::VrCockpitItemHand() != "off");
|
||||
if (!cockpit.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,13 @@ static RuntimeUserConfig Parse(const std::string& text) {
|
||||
}
|
||||
|
||||
int main() {
|
||||
for (std::string_view hand : {"left", "right", "off"}) {
|
||||
Require(Parse("[vr]\ncockpit_item_hand = \"" + std::string(hand) + "\"\n")
|
||||
.vrCockpitItemHand == std::string(hand));
|
||||
}
|
||||
Require(std::string_view(RuntimeConfigFile::kVrCockpitItemHandDefault) == "left");
|
||||
Require(!Parse("[vr]\n").vrCockpitItemHand.has_value());
|
||||
Require(!Parse("[vr]\ncockpit_item_hand = 1\n").vrCockpitItemHand.has_value());
|
||||
// [vr] foveation: the Quest's foveated rendering level, index-matched to
|
||||
// aurora_set_stereo_foveation.
|
||||
for (std::string_view level : RuntimeConfigFile::kVrFoveationLevels) {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "vr/mkw_vr_item.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace {
|
||||
struct GuestMemory {
|
||||
using AccessViolation = std::out_of_range;
|
||||
inline static std::unordered_map<uint32_t, uint8_t> bytes;
|
||||
inline static uint32_t fault = 0;
|
||||
static bool Contains(uint32_t address, uint32_t length) {
|
||||
for (uint32_t i = 0; i < length; ++i) if (!bytes.count(address + i)) return false;
|
||||
return true;
|
||||
}
|
||||
static uint8_t Read8(uint32_t address) {
|
||||
if (address == fault) throw AccessViolation("fault");
|
||||
return bytes.at(address);
|
||||
}
|
||||
static uint32_t Read32(uint32_t address) {
|
||||
uint32_t value = 0;
|
||||
for (uint32_t i = 0; i < 4; ++i) value = (value << 8) | Read8(address + i);
|
||||
return value;
|
||||
}
|
||||
static bool TryRead32(uint32_t address, uint32_t& out) {
|
||||
try { out = Read32(address); return true; } catch (const AccessViolation&) { return false; }
|
||||
}
|
||||
static void Write32(uint32_t address, uint32_t value) {
|
||||
for (uint32_t i = 0; i < 4; ++i) bytes[address + i] = uint8_t(value >> (24 - i * 8));
|
||||
}
|
||||
};
|
||||
|
||||
constexpr uint32_t manager = 0x81000000u, players = 0x82000000u;
|
||||
constexpr uint32_t player = players + 7 * 0x248u;
|
||||
int failures = 0;
|
||||
void Check(bool value, const char* name) {
|
||||
if (!value) { ++failures; std::cerr << "FAILED: " << name << '\n'; }
|
||||
}
|
||||
void Init() {
|
||||
GuestMemory::bytes.clear(); GuestMemory::fault = 0;
|
||||
GuestMemory::Write32(0x809C3618u, manager);
|
||||
GuestMemory::Write32(manager + 0x14u, players);
|
||||
for (uint32_t i = 0; i < 0x94u; ++i) GuestMemory::bytes[player + i] = 0;
|
||||
GuestMemory::bytes[player + 0x18u] = 7;
|
||||
GuestMemory::Write32(player + 0x90u, 1);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
using mkw::vr::detail::ReadHeldItem;
|
||||
for (uint32_t id = 0; id <= 0x12u; ++id) {
|
||||
Init(); GuestMemory::Write32(player + 0x8cu, id);
|
||||
const auto item = ReadHeldItem<GuestMemory>(7, 19);
|
||||
Check(item.valid && item.id == id && item.count == 1 && item.race_generation == 19,
|
||||
"all 19 IDs, nonzero racer, generation");
|
||||
}
|
||||
Init(); GuestMemory::Write32(player + 0x8cu, 0x10); GuestMemory::Write32(player + 0x90u, 3);
|
||||
Check(ReadHeldItem<GuestMemory>(7, 1).count == 3, "triple full");
|
||||
GuestMemory::Write32(player + 0x90u, 2);
|
||||
Check(ReadHeldItem<GuestMemory>(7, 1).count == 2, "triple remaining");
|
||||
GuestMemory::Write32(player + 0x90u, 0);
|
||||
Check(!ReadHeldItem<GuestMemory>(7, 1).valid, "inventory removed on use or damage");
|
||||
GuestMemory::Write32(player + 0x90u, 1);
|
||||
GuestMemory::Write32(player + 0x58u, 1);
|
||||
Check(!ReadHeldItem<GuestMemory>(7, 1).valid, "roulette spinning");
|
||||
GuestMemory::Write32(player + 0x58u, 0);
|
||||
for (uint32_t id : {0x13u, 0x14u, 0xffu}) {
|
||||
GuestMemory::Write32(player + 0x8cu, id);
|
||||
Check(!ReadHeldItem<GuestMemory>(7, 1).valid, "empty or unsupported");
|
||||
}
|
||||
Init(); GuestMemory::Write32(player + 0x8cu, 0x0au);
|
||||
Check(ReadHeldItem<GuestMemory>(7, 1).valid, "golden mushroom held");
|
||||
GuestMemory::Write32(player + 0x90u, 0);
|
||||
Check(!ReadHeldItem<GuestMemory>(7, 1).valid, "golden mushroom expiry");
|
||||
Init(); GuestMemory::fault = player + 0x8cu;
|
||||
const auto faulted = ReadHeldItem<GuestMemory>(7, 2);
|
||||
Check(!faulted.valid && faulted.race_generation == 2, "read fault preserves generation");
|
||||
Init(); GuestMemory::Write32(0x809C3618u, 0);
|
||||
Check(!ReadHeldItem<GuestMemory>(7, 1).valid, "invalid manager");
|
||||
Init(); GuestMemory::bytes[player + 0x18u] = 0;
|
||||
Check(!ReadHeldItem<GuestMemory>(7, 1).valid, "racer identity mismatch");
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Reference in new issue
Block a user