@quake2ts/engine 0.0.837 → 0.0.839

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1589,6 +1589,28 @@ var BSP_MAGIC = "IBSP";
1589
1589
  var BSP_VERSION = 38;
1590
1590
  var HEADER_LUMPS = 19;
1591
1591
  var HEADER_SIZE4 = 4 + 4 + HEADER_LUMPS * 8;
1592
+ var BspLump = /* @__PURE__ */ ((BspLump2) => {
1593
+ BspLump2[BspLump2["Entities"] = 0] = "Entities";
1594
+ BspLump2[BspLump2["Planes"] = 1] = "Planes";
1595
+ BspLump2[BspLump2["Vertices"] = 2] = "Vertices";
1596
+ BspLump2[BspLump2["Visibility"] = 3] = "Visibility";
1597
+ BspLump2[BspLump2["Nodes"] = 4] = "Nodes";
1598
+ BspLump2[BspLump2["TexInfo"] = 5] = "TexInfo";
1599
+ BspLump2[BspLump2["Faces"] = 6] = "Faces";
1600
+ BspLump2[BspLump2["Lighting"] = 7] = "Lighting";
1601
+ BspLump2[BspLump2["Leafs"] = 8] = "Leafs";
1602
+ BspLump2[BspLump2["LeafFaces"] = 9] = "LeafFaces";
1603
+ BspLump2[BspLump2["LeafBrushes"] = 10] = "LeafBrushes";
1604
+ BspLump2[BspLump2["Edges"] = 11] = "Edges";
1605
+ BspLump2[BspLump2["SurfEdges"] = 12] = "SurfEdges";
1606
+ BspLump2[BspLump2["Models"] = 13] = "Models";
1607
+ BspLump2[BspLump2["Brushes"] = 14] = "Brushes";
1608
+ BspLump2[BspLump2["BrushSides"] = 15] = "BrushSides";
1609
+ BspLump2[BspLump2["Pop"] = 16] = "Pop";
1610
+ BspLump2[BspLump2["Areas"] = 17] = "Areas";
1611
+ BspLump2[BspLump2["AreaPortals"] = 18] = "AreaPortals";
1612
+ return BspLump2;
1613
+ })(BspLump || {});
1592
1614
  var BspParseError = class extends Error {
1593
1615
  };
1594
1616
  var BspLoader = class {
@@ -9614,6 +9636,2111 @@ var createRenderer = (gl) => {
9614
9636
  }
9615
9637
  };
9616
9638
  };
