mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 03:00:14 +02:00
- 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.
529 lines
28 KiB
C++
529 lines
28 KiB
C++
// 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
|