9639
+
9640
+ // src/render/webgpu/context.ts
9641
+ async function createWebGPUContext(canvas, options) {
9642
+ if (!navigator.gpu) {
9643
+ throw new Error("WebGPU is not supported in this environment");
9644
+ }
9645
+ const adapter = await navigator.gpu.requestAdapter({
9646
+ powerPreference: options?.powerPreference || "high-performance"
9647
+ });
9648
+ if (!adapter) {
9649
+ throw new Error("No appropriate GPUAdapter found");
9650
+ }
9651
+ if (options?.requiredFeatures) {
9652
+ for (const feature of options.requiredFeatures) {
9653
+ if (!adapter.features.has(feature)) {
9654
+ throw new Error(`Required feature not available: ${feature}`);
9655
+ }
9656
+ }
9657
+ }
9658
+ const deviceDescriptor = {
9659
+ requiredFeatures: options?.requiredFeatures,
9660
+ requiredLimits: options?.requiredLimits
9661
+ };
9662
+ const device = await adapter.requestDevice(deviceDescriptor);
9663
+ let context;
9664
+ let format = "rgba8unorm";
9665
+ const depthFormat = "depth24plus";
9666
+ let isHeadless = true;
9667
+ let width = options?.width || 800;
9668
+ let height = options?.height || 600;
9669
+ if (canvas) {
9670
+ context = canvas.getContext("webgpu");
9671
+ if (!context) {
9672
+ throw new Error("Failed to get WebGPU context from canvas");
9673
+ }
9674
+ isHeadless = false;
9675
+ format = navigator.gpu.getPreferredCanvasFormat();
9676
+ width = canvas.width;
9677
+ height = canvas.height;
9678
+ context.configure({
9679
+ device,
9680
+ format,
9681
+ alphaMode: "opaque"
9682
+ // Standard for game rendering
9683
+ });
9684
+ }
9685
+ const features = /* @__PURE__ */ new Set();
9686
+ for (const feature of adapter.features) {
9687
+ features.add(feature);
9688
+ }
9689
+ return {
9690
+ adapter,
9691
+ device,
9692
+ context,
9693
+ format,
9694
+ depthFormat,
9695
+ features,
9696
+ limits: device.limits,
9697
+ isHeadless,
9698
+ width,
9699
+ height
9700
+ };
9701
+ }
9702
+ function queryCapabilities(state) {
9703
+ const { features, limits } = state;
9704
+ return {
9705
+ hasTimestampQuery: features.has("timestamp-query"),
9706
+ hasDepthClipControl: features.has("depth-clip-control"),
9707
+ hasTextureCompressionBC: features.has("texture-compression-bc"),
9708
+ hasTextureCompressionETC2: features.has("texture-compression-etc2"),
9709
+ hasTextureCompressionASTC: features.has("texture-compression-astc"),
9710
+ maxTextureDimension2D: limits.maxTextureDimension2D,
9711
+ maxBindGroups: limits.maxBindGroups,
9712
+ maxUniformBufferBindingSize: limits.maxUniformBufferBindingSize,
9713
+ maxStorageBufferBindingSize: limits.maxStorageBufferBindingSize
9714
+ };
9715
+ }
9716
+ function evaluateLightStyle2(pattern, time) {
9717
+ if (!pattern) return 1;
9718
+ const frame = Math.floor(time * 10) % pattern.length;
9719
+ const charCode = pattern.charCodeAt(frame);
9720
+ return (charCode - 97) / 12;
9721
+ }
9722
+ function sortVisibleFacesFrontToBack2(faces) {
9723
+ return [...faces].sort((a, b) => b.sortKey - a.sortKey);
9724
+ }
9725
+ function sortVisibleFacesBackToFront2(faces) {
9726
+ return [...faces].sort((a, b) => a.sortKey - b.sortKey);
9727
+ }
9728
+ var FrameRenderer = class {
9729
+ constructor(context, pipelines) {
9730
+ this.context = context;
9731
+ this.pipelines = pipelines;
9732
+ this.depthTexture = null;
9733
+ this.copyTexture = null;
9734
+ // Separate texture for headless output if no context exists
9735
+ this.headlessTarget = null;
9736
+ this.lastWidth = 0;
9737
+ this.lastHeight = 0;
9738
+ this.lastFrameTime = 0;
9739
+ // Current frame context (available during frame rendering)
9740
+ this.currentFrameContext = null;
9741
+ }
9742
+ ensureDepthTexture(width, height) {
9743
+ if (this.depthTexture && this.lastWidth === width && this.lastHeight === height) {
9744
+ return this.depthTexture.createView();
9745
+ }
9746
+ if (this.depthTexture) {
9747
+ this.depthTexture.destroy();
9748
+ }
9749
+ this.depthTexture = this.context.device.createTexture({
9750
+ size: [width, height],
9751
+ format: "depth24plus",
9752
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
9753
+ label: "depth-buffer"
9754
+ });
9755
+ if (this.copyTexture) {
9756
+ this.copyTexture.destroy();
9757
+ this.copyTexture = null;
9758
+ }
9759
+ this.lastWidth = width;
9760
+ this.lastHeight = height;
9761
+ return this.depthTexture.createView();
9762
+ }
9763
+ ensureCopyTexture(width, height) {
9764
+ if (this.copyTexture && this.lastWidth === width && this.lastHeight === height) {
9765
+ return this.copyTexture;
9766
+ }
9767
+ if (this.copyTexture) {
9768
+ this.copyTexture.destroy();
9769
+ }
9770
+ this.copyTexture = this.context.device.createTexture({
9771
+ size: [width, height],
9772
+ format: this.context.format,
9773
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
9774
+ label: "frame-copy-texture"
9775
+ });
9776
+ return this.copyTexture;
9777
+ }
9778
+ ensureHeadlessTarget(width, height) {
9779
+ if (this.headlessTarget && this.headlessTarget.width === width && this.headlessTarget.height === height) {
9780
+ return this.headlessTarget.createView();
9781
+ }
9782
+ if (this.headlessTarget) {
9783
+ this.headlessTarget.destroy();
9784
+ }
9785
+ this.headlessTarget = this.context.device.createTexture({
9786
+ size: [width, height],
9787
+ format: this.context.format,
9788
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.TEXTURE_BINDING,
9789
+ label: "headless-render-target"
9790
+ });
9791
+ return this.headlessTarget.createView();
9792
+ }
9793
+ beginFrame() {
9794
+ const { device, context, width, height } = this.context;
9795
+ const commandEncoder = device.createCommandEncoder({ label: "frame-command-encoder" });
9796
+ let renderTarget;
9797
+ if (context) {
9798
+ renderTarget = context.getCurrentTexture().createView();
9799
+ } else {
9800
+ renderTarget = this.ensureHeadlessTarget(width, height);
9801
+ }
9802
+ const depthTexture = this.ensureDepthTexture(width, height);
9803
+ return {
9804
+ device,
9805
+ commandEncoder,
9806
+ renderTarget,
9807
+ depthTexture,
9808
+ width,
9809
+ height
9810
+ };
9811
+ }
9812
+ /**
9813
+ * Begin 2D rendering pass. Called by WebGPURenderer.begin2D()
9814
+ */
9815
+ begin2DPass() {
9816
+ if (!this.currentFrameContext) {
9817
+ throw new Error("begin2DPass called outside of renderFrame");
9818
+ }
9819
+ const { commandEncoder, renderTarget, width, height } = this.currentFrameContext;
9820
+ this.pipelines.sprite.setProjection(width, height);
9821
+ this.pipelines.sprite.begin(commandEncoder, renderTarget);
9822
+ }
9823
+ /**
9824
+ * End 2D rendering pass. Called by WebGPURenderer.end2D()
9825
+ */
9826
+ end2DPass() {
9827
+ this.pipelines.sprite.end();
9828
+ }
9829
+ renderFrame(options) {
9830
+ const now = performance.now();
9831
+ const fps = this.lastFrameTime > 0 ? 1e3 / (now - this.lastFrameTime) : 0;
9832
+ this.lastFrameTime = now;
9833
+ const stats = {
9834
+ batches: 0,
9835
+ facesDrawn: 0,
9836
+ drawCalls: 0,
9837
+ skyDrawn: false,
9838
+ viewModelDrawn: false,
9839
+ fps: Math.round(fps),
9840
+ vertexCount: 0
9841
+ };
9842
+ const frameCtx = this.beginFrame();
9843
+ this.currentFrameContext = frameCtx;
9844
+ const { commandEncoder, renderTarget, depthTexture } = frameCtx;
9845
+ const {
9846
+ clearColor = [0, 0, 0, 1],
9847
+ world,
9848
+ camera,
9849
+ timeSeconds = 0,
9850
+ dlights,
9851
+ renderMode,
9852
+ disableLightmaps,
9853
+ lightmapOnly,
9854
+ brightness,
9855
+ gamma,
9856
+ fullbright,
9857
+ ambient,
9858
+ lightStyleOverrides,
9859
+ portalState
9860
+ } = options;
9861
+ const viewProjection = new Float32Array(options.camera.viewProjectionMatrix);
9862
+ const opaquePassDescriptor = {
9863
+ colorAttachments: [{
9864
+ view: renderTarget,
9865
+ clearValue: clearColor,
9866
+ loadOp: "clear",
9867
+ storeOp: "store"
9868
+ }],
9869
+ depthStencilAttachment: {
9870
+ view: depthTexture,
9871
+ depthClearValue: 1,
9872
+ depthLoadOp: "clear",
9873
+ depthStoreOp: "store"
9874
+ },
9875
+ label: "opaque-render-pass"
9876
+ };
9877
+ const opaquePass = commandEncoder.beginRenderPass(opaquePassDescriptor);
9878
+ if (options.sky && options.sky.cubemap) {
9879
+ {
9880
+ const cameraState = options.cameraState ?? options.camera.toState();
9881
+ const scroll = computeSkyScroll(options.timeSeconds ?? 0, options.sky.scrollSpeeds ?? [0.01, 0.02]);
9882
+ this.pipelines.skybox.draw(opaquePass, {
9883
+ cameraState,
9884
+ // NEW: let pipeline build matrices
9885
+ scroll,
9886
+ cubemap: options.sky.cubemap
9887
+ });
9888
+ }
9889
+ stats.skyDrawn = true;
9890
+ }
9891
+ const opaqueFaces = [];
9892
+ const transparentFaces = [];
9893
+ if (world) {
9894
+ world.materials?.update(timeSeconds);
9895
+ const frustum = extractFrustumPlanes(Array.from(viewProjection));
9896
+ const cameraPosition = {
9897
+ x: camera.position[0] ?? 0,
9898
+ y: camera.position[1] ?? 0,
9899
+ z: camera.position[2] ?? 0
9900
+ };
9901
+ const visibleFaces = gatherVisibleFaces(world.map, cameraPosition, frustum, portalState);
9902
+ for (const face of visibleFaces) {
9903
+ const geometry = world.surfaces[face.faceIndex];
9904
+ if (!geometry) continue;
9905
+ const isTransparent = (geometry.surfaceFlags & (shared.SURF_TRANS33 | shared.SURF_TRANS66 | shared.SURF_WARP)) !== 0;
9906
+ if (isTransparent) {
9907
+ transparentFaces.push(face);
9908
+ } else {
9909
+ opaqueFaces.push(face);
9910
+ }
9911
+ }
9912
+ const sortedOpaque = sortVisibleFacesFrontToBack2(opaqueFaces);
9913
+ let effectiveLightStyles = world.lightStyles || [];
9914
+ if (lightStyleOverrides && lightStyleOverrides.size > 0) {
9915
+ const styles = [...world.lightStyles || []];
9916
+ for (const [index, pattern] of lightStyleOverrides) {
9917
+ while (styles.length <= index) styles.push(1);
9918
+ styles[index] = evaluateLightStyle2(pattern, timeSeconds);
9919
+ }
9920
+ effectiveLightStyles = styles;
9921
+ }
9922
+ const drawSurfaceBatch = (faces, pass, lightStyles) => {
9923
+ for (const { faceIndex } of faces) {
9924
+ const geometry = world.surfaces[faceIndex];
9925
+ if (!geometry) continue;
9926
+ if ((geometry.surfaceFlags & shared.SURF_SKY) !== 0) continue;
9927
+ const faceStyles = world.map.faces[faceIndex]?.styles;
9928
+ this.pipelines.bsp.bind(pass, {
9929
+ modelViewProjection: viewProjection,
9930
+ styleIndices: faceStyles,
9931
+ styleValues: lightStyles,
9932
+ surfaceFlags: geometry.surfaceFlags,
9933
+ timeSeconds,
9934
+ // diffuseTexture: diffuse?.gpuTexture.createView(), // Need view
9935
+ // diffuseSampler: ...
9936
+ // lightmapTexture: ...
9937
+ // lightmapSampler: ...
9938
+ dlights,
9939
+ renderMode,
9940
+ lightmapOnly,
9941
+ brightness,
9942
+ gamma,
9943
+ fullbright,
9944
+ ambient,
9945
+ cameraPosition: options.camera.position
9946
+ });
9947
+ this.pipelines.bsp.draw(pass, geometry, renderMode);
9948
+ stats.facesDrawn++;
9949
+ stats.drawCalls++;
9950
+ stats.vertexCount += geometry.vertexCount;
9951
+ }
9952
+ };
9953
+ drawSurfaceBatch(sortedOpaque, opaquePass, effectiveLightStyles);
9954
+ opaquePass.end();
9955
+ const transparentPassDescriptor = {
9956
+ colorAttachments: [{
9957
+ view: renderTarget,
9958
+ loadOp: "load",
9959
+ storeOp: "store"
9960
+ }],
9961
+ depthStencilAttachment: {
9962
+ view: depthTexture,
9963
+ depthLoadOp: "load",
9964
+ depthStoreOp: "store"
9965
+ },
9966
+ label: "transparent-render-pass"
9967
+ };
9968
+ const transparentPass = commandEncoder.beginRenderPass(transparentPassDescriptor);
9969
+ if (transparentFaces.length > 0) {
9970
+ const sortedTransparent = sortVisibleFacesBackToFront2(transparentFaces);
9971
+ drawSurfaceBatch(sortedTransparent, transparentPass, effectiveLightStyles);
9972
+ }
9973
+ transparentPass.end();
9974
+ } else {
9975
+ opaquePass.end();
9976
+ }
9977
+ if (options.underwaterWarp || options.bloom) ;
9978
+ if (options.onDraw2D) {
9979
+ options.onDraw2D();
9980
+ }
9981
+ if (this.pipelines.sprite.isActive) {
9982
+ console.warn("2D render pass was not properly closed - auto-closing to prevent resource leak");
9983
+ this.end2DPass();
9984
+ }
9985
+ this.context.device.queue.submit([commandEncoder.finish()]);
9986
+ this.currentFrameContext = null;
9987
+ return stats;
9988
+ }
9989
+ };
9990
+
9991
+ // src/render/webgpu/shaders/mipmapShader.ts
9992
+ var MIPMAP_SHADER = `
9993
+ struct VSOutput {
9994
+ @builtin(position) position: vec4f,
9995
+ @location(0) texcoord: vec2f,
9996
+ };
9997
+
9998
+ @vertex
9999
+ fn vs_main(@builtin(vertex_index) vertexIndex : u32) -> VSOutput {
10000
+ var pos = array<vec2f, 6>(
10001
+ vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(-1.0, 1.0),
10002
+ vec2f(-1.0, 1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0)
10003
+ );
10004
+
10005
+ var output: VSOutput;
10006
+ output.position = vec4f(pos[vertexIndex], 0.0, 1.0);
10007
+ // Remap from [-1, 1] to [0, 1] for texcoord, Y is flipped in WebGPU clip space vs texture UV
10008
+ // Clip space: (-1,-1) is bottom-left. UV: (0,0) is top-left usually?
10009
+ // Wait, WebGPU clip space Y is up. UV (0,0) is top-left.
10010
+ // Quad vertices are full screen.
10011
+ output.texcoord = pos[vertexIndex] * vec2f(0.5, -0.5) + vec2f(0.5, 0.5);
10012
+ return output;
10013
+ }
10014
+
10015
+ @group(0) @binding(0) var imgSampler: sampler;
10016
+ @group(0) @binding(1) var img: texture_2d<f32>;
10017
+
10018
+ @fragment
10019
+ fn fs_main(in: VSOutput) -> @location(0) vec4f {
10020
+ return textureSample(img, imgSampler, in.texcoord);
10021
+ }
10022
+ `;
10023
+ var GPUBufferResource = class {
10024
+ constructor(device, descriptor) {
10025
+ this.device = device;
10026
+ this.size = Math.ceil(descriptor.size / 4) * 4;
10027
+ this.usage = descriptor.usage;
10028
+ this.buffer = device.createBuffer({
10029
+ size: this.size,
10030
+ usage: this.usage,
10031
+ label: descriptor.label,
10032
+ mappedAtCreation: descriptor.mappedAtCreation
10033
+ });
10034
+ }
10035
+ write(data, offset = 0) {
10036
+ const dataSize = data.byteLength;
10037
+ if (offset + dataSize > this.size) {
10038
+ throw new Error(`Buffer write out of bounds: offset ${offset} + data ${dataSize} > buffer ${this.size}`);
10039
+ }
10040
+ this.device.queue.writeBuffer(
10041
+ this.buffer,
10042
+ offset,
10043
+ data,
10044
+ 0
10045
+ // dataOffset
10046
+ );
10047
+ }
10048
+ async mapAsync(mode, offset = 0, size) {
10049
+ await this.buffer.mapAsync(mode, offset, size);
10050
+ }
10051
+ getMappedRange(offset = 0, size) {
10052
+ return this.buffer.getMappedRange(offset, size);
10053
+ }
10054
+ unmap() {
10055
+ this.buffer.unmap();
10056
+ }
10057
+ destroy() {
10058
+ this.buffer.destroy();
10059
+ }
10060
+ };
10061
+ var VertexBuffer2 = class extends GPUBufferResource {
10062
+ constructor(device, descriptor) {
10063
+ super(device, {
10064
+ size: descriptor.size,
10065
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST | (descriptor.usage || 0),
10066
+ label: descriptor.label
10067
+ });
10068
+ }
10069
+ };
10070
+ var IndexBuffer2 = class extends GPUBufferResource {
10071
+ constructor(device, descriptor) {
10072
+ super(device, {
10073
+ size: descriptor.size,
10074
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST | (descriptor.usage || 0),
10075
+ label: descriptor.label
10076
+ });
10077
+ }
10078
+ };
10079
+ var UniformBuffer = class extends GPUBufferResource {
10080
+ constructor(device, descriptor) {
10081
+ super(device, {
10082
+ size: descriptor.size,
10083
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST | (descriptor.usage || 0),
10084
+ label: descriptor.label
10085
+ });
10086
+ }
10087
+ };
10088
+ function getBlockSize(format) {
10089
+ switch (format) {
10090
+ case "rgba8unorm":
10091
+ case "rgba8unorm-srgb":
10092
+ case "bgra8unorm":
10093
+ case "bgra8unorm-srgb":
10094
+ case "rgba8sint":
10095
+ case "rgba8uint":
10096
+ return 4;
10097
+ case "rg8unorm":
10098
+ case "rg8sint":
10099
+ case "rg8uint":
10100
+ case "r16float":
10101
+ case "r16sint":
10102
+ case "r16uint":
10103
+ return 2;
10104
+ case "r8unorm":
10105
+ case "r8sint":
10106
+ case "r8uint":
10107
+ return 1;
10108
+ case "rgba16float":
10109
+ case "rgba16sint":
10110
+ case "rgba16uint":
10111
+ case "rgba32float":
10112
+ return 8;
10113
+ // wait, rgba16 is 8 bytes. rgba32 is 16 bytes.
10114
+ case "rgba32float":
10115
+ case "rgba32sint":
10116
+ case "rgba32uint":
10117
+ return 16;
10118
+ case "depth24plus":
10119
+ case "depth24plus-stencil8":
10120
+ // approximate
10121
+ case "depth32float":
10122
+ return 4;
10123
+ default:
10124
+ return 4;
10125
+ }
10126
+ }
10127
+ var _Texture2D = class _Texture2D {
10128
+ constructor(device, descriptor) {
10129
+ this.device = device;
10130
+ this.width = descriptor.width;
10131
+ this.height = descriptor.height;
10132
+ this.format = descriptor.format;
10133
+ const usage = descriptor.usage ?? GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT;
10134
+ this.texture = device.createTexture({
10135
+ size: [this.width, this.height, 1],
10136
+ format: this.format,
10137
+ usage,
10138
+ mipLevelCount: descriptor.mipLevelCount ?? 1,
10139
+ label: descriptor.label
10140
+ });
10141
+ }
10142
+ upload(data, options = {}) {
10143
+ const width = options.width ?? this.width;
10144
+ const height = options.height ?? this.height;
10145
+ const depthOrArrayLayers = options.depthOrArrayLayers ?? 1;
10146
+ const mipLevel = options.mipLevel ?? 0;
10147
+ const blockSize = getBlockSize(this.format);
10148
+ let bytesPerRow = options.bytesPerRow;
10149
+ if (!bytesPerRow) {
10150
+ bytesPerRow = width * blockSize;
10151
+ }
10152
+ this.device.queue.writeTexture(
10153
+ { texture: this.texture, mipLevel, origin: { x: 0, y: 0, z: 0 } },
10154
+ data,
10155
+ {
10156
+ offset: 0,
10157
+ bytesPerRow,
10158
+ rowsPerImage: options.rowsPerImage ?? height
10159
+ },
10160
+ { width, height, depthOrArrayLayers }
10161
+ );
10162
+ }
10163
+ getMipmapPipeline(format) {
10164
+ let devicePipelines = _Texture2D.mipmapPipelines.get(this.device);
10165
+ if (!devicePipelines) {
10166
+ devicePipelines = /* @__PURE__ */ new Map();
10167
+ _Texture2D.mipmapPipelines.set(this.device, devicePipelines);
10168
+ }
10169
+ let pipeline = devicePipelines.get(format);
10170
+ if (!pipeline) {
10171
+ const module = this.device.createShaderModule({
10172
+ code: MIPMAP_SHADER,
10173
+ label: "mipmap-shader"
10174
+ });
10175
+ pipeline = this.device.createRenderPipeline({
10176
+ layout: "auto",
10177
+ vertex: {
10178
+ module,
10179
+ entryPoint: "vs_main"
10180
+ },
10181
+ fragment: {
10182
+ module,
10183
+ entryPoint: "fs_main",
10184
+ targets: [{ format }]
10185
+ },
10186
+ primitive: {
10187
+ topology: "triangle-list"
10188
+ },
10189
+ label: `mipmap-pipeline-${format}`
10190
+ });
10191
+ devicePipelines.set(format, pipeline);
10192
+ }
10193
+ return pipeline;
10194
+ }
10195
+ getMipmapSampler() {
10196
+ let sampler = _Texture2D.mipmapSamplers.get(this.device);
10197
+ if (!sampler) {
10198
+ sampler = this.device.createSampler({
10199
+ minFilter: "linear",
10200
+ magFilter: "linear",
10201
+ label: "mipmap-sampler"
10202
+ });
10203
+ _Texture2D.mipmapSamplers.set(this.device, sampler);
10204
+ }
10205
+ return sampler;
10206
+ }
10207
+ generateMipmaps(commandEncoder) {
10208
+ const mipCount = this.texture.mipLevelCount;
10209
+ if (mipCount <= 1) return;
10210
+ const pipeline = this.getMipmapPipeline(this.format);
10211
+ const sampler = this.getMipmapSampler();
10212
+ for (let i = 1; i < mipCount; i++) {
10213
+ const srcView = this.texture.createView({
10214
+ baseMipLevel: i - 1,
10215
+ mipLevelCount: 1,
10216
+ label: `mipmap-src-${i - 1}`
10217
+ });
10218
+ const dstView = this.texture.createView({
10219
+ baseMipLevel: i,
10220
+ mipLevelCount: 1,
10221
+ label: `mipmap-dst-${i}`
10222
+ });
10223
+ const passEncoder = commandEncoder.beginRenderPass({
10224
+ colorAttachments: [{
10225
+ view: dstView,
10226
+ loadOp: "clear",
10227
+ storeOp: "store"
10228
+ }],
10229
+ label: `mipmap-pass-${i}`
10230
+ });
10231
+ const bindGroup = this.device.createBindGroup({
10232
+ layout: pipeline.getBindGroupLayout(0),
10233
+ entries: [
10234
+ { binding: 0, resource: sampler },
10235
+ { binding: 1, resource: srcView }
10236
+ ],
10237
+ label: `mipmap-bindgroup-${i}`
10238
+ });
10239
+ passEncoder.setPipeline(pipeline);
10240
+ passEncoder.setBindGroup(0, bindGroup);
10241
+ passEncoder.draw(6);
10242
+ passEncoder.end();
10243
+ }
10244
+ }
10245
+ createView(descriptor) {
10246
+ return this.texture.createView(descriptor);
10247
+ }
10248
+ destroy() {
10249
+ this.texture.destroy();
10250
+ }
10251
+ get memorySize() {
10252
+ const blockSize = getBlockSize(this.format);
10253
+ let size = 0;
10254
+ let w = this.width;
10255
+ let h = this.height;
10256
+ const mipCount = this.texture.mipLevelCount;
10257
+ for (let i = 0; i < mipCount; i++) {
10258
+ size += w * h * blockSize;
10259
+ w = Math.max(1, Math.floor(w / 2));
10260
+ h = Math.max(1, Math.floor(h / 2));
10261
+ }
10262
+ return size;
10263
+ }
10264
+ };
10265
+ // Cache pipelines per device and per format
10266
+ _Texture2D.mipmapPipelines = /* @__PURE__ */ new WeakMap();
10267
+ _Texture2D.mipmapSamplers = /* @__PURE__ */ new WeakMap();
10268
+ var Texture2D2 = _Texture2D;
10269
+ var Sampler = class {
10270
+ constructor(device, descriptor) {
10271
+ this.sampler = device.createSampler({
10272
+ ...descriptor,
10273
+ label: descriptor.label
10274
+ });
10275
+ }
10276
+ destroy() {
10277
+ }
10278
+ };
10279
+ function createLinearSampler(device) {
10280
+ return new Sampler(device, {
10281
+ minFilter: "linear",
10282
+ magFilter: "linear",
10283
+ mipmapFilter: "linear",
10284
+ label: "linear-sampler"
10285
+ });
10286
+ }
10287
+ var ShaderModule = class {
10288
+ constructor(device, descriptor) {
10289
+ this.device = device;
10290
+ this.module = device.createShaderModule({
10291
+ code: descriptor.code,
10292
+ label: descriptor.label
10293
+ });
10294
+ }
10295
+ get compilationInfo() {
10296
+ return this.module.getCompilationInfo();
10297
+ }
10298
+ };
10299
+ var RenderPipeline = class {
10300
+ constructor(device, descriptor) {
10301
+ this.device = device;
10302
+ const layout = descriptor.layout;
10303
+ this.pipeline = device.createRenderPipeline({
10304
+ layout,
10305
+ vertex: {
10306
+ module: descriptor.vertex.module.module,
10307
+ entryPoint: descriptor.vertex.entryPoint,
10308
+ buffers: descriptor.vertex.buffers
10309
+ },
10310
+ fragment: descriptor.fragment ? {
10311
+ module: descriptor.fragment.module.module,
10312
+ entryPoint: descriptor.fragment.entryPoint,
10313
+ targets: descriptor.fragment.targets
10314
+ } : void 0,
10315
+ primitive: descriptor.primitive,
10316
+ depthStencil: descriptor.depthStencil,
10317
+ multisample: descriptor.multisample,
10318
+ label: descriptor.label
10319
+ });
10320
+ }
10321
+ get layout() {
10322
+ throw new Error("Cannot retrieve layout from pipeline created with 'auto' layout");
10323
+ }
10324
+ destroy() {
10325
+ }
10326
+ };
10327
+ var BindGroupLayout = class {
10328
+ constructor(device, descriptor) {
10329
+ this.device = device;
10330
+ this.layout = device.createBindGroupLayout({
10331
+ entries: descriptor.entries,
10332
+ label: descriptor.label
10333
+ });
10334
+ }
10335
+ };
10336
+ var BindGroup = class {
10337
+ constructor(device, layout, entries, label) {
10338
+ this.device = device;
10339
+ const gpuEntries = entries.map((entry) => {
10340
+ let resource;
10341
+ if (entry.resource instanceof GPUBufferResource) {
10342
+ resource = { buffer: entry.resource.buffer };
10343
+ } else if (entry.resource instanceof Sampler) {
10344
+ resource = entry.resource.sampler;
10345
+ } else {
10346
+ resource = entry.resource;
10347
+ }
10348
+ return {
10349
+ binding: entry.binding,
10350
+ resource
10351
+ };
10352
+ });
10353
+ this.bindGroup = device.createBindGroup({
10354
+ layout: layout.layout,
10355
+ entries: gpuEntries,
10356
+ label
10357
+ });
10358
+ }
10359
+ destroy() {
10360
+ }
10361
+ };
10362
+ var BindGroupBuilder = class {
10363
+ constructor(label) {
10364
+ this.label = label;
10365
+ this.entries = [];
10366
+ }
10367
+ addUniformBuffer(binding, visibility) {
10368
+ this.entries.push({
10369
+ binding,
10370
+ visibility,
10371
+ buffer: { type: "uniform" }
10372
+ });
10373
+ return this;
10374
+ }
10375
+ addStorageBuffer(binding, visibility, type = "read-only-storage") {
10376
+ this.entries.push({
10377
+ binding,
10378
+ visibility,
10379
+ buffer: { type }
10380
+ });
10381
+ return this;
10382
+ }
10383
+ addTexture(binding, visibility, sampleType = "float", viewDimension = "2d") {
10384
+ this.entries.push({
10385
+ binding,
10386
+ visibility,
10387
+ texture: { sampleType, viewDimension }
10388
+ });
10389
+ return this;
10390
+ }
10391
+ addSampler(binding, visibility, type = "filtering") {
10392
+ this.entries.push({
10393
+ binding,
10394
+ visibility,
10395
+ sampler: { type }
10396
+ });
10397
+ return this;
10398
+ }
10399
+ build(device) {
10400
+ return new BindGroupLayout(device, {
10401
+ entries: this.entries,
10402
+ label: this.label
10403
+ });
10404
+ }
10405
+ };
10406
+
10407
+ // src/render/webgpu/shaders/spriteShader.ts
10408
+ var SPRITE_SHADER = `
10409
+ // Sprite Rendering Shader (WGSL)
10410
+
10411
+ struct VertexInput {
10412
+ @location(0) position: vec2f,
10413
+ @location(1) texcoord: vec2f,
10414
+ @location(2) color: vec4f,
10415
+ }
10416
+
10417
+ struct VertexOutput {
10418
+ @builtin(position) position: vec4f,
10419
+ @location(0) texcoord: vec2f,
10420
+ @location(1) color: vec4f,
10421
+ }
10422
+
10423
+ struct Uniforms {
10424
+ projection: mat4x4f,
10425
+ }
10426
+
10427
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
10428
+
10429
+ @vertex
10430
+ fn vs_main(input: VertexInput) -> VertexOutput {
10431
+ var output: VertexOutput;
10432
+ // Orthographic projection: Input is screen coords (pixels), Z is 0.0, W is 1.0
10433
+ output.position = uniforms.projection * vec4f(input.position, 0.0, 1.0);
10434
+ output.texcoord = input.texcoord;
10435
+ output.color = input.color;
10436
+ return output;
10437
+ }
10438
+
10439
+ @group(1) @binding(0) var texSampler: sampler;
10440
+ @group(1) @binding(1) var tex: texture_2d<f32>;
10441
+
10442
+ @fragment
10443
+ fn fs_main(input: VertexOutput) -> @location(0) vec4f {
10444
+ let texColor = textureSample(tex, texSampler, input.texcoord);
10445
+ return texColor * input.color;
10446
+ }
10447
+
10448
+ @fragment
10449
+ fn fs_solid(input: VertexOutput) -> @location(0) vec4f {
10450
+ return input.color;
10451
+ }
10452
+ `;
10453
+
10454
+ // src/render/webgpu/pipelines/sprite.ts
10455
+ var MAX_SPRITES = 2048;
10456
+ var VERTICES_PER_SPRITE = 4;
10457
+ var INDICES_PER_SPRITE = 6;
10458
+ var FLOATS_PER_VERTEX = 8;
10459
+ var SpriteRenderer2 = class {
10460
+ constructor(device, format) {
10461
+ this.device = device;
10462
+ this.format = format;
10463
+ this.currentVertexCount = 0;
10464
+ this.currentIndexCount = 0;
10465
+ this.currentTexture = null;
10466
+ this.drawCommands = [];
10467
+ this.textureBindGroups = /* @__PURE__ */ new Map();
10468
+ this._activeEncoder = null;
10469
+ this._activeRenderTarget = null;
10470
+ const shaderModule = new ShaderModule(device, {
10471
+ code: SPRITE_SHADER,
10472
+ label: "sprite-shader"
10473
+ });
10474
+ const vertexBufferLayout = {
10475
+ arrayStride: FLOATS_PER_VERTEX * 4,
10476
+ stepMode: "vertex",
10477
+ attributes: [
10478
+ { format: "float32x2", offset: 0, shaderLocation: 0 },
10479
+ // position
10480
+ { format: "float32x2", offset: 8, shaderLocation: 1 },
10481
+ // texcoord
10482
+ { format: "float32x4", offset: 16, shaderLocation: 2 }
10483
+ // color
10484
+ ]
10485
+ };
10486
+ const bindGroupBuilder = new BindGroupBuilder("sprite-uniform-layout");
10487
+ bindGroupBuilder.addUniformBuffer(0, GPUShaderStage.VERTEX);
10488
+ const uniformBindGroupLayout = bindGroupBuilder.build(device);
10489
+ const textureBindGroupBuilder = new BindGroupBuilder("sprite-texture-layout");
10490
+ textureBindGroupBuilder.addSampler(0, GPUShaderStage.FRAGMENT);
10491
+ textureBindGroupBuilder.addTexture(1, GPUShaderStage.FRAGMENT);
10492
+ this.textureBindGroupLayout = textureBindGroupBuilder.build(device);
10493
+ const pipelineLayout = device.createPipelineLayout({
10494
+ bindGroupLayouts: [
10495
+ uniformBindGroupLayout.layout,
10496
+ this.textureBindGroupLayout.layout
10497
+ ],
10498
+ label: "sprite-pipeline-layout"
10499
+ });
10500
+ const pipelineLayoutSolid = device.createPipelineLayout({
10501
+ bindGroupLayouts: [
10502
+ uniformBindGroupLayout.layout
10503
+ ],
10504
+ label: "sprite-pipeline-solid-layout"
10505
+ });
10506
+ const blendState = {
10507
+ color: {
10508
+ srcFactor: "src-alpha",
10509
+ dstFactor: "one-minus-src-alpha",
10510
+ operation: "add"
10511
+ },
10512
+ alpha: {
10513
+ srcFactor: "one",
10514
+ dstFactor: "one-minus-src-alpha",
10515
+ operation: "add"
10516
+ }
10517
+ };
10518
+ this.pipelineTextured = new RenderPipeline(device, {
10519
+ layout: pipelineLayout,
10520
+ vertex: {
10521
+ module: shaderModule,
10522
+ entryPoint: "vs_main",
10523
+ buffers: [vertexBufferLayout]
10524
+ },
10525
+ fragment: {
10526
+ module: shaderModule,
10527
+ entryPoint: "fs_main",
10528
+ targets: [{
10529
+ format: this.format,
10530
+ blend: blendState
10531
+ }]
10532
+ },
10533
+ primitive: {
10534
+ topology: "triangle-list",
10535
+ cullMode: "none"
10536
+ // Sprites are 2D, no culling usually needed
10537
+ },
10538
+ label: "sprite-pipeline-textured"
10539
+ });
10540
+ this.pipelineSolid = new RenderPipeline(device, {
10541
+ layout: pipelineLayoutSolid,
10542
+ vertex: {
10543
+ module: shaderModule,
10544
+ entryPoint: "vs_main",
10545
+ buffers: [vertexBufferLayout]
10546
+ },
10547
+ fragment: {
10548
+ module: shaderModule,
10549
+ entryPoint: "fs_solid",
10550
+ targets: [{
10551
+ format: this.format,
10552
+ blend: blendState
10553
+ }]
10554
+ },
10555
+ primitive: {
10556
+ topology: "triangle-list",
10557
+ cullMode: "none"
10558
+ },
10559
+ label: "sprite-pipeline-solid"
10560
+ });
10561
+ this.vertexBuffer = new VertexBuffer2(device, {
10562
+ size: MAX_SPRITES * VERTICES_PER_SPRITE * FLOATS_PER_VERTEX * 4,
10563
+ label: "sprite-vertex-buffer",
10564
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
10565
+ });
10566
+ this.indexBuffer = new IndexBuffer2(device, {
10567
+ size: MAX_SPRITES * INDICES_PER_SPRITE * 2,
10568
+ // Uint16
10569
+ label: "sprite-index-buffer",
10570
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST
10571
+ });
10572
+ this.uniformBuffer = new UniformBuffer(device, {
10573
+ size: 64,
10574
+ // mat4
10575
+ label: "sprite-uniform-buffer",
10576
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
10577
+ });
10578
+ this.uniformBindGroup = new BindGroup(
10579
+ device,
10580
+ uniformBindGroupLayout,
10581
+ [
10582
+ { binding: 0, resource: this.uniformBuffer }
10583
+ ],
10584
+ "sprite-uniform-bind-group"
10585
+ );
10586
+ this.vertexData = new Float32Array(MAX_SPRITES * VERTICES_PER_SPRITE * FLOATS_PER_VERTEX);
10587
+ const indices = new Uint16Array(MAX_SPRITES * INDICES_PER_SPRITE);
10588
+ for (let i = 0; i < MAX_SPRITES; i++) {
10589
+ const v = i * 4;
10590
+ const ii = i * 6;
10591
+ indices[ii] = v;
10592
+ indices[ii + 1] = v + 1;
10593
+ indices[ii + 2] = v + 2;
10594
+ indices[ii + 3] = v;
10595
+ indices[ii + 4] = v + 2;
10596
+ indices[ii + 5] = v + 3;
10597
+ }
10598
+ this.indexBuffer.write(indices);
10599
+ this.projectionMatrix = glMatrix.mat4.create();
10600
+ this.defaultSampler = createLinearSampler(device);
10601
+ }
10602
+ setProjection(width, height) {
10603
+ glMatrix.mat4.ortho(this.projectionMatrix, 0, width, height, 0, -1, 1);
10604
+ this.uniformBuffer.write(this.projectionMatrix);
10605
+ }
10606
+ begin(commandEncoder, renderTarget) {
10607
+ this.currentVertexCount = 0;
10608
+ this.currentIndexCount = 0;
10609
+ this.drawCommands = [];
10610
+ this.currentTexture = null;
10611
+ this._activeEncoder = commandEncoder;
10612
+ this._activeRenderTarget = renderTarget;
10613
+ }
10614
+ drawTexturedQuad(x, y, w, h, texture, u1 = 0, v1 = 0, u2 = 1, v2 = 1, color = [1, 1, 1, 1]) {
10615
+ if (this.currentVertexCount + VERTICES_PER_SPRITE > MAX_SPRITES * VERTICES_PER_SPRITE) {
10616
+ this.flush();
10617
+ }
10618
+ if (this.drawCommands.length === 0 || this.currentTexture !== texture) {
10619
+ this.drawCommands.push({
10620
+ texture,
10621
+ start: this.currentIndexCount,
10622
+ count: 0
10623
+ });
10624
+ this.currentTexture = texture;
10625
+ }
10626
+ const offset = this.currentVertexCount * FLOATS_PER_VERTEX;
10627
+ const [r, g, b, a] = color;
10628
+ this.vertexData[offset] = x;
10629
+ this.vertexData[offset + 1] = y;
10630
+ this.vertexData[offset + 2] = u1;
10631
+ this.vertexData[offset + 3] = v1;
10632
+ this.vertexData[offset + 4] = r;
10633
+ this.vertexData[offset + 5] = g;
10634
+ this.vertexData[offset + 6] = b;
10635
+ this.vertexData[offset + 7] = a;
10636
+ this.vertexData[offset + 8] = x;
10637
+ this.vertexData[offset + 9] = y + h;
10638
+ this.vertexData[offset + 10] = u1;
10639
+ this.vertexData[offset + 11] = v2;
10640
+ this.vertexData[offset + 12] = r;
10641
+ this.vertexData[offset + 13] = g;
10642
+ this.vertexData[offset + 14] = b;
10643
+ this.vertexData[offset + 15] = a;
10644
+ this.vertexData[offset + 16] = x + w;
10645
+ this.vertexData[offset + 17] = y + h;
10646
+ this.vertexData[offset + 18] = u2;
10647
+ this.vertexData[offset + 19] = v2;
10648
+ this.vertexData[offset + 20] = r;
10649
+ this.vertexData[offset + 21] = g;
10650
+ this.vertexData[offset + 22] = b;
10651
+ this.vertexData[offset + 23] = a;
10652
+ this.vertexData[offset + 24] = x + w;
10653
+ this.vertexData[offset + 25] = y;
10654
+ this.vertexData[offset + 26] = u2;
10655
+ this.vertexData[offset + 27] = v1;
10656
+ this.vertexData[offset + 28] = r;
10657
+ this.vertexData[offset + 29] = g;
10658
+ this.vertexData[offset + 30] = b;
10659
+ this.vertexData[offset + 31] = a;
10660
+ this.currentVertexCount += 4;
10661
+ this.currentIndexCount += 6;
10662
+ this.drawCommands[this.drawCommands.length - 1].count += 6;
10663
+ }
10664
+ drawSolidRect(x, y, w, h, color) {
10665
+ if (this.currentTexture !== null) {
10666
+ this.drawCommands.push({
10667
+ texture: null,
10668
+ start: this.currentIndexCount,
10669
+ count: 0
10670
+ });
10671
+ this.currentTexture = null;
10672
+ }
10673
+ this.drawTexturedQuad(x, y, w, h, null, 0, 0, 0, 0, color);
10674
+ }
10675
+ flush() {
10676
+ if (this.currentVertexCount === 0) return;
10677
+ if (!this._activeEncoder || !this._activeRenderTarget) return;
10678
+ const usedFloats = this.currentVertexCount * FLOATS_PER_VERTEX;
10679
+ this.vertexBuffer.write(this.vertexData.subarray(0, usedFloats));
10680
+ const passEncoder = this._activeEncoder.beginRenderPass({
10681
+ colorAttachments: [{
10682
+ view: this._activeRenderTarget,
10683
+ loadOp: "load",
10684
+ storeOp: "store"
10685
+ }],
10686
+ label: "sprite-pass"
10687
+ });
10688
+ passEncoder.setBindGroup(0, this.uniformBindGroup.bindGroup);
10689
+ passEncoder.setVertexBuffer(0, this.vertexBuffer.buffer);
10690
+ passEncoder.setIndexBuffer(this.indexBuffer.buffer, "uint16");
10691
+ for (const cmd of this.drawCommands) {
10692
+ if (cmd.count === 0) continue;
10693
+ if (cmd.texture) {
10694
+ passEncoder.setPipeline(this.pipelineTextured.pipeline);
10695
+ let bindGroup = this.textureBindGroups.get(cmd.texture);
10696
+ if (!bindGroup) {
10697
+ bindGroup = new BindGroup(
10698
+ this.device,
10699
+ this.textureBindGroupLayout,
10700
+ [
10701
+ { binding: 0, resource: this.defaultSampler },
10702
+ { binding: 1, resource: cmd.texture.createView() }
10703
+ ],
10704
+ `sprite-texture-bind-group-${cmd.texture.texture.label}`
10705
+ );
10706
+ this.textureBindGroups.set(cmd.texture, bindGroup);
10707
+ }
10708
+ passEncoder.setBindGroup(1, bindGroup.bindGroup);
10709
+ } else {
10710
+ passEncoder.setPipeline(this.pipelineSolid.pipeline);
10711
+ }
10712
+ passEncoder.drawIndexed(cmd.count, 1, cmd.start, 0, 0);
10713
+ }
10714
+ passEncoder.end();
10715
+ this.currentVertexCount = 0;
10716
+ this.currentIndexCount = 0;
10717
+ this.drawCommands = [];
10718
+ this.currentTexture = null;
10719
+ }
10720
+ end() {
10721
+ this.flush();
10722
+ this._activeEncoder = null;
10723
+ this._activeRenderTarget = null;
10724
+ }
10725
+ /**
10726
+ * Check if sprite renderer is currently in an active render pass
10727
+ * Ref: Defensive check to prevent GPU resource leaks
10728
+ */
10729
+ get isActive() {
10730
+ return this._activeEncoder !== null;
10731
+ }
10732
+ destroy() {
10733
+ this.vertexBuffer.destroy();
10734
+ this.indexBuffer.destroy();
10735
+ this.uniformBuffer.destroy();
10736
+ }
10737
+ };
10738
+
10739
+ // raw-loader:/home/runner/work/quake2/quake2/quake2ts/packages/engine/src/render/webgpu/shaders/skybox.wgsl
10740
+ var skybox_default = "struct Uniforms {\n viewProjection: mat4x4<f32>,\n scroll: vec2<f32>,\n}\n\n@group(0) @binding(0) var<uniform> uniforms: Uniforms;\n@group(0) @binding(1) var t_skybox: texture_cube<f32>;\n@group(0) @binding(2) var s_skybox: sampler;\n\nstruct VertexOutput {\n @builtin(position) position: vec4<f32>,\n @location(0) direction: vec3<f32>,\n}\n\n@vertex\nfn vertexMain(@location(0) position: vec3<f32>) -> VertexOutput {\n var output: VertexOutput;\n\n // Normalize input position (Quake Coordinates)\n var qDir = normalize(position);\n\n // Apply scrolling in Quake Coordinates (Horizontal Plane X/Y)\n // This ensures clouds scroll horizontally regardless of the final mapping.\n qDir.x += uniforms.scroll.x;\n qDir.y += uniforms.scroll.y;\n\n // Transform Quake coordinates (X-Fwd, Y-Left, Z-Up)\n // to WebGPU/GL cubemap coordinates (Right-handed? -Z Fwd, +X Right, +Y Up)\n // Quake X -> GL -Z\n // Quake Y -> GL -X\n // Quake Z -> GL Y\n var dir = vec3<f32>(-qDir.y, qDir.z, -qDir.x);\n\n output.direction = dir;\n\n output.position = uniforms.viewProjection * vec4<f32>(position, 1.0);\n return output;\n}\n\n@fragment\nfn fragmentMain(@location(0) direction: vec3<f32>) -> @location(0) vec4<f32> {\n return textureSample(t_skybox, s_skybox, direction);\n}\n";
10741
+
10742
+ // src/render/types/coordinates.ts
10743
+ var CoordinateSystem = /* @__PURE__ */ ((CoordinateSystem2) => {
10744
+ CoordinateSystem2["QUAKE"] = "quake";
10745
+ CoordinateSystem2["OPENGL"] = "opengl";
10746
+ CoordinateSystem2["WEBGPU"] = "webgpu";
10747
+ return CoordinateSystem2;
10748
+ })(CoordinateSystem || {});
10749
+
10750
+ // src/render/matrix/webgpu.ts
10751
+ var WebGPUMatrixBuilder = class {
10752
+ constructor() {
10753
+ this.coordinateSystem = "webgpu" /* WEBGPU */;
10754
+ }
10755
+ buildProjectionMatrix(camera) {
10756
+ const projection = glMatrix.mat4.create();
10757
+ const f = 1 / Math.tan(camera.fov * shared.DEG2RAD / 2);
10758
+ const rangeInv = 1 / (camera.near - camera.far);
10759
+ projection[0] = f / camera.aspect;
10760
+ projection[5] = f;
10761
+ projection[10] = camera.far * rangeInv;
10762
+ projection[11] = -1;
10763
+ projection[14] = camera.near * camera.far * rangeInv;
10764
+ return projection;
10765
+ }
10766
+ buildViewMatrix(camera) {
10767
+ const [pitch, yaw, roll] = camera.angles;
10768
+ const pitchRad = pitch * shared.DEG2RAD;
10769
+ const yawRad = yaw * shared.DEG2RAD;
10770
+ const rollRad = roll * shared.DEG2RAD;
10771
+ const rotationQuake = glMatrix.mat4.create();
10772
+ glMatrix.mat4.identity(rotationQuake);
10773
+ glMatrix.mat4.rotateZ(rotationQuake, rotationQuake, -yawRad);
10774
+ glMatrix.mat4.rotateY(rotationQuake, rotationQuake, -pitchRad);
10775
+ glMatrix.mat4.rotateX(rotationQuake, rotationQuake, -rollRad);
10776
+ glMatrix.mat4.fromValues(
10777
+ 0,
10778
+ 0,
10779
+ -1,
10780
+ 0,
10781
+ // Column 0: Quake X maps to View Z? Wait.
10782
+ -1,
10783
+ 0,
10784
+ 0,
10785
+ 0,
10786
+ // Column 1: Quake Y maps to View X?
10787
+ 0,
10788
+ 1,
10789
+ 0,
10790
+ 0,
10791
+ // Column 2: Quake Z maps to View Y?
10792
+ 0,
10793
+ 0,
10794
+ 0,
10795
+ 1
10796
+ );
10797
+ const quakeToWgpu = glMatrix.mat4.fromValues(
10798
+ 0,
10799
+ 0,
10800
+ -1,
10801
+ 0,
10802
+ // Col 0: X -> -Z
10803
+ -1,
10804
+ 0,
10805
+ 0,
10806
+ 0,
10807
+ // Col 1: Y -> -X
10808
+ 0,
10809
+ 1,
10810
+ 0,
10811
+ 0,
10812
+ // Col 2: Z -> Y
10813
+ 0,
10814
+ 0,
10815
+ 0,
10816
+ 1
10817
+ );
10818
+ const rotationView = glMatrix.mat4.create();
10819
+ glMatrix.mat4.multiply(rotationView, quakeToWgpu, rotationQuake);
10820
+ const cameraPos = glMatrix.vec3.fromValues(
10821
+ camera.position[0],
10822
+ camera.position[1],
10823
+ camera.position[2]
10824
+ );
10825
+ const t = glMatrix.vec3.transformMat4(glMatrix.vec3.create(), cameraPos, rotationView);
10826
+ glMatrix.vec3.negate(t, t);
10827
+ const view = glMatrix.mat4.clone(rotationView);
10828
+ view[12] = t[0];
10829
+ view[13] = t[1];
10830
+ view[14] = t[2];
10831
+ return view;
10832
+ }
10833
+ };
10834
+ var SKYBOX_POSITIONS2 = new Float32Array([
10835
+ // Front face (+X) - looking forward
10836
+ 1,
10837
+ -1,
10838
+ -1,
10839
+ 1,
10840
+ 1,
10841
+ -1,
10842
+ 1,
10843
+ 1,
10844
+ 1,
10845
+ 1,
10846
+ -1,
10847
+ -1,
10848
+ 1,
10849
+ 1,
10850
+ 1,
10851
+ 1,
10852
+ -1,
10853
+ 1,
10854
+ // Back face (-X) - looking backward
10855
+ -1,
10856
+ 1,
10857
+ -1,
10858
+ -1,
10859
+ -1,
10860
+ -1,
10861
+ -1,
10862
+ -1,
10863
+ 1,
10864
+ -1,
10865
+ 1,
10866
+ -1,
10867
+ -1,
10868
+ -1,
10869
+ 1,
10870
+ -1,
10871
+ 1,
10872
+ 1,
10873
+ // Left face (+Y) - looking left
10874
+ -1,
10875
+ 1,
10876
+ -1,
10877
+ -1,
10878
+ 1,
10879
+ 1,
10880
+ 1,
10881
+ 1,
10882
+ 1,
10883
+ -1,
10884
+ 1,
10885
+ -1,
10886
+ 1,
10887
+ 1,
10888
+ 1,
10889
+ 1,
10890
+ 1,
10891
+ -1,
10892
+ // Right face (-Y) - looking right
10893
+ 1,
10894
+ -1,
10895
+ -1,
10896
+ 1,
10897
+ -1,
10898
+ 1,
10899
+ -1,
10900
+ -1,
10901
+ 1,
10902
+ 1,
10903
+ -1,
10904
+ -1,
10905
+ -1,
10906
+ -1,
10907
+ 1,
10908
+ -1,
10909
+ -1,
10910
+ -1,
10911
+ // Top face (+Z) - looking up
10912
+ -1,
10913
+ -1,
10914
+ 1,
10915
+ 1,
10916
+ -1,
10917
+ 1,
10918
+ 1,
10919
+ 1,
10920
+ 1,
10921
+ -1,
10922
+ -1,
10923
+ 1,
10924
+ 1,
10925
+ 1,
10926
+ 1,
10927
+ -1,
10928
+ 1,
10929
+ 1,
10930
+ // Bottom face (-Z) - looking down
10931
+ -1,
10932
+ 1,
10933
+ -1,
10934
+ 1,
10935
+ 1,
10936
+ -1,
10937
+ 1,
10938
+ -1,
10939
+ -1,
10940
+ -1,
10941
+ 1,
10942
+ -1,
10943
+ 1,
10944
+ -1,
10945
+ -1,
10946
+ -1,
10947
+ -1,
10948
+ -1
10949
+ ]);
10950
+ var SkyboxPipeline3 = class {
10951
+ constructor(device, format) {
10952
+ this.device = device;
10953
+ this.format = format;
10954
+ this.matrixBuilder = new WebGPUMatrixBuilder();
10955
+ const module = device.createShaderModule({
10956
+ label: "skybox-shader",
10957
+ code: skybox_default
10958
+ });
10959
+ this.vertexBuffer = device.createBuffer({
10960
+ label: "skybox-vertex-buffer",
10961
+ size: SKYBOX_POSITIONS2.byteLength,
10962
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
10963
+ mappedAtCreation: true
10964
+ });
10965
+ new Float32Array(this.vertexBuffer.getMappedRange()).set(SKYBOX_POSITIONS2);
10966
+ this.vertexBuffer.unmap();
10967
+ this.uniformBuffer = device.createBuffer({
10968
+ label: "skybox-uniform-buffer",
10969
+ size: 80,
10970
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
10971
+ });
10972
+ this.sampler = device.createSampler({
10973
+ label: "skybox-sampler",
10974
+ minFilter: "linear",
10975
+ magFilter: "linear",
10976
+ addressModeU: "clamp-to-edge",
10977
+ addressModeV: "clamp-to-edge",
10978
+ addressModeW: "clamp-to-edge"
10979
+ });
10980
+ const bindGroupLayout = device.createBindGroupLayout({
10981
+ label: "skybox-bind-group-layout",
10982
+ entries: [
10983
+ {
10984
+ binding: 0,
10985
+ visibility: GPUShaderStage.VERTEX,
10986
+ buffer: { type: "uniform" }
10987
+ },
10988
+ {
10989
+ binding: 1,
10990
+ visibility: GPUShaderStage.FRAGMENT,
10991
+ texture: { viewDimension: "cube" }
10992
+ },
10993
+ {
10994
+ binding: 2,
10995
+ visibility: GPUShaderStage.FRAGMENT,
10996
+ sampler: { type: "filtering" }
10997
+ }
10998
+ ]
10999
+ });
11000
+ this.pipeline = device.createRenderPipeline({
11001
+ label: "skybox-pipeline",
11002
+ layout: device.createPipelineLayout({
11003
+ bindGroupLayouts: [bindGroupLayout]
11004
+ }),
11005
+ vertex: {
11006
+ module,
11007
+ entryPoint: "vertexMain",
11008
+ buffers: [{
11009
+ arrayStride: 12,
11010
+ // vec3<f32>
11011
+ attributes: [{
11012
+ shaderLocation: 0,
11013
+ offset: 0,
11014
+ format: "float32x3"
11015
+ }]
11016
+ }]
11017
+ },
11018
+ fragment: {
11019
+ module,
11020
+ entryPoint: "fragmentMain",
11021
+ targets: [{
11022
+ format: this.format,
11023
+ blend: {
11024
+ // Standard alpha blending (though skybox is usually opaque)
11025
+ color: {
11026
+ srcFactor: "src-alpha",
11027
+ dstFactor: "one-minus-src-alpha",
11028
+ operation: "add"
11029
+ },
11030
+ alpha: {
11031
+ srcFactor: "one",
11032
+ dstFactor: "one-minus-src-alpha",
11033
+ operation: "add"
11034
+ }
11035
+ }
11036
+ }]
11037
+ },
11038
+ depthStencil: {
11039
+ format: "depth24plus",
11040
+ depthWriteEnabled: false,
11041
+ depthCompare: "always"
11042
+ // Skybox is usually drawn first or last. If first, always pass.
11043
+ },
11044
+ primitive: {
11045
+ topology: "triangle-list",
11046
+ cullMode: "none"
11047
+ // Inside the box
11048
+ }
11049
+ });
11050
+ this.bindGroupHelper = new SkyboxBindGroupHelper(device, bindGroupLayout, this.uniformBuffer, this.sampler);
11051
+ }
11052
+ draw(passEncoder, options) {
11053
+ let viewProjection;
11054
+ let useNative = 0;
11055
+ if (options.cameraState) {
11056
+ const view = this.matrixBuilder.buildViewMatrix(options.cameraState);
11057
+ const projection = this.matrixBuilder.buildProjectionMatrix(options.cameraState);
11058
+ view[12] = 0;
11059
+ view[13] = 0;
11060
+ view[14] = 0;
11061
+ const vp = glMatrix.mat4.create();
11062
+ glMatrix.mat4.multiply(vp, projection, view);
11063
+ viewProjection = vp;
11064
+ useNative = 1;
11065
+ } else if (options.viewProjection) {
11066
+ viewProjection = options.viewProjection;
11067
+ useNative = 0;
11068
+ } else {
11069
+ throw new Error("SkyboxPipeline: Either cameraState or viewProjection must be provided");
11070
+ }
11071
+ const uniformData = new Float32Array(20);
11072
+ uniformData.set(viewProjection);
11073
+ uniformData[16] = options.scroll[0];
11074
+ uniformData[17] = options.scroll[1];
11075
+ uniformData[18] = useNative;
11076
+ this.device.queue.writeBuffer(this.uniformBuffer, 0, uniformData);
11077
+ const bindGroup = this.bindGroupHelper.getBindGroup(options.cubemap);
11078
+ passEncoder.setPipeline(this.pipeline);
11079
+ passEncoder.setBindGroup(0, bindGroup);
11080
+ passEncoder.setVertexBuffer(0, this.vertexBuffer);
11081
+ passEncoder.draw(SKYBOX_POSITIONS2.length / 3);
11082
+ }
11083
+ destroy() {
11084
+ this.vertexBuffer.destroy();
11085
+ this.uniformBuffer.destroy();
11086
+ }
11087
+ };
11088
+ var SkyboxBindGroupHelper = class {
11089
+ constructor(device, layout, uniformBuffer, sampler) {
11090
+ this.device = device;
11091
+ this.layout = layout;
11092
+ this.uniformBuffer = uniformBuffer;
11093
+ this.sampler = sampler;
11094
+ this.bindGroupCache = /* @__PURE__ */ new Map();
11095
+ }
11096
+ getBindGroup(cubemap) {
11097
+ if (this.bindGroupCache.has(cubemap)) {
11098
+ return this.bindGroupCache.get(cubemap);
11099
+ }
11100
+ const bindGroup = this.device.createBindGroup({
11101
+ layout: this.layout,
11102
+ entries: [
11103
+ {
11104
+ binding: 0,
11105
+ resource: { buffer: this.uniformBuffer }
11106
+ },
11107
+ {
11108
+ binding: 1,
11109
+ resource: cubemap.createView()
11110
+ },
11111
+ {
11112
+ binding: 2,
11113
+ resource: this.sampler
11114
+ }
11115
+ ]
11116
+ });
11117
+ this.bindGroupCache.set(cubemap, bindGroup);
11118
+ return bindGroup;
11119
+ }
11120
+ };
11121
+
11122
+ // raw-loader:/home/runner/work/quake2/quake2/quake2ts/packages/engine/src/render/webgpu/shaders/bsp.wgsl
11123
+ var bsp_default = "struct DLight {\n position: vec3<f32>,\n intensity: f32,\n color: vec3<f32>,\n padding: f32,\n}\n\nstruct FrameUniforms {\n viewProjection: mat4x4<f32>, // 0-64\n cameraPosition: vec3<f32>, // 64-76\n pad0: f32, // 76-80 (Explicit padding to align next field to 80)\n time: f32, // 80-84\n brightness: f32, // 84-88\n gamma: f32, // 88-92\n ambient: f32, // 92-96\n numDlights: u32, // 96-100\n fullbright: u32, // 100-104\n pad1: vec2<f32>, // 104-112 (Aligns next field to 112)\n pad2: vec4<f32>, // 112-128 (Aligns next field to 128)\n dlights: array<DLight, 32>, // Starts at 128\n}\n\nstruct SurfaceUniforms {\n texScroll: vec2<f32>,\n lightmapScroll: vec2<f32>,\n lightStyleFactors: vec4<f32>,\n styleLayerMapping: vec4<f32>,\n solidColor: vec4<f32>,\n alpha: f32,\n applyLightmap: u32,\n warp: u32,\n lightmapOnly: u32,\n renderMode: u32, // 0: Texture, 1: Solid, 2: Faceted\n padding: vec3<f32>,\n}\n\n@group(0) @binding(0) var<uniform> frame: FrameUniforms;\n@group(1) @binding(0) var<uniform> surface: SurfaceUniforms;\n@group(2) @binding(0) var diffuseMap: texture_2d<f32>;\n@group(2) @binding(1) var diffuseSampler: sampler;\n@group(2) @binding(2) var lightmapAtlas: texture_2d<f32>;\n@group(2) @binding(3) var lightmapSampler: sampler;\n\nstruct VertexInput {\n @location(0) position: vec3<f32>,\n @location(1) texCoord: vec2<f32>,\n @location(2) lightmapCoord: vec2<f32>,\n @location(3) lightmapStep: f32,\n}\n\nstruct VertexOutput {\n @builtin(position) position: vec4<f32>,\n @location(0) texCoord: vec2<f32>,\n @location(1) lightmapCoord: vec2<f32>,\n @location(2) lightmapStep: f32,\n @location(3) worldPos: vec3<f32>,\n @location(4) screenPos: vec4<f32>,\n}\n\n@vertex\nfn vertexMain(input: VertexInput) -> VertexOutput {\n var output: VertexOutput;\n\n var pos = input.position;\n var tex = input.texCoord;\n var lm = input.lightmapCoord;\n\n // Vertex Warping\n if (surface.warp != 0u) {\n let amp = 0.125;\n let s = tex.x + sin((tex.y * 0.125 + frame.time) * 1.0) * amp;\n let t = tex.y + sin((tex.x * 0.125 + frame.time) * 1.0) * amp;\n tex = vec2<f32>(s, t);\n }\n\n output.texCoord = tex + surface.texScroll;\n output.lightmapCoord = lm + surface.lightmapScroll;\n output.lightmapStep = input.lightmapStep;\n output.worldPos = pos;\n output.position = frame.viewProjection * vec4<f32>(pos, 1.0);\n output.screenPos = output.position;\n\n return output;\n}\n\n@fragment\nfn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {\n var finalColor: vec4<f32>;\n\n if (surface.renderMode == 0u) {\n // TEXTURED MODE\n var base = vec4<f32>(1.0, 1.0, 1.0, 1.0);\n if (surface.lightmapOnly == 0u) {\n base = textureSample(diffuseMap, diffuseSampler, input.texCoord);\n }\n\n var totalLight = vec3<f32>(0.0, 0.0, 0.0);\n\n if (frame.fullbright != 0u) {\n totalLight = vec3<f32>(1.0, 1.0, 1.0);\n } else {\n // Apply Lightmaps\n if (surface.applyLightmap != 0u) {\n var hasLight = false;\n for (var i = 0; i < 4; i++) {\n let layer = surface.styleLayerMapping[i];\n let factor = surface.lightStyleFactors[i];\n\n if (layer >= -0.5) {\n let offset = vec2<f32>(0.0, layer * input.lightmapStep);\n totalLight += textureSample(lightmapAtlas, lightmapSampler, input.lightmapCoord + offset).rgb * factor;\n hasLight = true;\n }\n }\n\n // If lightmap enabled but no active layers, fallback to white?\n // Quake 2 logic usually implies at least one style is active if surfaced has SURF_LIGHT\n // But if we have no lightmap data, we start black.\n if (!hasLight) {\n // Fallback to avoid pitch black if lightmap intended but missing?\n // totalLight = vec3<f32>(1.0, 1.0, 1.0);\n }\n }\n\n // Apply Dynamic Lights\n for (var i = 0u; i < 32u; i++) {\n if (i >= frame.numDlights) {\n break;\n }\n let dlight = frame.dlights[i];\n let dist = distance(input.worldPos, dlight.position);\n\n if (dist < dlight.intensity) {\n let contribution = (dlight.intensity - dist) * (1.0 / 255.0);\n totalLight += dlight.color * contribution;\n }\n }\n }\n\n totalLight = max(totalLight, vec3<f32>(frame.ambient));\n totalLight *= frame.brightness;\n base = vec4<f32>(base.rgb * totalLight, base.a);\n\n // Gamma correction\n if (frame.gamma != 1.0) {\n base = vec4<f32>(pow(base.rgb, vec3<f32>(1.0 / frame.gamma)), base.a);\n }\n\n finalColor = vec4<f32>(base.rgb, base.a * surface.alpha);\n\n } else {\n // SOLID / WIREFRAME / FACETED\n var color = surface.solidColor.rgb;\n if (surface.renderMode == 2u) {\n // FACETED\n let fdx = dpdx(input.worldPos);\n let fdy = dpdy(input.worldPos);\n let faceNormal = normalize(cross(fdx, fdy));\n let lightDir = normalize(vec3<f32>(0.5, 0.5, 1.0));\n let diff = max(dot(faceNormal, lightDir), 0.2);\n color *= diff;\n }\n finalColor = vec4<f32>(color, surface.solidColor.a * surface.alpha);\n }\n\n // Alpha Test (Simple discard for fence textures if alpha is very low)\n if (finalColor.a < 0.01) {\n discard;\n }\n\n return finalColor;\n}\n";
11124
+
11125
+ // src/render/webgpu/pipelines/bspPipeline.ts
11126
+ var DEFAULT_STYLE_INDICES2 = [0, 255, 255, 255];
11127
+ var DEFAULT_STYLE_LAYERS2 = [0, -1, -1, -1];
11128
+ function resolveLightStyles2(styleIndices = DEFAULT_STYLE_INDICES2, styleValues = []) {
11129
+ const factors = new Float32Array(4);
11130
+ for (let i = 0; i < 4; i += 1) {
11131
+ const styleIndex = styleIndices[i] ?? 255;
11132
+ if (styleIndex === 255) {
11133
+ factors[i] = 0;
11134
+ continue;
11135
+ }
11136
+ const value = styleValues[styleIndex];
11137
+ factors[i] = value !== void 0 ? value : 1;
11138
+ }
11139
+ return factors;
11140
+ }
11141
+ function computeFlowOffset2(timeSeconds) {
11142
+ const cycle = timeSeconds * 0.25 % 1;
11143
+ return [-cycle, 0];
11144
+ }
11145
+ function deriveSurfaceRenderState2(surfaceFlags = shared.SURF_NONE, timeSeconds = 0) {
11146
+ const flowing = (surfaceFlags & shared.SURF_FLOWING) !== 0;
11147
+ const warp = (surfaceFlags & shared.SURF_WARP) !== 0;
11148
+ const sky = (surfaceFlags & shared.SURF_SKY) !== 0;
11149
+ const trans33 = (surfaceFlags & shared.SURF_TRANS33) !== 0;
11150
+ const trans66 = (surfaceFlags & shared.SURF_TRANS66) !== 0;
11151
+ const alpha = trans33 ? 0.33 : trans66 ? 0.66 : 1;
11152
+ const blend = trans33 || trans66 || warp;
11153
+ const depthWrite = !blend && !sky;
11154
+ const flowOffset = flowing ? computeFlowOffset2(timeSeconds) : [0, 0];
11155
+ return {
11156
+ alpha,
11157
+ blend,
11158
+ depthWrite,
11159
+ warp,
11160
+ flowOffset,
11161
+ sky
11162
+ };
11163
+ }
11164
+ var BspSurfacePipeline3 = class {
11165
+ constructor(device, format, depthFormat) {
11166
+ // Cache for texture bind groups
11167
+ this.textureBindGroupCache = /* @__PURE__ */ new Map();
11168
+ this.device = device;
11169
+ const frameBufferSize = 2048;
11170
+ const surfaceBufferSize = 256;
11171
+ this.frameUniformBuffer = device.createBuffer({
11172
+ size: frameBufferSize,
11173
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
11174
+ });
11175
+ this.surfaceUniformBuffer = device.createBuffer({
11176
+ size: surfaceBufferSize,
11177
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
11178
+ });
11179
+ this.frameBindGroupLayout = device.createBindGroupLayout({
11180
+ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }]
11181
+ });
11182
+ this.surfaceBindGroupLayout = device.createBindGroupLayout({
11183
+ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }]
11184
+ });
11185
+ this.textureBindGroupLayout = device.createBindGroupLayout({
11186
+ entries: [
11187
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: {} },
11188
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
11189
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} },
11190
+ { binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} }
11191
+ ]
11192
+ });
11193
+ this.defaultSampler = createLinearSampler(device);
11194
+ this.defaultWhiteTexture = new Texture2D2(device, {
11195
+ width: 1,
11196
+ height: 1,
11197
+ format: "rgba8unorm",
11198
+ label: "bsp-default-white"
11199
+ });
11200
+ this.defaultWhiteTexture.upload(new Uint8Array([255, 255, 255, 255]));
11201
+ this.defaultBindGroup = device.createBindGroup({
11202
+ layout: this.textureBindGroupLayout,
11203
+ entries: [
11204
+ { binding: 0, resource: this.defaultWhiteTexture.createView() },
11205
+ { binding: 1, resource: this.defaultSampler.sampler },
11206
+ { binding: 2, resource: this.defaultWhiteTexture.createView() },
11207
+ // Use white texture as lightmap fallback
11208
+ { binding: 3, resource: this.defaultSampler.sampler }
11209
+ ]
11210
+ });
11211
+ this.createPipelines(format, depthFormat);
11212
+ this.frameBindGroup = device.createBindGroup({
11213
+ layout: this.frameBindGroupLayout,
11214
+ entries: [{ binding: 0, resource: { buffer: this.frameUniformBuffer } }]
11215
+ });
11216
+ }
11217
+ createPipelines(format, depthFormat) {
11218
+ const module = this.device.createShaderModule({
11219
+ code: bsp_default
11220
+ });
11221
+ const pipelineLayout = this.device.createPipelineLayout({
11222
+ bindGroupLayouts: [
11223
+ this.frameBindGroupLayout,
11224
+ this.surfaceBindGroupLayout,
11225
+ this.textureBindGroupLayout
11226
+ ]
11227
+ });
11228
+ const vertexState = {
11229
+ module,
11230
+ entryPoint: "vertexMain",
11231
+ buffers: [
11232
+ {
11233
+ // Interleaved buffer: pos(3), tex(2), lm(2), step(1) -> 8 floats -> 32 bytes
11234
+ arrayStride: 32,
11235
+ attributes: [
11236
+ { shaderLocation: 0, offset: 0, format: "float32x3" },
11237
+ // Position
11238
+ { shaderLocation: 1, offset: 12, format: "float32x2" },
11239
+ // TexCoord
11240
+ { shaderLocation: 2, offset: 20, format: "float32x2" },
11241
+ // LightmapCoord
11242
+ { shaderLocation: 3, offset: 28, format: "float32" }
11243
+ // LightmapStep
11244
+ ]
11245
+ }
11246
+ ]
11247
+ };
11248
+ this.pipeline = this.device.createRenderPipeline({
11249
+ layout: pipelineLayout,
11250
+ vertex: vertexState,
11251
+ fragment: {
11252
+ module,
11253
+ entryPoint: "fragmentMain",
11254
+ targets: [{
11255
+ format,
11256
+ blend: {
11257
+ color: {
11258
+ srcFactor: "src-alpha",
11259
+ dstFactor: "one-minus-src-alpha",
11260
+ operation: "add"
11261
+ },
11262
+ alpha: {
11263
+ srcFactor: "one",
11264
+ dstFactor: "one-minus-src-alpha",
11265
+ operation: "add"
11266
+ }
11267
+ }
11268
+ }]
11269
+ },
11270
+ primitive: {
11271
+ topology: "triangle-list",
11272
+ cullMode: "back"
11273
+ // Quake 2 is usually back-face culled
11274
+ },
11275
+ depthStencil: {
11276
+ format: depthFormat,
11277
+ depthWriteEnabled: true,
11278
+ depthCompare: "less"
11279
+ }
11280
+ });
11281
+ this.pipelineWireframe = this.device.createRenderPipeline({
11282
+ layout: pipelineLayout,
11283
+ vertex: vertexState,
11284
+ fragment: {
11285
+ module,
11286
+ entryPoint: "fragmentMain",
11287
+ targets: [{ format, writeMask: GPUColorWrite.ALL }]
11288
+ },
11289
+ primitive: {
11290
+ topology: "line-list"
11291
+ },
11292
+ depthStencil: {
11293
+ format: depthFormat,
11294
+ depthWriteEnabled: true,
11295
+ depthCompare: "less"
11296
+ }
11297
+ });
11298
+ }
11299
+ bind(passEncoder, options) {
11300
+ const {
11301
+ modelViewProjection,
11302
+ styleIndices = DEFAULT_STYLE_INDICES2,
11303
+ styleLayers = DEFAULT_STYLE_LAYERS2,
11304
+ styleValues = [],
11305
+ diffuseTexture,
11306
+ diffuseSampler,
11307
+ lightmapTexture,
11308
+ lightmapSampler,
11309
+ surfaceFlags = shared.SURF_NONE,
11310
+ timeSeconds = 0,
11311
+ texScroll,
11312
+ alpha,
11313
+ warp,
11314
+ dlights = [],
11315
+ renderMode,
11316
+ lightmapOnly,
11317
+ brightness = 1,
11318
+ gamma = 1,
11319
+ fullbright = false,
11320
+ ambient = 0,
11321
+ cameraPosition = [0, 0, 0]
11322
+ } = options;
11323
+ const state = deriveSurfaceRenderState2(surfaceFlags, timeSeconds);
11324
+ const styles = resolveLightStyles2(styleIndices, styleValues);
11325
+ const finalScrollX = texScroll ? texScroll[0] : state.flowOffset[0];
11326
+ const finalScrollY = texScroll ? texScroll[1] : state.flowOffset[1];
11327
+ const finalAlpha = alpha !== void 0 ? alpha : state.alpha;
11328
+ const finalWarp = warp !== void 0 ? warp : state.warp;
11329
+ const frameData = new Float32Array(512);
11330
+ frameData.set(modelViewProjection, 0);
11331
+ frameData.set(cameraPosition, 16);
11332
+ frameData[19] = 0;
11333
+ frameData[20] = timeSeconds;
11334
+ frameData[21] = brightness;
11335
+ frameData[22] = gamma;
11336
+ frameData[23] = ambient;
11337
+ const numDlights = Math.min(dlights.length, MAX_DLIGHTS);
11338
+ const numDlightsView = new Uint32Array(frameData.buffer, 24 * 4, 1);
11339
+ numDlightsView[0] = numDlights;
11340
+ const fullbrightView = new Uint32Array(frameData.buffer, 25 * 4, 1);
11341
+ fullbrightView[0] = fullbright ? 1 : 0;
11342
+ let lightOffset = 32;
11343
+ for (let i = 0; i < numDlights; i++) {
11344
+ const l = dlights[i];
11345
+ frameData[lightOffset + 0] = l.origin.x;
11346
+ frameData[lightOffset + 1] = l.origin.y;
11347
+ frameData[lightOffset + 2] = l.origin.z;
11348
+ frameData[lightOffset + 3] = l.intensity;
11349
+ frameData[lightOffset + 4] = l.color.x;
11350
+ frameData[lightOffset + 5] = l.color.y;
11351
+ frameData[lightOffset + 6] = l.color.z;
11352
+ frameData[lightOffset + 7] = 0;
11353
+ lightOffset += 8;
11354
+ }
11355
+ this.device.queue.writeBuffer(this.frameUniformBuffer, 0, frameData, 0, lightOffset);
11356
+ const surfaceData = new Float32Array(32);
11357
+ surfaceData[0] = finalScrollX;
11358
+ surfaceData[1] = finalScrollY;
11359
+ surfaceData[2] = state.flowOffset[0];
11360
+ surfaceData[3] = state.flowOffset[1];
11361
+ surfaceData.set(styles, 4);
11362
+ surfaceData.set(styleLayers, 8);
11363
+ let modeInt = 0;
11364
+ let color = [1, 1, 1, 1];
11365
+ if (renderMode) {
11366
+ if (renderMode.mode === "solid" || renderMode.mode === "wireframe") {
11367
+ modeInt = 1;
11368
+ } else if (renderMode.mode === "solid-faceted") {
11369
+ modeInt = 2;
11370
+ }
11371
+ if (renderMode.color) {
11372
+ color = [...renderMode.color];
11373
+ } else if (renderMode.generateRandomColor) {
11374
+ color = [1, 1, 1, 1];
11375
+ }
11376
+ }
11377
+ surfaceData.set(color, 12);
11378
+ surfaceData[16] = finalAlpha;
11379
+ const surfaceUint = new Uint32Array(surfaceData.buffer);
11380
+ const applyLightmap = !state.sky && lightmapSampler !== void 0 && !finalWarp;
11381
+ surfaceUint[17] = applyLightmap ? 1 : 0;
11382
+ surfaceUint[18] = finalWarp ? 1 : 0;
11383
+ surfaceUint[19] = lightmapOnly ? 1 : 0;
11384
+ surfaceUint[20] = modeInt;
11385
+ this.device.queue.writeBuffer(this.surfaceUniformBuffer, 0, surfaceData);
11386
+ const surfaceBindGroup = this.device.createBindGroup({
11387
+ layout: this.surfaceBindGroupLayout,
11388
+ entries: [{ binding: 0, resource: { buffer: this.surfaceUniformBuffer } }]
11389
+ });
11390
+ if (diffuseTexture && diffuseSampler && lightmapTexture && lightmapSampler) {
11391
+ const textureBindGroup = this.device.createBindGroup({
11392
+ layout: this.textureBindGroupLayout,
11393
+ entries: [
11394
+ { binding: 0, resource: diffuseTexture },
11395
+ { binding: 1, resource: diffuseSampler },
11396
+ { binding: 2, resource: lightmapTexture },
11397
+ { binding: 3, resource: lightmapSampler }
11398
+ ]
11399
+ });
11400
+ passEncoder.setBindGroup(2, textureBindGroup);
11401
+ } else {
11402
+ passEncoder.setBindGroup(2, this.defaultBindGroup);
11403
+ }
11404
+ const pipeline = renderMode && renderMode.mode === "wireframe" ? this.pipelineWireframe : this.pipeline;
11405
+ passEncoder.setPipeline(pipeline);
11406
+ passEncoder.setBindGroup(0, this.frameBindGroup);
11407
+ passEncoder.setBindGroup(1, surfaceBindGroup);
11408
+ return state;
11409
+ }
11410
+ draw(passEncoder, geometry, renderMode) {
11411
+ if (!geometry.gpuIndexBuffer) return;
11412
+ if (renderMode && renderMode.mode === "wireframe") ; else {
11413
+ passEncoder.setVertexBuffer(0, geometry.gpuVertexBuffer);
11414
+ passEncoder.setIndexBuffer(geometry.gpuIndexBuffer, "uint16");
11415
+ passEncoder.drawIndexed(geometry.indexCount);
11416
+ }
11417
+ }
11418
+ destroy() {
11419
+ this.frameUniformBuffer.destroy();
11420
+ this.surfaceUniformBuffer.destroy();
11421
+ this.defaultWhiteTexture.destroy();
11422
+ }
11423
+ };
11424
+
11425
+ // src/render/webgpu/renderer.ts
11426
+ var WebGPURendererImpl = class {
11427
+ constructor(context, frameRenderer, pipelines) {
11428
+ this.context = context;
11429
+ this.frameRenderer = frameRenderer;
11430
+ this.pipelines = pipelines;
11431
+ this.type = "webgpu";
11432
+ // Texture cache for registered pics
11433
+ this.picCache = /* @__PURE__ */ new Map();
11434
+ this.font = null;
11435
+ // 2D rendering state
11436
+ this.is2DActive = false;
11437
+ this.whiteTexture = new Texture2D2(context.device, {
11438
+ width: 1,
11439
+ height: 1,
11440
+ format: context.format,
11441
+ label: "white-texture"
11442
+ });
11443
+ this.whiteTexture.upload(new Uint8Array([255, 255, 255, 255]));
11444
+ this.collisionVis = null;
11445
+ this.debug = null;
11446
+ this.particleSystem = null;
11447
+ }
11448
+ get device() {
11449
+ return this.context.device;
11450
+ }
11451
+ get width() {
11452
+ return this.context.width;
11453
+ }
11454
+ get height() {
11455
+ return this.context.height;
11456
+ }
11457
+ // =========================================================================
11458
+ // Frame Rendering
11459
+ // =========================================================================
11460
+ renderFrame(options, entities = [], renderOptions) {
11461
+ const localOptions = options;
11462
+ let culledLights = localOptions.dlights;
11463
+ if (localOptions.dlights && localOptions.dlights.length > 0) {
11464
+ const viewProjection = new Float32Array(localOptions.camera.viewProjectionMatrix);
11465
+ const frustumPlanes = extractFrustumPlanes(viewProjection);
11466
+ const cameraPos = { x: localOptions.camera.position[0], y: localOptions.camera.position[1], z: localOptions.camera.position[2] };
11467
+ culledLights = cullLights(
11468
+ localOptions.dlights,
11469
+ frustumPlanes,
11470
+ cameraPos,
11471
+ 32
11472
+ // Max lights
11473
+ );
11474
+ }
11475
+ const augmentedOptions = {
11476
+ ...localOptions,
11477
+ dlights: culledLights
11478
+ };
11479
+ this.frameRenderer.renderFrame(augmentedOptions);
11480
+ }
11481
+ // =========================================================================
11482
+ // Geometry Management
11483
+ // =========================================================================
11484
+ uploadBspGeometry(surfaces) {
11485
+ for (const surface of surfaces) {
11486
+ if (surface.gpuVertexBuffer && surface.gpuIndexBuffer) continue;
11487
+ const vb = new VertexBuffer2(this.device, {
11488
+ size: surface.vertexData.byteLength,
11489
+ label: `bsp-surface-vb-${surface.texture}`
11490
+ });
11491
+ vb.write(surface.vertexData);
11492
+ const ib = new IndexBuffer2(this.device, {
11493
+ size: surface.indexData.byteLength,
11494
+ label: `bsp-surface-ib-${surface.texture}`
11495
+ });
11496
+ ib.write(surface.indexData);
11497
+ const mutableSurface = surface;
11498
+ mutableSurface.gpuVertexBuffer = vb.buffer;
11499
+ mutableSurface.gpuIndexBuffer = ib.buffer;
11500
+ }
11501
+ }
11502
+ // =========================================================================
11503
+ // Texture Management
11504
+ // =========================================================================
11505
+ async registerPic(name, data) {
11506
+ if (this.picCache.has(name)) {
11507
+ return this.picCache.get(name);
11508
+ }
11509
+ const texture = new Texture2D2(this.device, {
11510
+ width: 256,
11511
+ height: 256,
11512
+ format: this.context.format,
11513
+ label: `pic-${name}`
11514
+ });
11515
+ texture.upload(data);
11516
+ this.picCache.set(name, texture);
11517
+ return texture;
11518
+ }
11519
+ registerTexture(name, texture) {
11520
+ if (this.picCache.has(name)) {
11521
+ return this.picCache.get(name);
11522
+ }
11523
+ const gpuTexture = new Texture2D2(this.device, {
11524
+ width: texture.width,
11525
+ height: texture.height,
11526
+ format: this.context.format,
11527
+ mipLevelCount: texture.levels.length,
11528
+ label: `texture-${name}`
11529
+ });
11530
+ for (const mipLevel of texture.levels) {
11531
+ gpuTexture.upload(mipLevel.rgba, {
11532
+ width: mipLevel.width,
11533
+ height: mipLevel.height,
11534
+ mipLevel: mipLevel.level
11535
+ });
11536
+ }
11537
+ this.picCache.set(name, gpuTexture);
11538
+ return gpuTexture;
11539
+ }
11540
+ // =========================================================================
11541
+ // 2D Drawing API
11542
+ // =========================================================================
11543
+ begin2D() {
11544
+ this.frameRenderer.begin2DPass();
11545
+ this.is2DActive = true;
11546
+ }
11547
+ end2D() {
11548
+ this.frameRenderer.end2DPass();
11549
+ this.is2DActive = false;
11550
+ }
11551
+ drawPic(x, y, pic, color = [1, 1, 1, 1]) {
11552
+ if (!this.is2DActive) {
11553
+ throw new Error("drawPic called outside begin2D/end2D");
11554
+ }
11555
+ this.pipelines.sprite.drawTexturedQuad(
11556
+ x,
11557
+ y,
11558
+ pic.width,
11559
+ pic.height,
11560
+ pic,
11561
+ 0,
11562
+ 0,
11563
+ 1,
11564
+ 1,
11565
+ color
11566
+ );
11567
+ }
11568
+ drawfillRect(x, y, width, height, color) {
11569
+ if (!this.is2DActive) {
11570
+ throw new Error("drawfillRect called outside begin2D/end2D");
11571
+ }
11572
+ this.pipelines.sprite.drawSolidRect(x, y, width, height, color);
11573
+ }
11574
+ drawString(x, y, text, color = [1, 1, 1, 1]) {
11575
+ if (!this.is2DActive) {
11576
+ throw new Error("drawString called outside begin2D/end2D");
11577
+ }
11578
+ if (!this.font) {
11579
+ return;
11580
+ }
11581
+ const segments = parseColorString(text);
11582
+ let currentX = x;
11583
+ const charWidth = 8;
11584
+ const charHeight = 8;
11585
+ for (const segment of segments) {
11586
+ const segmentColor = segment.color || color;
11587
+ for (const char of segment.text) {
11588
+ const code = char.charCodeAt(0);
11589
+ const col = code % 16;
11590
+ const row = Math.floor(code / 16);
11591
+ const u0 = col / 16;
11592
+ const v0 = row / 16;
11593
+ const u1 = (col + 1) / 16;
11594
+ const v1 = (row + 1) / 16;
11595
+ this.pipelines.sprite.drawTexturedQuad(
11596
+ currentX,
11597
+ y,
11598
+ charWidth,
11599
+ charHeight,
11600
+ this.font,
11601
+ u0,
11602
+ v0,
11603
+ u1,
11604
+ v1,
11605
+ segmentColor
11606
+ );
11607
+ currentX += charWidth;
11608
+ }
11609
+ }
11610
+ }
11611
+ drawCenterString(y, text) {
11612
+ if (!this.is2DActive) {
11613
+ throw new Error("drawCenterString called outside begin2D/end2D");
11614
+ }
11615
+ const stripped = text.replace(/\^[0-9]/g, "");
11616
+ const charWidth = 8;
11617
+ const width = stripped.length * charWidth;
11618
+ const x = (this.width - width) / 2;
11619
+ this.drawString(x, y, text);
11620
+ }
11621
+ // =========================================================================
11622
+ // Entity Highlighting (Stubs)
11623
+ // =========================================================================
11624
+ setEntityHighlight(entityId, color) {
11625
+ }
11626
+ clearEntityHighlight(entityId) {
11627
+ }
11628
+ highlightSurface(faceIndex, color) {
11629
+ }
11630
+ removeSurfaceHighlight(faceIndex) {
11631
+ }
11632
+ // =========================================================================
11633
+ // Render Settings (Stubs)
11634
+ // =========================================================================
11635
+ setDebugMode(mode) {
11636
+ }
11637
+ setBrightness(value) {
11638
+ }
11639
+ setGamma(value) {
11640
+ }
11641
+ setFullbright(enabled) {
11642
+ }
11643
+ setAmbient(value) {
11644
+ }
11645
+ setLightStyle(index, pattern) {
11646
+ }
11647
+ setUnderwaterWarp(enabled) {
11648
+ }
11649
+ setBloom(enabled) {
11650
+ }
11651
+ setBloomIntensity(value) {
11652
+ }
11653
+ setLodBias(bias) {
11654
+ }
11655
+ // =========================================================================
11656
+ // Instanced Rendering (Stub)
11657
+ // =========================================================================
11658
+ renderInstanced(model, instances) {
11659
+ }
11660
+ // =========================================================================
11661
+ // Performance and Memory
11662
+ // =========================================================================
11663
+ getPerformanceReport() {
11664
+ return {
11665
+ frameTimeMs: 0,
11666
+ gpuTimeMs: 0,
11667
+ cpuFrameTimeMs: 0,
11668
+ drawCalls: 0,
11669
+ triangles: 0,
11670
+ vertices: 0,
11671
+ textureBinds: 0,
11672
+ shaderSwitches: 0,
11673
+ visibleSurfaces: 0,
11674
+ culledSurfaces: 0,
11675
+ visibleEntities: 0,
11676
+ culledEntities: 0,
11677
+ memoryUsageMB: {
11678
+ textures: 0,
11679
+ geometry: 0,
11680
+ total: 0
11681
+ }
11682
+ };
11683
+ }
11684
+ getMemoryUsage() {
11685
+ return {
11686
+ texturesBytes: 0,
11687
+ geometryBytes: 0,
11688
+ shadersBytes: 0,
11689
+ buffersBytes: 0,
11690
+ totalBytes: 0
11691
+ };
11692
+ }
11693
+ // =========================================================================
11694
+ // Lifecycle
11695
+ // =========================================================================
11696
+ resize(width, height) {
11697
+ if (this.context.context && !this.context.isHeadless) {
11698
+ this.context.width = width;
11699
+ this.context.height = height;
11700
+ } else {
11701
+ this.context.width = width;
11702
+ this.context.height = height;
11703
+ }
11704
+ }
11705
+ dispose() {
11706
+ this.pipelines.sprite.destroy();
11707
+ this.pipelines.skybox.destroy();
11708
+ this.pipelines.bsp.destroy();
11709
+ for (const texture of this.picCache.values()) {
11710
+ texture.destroy();
11711
+ }
11712
+ this.picCache.clear();
11713
+ this.whiteTexture.destroy();
11714
+ this.context.device.destroy();
11715
+ }
11716
+ destroy() {
11717
+ this.dispose();
11718
+ }
11719
+ };
11720
+ async function createWebGPURenderer(canvas, options) {
11721
+ const context = await createWebGPUContext(canvas, options);
11722
+ const spriteRenderer = new SpriteRenderer2(context.device, context.format);
11723
+ const skyboxPipeline = new SkyboxPipeline3(context.device, context.format);
11724
+ const bspPipeline = new BspSurfacePipeline3(
11725
+ context.device,
11726
+ context.format,
11727
+ context.depthFormat || "depth24plus"
11728
+ );
11729
+ const pipelines = {
11730
+ sprite: spriteRenderer,
11731
+ skybox: skyboxPipeline,
11732
+ bsp: bspPipeline
11733
+ };
11734
+ if (canvas) {
11735
+ context.width = canvas.width;
11736
+ context.height = canvas.height;
11737
+ } else {
11738
+ context.width = 800;
11739
+ context.height = 600;
11740
+ }
11741
+ const frameRenderer = new FrameRenderer(context, pipelines);
11742
+ return new WebGPURendererImpl(context, frameRenderer, pipelines);
11743
+ }
9617
11744
  var DemoReader = class {
9618
11745
  constructor(buffer) {
9619
11746
  this.messageOffsets = [];
@@ -17430,83 +19557,6 @@ function validateEntity(entity) {
17430
19557
  };
17431
19558
  }
17432
19559
 
17433
- // src/render/webgpu/context.ts
17434
- async function createWebGPUContext(canvas, options) {
17435
- if (!navigator.gpu) {
17436
- throw new Error("WebGPU is not supported in this environment");
17437
- }
17438
- const adapter = await navigator.gpu.requestAdapter({
17439
- powerPreference: options?.powerPreference || "high-performance"
17440
- });
17441
- if (!adapter) {
17442
- throw new Error("No appropriate GPUAdapter found");
17443
- }
17444
- if (options?.requiredFeatures) {
17445
- for (const feature of options.requiredFeatures) {
17446
- if (!adapter.features.has(feature)) {
17447
- throw new Error(`Required feature not available: ${feature}`);
17448
- }
17449
- }
17450
- }
17451
- const deviceDescriptor = {
17452
- requiredFeatures: options?.requiredFeatures,
17453
- requiredLimits: options?.requiredLimits
17454
- };
17455
- const device = await adapter.requestDevice(deviceDescriptor);
17456
- let context;
17457
- let format = "rgba8unorm";
17458
- const depthFormat = "depth24plus";
17459
- let isHeadless = true;
17460
- let width = options?.width || 800;
17461
- let height = options?.height || 600;
17462
- if (canvas) {
17463
- context = canvas.getContext("webgpu");
17464
- if (!context) {
17465
- throw new Error("Failed to get WebGPU context from canvas");
17466
- }
17467
- isHeadless = false;
17468
- format = navigator.gpu.getPreferredCanvasFormat();
17469
- width = canvas.width;
17470
- height = canvas.height;
17471
- context.configure({
17472
- device,
17473
- format,
17474
- alphaMode: "opaque"
17475
- // Standard for game rendering
17476
- });
17477
- }
17478
- const features = /* @__PURE__ */ new Set();
17479
- for (const feature of adapter.features) {
17480
- features.add(feature);
17481
- }
17482
- return {
17483
- adapter,
17484
- device,
17485
- context,
17486
- format,
17487
- depthFormat,
17488
- features,
17489
- limits: device.limits,
17490
- isHeadless,
17491
- width,
17492
- height
17493
- };
17494
- }
17495
- function queryCapabilities(state) {
17496
- const { features, limits } = state;
17497
- return {
17498
- hasTimestampQuery: features.has("timestamp-query"),
17499
- hasDepthClipControl: features.has("depth-clip-control"),
17500
- hasTextureCompressionBC: features.has("texture-compression-bc"),
17501
- hasTextureCompressionETC2: features.has("texture-compression-etc2"),
17502
- hasTextureCompressionASTC: features.has("texture-compression-astc"),
17503
- maxTextureDimension2D: limits.maxTextureDimension2D,
17504
- maxBindGroups: limits.maxBindGroups,
17505
- maxUniformBufferBindingSize: limits.maxUniformBufferBindingSize,
17506
- maxStorageBufferBindingSize: limits.maxStorageBufferBindingSize
17507
- };
17508
- }
17509
-
17510
19560
  // src/render/webgpu/headless.ts
17511
19561
  function createHeadlessRenderTarget(device, width, height, format = "rgba8unorm") {
17512
19562
  const usage = typeof GPUTextureUsage !== "undefined" ? GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC : 17;
@@ -17690,16 +19740,6 @@ var NullRenderer = class {
17690
19740
  return this.frameCount;
17691
19741
  }
17692
19742
  };
17693
-
17694
- // src/render/types/coordinates.ts
17695
- var CoordinateSystem = /* @__PURE__ */ ((CoordinateSystem2) => {
17696
- CoordinateSystem2["QUAKE"] = "quake";
17697
- CoordinateSystem2["OPENGL"] = "opengl";
17698
- CoordinateSystem2["WEBGPU"] = "webgpu";
17699
- return CoordinateSystem2;
17700
- })(CoordinateSystem || {});
17701
-
17702
- // src/render/matrix/webgl.ts
17703
19743
  var WebGLMatrixBuilder = class {
17704
19744
  constructor() {
17705
19745
  this.coordinateSystem = "opengl" /* OPENGL */;
@@ -17766,89 +19806,6 @@ var WebGLMatrixBuilder = class {
17766
19806
  return view;
17767
19807
  }
17768
19808
  };
17769
- var WebGPUMatrixBuilder = class {
17770
- constructor() {
17771
- this.coordinateSystem = "webgpu" /* WEBGPU */;
17772
- }
17773
- buildProjectionMatrix(camera) {
17774
- const projection = glMatrix.mat4.create();
17775
- const f = 1 / Math.tan(camera.fov * shared.DEG2RAD / 2);
17776
- const rangeInv = 1 / (camera.near - camera.far);
17777
- projection[0] = f / camera.aspect;
17778
- projection[5] = f;
17779
- projection[10] = camera.far * rangeInv;
17780
- projection[11] = -1;
17781
- projection[14] = camera.near * camera.far * rangeInv;
17782
- return projection;
17783
- }
17784
- buildViewMatrix(camera) {
17785
- const [pitch, yaw, roll] = camera.angles;
17786
- const pitchRad = pitch * shared.DEG2RAD;
17787
- const yawRad = yaw * shared.DEG2RAD;
17788
- const rollRad = roll * shared.DEG2RAD;
17789
- const rotationQuake = glMatrix.mat4.create();
17790
- glMatrix.mat4.identity(rotationQuake);
17791
- glMatrix.mat4.rotateZ(rotationQuake, rotationQuake, -yawRad);
17792
- glMatrix.mat4.rotateY(rotationQuake, rotationQuake, -pitchRad);
17793
- glMatrix.mat4.rotateX(rotationQuake, rotationQuake, -rollRad);
17794
- glMatrix.mat4.fromValues(
17795
- 0,
17796
- 0,
17797
- -1,
17798
- 0,
17799
- // Column 0: Quake X maps to View Z? Wait.
17800
- -1,
17801
- 0,
17802
- 0,
17803
- 0,
17804
- // Column 1: Quake Y maps to View X?
17805
- 0,
17806
- 1,
17807
- 0,
17808
- 0,
17809
- // Column 2: Quake Z maps to View Y?
17810
- 0,
17811
- 0,
17812
- 0,
17813
- 1
17814
- );
17815
- const quakeToWgpu = glMatrix.mat4.fromValues(
17816
- 0,
17817
- 0,
17818
- -1,
17819
- 0,
17820
- // Col 0: X -> -Z
17821
- -1,
17822
- 0,
17823
- 0,
17824
- 0,
17825
- // Col 1: Y -> -X
17826
- 0,
17827
- 1,
17828
- 0,
17829
- 0,
17830
- // Col 2: Z -> Y
17831
- 0,
17832
- 0,
17833
- 0,
17834
- 1
17835
- );
17836
- const rotationView = glMatrix.mat4.create();
17837
- glMatrix.mat4.multiply(rotationView, quakeToWgpu, rotationQuake);
17838
- const cameraPos = glMatrix.vec3.fromValues(
17839
- camera.position[0],
17840
- camera.position[1],
17841
- camera.position[2]
17842
- );
17843
- const t = glMatrix.vec3.transformMat4(glMatrix.vec3.create(), cameraPos, rotationView);
17844
- glMatrix.vec3.negate(t, t);
17845
- const view = glMatrix.mat4.clone(rotationView);
17846
- view[12] = t[0];
17847
- view[13] = t[1];
17848
- view[14] = t[2];
17849
- return view;
17850
- }
17851
- };
17852
19809
  var IdentityMatrixBuilder = class {
17853
19810
  constructor() {
17854
19811
  this.coordinateSystem = "quake" /* QUAKE */;
@@ -18561,6 +20518,7 @@ exports.BSP_SURFACE_FRAGMENT_SOURCE = BSP_SURFACE_FRAGMENT_SOURCE;
18561
20518
  exports.BSP_SURFACE_VERTEX_SOURCE = BSP_SURFACE_VERTEX_SOURCE;
18562
20519
  exports.BSP_VERTEX_LAYOUT = BSP_VERTEX_LAYOUT;
18563
20520
  exports.BspLoader = BspLoader;
20521
+ exports.BspLump = BspLump;
18564
20522
  exports.BspParseError = BspParseError;
18565
20523
  exports.BspSurfacePipeline = BspSurfacePipeline;
18566
20524
  exports.Camera = Camera;
@@ -18668,6 +20626,7 @@ exports.createProgramFromSources = createProgramFromSources;
18668
20626
  exports.createRenderer = createRenderer;
18669
20627
  exports.createWebGLContext = createWebGLContext;
18670
20628
  exports.createWebGPUContext = createWebGPUContext;
20629
+ exports.createWebGPURenderer = createWebGPURenderer;
18671
20630
  exports.decodeOgg = decodeOgg;
18672
20631
  exports.deriveSurfaceRenderState = deriveSurfaceRenderState;
18673
20632
  exports.detectFileType = detectFileType;