bun-webgpu
Advanced tools
| export interface BlockBuffer { | ||
| __type: "BlockBuffer"; | ||
| buffer: ArrayBuffer; | ||
| index: number; | ||
| } | ||
| /** | ||
| * Buffer pool with minimal overhead. | ||
| * To control when ArrayBuffers are allocated and freed | ||
| * and to avoid some gc runs. | ||
| */ | ||
| export declare class BufferPool { | ||
| private buffers; | ||
| readonly blockSize: number; | ||
| private freeBlocks; | ||
| private readonly minBlocks; | ||
| private readonly maxBlocks; | ||
| private currentBlocks; | ||
| private allocatedCount; | ||
| private bufferToBlockIndex; | ||
| constructor(minBlocks: number, maxBlocks: number, blockSize: number); | ||
| private initializePool; | ||
| private expandPool; | ||
| /** | ||
| * Request a block. Returns an object with the pre-allocated ArrayBuffer and its index, or throws if out of memory. | ||
| */ | ||
| request(): BlockBuffer; | ||
| /** | ||
| * Release a block using the ArrayBuffer returned from request(). | ||
| */ | ||
| release(buffer: ArrayBuffer): void; | ||
| /** | ||
| * Release a block by its index. | ||
| */ | ||
| releaseBlock(blockIndex: number): void; | ||
| /** | ||
| * Get the ArrayBuffer for a specific block index. | ||
| */ | ||
| getBuffer(blockIndex: number): ArrayBuffer; | ||
| reset(): void; | ||
| get totalBlockCount(): number; | ||
| get maxBlockCount(): number; | ||
| get minBlockCount(): number; | ||
| get allocatedBlockCount(): number; | ||
| get freeBlockCount(): number; | ||
| get hasAvailableBlocks(): boolean; | ||
| get utilizationRatio(): number; | ||
| } |
+29
| export declare const TextureUsageFlags: { | ||
| readonly COPY_SRC: number; | ||
| readonly COPY_DST: number; | ||
| readonly TEXTURE_BINDING: number; | ||
| readonly STORAGE_BINDING: number; | ||
| readonly RENDER_ATTACHMENT: number; | ||
| readonly TRANSIENT_ATTACHMENT: number; | ||
| }; | ||
| export declare const BufferUsageFlags: { | ||
| readonly MAP_READ: number; | ||
| readonly MAP_WRITE: number; | ||
| readonly COPY_SRC: number; | ||
| readonly COPY_DST: number; | ||
| readonly INDEX: number; | ||
| readonly VERTEX: number; | ||
| readonly UNIFORM: number; | ||
| readonly STORAGE: number; | ||
| readonly INDIRECT: number; | ||
| readonly QUERY_RESOLVE: number; | ||
| }; | ||
| export declare const ShaderStageFlags: { | ||
| readonly VERTEX: number; | ||
| readonly FRAGMENT: number; | ||
| readonly COMPUTE: number; | ||
| }; | ||
| export declare const MapModeFlags: { | ||
| readonly READ: number; | ||
| readonly WRITE: number; | ||
| }; |
+545
| import { FFIType } from "bun:ffi"; | ||
| type StripZWPrefix<KeyType extends string> = KeyType extends `zw${infer Rest}` ? `w${Rest}` : KeyType; | ||
| type TransformedSymbolKeys<T extends object> = { | ||
| [K in keyof T as StripZWPrefix<K & string>]: T[K]; | ||
| }; | ||
| export declare function loadLibrary(libPath?: string): TransformedSymbolKeys<import("bun:ffi").ConvertFns<{ | ||
| zwgpuCreateInstance: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuInstanceCreateSurface: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuInstanceProcessEvents: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuInstanceRequestAdapter: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.uint64_t; | ||
| }; | ||
| zwgpuInstanceWaitAny: { | ||
| args: (FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.uint32_t; | ||
| }; | ||
| zwgpuInstanceGetWGSLLanguageFeatures: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.uint32_t; | ||
| }; | ||
| zwgpuInstanceRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuInstanceAddRef: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuAdapterCreateDevice: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuAdapterGetInfo: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.uint32_t; | ||
| }; | ||
| zwgpuAdapterRequestDevice: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.uint64_t; | ||
| }; | ||
| zwgpuAdapterRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuAdapterGetFeatures: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuAdapterGetLimits: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.uint32_t; | ||
| }; | ||
| zwgpuDeviceGetAdapterInfo: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.uint32_t; | ||
| }; | ||
| zwgpuDeviceCreateBuffer: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuDeviceCreateTexture: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuDeviceCreateSampler: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuDeviceCreateShaderModule: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuDeviceCreateBindGroupLayout: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuDeviceCreateBindGroup: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuDeviceCreatePipelineLayout: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuDeviceCreateRenderPipeline: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuDeviceCreateComputePipeline: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuDeviceCreateRenderBundleEncoder: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuDeviceCreateCommandEncoder: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuDeviceCreateQuerySet: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuDeviceGetQueue: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuDeviceGetLimits: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.uint32_t; | ||
| }; | ||
| zwgpuDeviceHasFeature: { | ||
| args: (FFIType.uint32_t | FFIType.ptr)[]; | ||
| returns: FFIType.bool; | ||
| }; | ||
| zwgpuDeviceGetFeatures: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuDevicePushErrorScope: { | ||
| args: (FFIType.uint32_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuDevicePopErrorScope: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.uint64_t; | ||
| }; | ||
| zwgpuDeviceTick: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuDeviceInjectError: { | ||
| args: (FFIType.uint32_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuDeviceCreateComputePipelineAsync: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.uint64_t; | ||
| }; | ||
| zwgpuDeviceCreateRenderPipelineAsync: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.uint64_t; | ||
| }; | ||
| zwgpuDeviceDestroy: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuDeviceRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuBufferGetMappedRange: { | ||
| args: (FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuBufferGetConstMappedRange: { | ||
| args: (FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuBufferUnmap: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuBufferMapAsync: { | ||
| args: (FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.uint64_t; | ||
| }; | ||
| zwgpuBufferDestroy: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuBufferRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuTextureCreateView: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuTextureDestroy: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuTextureRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuTextureViewRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuSamplerRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuShaderModuleGetCompilationInfo: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.uint64_t; | ||
| }; | ||
| zwgpuShaderModuleRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuBindGroupLayoutRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuBindGroupRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuPipelineLayoutRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuQuerySetDestroy: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuQuerySetRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPipelineRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPipelineGetBindGroupLayout: { | ||
| args: (FFIType.uint32_t | FFIType.ptr)[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuComputePipelineRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuComputePipelineGetBindGroupLayout: { | ||
| args: (FFIType.uint32_t | FFIType.ptr)[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuCommandEncoderBeginRenderPass: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuCommandEncoderBeginComputePass: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuCommandEncoderClearBuffer: { | ||
| args: (FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuCommandEncoderCopyBufferToBuffer: { | ||
| args: (FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuCommandEncoderCopyBufferToTexture: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuCommandEncoderCopyTextureToBuffer: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuCommandEncoderCopyTextureToTexture: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuCommandEncoderResolveQuerySet: { | ||
| args: (FFIType.uint32_t | FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuCommandEncoderFinish: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuCommandEncoderRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuCommandEncoderPushDebugGroup: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuCommandEncoderPopDebugGroup: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuCommandEncoderInsertDebugMarker: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderSetScissorRect: { | ||
| args: (FFIType.uint32_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderSetViewport: { | ||
| args: (FFIType.float | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderSetBlendConstant: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderSetStencilReference: { | ||
| args: (FFIType.uint32_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderSetPipeline: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderSetBindGroup: { | ||
| args: (FFIType.uint32_t | FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderSetVertexBuffer: { | ||
| args: (FFIType.uint32_t | FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderSetIndexBuffer: { | ||
| args: (FFIType.uint32_t | FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderDraw: { | ||
| args: (FFIType.uint32_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderDrawIndexed: { | ||
| args: (FFIType.int32_t | FFIType.uint32_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderDrawIndirect: { | ||
| args: (FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderDrawIndexedIndirect: { | ||
| args: (FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderExecuteBundles: { | ||
| args: (FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderEnd: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderPushDebugGroup: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderPopDebugGroup: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderInsertDebugMarker: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderBeginOcclusionQuery: { | ||
| args: (FFIType.uint32_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderPassEncoderEndOcclusionQuery: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuComputePassEncoderSetPipeline: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuComputePassEncoderSetBindGroup: { | ||
| args: (FFIType.uint32_t | FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuComputePassEncoderDispatchWorkgroups: { | ||
| args: (FFIType.uint32_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuComputePassEncoderDispatchWorkgroupsIndirect: { | ||
| args: (FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuComputePassEncoderEnd: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuComputePassEncoderRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuComputePassEncoderPushDebugGroup: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuComputePassEncoderPopDebugGroup: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuComputePassEncoderInsertDebugMarker: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuCommandBufferRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuQueueSubmit: { | ||
| args: (FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuQueueWriteBuffer: { | ||
| args: (FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuQueueWriteTexture: { | ||
| args: (FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuQueueOnSubmittedWorkDone: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.uint64_t; | ||
| }; | ||
| zwgpuQueueRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuSurfaceConfigure: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuSurfaceUnconfigure: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuSurfaceGetCurrentTexture: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuSurfacePresent: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuSurfaceRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuAdapterInfoFreeMembers: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuSurfaceCapabilitiesFreeMembers: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuSupportedFeaturesFreeMembers: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuSharedBufferMemoryEndAccessStateFreeMembers: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuSharedTextureMemoryEndAccessStateFreeMembers: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuSupportedWGSLLanguageFeaturesFreeMembers: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderBundleRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderBundleEncoderDraw: { | ||
| args: (FFIType.uint32_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderBundleEncoderDrawIndexed: { | ||
| args: (FFIType.int32_t | FFIType.uint32_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderBundleEncoderDrawIndirect: { | ||
| args: (FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderBundleEncoderDrawIndexedIndirect: { | ||
| args: (FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderBundleEncoderFinish: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.ptr; | ||
| }; | ||
| zwgpuRenderBundleEncoderSetBindGroup: { | ||
| args: (FFIType.uint32_t | FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderBundleEncoderSetIndexBuffer: { | ||
| args: (FFIType.uint32_t | FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderBundleEncoderSetPipeline: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderBundleEncoderSetVertexBuffer: { | ||
| args: (FFIType.uint32_t | FFIType.uint64_t | FFIType.ptr)[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderBundleEncoderRelease: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderBundleEncoderPushDebugGroup: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderBundleEncoderPopDebugGroup: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| zwgpuRenderBundleEncoderInsertDebugMarker: { | ||
| args: FFIType.ptr[]; | ||
| returns: FFIType.void; | ||
| }; | ||
| }>>; | ||
| export type FFISymbols = ReturnType<typeof loadLibrary>; | ||
| export {}; |
+31
| import { type Pointer } from "bun:ffi"; | ||
| import { type FFISymbols } from "./ffi"; | ||
| export declare class InstanceTicker { | ||
| readonly instancePtr: Pointer; | ||
| private lib; | ||
| private _waiting; | ||
| private _ticking; | ||
| private _accTime; | ||
| private _lastTime; | ||
| constructor(instancePtr: Pointer, lib: FFISymbols); | ||
| register(): void; | ||
| unregister(): void; | ||
| hasWaiting(): boolean; | ||
| processEvents(): void; | ||
| private scheduleTick; | ||
| } | ||
| export declare class GPUImpl implements GPU { | ||
| private instancePtr; | ||
| private lib; | ||
| __brand: "GPU"; | ||
| private _destroyed; | ||
| private _ticker; | ||
| private _wgslLanguageFeatures; | ||
| constructor(instancePtr: Pointer, lib: FFISymbols); | ||
| getPreferredCanvasFormat(): GPUTextureFormat; | ||
| get wgslLanguageFeatures(): WGSLLanguageFeatures; | ||
| requestAdapter(options?: GPURequestAdapterOptions & { | ||
| featureLevel?: 'core' | 'compatibility'; | ||
| }): Promise<GPUAdapter | null>; | ||
| destroy(): undefined; | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| import type { InstanceTicker } from "./GPU"; | ||
| export declare class GPUUncapturedErrorEventImpl extends Event implements GPUUncapturedErrorEvent { | ||
| __brand: "GPUUncapturedErrorEvent"; | ||
| error: GPUError; | ||
| constructor(error: GPUError); | ||
| } | ||
| export declare class GPUAdapterImpl implements GPUAdapter { | ||
| readonly adapterPtr: Pointer; | ||
| private instancePtr; | ||
| private lib; | ||
| private instanceTicker; | ||
| __brand: "GPUAdapter"; | ||
| private _features; | ||
| private _limits; | ||
| private _info; | ||
| private _destroyed; | ||
| private _device; | ||
| private _state; | ||
| constructor(adapterPtr: Pointer, instancePtr: Pointer, lib: FFISymbols, instanceTicker: InstanceTicker); | ||
| get info(): GPUAdapterInfo; | ||
| get features(): GPUSupportedFeatures; | ||
| get limits(): GPUSupportedLimits; | ||
| get isFallbackAdapter(): boolean; | ||
| private handleUncapturedError; | ||
| private handleDeviceLost; | ||
| requestDevice(descriptor?: GPUDeviceDescriptor): Promise<GPUDevice>; | ||
| destroy(): undefined; | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| export declare class GPUBindGroupImpl implements GPUBindGroup { | ||
| __brand: "GPUBindGroup"; | ||
| label: string; | ||
| readonly ptr: Pointer; | ||
| private lib; | ||
| constructor(ptr: Pointer, lib: FFISymbols, label?: string); | ||
| destroy(): undefined; | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| export declare class GPUBindGroupLayoutImpl implements GPUBindGroupLayout { | ||
| __brand: "GPUBindGroupLayout"; | ||
| label: string; | ||
| readonly ptr: Pointer; | ||
| private lib; | ||
| constructor(ptr: Pointer, lib: FFISymbols, label?: string); | ||
| destroy(): undefined; | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| import type { InstanceTicker } from "./GPU"; | ||
| import type { GPUDeviceImpl } from "./GPUDevice"; | ||
| import { EventEmitter } from "events"; | ||
| export declare class GPUBufferImpl extends EventEmitter implements GPUBuffer { | ||
| readonly bufferPtr: Pointer; | ||
| private readonly device; | ||
| private lib; | ||
| private instanceTicker; | ||
| private _size; | ||
| private _descriptor; | ||
| private _mapState; | ||
| private _pendingMap; | ||
| private _mapCallback; | ||
| private _mapCallbackPromiseData; | ||
| private _destroyed; | ||
| private _mappedOffset; | ||
| private _mappedSize; | ||
| private _returnedRanges; | ||
| private _detachableArrayBuffers; | ||
| __brand: "GPUBuffer"; | ||
| label: string; | ||
| ptr: Pointer; | ||
| constructor(bufferPtr: Pointer, device: GPUDeviceImpl, lib: FFISymbols, descriptor: GPUBufferDescriptor, instanceTicker: InstanceTicker); | ||
| private _checkRangeOverlap; | ||
| private _createDetachableArrayBuffer; | ||
| get size(): GPUSize64; | ||
| get usage(): GPUFlagsConstant; | ||
| get mapState(): GPUBufferMapState; | ||
| mapAsync(mode: GPUMapModeFlags, offset?: GPUSize64, size?: GPUSize64): Promise<undefined>; | ||
| private _validateAlignment; | ||
| getMappedRangePtr(offset?: GPUSize64, size?: GPUSize64): Pointer; | ||
| _getConstMappedRangePtr(offset: GPUSize64, size: GPUSize64): Pointer; | ||
| getMappedRange(offset?: GPUSize64, size?: GPUSize64): ArrayBuffer; | ||
| _getConstMappedRange(offset: GPUSize64, size: GPUSize64): ArrayBuffer; | ||
| _detachBuffers(): void; | ||
| unmap(): undefined; | ||
| release(): undefined; | ||
| destroy(): undefined; | ||
| } |
| import type { Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| export declare class GPUCommandBufferImpl implements GPUCommandBuffer { | ||
| readonly bufferPtr: Pointer; | ||
| private lib; | ||
| __brand: "GPUCommandBuffer"; | ||
| label: string; | ||
| readonly ptr: Pointer; | ||
| constructor(bufferPtr: Pointer, lib: FFISymbols, label?: string); | ||
| _destroy(): undefined; | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| export declare class GPUCommandEncoderImpl implements GPUCommandEncoder { | ||
| readonly encoderPtr: Pointer; | ||
| private lib; | ||
| __brand: "GPUCommandEncoder"; | ||
| label: string; | ||
| readonly ptr: Pointer; | ||
| private _destroyed; | ||
| constructor(encoderPtr: Pointer, lib: FFISymbols); | ||
| beginRenderPass(descriptor: GPURenderPassDescriptor): GPURenderPassEncoder; | ||
| beginComputePass(descriptor?: GPUComputePassDescriptor): GPUComputePassEncoder; | ||
| copyBufferToBuffer(source: GPUBuffer, destination: GPUBuffer, size?: number): undefined; | ||
| copyBufferToBuffer(source: GPUBuffer, sourceOffset: number, destination: GPUBuffer, destinationOffset: number, size: number): undefined; | ||
| copyBufferToTexture(source: GPUTexelCopyBufferInfo, destination: GPUTexelCopyTextureInfo, copySize: GPUExtent3DStrict): undefined; | ||
| copyTextureToBuffer(source: GPUTexelCopyTextureInfo, destination: GPUTexelCopyBufferInfo, copySize: GPUExtent3DStrict): undefined; | ||
| copyTextureToTexture(source: GPUTexelCopyTextureInfo, destination: GPUTexelCopyTextureInfo, copySize: GPUExtent3DStrict): undefined; | ||
| clearBuffer(buffer: GPUBuffer, offset?: GPUSize64, size?: GPUSize64): undefined; | ||
| resolveQuerySet(querySet: GPUQuerySet, firstQuery: GPUSize32, queryCount: GPUSize32, destination: GPUBuffer, destinationOffset: GPUSize64): undefined; | ||
| finish(descriptor?: GPUCommandBufferDescriptor): GPUCommandBuffer; | ||
| pushDebugGroup(message: string): undefined; | ||
| popDebugGroup(): undefined; | ||
| insertDebugMarker(markerLabel: string): undefined; | ||
| /** | ||
| * Note: Command encoders are destroyed automatically when finished. | ||
| */ | ||
| _destroy(): undefined; | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import { type FFISymbols } from "./ffi"; | ||
| export declare class GPUComputePassEncoderImpl implements GPUComputePassEncoder { | ||
| ptr: Pointer; | ||
| __brand: "GPUComputePassEncoder"; | ||
| private lib; | ||
| label: string; | ||
| constructor(ptr: Pointer, lib: FFISymbols); | ||
| setPipeline(pipeline: GPUComputePipeline): undefined; | ||
| setBindGroup(groupIndex: number, bindGroup: GPUBindGroup | null, dynamicOffsets?: Uint32Array | number[]): undefined; | ||
| dispatchWorkgroups(workgroupCountX: number, workgroupCountY?: number, workgroupCountZ?: number): undefined; | ||
| dispatchWorkgroupsIndirect(indirectBuffer: GPUBuffer, indirectOffset: number | bigint): undefined; | ||
| end(): undefined; | ||
| pushDebugGroup(message: string): undefined; | ||
| popDebugGroup(): undefined; | ||
| insertDebugMarker(markerLabel: string): undefined; | ||
| destroy(): undefined; | ||
| } |
| import type { Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| export declare class GPUComputePipelineImpl implements GPUComputePipeline { | ||
| private lib; | ||
| __brand: "GPUComputePipeline"; | ||
| label: string; | ||
| readonly ptr: Pointer; | ||
| constructor(ptr: Pointer, lib: FFISymbols, label?: string); | ||
| getBindGroupLayout(index: number): GPUBindGroupLayout; | ||
| destroy(): undefined; | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import { type FFISymbols } from "./ffi"; | ||
| import type { InstanceTicker } from "./GPU"; | ||
| import { EventEmitter } from "events"; | ||
| type EventListenerOptions = any; | ||
| export type DeviceErrorCallback = (this: GPUDevice, ev: GPUUncapturedErrorEvent) => any; | ||
| export declare class DeviceTicker { | ||
| readonly devicePtr: Pointer; | ||
| private lib; | ||
| private _waiting; | ||
| private _ticking; | ||
| constructor(devicePtr: Pointer, lib: FFISymbols); | ||
| register(): void; | ||
| unregister(): void; | ||
| hasWaiting(): boolean; | ||
| private scheduleTick; | ||
| } | ||
| export declare class GPUDeviceImpl extends EventEmitter implements GPUDevice { | ||
| readonly devicePtr: Pointer; | ||
| private lib; | ||
| private instanceTicker; | ||
| readonly ptr: Pointer; | ||
| readonly queuePtr: Pointer; | ||
| private _queue; | ||
| private _userUncapturedErrorCallback; | ||
| private _ticker; | ||
| private _lost; | ||
| private _lostPromiseResolve; | ||
| private _features; | ||
| private _limits; | ||
| private _info; | ||
| private _destroyed; | ||
| private _errorScopePopId; | ||
| private _popErrorScopeCallback; | ||
| private _popErrorScopePromises; | ||
| private _createComputePipelineAsyncCallback; | ||
| private _createComputePipelineAsyncPromises; | ||
| private _createRenderPipelineAsyncCallback; | ||
| private _createRenderPipelineAsyncPromises; | ||
| private _buffers; | ||
| __brand: "GPUDevice"; | ||
| label: string; | ||
| constructor(devicePtr: Pointer, lib: FFISymbols, instanceTicker: InstanceTicker); | ||
| tick(): undefined; | ||
| addEventListener<K extends keyof __GPUDeviceEventMap>(type: K, listener: (this: GPUDevice, ev: __GPUDeviceEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; | ||
| removeEventListener<K extends keyof __GPUDeviceEventMap>(type: K, listener: (this: GPUDevice, ev: __GPUDeviceEventMap[K]) => any, options?: boolean | EventListenerOptions): void; | ||
| handleUncapturedError(event: GPUUncapturedErrorEvent): void; | ||
| handleDeviceLost(reason: GPUDeviceLostReason, message: string, override?: boolean): void; | ||
| dispatchEvent(event: Event): boolean; | ||
| pushErrorScope(filter: GPUErrorFilter): undefined; | ||
| popErrorScope(): Promise<GPUError | null>; | ||
| injectError(type: 'validation' | 'out-of-memory' | 'internal', message: string): undefined; | ||
| set onuncapturederror(listener: DeviceErrorCallback | null); | ||
| get lost(): Promise<GPUDeviceLostInfo>; | ||
| get features(): GPUSupportedFeatures; | ||
| get limits(): GPUSupportedLimits; | ||
| get adapterInfo(): GPUAdapterInfo; | ||
| get queue(): GPUQueue; | ||
| destroy(): undefined; | ||
| createBuffer(descriptor: GPUBufferDescriptor): GPUBuffer; | ||
| createTexture(descriptor: GPUTextureDescriptor): GPUTexture; | ||
| createSampler(descriptor?: GPUSamplerDescriptor): GPUSampler; | ||
| importExternalTexture(descriptor: GPUExternalTextureDescriptor): GPUExternalTexture; | ||
| createBindGroupLayout(descriptor: GPUBindGroupLayoutDescriptor): GPUBindGroupLayout; | ||
| createPipelineLayout(descriptor: GPUPipelineLayoutDescriptor): GPUPipelineLayout; | ||
| createShaderModule(descriptor: GPUShaderModuleDescriptor): GPUShaderModule; | ||
| createBindGroup(descriptor: GPUBindGroupDescriptor): GPUBindGroup; | ||
| _prepareComputePipelineDescriptor(descriptor: GPUComputePipelineDescriptor): ArrayBuffer; | ||
| createComputePipeline(descriptor: GPUComputePipelineDescriptor): GPUComputePipeline; | ||
| _prepareRenderPipelineDescriptor(descriptor: GPURenderPipelineDescriptor): ArrayBuffer; | ||
| createRenderPipeline(descriptor: GPURenderPipelineDescriptor): GPURenderPipeline; | ||
| createCommandEncoder(descriptor?: GPUCommandEncoderDescriptor): GPUCommandEncoder; | ||
| createComputePipelineAsync(descriptor: GPUComputePipelineDescriptor): Promise<GPUComputePipeline>; | ||
| createRenderPipelineAsync(descriptor: GPURenderPipelineDescriptor): Promise<GPURenderPipeline>; | ||
| createRenderBundleEncoder(descriptor: GPURenderBundleEncoderDescriptor): GPURenderBundleEncoder; | ||
| createQuerySet(descriptor: GPUQuerySetDescriptor): GPUQuerySet; | ||
| } | ||
| export {}; |
| import type { Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| export declare class GPUPipelineLayoutImpl implements GPUPipelineLayout { | ||
| private lib; | ||
| __brand: "GPUPipelineLayout"; | ||
| label: string; | ||
| readonly ptr: Pointer; | ||
| constructor(ptr: Pointer, lib: FFISymbols, label?: string); | ||
| destroy(): undefined; | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import { type FFISymbols } from "./ffi"; | ||
| export declare class GPUQuerySetImpl implements GPUQuerySet { | ||
| readonly ptr: Pointer; | ||
| private lib; | ||
| readonly type: GPUQueryType; | ||
| readonly count: number; | ||
| __brand: "GPUQuerySet"; | ||
| label: string; | ||
| constructor(ptr: Pointer, lib: FFISymbols, type: GPUQueryType, count: number, label?: string); | ||
| destroy(): undefined; | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| import { InstanceTicker } from "./GPU"; | ||
| export type PtrSource = ArrayBuffer | ArrayBufferView; | ||
| export declare const QueueWorkDoneStatus: { | ||
| readonly Success: 1; | ||
| readonly CallbackCancelled: 2; | ||
| readonly Error: 3; | ||
| readonly Force32: 2147483647; | ||
| }; | ||
| export declare class GPUQueueImpl implements GPUQueue { | ||
| readonly ptr: Pointer; | ||
| private lib; | ||
| private instanceTicker; | ||
| __brand: "GPUQueue"; | ||
| label: string; | ||
| private _onSubmittedWorkDoneCallback; | ||
| private _onSubmittedWorkDoneResolves; | ||
| private _onSubmittedWorkDoneRejects; | ||
| constructor(ptr: Pointer, lib: FFISymbols, instanceTicker: InstanceTicker); | ||
| submit(commandBuffers: Iterable<GPUCommandBuffer>): undefined; | ||
| onSubmittedWorkDone(): Promise<undefined>; | ||
| writeBuffer(buffer: GPUBuffer, bufferOffset: number, data: PtrSource, dataOffset?: number, size?: number): undefined; | ||
| writeTexture(destination: GPUTexelCopyTextureInfo, data: PtrSource, dataLayout: GPUTexelCopyBufferLayout, writeSize: GPUExtent3DStrict): undefined; | ||
| copyBufferToBuffer(source: GPUTexelCopyBufferInfo, destination: GPUTexelCopyBufferInfo, size: number): undefined; | ||
| copyBufferToTexture(source: GPUTexelCopyBufferInfo, destination: GPUTexelCopyTextureInfo, size: GPUExtent3D): undefined; | ||
| copyExternalImageToTexture(source: GPUCopyExternalImageSourceInfo, destination: GPUCopyExternalImageDestInfo, copySize: GPUExtent3DStrict): undefined; | ||
| destroy(): undefined; | ||
| } |
| import type { Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| export declare class GPURenderBundleImpl implements GPURenderBundle { | ||
| __brand: "GPURenderBundle"; | ||
| label: string; | ||
| readonly ptr: Pointer; | ||
| private lib; | ||
| private _destroyed; | ||
| constructor(ptr: Pointer, lib: FFISymbols, label?: string); | ||
| destroy(): undefined; | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| export declare class GPURenderBundleEncoderImpl implements GPURenderBundleEncoder { | ||
| __brand: "GPURenderBundleEncoder"; | ||
| private _lib; | ||
| private _destroyed; | ||
| label: string; | ||
| ptr: Pointer; | ||
| constructor(ptr: Pointer, lib: FFISymbols, descriptor: GPURenderBundleEncoderDescriptor); | ||
| setBindGroup(groupIndex: number, bindGroup: GPUBindGroup | null, dynamicOffsets?: Uint32Array | number[]): undefined; | ||
| setPipeline(pipeline: GPURenderPipeline): undefined; | ||
| setIndexBuffer(buffer: GPUBuffer, indexFormat: GPUIndexFormat, offset?: number, size?: number): undefined; | ||
| setVertexBuffer(slot: number, buffer: GPUBuffer | null, offset?: number, size?: number): undefined; | ||
| draw(vertexCount: number, instanceCount?: number, firstVertex?: number, firstInstance?: number): undefined; | ||
| drawIndexed(indexCount: number, instanceCount?: number, firstIndex?: number, baseVertex?: number, firstInstance?: number): undefined; | ||
| drawIndirect(indirectBuffer: GPUBuffer, indirectOffset: number): undefined; | ||
| drawIndexedIndirect(indirectBuffer: GPUBuffer, indirectOffset: number): undefined; | ||
| finish(descriptor?: GPURenderBundleDescriptor): GPURenderBundle; | ||
| _destroy(): void; | ||
| pushDebugGroup(groupLabel: string): undefined; | ||
| popDebugGroup(): undefined; | ||
| insertDebugMarker(markerLabel: string): undefined; | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import { type FFISymbols } from "./ffi"; | ||
| export declare class GPURenderPassEncoderImpl implements GPURenderPassEncoder { | ||
| ptr: Pointer; | ||
| __brand: "GPURenderPassEncoder"; | ||
| label: string; | ||
| private lib; | ||
| constructor(ptr: Pointer, lib: FFISymbols); | ||
| setBlendConstant(color: GPUColor): undefined; | ||
| setStencilReference(reference: GPUStencilValue): undefined; | ||
| beginOcclusionQuery(queryIndex: GPUSize32): undefined; | ||
| endOcclusionQuery(): undefined; | ||
| executeBundles(bundles: Iterable<GPURenderBundle>): undefined; | ||
| setPipeline(pipeline: GPURenderPipeline): undefined; | ||
| setBindGroup(index: GPUIndex32, bindGroup: GPUBindGroup | null | undefined, dynamicOffsets?: Uint32Array | number[]): undefined; | ||
| setVertexBuffer(slot: number, buffer: GPUBuffer | null | undefined, offset?: number | bigint, size?: number | bigint): undefined; | ||
| setIndexBuffer(buffer: GPUBuffer | null | undefined, format: GPUIndexFormat, offset?: number | bigint, size?: number | bigint): undefined; | ||
| setViewport(x: number, y: number, width: number, height: number, minDepth: number, maxDepth: number): undefined; | ||
| setScissorRect(x: number, y: number, width: number, height: number): undefined; | ||
| draw(vertexCount: number, instanceCount?: number, firstVertex?: number, firstInstance?: number): undefined; | ||
| drawIndexed(indexCount: number, instanceCount?: number, firstIndex?: number, baseVertex?: number, firstInstance?: number): undefined; | ||
| drawIndirect(indirectBuffer: GPUBuffer, indirectOffset: number | bigint): undefined; | ||
| drawIndexedIndirect(indirectBuffer: GPUBuffer, indirectOffset: number): undefined; | ||
| end(): undefined; | ||
| pushDebugGroup(message: string): undefined; | ||
| popDebugGroup(): undefined; | ||
| insertDebugMarker(markerLabel: string): undefined; | ||
| destroy(): undefined; | ||
| } |
| import type { Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| export declare class GPURenderPipelineImpl implements GPURenderPipeline { | ||
| private lib; | ||
| __brand: "GPURenderPipeline"; | ||
| label: string; | ||
| readonly ptr: Pointer; | ||
| constructor(ptr: Pointer, lib: FFISymbols, label?: string); | ||
| getBindGroupLayout(index: number): GPUBindGroupLayout; | ||
| destroy(): undefined; | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| export declare class GPUSamplerImpl implements GPUSampler { | ||
| readonly samplerPtr: Pointer; | ||
| private lib; | ||
| __brand: "GPUSampler"; | ||
| label: string; | ||
| ptr: Pointer; | ||
| constructor(samplerPtr: Pointer, lib: FFISymbols, label?: string); | ||
| destroy(): undefined; | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import { type FFISymbols } from "./ffi"; | ||
| export declare class GPUShaderModuleImpl implements GPUShaderModule { | ||
| readonly ptr: Pointer; | ||
| private lib; | ||
| readonly label: string; | ||
| __brand: "GPUShaderModule"; | ||
| constructor(ptr: Pointer, lib: FFISymbols, label: string); | ||
| getCompilationInfo(): Promise<GPUCompilationInfo>; | ||
| destroy(): undefined; | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| export declare class GPUTextureImpl implements GPUTexture { | ||
| readonly texturePtr: Pointer; | ||
| private lib; | ||
| private _width; | ||
| private _height; | ||
| private _depthOrArrayLayers; | ||
| private _format; | ||
| private _dimension; | ||
| private _mipLevelCount; | ||
| private _sampleCount; | ||
| private _usage; | ||
| __brand: "GPUTexture"; | ||
| label: string; | ||
| ptr: Pointer; | ||
| constructor(texturePtr: Pointer, lib: FFISymbols, _width: number, _height: number, _depthOrArrayLayers: number, _format: GPUTextureFormat, _dimension: GPUTextureDimension, _mipLevelCount: number, _sampleCount: number, _usage: GPUTextureUsageFlags); | ||
| get width(): number; | ||
| get height(): number; | ||
| get depthOrArrayLayers(): number; | ||
| get format(): GPUTextureFormat; | ||
| get dimension(): GPUTextureDimension; | ||
| get mipLevelCount(): number; | ||
| get sampleCount(): number; | ||
| get usage(): GPUFlagsConstant; | ||
| createView(descriptor?: GPUTextureViewDescriptor): GPUTextureView; | ||
| destroy(): undefined; | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| export declare class GPUTextureViewImpl implements GPUTextureView { | ||
| readonly viewPtr: Pointer; | ||
| private lib; | ||
| __brand: "GPUTextureView"; | ||
| label: string; | ||
| ptr: Pointer; | ||
| constructor(viewPtr: Pointer, lib: FFISymbols, label?: string); | ||
| destroy(): undefined; | ||
| } |
Sorry, the diff of this file is too big to display
| export declare class GPUCanvasContextMock implements GPUCanvasContext { | ||
| readonly canvas: HTMLCanvasElement | OffscreenCanvas; | ||
| readonly __brand: "GPUCanvasContext"; | ||
| private _configuration; | ||
| private _currentTexture; | ||
| private _nextTexture; | ||
| private width; | ||
| private height; | ||
| private _device; | ||
| constructor(canvas: HTMLCanvasElement | OffscreenCanvas, width: number, height: number); | ||
| configure(descriptor: GPUCanvasConfiguration): undefined; | ||
| unconfigure(): undefined; | ||
| getConfiguration(): GPUCanvasConfigurationOut | null; | ||
| setSize(width: number, height: number): undefined; | ||
| private createTextures; | ||
| private createRenderTexture; | ||
| getCurrentTexture(): GPUTexture; | ||
| switchTextures(): GPUTexture; | ||
| } |
+74
| import { type Pointer } from "bun:ffi"; | ||
| export declare const AsyncStatus: { | ||
| readonly Success: 1; | ||
| readonly CallbackCancelled: 2; | ||
| readonly Error: 3; | ||
| readonly Aborted: 4; | ||
| readonly Force32: 2147483647; | ||
| }; | ||
| export declare const WGPUErrorType: { | ||
| readonly "no-error": 1; | ||
| readonly validation: 2; | ||
| readonly "out-of-memory": 3; | ||
| readonly internal: 4; | ||
| readonly unknown: 5; | ||
| readonly "force-32": 2147483647; | ||
| }; | ||
| export declare function packUserDataId(id: number): ArrayBuffer; | ||
| export declare function unpackUserDataId(userDataPtr: Pointer): number; | ||
| export declare class GPUAdapterInfoImpl implements GPUAdapterInfo { | ||
| __brand: "GPUAdapterInfo"; | ||
| vendor: string; | ||
| architecture: string; | ||
| device: string; | ||
| description: string; | ||
| subgroupMinSize: number; | ||
| subgroupMaxSize: number; | ||
| isFallbackAdapter: boolean; | ||
| constructor(); | ||
| } | ||
| export declare function normalizeIdentifier(input: string): string; | ||
| export declare function decodeCallbackMessage(messagePtr: Pointer | null, messageSize?: number | bigint): string; | ||
| export declare const DEFAULT_SUPPORTED_LIMITS: Omit<GPUSupportedLimits, '__brand'> & { | ||
| maxImmediateSize: number; | ||
| }; | ||
| export declare class GPUSupportedLimitsImpl implements GPUSupportedLimits { | ||
| __brand: "GPUSupportedLimits"; | ||
| maxTextureDimension1D: number; | ||
| maxTextureDimension2D: number; | ||
| maxTextureDimension3D: number; | ||
| maxTextureArrayLayers: number; | ||
| maxBindGroups: number; | ||
| maxBindGroupsPlusVertexBuffers: number; | ||
| maxBindingsPerBindGroup: number; | ||
| maxStorageBuffersInFragmentStage: number; | ||
| maxStorageBuffersInVertexStage: number; | ||
| maxStorageTexturesInFragmentStage: number; | ||
| maxStorageTexturesInVertexStage: number; | ||
| maxDynamicUniformBuffersPerPipelineLayout: number; | ||
| maxDynamicStorageBuffersPerPipelineLayout: number; | ||
| maxSampledTexturesPerShaderStage: number; | ||
| maxSamplersPerShaderStage: number; | ||
| maxStorageBuffersPerShaderStage: number; | ||
| maxStorageTexturesPerShaderStage: number; | ||
| maxUniformBuffersPerShaderStage: number; | ||
| maxUniformBufferBindingSize: number; | ||
| maxStorageBufferBindingSize: number; | ||
| minUniformBufferOffsetAlignment: number; | ||
| minStorageBufferOffsetAlignment: number; | ||
| maxVertexBuffers: number; | ||
| maxBufferSize: number; | ||
| maxVertexAttributes: number; | ||
| maxVertexBufferArrayStride: number; | ||
| maxInterStageShaderComponents: number; | ||
| maxInterStageShaderVariables: number; | ||
| maxColorAttachments: number; | ||
| maxColorAttachmentBytesPerSample: number; | ||
| maxComputeWorkgroupStorageSize: number; | ||
| maxComputeInvocationsPerWorkgroup: number; | ||
| maxComputeWorkgroupSizeX: number; | ||
| maxComputeWorkgroupSizeY: number; | ||
| maxComputeWorkgroupSizeZ: number; | ||
| maxComputeWorkgroupsPerDimension: number; | ||
| constructor(); | ||
| } |
Sorry, the diff of this file is too big to display
+115
| import { type Pointer } from "bun:ffi"; | ||
| export declare const pointerSize: number; | ||
| export type PrimitiveType = 'u8' | 'u16' | 'u32' | 'u64' | 'f32' | 'f64' | 'pointer' | 'i32' | 'i16' | 'bool_u8' | 'bool_u32'; | ||
| interface PointyObject { | ||
| ptr: Pointer | number | bigint | null; | ||
| } | ||
| interface ObjectPointerDef<T extends PointyObject> { | ||
| __type: 'objectPointer'; | ||
| } | ||
| /** | ||
| * Type helper for creating object pointers for structs. | ||
| */ | ||
| export declare function objectPtr<T extends PointyObject>(): ObjectPointerDef<T>; | ||
| type Prettify<T> = { | ||
| [K in keyof T]: T[K]; | ||
| } & {}; | ||
| export type Simplify<T> = T extends (...args: any[]) => any ? T : T extends object ? Prettify<T> : T; | ||
| type PrimitiveToTSType<T extends PrimitiveType> = T extends 'u8' | 'u16' | 'u32' | 'i16' | 'i32' | 'f32' | 'f64' ? number : T extends 'u64' ? bigint | number : T extends 'bool_u8' | 'bool_u32' ? boolean : T extends 'pointer' ? number | bigint : never; | ||
| type FieldDefInputType<Def> = Def extends PrimitiveType ? PrimitiveToTSType<Def> : Def extends 'cstring' | 'char*' ? string | null : Def extends EnumDef<infer E> ? keyof E : Def extends StructDef<any, infer InputType> ? InputType : Def extends ObjectPointerDef<infer T> ? T | null : Def extends readonly [infer InnerDef] ? InnerDef extends PrimitiveType ? Iterable<PrimitiveToTSType<InnerDef>> : InnerDef extends EnumDef<infer E> ? Iterable<keyof E> : InnerDef extends StructDef<any, infer InputType> ? Iterable<InputType> : InnerDef extends ObjectPointerDef<infer T> ? (T | null)[] : never : never; | ||
| type FieldDefOutputType<Def> = Def extends PrimitiveType ? PrimitiveToTSType<Def> : Def extends 'cstring' | 'char*' ? string | null : Def extends EnumDef<infer E> ? keyof E : Def extends StructDef<infer OutputType, any> ? OutputType : Def extends ObjectPointerDef<infer T> ? T | null : Def extends readonly [infer InnerDef] ? InnerDef extends PrimitiveType ? Iterable<PrimitiveToTSType<InnerDef>> : InnerDef extends EnumDef<infer E> ? Iterable<keyof E> : InnerDef extends StructDef<infer OutputType, any> ? Iterable<OutputType> : InnerDef extends ObjectPointerDef<infer T> ? (T | null)[] : never : never; | ||
| type IsOptional<Options extends StructFieldOptions | undefined> = Options extends { | ||
| optional: true; | ||
| } ? true : Options extends { | ||
| default: any; | ||
| } ? true : Options extends { | ||
| lengthOf: string; | ||
| } ? true : Options extends { | ||
| condition: () => boolean; | ||
| } ? true : false; | ||
| export type StructObjectInputType<Fields extends readonly StructField[]> = { | ||
| [F in Fields[number] as IsOptional<F[2]> extends false ? F[0] : never]: FieldDefInputType<F[1]>; | ||
| } & { | ||
| [F in Fields[number] as IsOptional<F[2]> extends true ? F[0] : never]?: FieldDefInputType<F[1]> | null; | ||
| }; | ||
| export type StructObjectOutputType<Fields extends readonly StructField[]> = { | ||
| [F in Fields[number] as IsOptional<F[2]> extends false ? F[0] : never]: FieldDefOutputType<F[1]>; | ||
| } & { | ||
| [F in Fields[number] as IsOptional<F[2]> extends true ? F[0] : never]?: FieldDefOutputType<F[1]> | null; | ||
| }; | ||
| type DefineStructReturnType<Fields extends readonly StructField[], Options extends StructDefOptions | undefined> = StructDef<Simplify<Options extends { | ||
| reduceValue: (value: any) => infer R; | ||
| } ? R : StructObjectOutputType<Fields>>, Simplify<Options extends { | ||
| mapValue: (value: infer V) => any; | ||
| } ? V : StructObjectInputType<Fields>>>; | ||
| interface AllocStructOptions { | ||
| lengths?: Record<string, number>; | ||
| } | ||
| interface AllocStructResult { | ||
| buffer: ArrayBuffer; | ||
| view: DataView; | ||
| subBuffers?: Record<string, ArrayBuffer>; | ||
| } | ||
| export declare function allocStruct(structDef: StructDef<any, any>, options?: AllocStructOptions): AllocStructResult; | ||
| export interface EnumDef<T extends Record<string, number>> { | ||
| __type: 'enum'; | ||
| type: Exclude<PrimitiveType, 'bool_u8' | 'bool_u32'>; | ||
| to(value: keyof T): number; | ||
| from(value: number | bigint): keyof T; | ||
| enum: T; | ||
| } | ||
| export declare function defineEnum<T extends Record<string, number>>(mapping: T, base?: Exclude<PrimitiveType, 'bool_u8' | 'bool_u32'>): EnumDef<T>; | ||
| type ValidationFunction = (value: any, fieldName: string, options: { | ||
| hints?: any; | ||
| input?: any; | ||
| }) => void | never; | ||
| interface StructFieldOptions { | ||
| optional?: boolean; | ||
| mapOptionalInline?: boolean; | ||
| unpackTransform?: (value: any) => any; | ||
| packTransform?: (value: any) => any; | ||
| lengthOf?: string; | ||
| asPointer?: boolean; | ||
| default?: any; | ||
| condition?: () => boolean; | ||
| validate?: ValidationFunction | ValidationFunction[]; | ||
| } | ||
| type StructField = readonly [string, PrimitiveType, StructFieldOptions?] | readonly [string, EnumDef<any>, StructFieldOptions?] | readonly [string, StructDef<any>, StructFieldOptions?] | readonly [string, 'cstring' | 'char*', StructFieldOptions?] | readonly [string, ObjectPointerDef<any>, StructFieldOptions?] | readonly [string, readonly [EnumDef<any> | StructDef<any> | PrimitiveType | ObjectPointerDef<any>], StructFieldOptions?]; | ||
| export interface StructFieldPackOptions { | ||
| validationHints?: any; | ||
| } | ||
| export interface StructFieldDescription { | ||
| name: string; | ||
| offset: number; | ||
| size: number; | ||
| align: number; | ||
| optional: boolean; | ||
| type: PrimitiveType | EnumDef<any> | StructDef<any> | 'cstring' | 'char*' | ObjectPointerDef<any> | readonly [any]; | ||
| lengthOf?: string; | ||
| } | ||
| export interface ArrayFieldMetadata { | ||
| elementSize: number; | ||
| arrayOffset: number; | ||
| lengthOffset: number; | ||
| lengthPack: (view: DataView, offset: number, value: number) => void; | ||
| } | ||
| export interface StructDef<OutputType, InputType = OutputType> { | ||
| __type: 'struct'; | ||
| size: number; | ||
| align: number; | ||
| hasMapValue: boolean; | ||
| layoutByName: Map<string, StructFieldDescription>; | ||
| arrayFields: Map<string, ArrayFieldMetadata>; | ||
| pack(obj: Simplify<InputType>, options?: StructFieldPackOptions): ArrayBuffer; | ||
| packInto(obj: Simplify<InputType>, view: DataView, offset: number, options?: StructFieldPackOptions): void; | ||
| unpack(buf: ArrayBuffer | SharedArrayBuffer): Simplify<OutputType>; | ||
| describe(): StructFieldDescription[]; | ||
| } | ||
| export interface StructDefOptions { | ||
| default?: Record<string, any>; | ||
| mapValue?: (value: any) => any; | ||
| reduceValue?: (value: any) => any; | ||
| } | ||
| export declare function packObjectArray(val: (PointyObject | null)[]): DataView<ArrayBuffer>; | ||
| export declare function defineStruct<const Fields extends readonly StructField[], const Opts extends StructDefOptions = {}>(fields: Fields & StructField[], structDefOptions?: Opts): DefineStructReturnType<Fields, Opts>; | ||
| export {}; |
| export declare function fatalError(...args: any[]): never; | ||
| export declare class OperationError extends Error { | ||
| constructor(message: string); | ||
| } | ||
| export declare class GPUErrorImpl extends Error implements GPUError { | ||
| constructor(message: string); | ||
| } | ||
| export declare class GPUOutOfMemoryError extends Error { | ||
| constructor(message: string); | ||
| } | ||
| export declare class GPUInternalError extends Error { | ||
| constructor(message: string); | ||
| } | ||
| export declare class GPUValidationError extends Error { | ||
| constructor(message: string); | ||
| } | ||
| export declare class GPUPipelineErrorImpl extends DOMException implements GPUPipelineError { | ||
| readonly reason: GPUPipelineErrorReason; | ||
| readonly __brand: 'GPUPipelineError'; | ||
| constructor(message: string, options: GPUPipelineErrorInit); | ||
| } | ||
| export declare class AbortError extends Error { | ||
| constructor(message: string); | ||
| } | ||
| export declare function createWGPUError(type: number, message: string): GPUErrorImpl | GPUOutOfMemoryError | GPUInternalError | GPUValidationError; |
+49
-0
@@ -125,1 +125,50 @@ import { Pointer } from "bun:ffi"; | ||
| import { GPUImpl } from "./GPU"; | ||
| export * from "./mocks/GPUCanvasContext"; | ||
| export declare function createGPUInstance(libPath?: string): GPUImpl; | ||
| export declare const globals: { | ||
| GPUPipelineError: any; | ||
| AbortError: any; | ||
| GPUError: any; | ||
| GPUOutOfMemoryError: any; | ||
| GPUInternalError: any; | ||
| GPUValidationError: any; | ||
| GPUTextureUsage: { | ||
| readonly COPY_SRC: number; | ||
| readonly COPY_DST: number; | ||
| readonly TEXTURE_BINDING: number; | ||
| readonly STORAGE_BINDING: number; | ||
| readonly RENDER_ATTACHMENT: number; | ||
| readonly TRANSIENT_ATTACHMENT: number; | ||
| }; | ||
| GPUBufferUsage: { | ||
| readonly MAP_READ: number; | ||
| readonly MAP_WRITE: number; | ||
| readonly COPY_SRC: number; | ||
| readonly COPY_DST: number; | ||
| readonly INDEX: number; | ||
| readonly VERTEX: number; | ||
| readonly UNIFORM: number; | ||
| readonly STORAGE: number; | ||
| readonly INDIRECT: number; | ||
| readonly QUERY_RESOLVE: number; | ||
| }; | ||
| GPUShaderStage: { | ||
| readonly VERTEX: number; | ||
| readonly FRAGMENT: number; | ||
| readonly COMPUTE: number; | ||
| }; | ||
| GPUMapMode: { | ||
| readonly READ: number; | ||
| readonly WRITE: number; | ||
| }; | ||
| GPUDevice: any; | ||
| GPUAdapterInfo: any; | ||
| GPUSupportedLimits: any; | ||
| }; | ||
| export declare function setupGlobals({ libPath }?: { | ||
| libPath?: string; | ||
| }): Promise<void>; | ||
| export declare function createWebGPUDevice(): Promise<GPUDevice>; |
+36
-17
| { | ||
| "name": "bun-webgpu", | ||
| "version": "0.1.0", | ||
| "module": "src/index.ts", | ||
| "main": "src/index.ts", | ||
| "module": "index.js", | ||
| "main": "index.js", | ||
| "types": "index.d.ts", | ||
| "type": "module", | ||
| "version": "0.1.1", | ||
| "description": "Native WebGPU implementation for Bun runtime", | ||
| "keywords": [ | ||
| "webgpu", | ||
| "bun", | ||
| "gpu", | ||
| "graphics", | ||
| "compute", | ||
| "dawn" | ||
| ], | ||
| "license": "Apache-2.0", | ||
| "scripts": { | ||
| "test": "bun test", | ||
| "build:dev": "cd src/zig && zig build -Doptimize=Debug", | ||
| "build:prod": "cd src/zig && zig build -Doptimize=ReleaseFast", | ||
| "build:linux": "./build-linux.sh", | ||
| "build:windows": "pwsh build-windows.ps1" | ||
| "author": "SST", | ||
| "homepage": "https://github.com/sst/bun-webgpu", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/sst/bun-webgpu" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/bun": "latest", | ||
| "@webgpu/types": "^0.1.60", | ||
| "commander": "^14.0.0", | ||
| "sharp": "^0.34.0" | ||
| "bugs": { | ||
| "url": "https://github.com/sst/bun-webgpu/issues" | ||
| }, | ||
| "peerDependencies": { | ||
| "typescript": "^5" | ||
| "exports": { | ||
| ".": { | ||
| "import": "./index.js", | ||
| "require": "./index.js", | ||
| "types": "./index.d.ts" | ||
| } | ||
| }, | ||
| "dependencies": { | ||
| "@webgpu/types": "^0.1.60" | ||
| }, | ||
| "optionalDependencies": { | ||
| "bun-webgpu-darwin-x64": "^0.1.1", | ||
| "bun-webgpu-darwin-arm64": "^0.1.1", | ||
| "bun-webgpu-linux-x64": "^0.1.1", | ||
| "bun-webgpu-win32-x64": "^0.1.1" | ||
| } | ||
| } | ||
| } |
+1
-1
@@ -30,3 +30,3 @@ # bun-webgpu | ||
| * **Pre-built Dawn Libraries**: This project relies on pre-built Dawn libraries. | ||
| * See [packages/bun-webgpu/dawn/README.md](./dawn/README.md) for details on how to download the required Dawn shared libraries. | ||
| * See [packages/bun-webgpu/dawn/README.md](https://github.com/sst/bun-webgpu/blob/HEAD/dawn/README.md) for details on how to download the required Dawn shared libraries. | ||
| * Basically just run `bun run ./dawn/download_artifacts.ts` | ||
@@ -33,0 +33,0 @@ |
| #!/bin/bash | ||
| set -e | ||
| echo "Building WebGPU wrapper manually..." | ||
| # Create temporary directory for build artifacts | ||
| TEMP_DIR=$(mktemp -d) | ||
| echo "Using temporary directory: $TEMP_DIR" | ||
| # Ensure cleanup on exit | ||
| trap "rm -rf $TEMP_DIR" EXIT | ||
| # Compile the Zig code to object file in temp directory | ||
| zig build-obj src/zig/lib.zig \ | ||
| -target x86_64-linux-gnu \ | ||
| -OReleaseFast \ | ||
| -I dawn/libs/x86_64-linux/include \ | ||
| -lc \ | ||
| --name webgpu_wrapper \ | ||
| --cache-dir "$TEMP_DIR" \ | ||
| --global-cache-dir "$TEMP_DIR" | ||
| # Move the object file to temp directory for linking | ||
| mv webgpu_wrapper.o "$TEMP_DIR/" | ||
| # Ensure the output directory exists | ||
| mkdir -p src/lib/x86_64-linux | ||
| # Link everything into a shared library | ||
| g++ -shared \ | ||
| -o src/lib/x86_64-linux/libwebgpu_wrapper.so \ | ||
| "$TEMP_DIR/webgpu_wrapper.o" \ | ||
| dawn/libs/x86_64-linux/libwebgpu_dawn.a \ | ||
| -lstdc++ \ | ||
| -lm \ | ||
| -lpthread \ | ||
| -ldl |
| #!/usr/bin/env pwsh | ||
| param( | ||
| [Parameter(Mandatory=$false)] | ||
| [ValidateSet("Debug", "Release")] | ||
| [string]$BuildType = "Release" | ||
| ) | ||
| Write-Host "Building WebGPU wrapper manually for Windows ($BuildType build)..." | ||
| # Find Visual Studio installation and MSVC tools | ||
| $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" | ||
| if (Test-Path $vswhere) { | ||
| $vsPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath | ||
| if ($vsPath) { | ||
| $msvcPath = Get-ChildItem -Path "$vsPath\VC\Tools\MSVC" | Sort-Object Name -Descending | Select-Object -First 1 | ||
| $linkExe = "$($msvcPath.FullName)\bin\Hostx64\x64\link.exe" | ||
| Write-Host "Found MSVC linker at: $linkExe" | ||
| } | ||
| } | ||
| # Fallback to searching PATH for link.exe specifically | ||
| if (-not $linkExe -or -not (Test-Path $linkExe)) { | ||
| $linkExe = Get-Command "link.exe" -ErrorAction SilentlyContinue | Where-Object { $_.Source -like "*Microsoft*" -or $_.Source -like "*VC*" } | Select-Object -First 1 -ExpandProperty Source | ||
| if ($linkExe) { | ||
| Write-Host "Found MSVC linker in PATH at: $linkExe" | ||
| } else { | ||
| throw "Could not find MSVC link.exe. Please ensure Visual Studio Build Tools are installed." | ||
| } | ||
| } | ||
| # Create temporary directory for build artifacts | ||
| $TempDir = New-TemporaryFile | ForEach-Object { Remove-Item $_; New-Item -ItemType Directory -Path $_ } | ||
| Write-Host "Using temporary directory: $TempDir" | ||
| try { | ||
| # Determine optimization flags and output names based on build type | ||
| $zigOptFlag = if ($BuildType -eq "Debug") { "-ODebug" } else { "-OReleaseFast" } | ||
| Write-Host "Using optimization: $zigOptFlag" | ||
| # Compile the Zig code to object file in temp directory | ||
| & zig build-obj src/zig/lib.zig ` | ||
| -target x86_64-windows-msvc ` | ||
| $zigOptFlag ` | ||
| -I dawn/libs/x86_64-windows/include ` | ||
| -I dawn/include ` | ||
| -lc ` | ||
| --name webgpu_wrapper ` | ||
| --cache-dir "$TempDir" ` | ||
| --global-cache-dir "$TempDir" | ||
| if ($LASTEXITCODE -ne 0) { | ||
| throw "Failed to compile Zig object file" | ||
| } | ||
| # Move the object file to temp directory for linking | ||
| Move-Item webgpu_wrapper.obj "$TempDir\" | ||
| # Generate .def file from object file symbols | ||
| Write-Host "Generating module definition file from object symbols..." | ||
| $objFile = "$TempDir\webgpu_wrapper.obj" | ||
| $defFile = "$TempDir\webgpu_wrapper.def" | ||
| # Use dumpbin to extract symbols and create .def file | ||
| $dumpbinExe = Split-Path $linkExe | Join-Path -ChildPath "dumpbin.exe" | ||
| if (-not (Test-Path $dumpbinExe)) { | ||
| # Fallback: look for dumpbin in the same directory structure | ||
| $vcToolsDir = Split-Path (Split-Path (Split-Path $linkExe)) | ||
| $dumpbinExe = Get-ChildItem -Path $vcToolsDir -Recurse -Name "dumpbin.exe" | Select-Object -First 1 | ||
| if ($dumpbinExe) { | ||
| $dumpbinExe = Join-Path $vcToolsDir $dumpbinExe | ||
| } else { | ||
| throw "Could not find dumpbin.exe" | ||
| } | ||
| } | ||
| Write-Host "Using dumpbin: $dumpbinExe" | ||
| # Extract symbols from object file | ||
| $symbols = & "$dumpbinExe" /SYMBOLS "$objFile" | Where-Object { | ||
| $_ -match "External.*zwgpu" | ||
| } | ForEach-Object { | ||
| if ($_ -match "\|\s+(\w+)") { | ||
| $matches[1] | ||
| } | ||
| } | Sort-Object -Unique | ||
| # Create .def file | ||
| "EXPORTS" | Out-File -FilePath $defFile -Encoding ASCII | ||
| $symbols | Out-File -FilePath $defFile -Append -Encoding ASCII | ||
| Write-Host "Generated .def file with $($symbols.Count) symbols" | ||
| # Ensure the output directory exists | ||
| New-Item -ItemType Directory -Path "src\lib\x86_64-windows" -Force | Out-Null | ||
| # Select appropriate runtime libraries and flags based on build type | ||
| if ($BuildType -eq "Debug") { | ||
| $runtimeLibs = "msvcrtd.lib vcruntimed.lib ucrtd.lib" | ||
| $noDefaultLibs = "/NODEFAULTLIB:MSVCRT" | ||
| Write-Host "Using debug runtime libraries" | ||
| } else { | ||
| $runtimeLibs = "msvcrt.lib vcruntime.lib ucrt.lib" | ||
| $noDefaultLibs = "" | ||
| Write-Host "Using release runtime libraries" | ||
| } | ||
| # Link everything into a shared library using MSVC linker | ||
| Write-Host "Linking with: $linkExe" | ||
| $linkArgs = @( | ||
| "/DLL" | ||
| "/OUT:src\lib\x86_64-windows\webgpu_wrapper.dll" | ||
| "/IMPLIB:src\lib\x86_64-windows\webgpu_wrapper.lib" | ||
| "/DEF:$defFile" | ||
| "$TempDir\webgpu_wrapper.obj" | ||
| "dawn\libs\x86_64-windows\webgpu_dawn.lib" | ||
| "user32.lib", "kernel32.lib", "gdi32.lib", "ole32.lib", "uuid.lib" | ||
| "d3d11.lib", "d3d12.lib", "dxgi.lib", "dxguid.lib" | ||
| $runtimeLibs.Split(' ') | ||
| "ntdll.lib" | ||
| "/LIBPATH:`"C:\Program Files (x86)\Windows Kits\10\Lib\10.0.22621.0\ucrt\x64`"" | ||
| "/LIBPATH:`"C:\Program Files (x86)\Windows Kits\10\Lib\10.0.22621.0\um\x64`"" | ||
| "/MACHINE:X64" | ||
| "/SUBSYSTEM:WINDOWS" | ||
| ) | ||
| if ($noDefaultLibs) { | ||
| $linkArgs += $noDefaultLibs | ||
| } | ||
| & "$linkExe" @linkArgs | ||
| if ($LASTEXITCODE -ne 0) { | ||
| throw "Failed to link shared library" | ||
| } | ||
| Write-Host "Build completed successfully!" -ForegroundColor Green | ||
| Write-Host "Output: src\lib\x86_64-windows\webgpu_wrapper.dll" | ||
| } finally { | ||
| # Cleanup | ||
| Remove-Item -Recurse -Force $TempDir -ErrorAction SilentlyContinue | ||
| } |
Sorry, the diff of this file is not supported yet
| #!/usr/bin/env bun | ||
| import path from 'path'; | ||
| import fs from 'fs/promises'; | ||
| import { tmpdir } from 'os'; | ||
| import { program } from 'commander'; | ||
| interface Args { | ||
| runPath: string; | ||
| buildType: 'debug' | 'release'; | ||
| platform?: string; | ||
| } | ||
| interface ProcessableArtifact { | ||
| artifactData: any; // Original artifact data from GitHub API | ||
| platformId: string; // e.g., 'macos-latest', 'windows-latest' | ||
| } | ||
| const platformDirNameMap: Record<string, string> = { | ||
| 'macos-latest': 'aarch64-macos', | ||
| 'macos-13': 'x86_64-macos', | ||
| 'ubuntu-latest': 'x86_64-linux', | ||
| 'windows-latest': 'x86_64-windows', | ||
| }; | ||
| function getCacheDir(runPath: string): string { | ||
| return path.join(tmpdir(), 'gh-artifacts-cache', runPath); | ||
| } | ||
| function getArtifactCacheDir(runPath: string, artifactName: string): string { | ||
| return path.join(getCacheDir(runPath), artifactName); | ||
| } | ||
| async function isArtifactCached(runPath: string, artifactName: string): Promise<boolean> { | ||
| const cacheDir = getArtifactCacheDir(runPath, artifactName); | ||
| try { | ||
| const files = await fs.readdir(cacheDir); | ||
| return files.length > 0; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| async function getCachedArtifactPath(runPath: string, artifactName: string): Promise<string | null> { | ||
| const cacheDir = getArtifactCacheDir(runPath, artifactName); | ||
| try { | ||
| const files = await fs.readdir(cacheDir); | ||
| if (files.length > 0) { | ||
| return path.join(cacheDir, files[0]!); | ||
| } | ||
| } catch { | ||
| // Directory doesn't exist or other error | ||
| } | ||
| return null; | ||
| } | ||
| async function main(args: Args) { | ||
| console.log('Using gh CLI for GitHub API requests and artifact downloads.'); | ||
| console.log('Fetching artifacts for:', args.runPath, 'Build type:', args.buildType); | ||
| const pathParts = args.runPath.split('/'); | ||
| if (pathParts.length < 5) { | ||
| console.error('Invalid run path format. Expected: owner/repo/actions/runs/runId_number'); | ||
| process.exit(1); | ||
| } | ||
| const [owner, repo, , , runIdStr] = pathParts; | ||
| const runId = parseInt(runIdStr!, 10); | ||
| if (!owner || !repo || !runIdStr || isNaN(runId)) { | ||
| console.error('Invalid run path format or run ID. Expected: owner/repo/actions/runs/runId_number'); | ||
| process.exit(1); | ||
| } | ||
| const listArtifactsEndpoint = `repos/${owner}/${repo}/actions/runs/${runId}/artifacts`; | ||
| console.log(`Listing artifacts using: gh api ${listArtifactsEndpoint}`); | ||
| try { | ||
| const listProc = Bun.spawnSync(['gh', 'api', listArtifactsEndpoint], { | ||
| stdout: 'pipe', | ||
| stderr: 'pipe', | ||
| }); | ||
| if (listProc.exitCode !== 0) { | ||
| console.error(`Error listing artifacts with gh api. Exit code: ${listProc.exitCode}`); | ||
| if (listProc.stdout) console.error(`Stdout: ${listProc.stdout.toString()}`); | ||
| if (listProc.stderr) console.error(`Stderr: ${listProc.stderr.toString()}`); | ||
| console.error("Please ensure 'gh' CLI is installed, authenticated ('gh auth login'), and has necessary permissions."); | ||
| process.exit(1); | ||
| } | ||
| const listOutput = listProc.stdout.toString(); | ||
| const data = JSON.parse(listOutput) as { artifacts: any[], message?: string, total_count?: number }; | ||
| if (data.message && data.message.toLowerCase().includes("not found")) { | ||
| console.error(`Error: Could not find run or artifacts via gh api. Message: ${data.message}`); | ||
| process.exit(1); | ||
| } | ||
| if (!data.artifacts || data.artifacts.length === 0) { | ||
| console.log('No artifacts found for this run.'); | ||
| return; | ||
| } | ||
| const buildTypeSuffix = args.buildType === 'debug' ? 'Debug' : 'Release'; | ||
| const platformMatchers = [ | ||
| { id: 'windows-latest', test: (name: string) => name.includes('-windows') }, | ||
| { id: 'ubuntu-latest', test: (name: string) => name.includes('-ubuntu') || name.includes('-linux') }, | ||
| { id: 'macos-13', test: (name: string) => name.includes('-macos-13') }, | ||
| { id: 'macos-latest', test: (name: string) => name.includes('-macos-') && !name.includes('-macos-13') }, | ||
| ]; | ||
| const artifactsToProcess = new Set<ProcessableArtifact>(); | ||
| for (const artifact of data.artifacts) { | ||
| if (!artifact.name.endsWith(buildTypeSuffix) || artifact.expired) { | ||
| continue; | ||
| } | ||
| let matchedPlatformId: string | undefined = undefined; | ||
| for (const matcher of platformMatchers) { | ||
| if (matcher.test(artifact.name)) { | ||
| matchedPlatformId = matcher.id; | ||
| break; | ||
| } | ||
| } | ||
| if (!matchedPlatformId) { | ||
| continue; | ||
| } | ||
| if (args.platform) { | ||
| if (platformDirNameMap[args.platform]) { | ||
| if (matchedPlatformId === args.platform) { | ||
| artifactsToProcess.add({ artifactData: artifact, platformId: matchedPlatformId }); | ||
| } | ||
| } else { | ||
| const artifactPlatformGenericName = platformDirNameMap[matchedPlatformId]?.split('-')[1]; //e.g. 'macos' from 'aarch64-macos' | ||
| if (artifactPlatformGenericName && artifactPlatformGenericName.includes(args.platform.toLowerCase())) { | ||
| artifactsToProcess.add({ artifactData: artifact, platformId: matchedPlatformId }); | ||
| } else if (matchedPlatformId.toLowerCase().includes(args.platform.toLowerCase())) { | ||
| artifactsToProcess.add({ artifactData: artifact, platformId: matchedPlatformId }); | ||
| } | ||
| } | ||
| } else { | ||
| artifactsToProcess.add({ artifactData: artifact, platformId: matchedPlatformId }); | ||
| } | ||
| } | ||
| if (artifactsToProcess.size === 0) { | ||
| if (args.platform) { | ||
| console.log(`No artifacts found matching platform '${args.platform}' and build type '${args.buildType}'.`); | ||
| } else { | ||
| console.log(`No artifacts found matching the specified platforms and build type '${args.buildType}'.`); | ||
| } | ||
| return; | ||
| } | ||
| for (const item of artifactsToProcess) { | ||
| const { artifactData, platformId } = item; | ||
| const artifactName = artifactData.name; | ||
| const artifactId = artifactData.id; | ||
| const artifactSizeBytes = artifactData.size_in_bytes; | ||
| const finalDirName = platformDirNameMap[platformId]; | ||
| if (!finalDirName) { | ||
| console.warn(`No directory mapping found for platformId: ${platformId}. Skipping artifact: ${artifactName}`); | ||
| continue; | ||
| } | ||
| const artifactDir = path.join(__dirname, 'libs', finalDirName); | ||
| console.log(`Processing artifact: ${artifactName}, ID: ${artifactId}, Size: ${artifactSizeBytes} bytes`); | ||
| console.log(`Target directory: ${artifactDir}`); | ||
| let downloadedArchivePath: string; | ||
| try { | ||
| const isCached = await isArtifactCached(args.runPath, artifactName); | ||
| if (isCached) { | ||
| const cachedPath = await getCachedArtifactPath(args.runPath, artifactName); | ||
| if (cachedPath) { | ||
| console.log(`Using cached artifact: ${cachedPath}`); | ||
| downloadedArchivePath = cachedPath; | ||
| } else { | ||
| throw new Error('Cached artifact path not found'); | ||
| } | ||
| } else { | ||
| const artifactCacheDir = getArtifactCacheDir(args.runPath, artifactName); | ||
| await fs.mkdir(artifactCacheDir, { recursive: true }); | ||
| console.log(`Created cache directory: ${artifactCacheDir}`); | ||
| console.log(`Downloading "${artifactName}" using gh run download...`); | ||
| const ghDownloadProc = Bun.spawnSync([ | ||
| 'gh', | ||
| 'run', | ||
| 'download', | ||
| runId.toString(), | ||
| '--repo', `${owner}/${repo}`, | ||
| '--name', artifactName, | ||
| '--dir', artifactCacheDir, | ||
| ], { | ||
| stdout: 'pipe', | ||
| stderr: 'pipe', | ||
| }); | ||
| if (ghDownloadProc.exitCode !== 0) { | ||
| console.error(`Error downloading artifact "${artifactName}" with gh run download. Exit code: ${ghDownloadProc.exitCode}`); | ||
| if (ghDownloadProc.stdout) console.error(`Stdout: ${ghDownloadProc.stdout.toString()}`); | ||
| if (ghDownloadProc.stderr) console.error(`Stderr: ${ghDownloadProc.stderr.toString()}`); | ||
| continue; | ||
| } | ||
| console.log(`Artifact "${artifactName}" downloaded by gh to ${artifactCacheDir}`); | ||
| if (ghDownloadProc.stdout.length > 0) console.log(`gh download stdout: ${ghDownloadProc.stdout.toString()}`); | ||
| if (ghDownloadProc.stderr.length > 0) console.warn(`gh download stderr: ${ghDownloadProc.stderr.toString()}`); | ||
| const filesInCacheDir = await fs.readdir(artifactCacheDir); | ||
| if (filesInCacheDir.length === 0) { | ||
| console.error(`No files found in cache directory: ${artifactCacheDir}`); | ||
| continue; | ||
| } | ||
| if (filesInCacheDir.length > 1) { | ||
| console.warn(`Multiple files found in cache directory: ${artifactCacheDir}. Using the first one: ${filesInCacheDir[0]}`); | ||
| } | ||
| const downloadedArchiveFileName = filesInCacheDir[0]!; | ||
| downloadedArchivePath = path.join(artifactCacheDir, downloadedArchiveFileName); | ||
| console.log(`Downloaded and cached archive: ${downloadedArchivePath}`); | ||
| } | ||
| await fs.rm(artifactDir, { recursive: true, force: true }); | ||
| await fs.mkdir(artifactDir, { recursive: true }); | ||
| console.log(`Ensured final artifact directory is clean: ${artifactDir}`); | ||
| try { | ||
| await fs.access(downloadedArchivePath); | ||
| } catch (e) { | ||
| console.error(`Archive file not found at path: ${downloadedArchivePath}`); | ||
| continue; | ||
| } | ||
| console.log(`Unpacking ${downloadedArchivePath} to ${artifactDir} using tar...`); | ||
| const tarProc = Bun.spawnSync( | ||
| ['tar', 'xf', downloadedArchivePath, '-C', artifactDir, '--strip-components=1'], | ||
| { stdout: 'pipe', stderr: 'pipe' } | ||
| ); | ||
| const stdoutContent = tarProc.stdout.toString('utf-8'); | ||
| const stderrContent = tarProc.stderr.toString('utf-8'); | ||
| if (tarProc.exitCode !== 0) { | ||
| console.error(`Error unpacking "${artifactName}" with tar. Exit code: ${tarProc.exitCode}`); | ||
| if (stdoutContent.length > 0) console.error(`Stdout: ${stdoutContent}`); | ||
| if (stderrContent.length > 0) console.error(`Stderr: ${stderrContent}`); | ||
| } else { | ||
| console.log(`Unpacked "${artifactName}" to ${artifactDir} successfully.`); | ||
| if (stdoutContent.length > 0) console.log(`tar stdout: ${stdoutContent}`); | ||
| if (stderrContent.length > 0) console.warn(`tar stderr (on success): ${stderrContent}`); | ||
| } | ||
| } catch (error) { | ||
| console.error(`Error processing artifact ${artifactName}:`, error); | ||
| } | ||
| } | ||
| console.log(`\nArtifacts cached in: ${getCacheDir(args.runPath)}`); | ||
| } catch (error) { | ||
| console.error('An unexpected error occurred:', error); | ||
| if (error instanceof Error && error.message.includes('ENOENT')) { | ||
| console.error("This might be because the 'gh' command was not found. Please ensure it is installed and in your PATH."); | ||
| } | ||
| process.exit(1); | ||
| } | ||
| } | ||
| if (import.meta.main) { | ||
| const defaultRunPath = 'kommander/dawn/actions/runs/15558254019'; | ||
| const defaultBuildType = 'release'; | ||
| program | ||
| .name('download_artifacts.ts') | ||
| .description('Downloads and unpacks build artifacts from a GitHub Actions run.') | ||
| .option('-r, --runPath <path>', 'GitHub Actions run path (e.g., owner/repo/actions/runs/runId)', defaultRunPath) | ||
| .option('-b, --buildType <type>', "Build type: 'debug' or 'release'", defaultBuildType) | ||
| .option('-p, --platform <name>', 'Optional: Specific platform to download (e.g., macos-latest, windows-latest, linux, macos)') | ||
| .action((options) => { | ||
| const runPath = options.runPath; | ||
| const buildType = options.buildType.toLowerCase(); | ||
| const platform = options.platform; | ||
| console.log(`Using runPath: ${runPath}`); | ||
| console.log(`Using buildType: ${buildType}`); | ||
| if (platform) { | ||
| console.log(`Using platform: ${platform}`); | ||
| } | ||
| if (buildType !== 'debug' && buildType !== 'release') { | ||
| console.error(`Invalid build type "${buildType}". Must be "debug" or "release".`); | ||
| program.help(); | ||
| } | ||
| main({ runPath, buildType: buildType as 'debug' | 'release', platform }); | ||
| }); | ||
| program.parse(process.argv); | ||
| } |
| # Dawn Libs | ||
| Download and manage pre-built Dawn libraries using the provided script. | ||
| ## Prerequisites | ||
| 1. **Bun**: Ensure you have BunJS installed. (https://bun.sh) | ||
| 2. **GitHub CLI (`gh`)**: The script uses the `gh` command-line tool to interact with the GitHub API and download artifacts. | ||
| * Install `gh` from [cli.github.com](https://cli.github.com/). | ||
| * Authenticate with GitHub by running: `gh auth login` | ||
| ## Downloading Artifacts | ||
| Use `download_artifacts.ts` to automate the downloading and unpacking of Dawn build artifacts from GitHub Actions. | ||
| ### How to Run | ||
| Navigate to the `packages/bun-webgpu/dawn/` directory and run the script using Bun: | ||
| ```bash | ||
| bun run ./download_artifacts.ts [<run_path_or_build_type>] [<build_type_if_run_path_first>] | ||
| ``` | ||
| **Arguments:** | ||
| * **`run_path_or_build_type`** (optional): | ||
| * If this is the only argument provided: | ||
| * If it's `debug` or `release`, it specifies the build type, and the default run path is used. | ||
| * Otherwise, it's treated as the `run_path`. | ||
| * Default `run_path`: `google/dawn/actions/runs/14686102284` (This is an example, you'll likely want to update this to a more recent run). | ||
| * The `run_path` format is `OWNER/REPO/actions/runs/RUN_ID`. | ||
| * **`build_type_if_run_path_first`** (optional): | ||
| * Specifies the build type (`debug` or `release`) if the first argument was a `run_path`. | ||
| * Default `build_type`: `release` | ||
| **Examples:** | ||
| * Download `release` artifacts from the default run: | ||
| ```bash | ||
| bun run ./download_artifacts.ts | ||
| ``` | ||
| or | ||
| ```bash | ||
| bun run ./download_artifacts.ts release | ||
| ``` | ||
| * Download `debug` artifacts from the default run: | ||
| ```bash | ||
| bun run ./download_artifacts.ts debug | ||
| ``` | ||
| * Download `release` artifacts from a specific run: | ||
| ```bash | ||
| bun run ./download_artifacts.ts google/dawn/actions/runs/YOUR_RUN_ID | ||
| ``` | ||
| or | ||
| ```bash | ||
| bun run ./download_artifacts.ts google/dawn/actions/runs/YOUR_RUN_ID release | ||
| ``` | ||
| * Download `debug` artifacts from a specific run: | ||
| ```bash | ||
| bun run ./download_artifacts.ts google/dawn/actions/runs/YOUR_RUN_ID debug | ||
| ``` | ||
-38
| #!/bin/bash | ||
| set -e | ||
| REPO_URL="git@github.com:gpuweb/cts.git" | ||
| REPO_DIR="cts" | ||
| SCRIPT_TO_RUN="../tools/cts.ts" | ||
| if [ -d "$REPO_DIR" ]; then | ||
| echo "Directory $REPO_DIR already exists. Skipping clone." | ||
| else | ||
| echo "Cloning $REPO_URL..." | ||
| git clone $REPO_URL $REPO_DIR | ||
| fi | ||
| cd $REPO_DIR | ||
| echo "Current directory: $(pwd)" | ||
| debug_flag="false" | ||
| # Check if --debug is in the arguments | ||
| for arg in "$@"; do | ||
| if [ "$arg" = "--debug" ]; then | ||
| debug_flag="true" | ||
| break | ||
| fi | ||
| done | ||
| echo "Debug flag: $debug_flag" | ||
| echo "Script to run: $SCRIPT_TO_RUN" | ||
| echo "GPU provider path: $(realpath $(pwd)/../tools/setup.ts)" | ||
| echo "All args: $@" | ||
| echo "Full command that will be executed:" | ||
| echo "DEBUG=$debug_flag bun \"$SCRIPT_TO_RUN\" -- \"--gpu-provider\" \"$(pwd)/../tools/setup.ts\" $@" | ||
| echo "" | ||
| echo "Executing command..." | ||
| DEBUG=$debug_flag bun "$SCRIPT_TO_RUN" -- "--expectations" "$(pwd)/../tools/expectations.ts" "--gpu-provider" "$(pwd)/../tools/setup.ts" $@ |
| import { expect, describe, it } from "bun:test"; | ||
| import { BufferPool } from "./buffer_pool"; | ||
| describe("BufferPool", () => { | ||
| describe("constructor", () => { | ||
| it("should create pool with correct parameters", () => { | ||
| const pool = new BufferPool(5, 10, 256); | ||
| expect(pool.blockSize).toBe(256); | ||
| expect(pool.minBlockCount).toBe(5); | ||
| expect(pool.maxBlockCount).toBe(10); | ||
| expect(pool.totalBlockCount).toBe(5); // starts with min | ||
| expect(pool.allocatedBlockCount).toBe(0); | ||
| expect(pool.freeBlockCount).toBe(5); | ||
| expect(pool.hasAvailableBlocks).toBe(true); | ||
| expect(pool.utilizationRatio).toBe(0); | ||
| }); | ||
| it("should create pool with different sizes", () => { | ||
| const smallPool = new BufferPool(2, 5, 128); | ||
| const largePool = new BufferPool(50, 100, 4096); | ||
| expect(smallPool.totalBlockCount).toBe(2); | ||
| expect(smallPool.maxBlockCount).toBe(5); | ||
| expect(smallPool.blockSize).toBe(128); | ||
| expect(largePool.totalBlockCount).toBe(50); | ||
| expect(largePool.maxBlockCount).toBe(100); | ||
| expect(largePool.blockSize).toBe(4096); | ||
| }); | ||
| it("should throw on invalid min blocks", () => { | ||
| expect(() => new BufferPool(0, 10, 256)).toThrow('Min blocks, max blocks, and block size must be positive'); | ||
| expect(() => new BufferPool(-1, 10, 256)).toThrow('Min blocks, max blocks, and block size must be positive'); | ||
| }); | ||
| it("should throw on invalid max blocks", () => { | ||
| expect(() => new BufferPool(5, 0, 256)).toThrow('Min blocks, max blocks, and block size must be positive'); | ||
| expect(() => new BufferPool(5, -1, 256)).toThrow('Min blocks, max blocks, and block size must be positive'); | ||
| }); | ||
| it("should throw on invalid block size", () => { | ||
| expect(() => new BufferPool(5, 10, 0)).toThrow('Min blocks, max blocks, and block size must be positive'); | ||
| expect(() => new BufferPool(5, 10, -1)).toThrow('Min blocks, max blocks, and block size must be positive'); | ||
| }); | ||
| it("should throw when min > max", () => { | ||
| expect(() => new BufferPool(10, 5, 256)).toThrow('Min blocks cannot be greater than max blocks'); | ||
| }); | ||
| it("should allow min == max", () => { | ||
| const pool = new BufferPool(5, 5, 256); | ||
| expect(pool.minBlockCount).toBe(5); | ||
| expect(pool.maxBlockCount).toBe(5); | ||
| expect(pool.totalBlockCount).toBe(5); | ||
| }); | ||
| it("should throw on both parameters invalid", () => { | ||
| expect(() => new BufferPool(0, 0, 256)).toThrow('Min blocks, max blocks, and block size must be positive'); | ||
| expect(() => new BufferPool(-5, -10, 256)).toThrow('Min blocks, max blocks, and block size must be positive'); | ||
| }); | ||
| }); | ||
| describe("request", () => { | ||
| it("should return buffer and index with correct values", () => { | ||
| const pool = new BufferPool(2, 5, 256); | ||
| const { buffer, index } = pool.request(); | ||
| expect(buffer).not.toBeNull(); | ||
| expect(buffer.byteLength).toBe(256); | ||
| expect(buffer).toBeInstanceOf(ArrayBuffer); | ||
| expect(index).toBe(1); | ||
| }); | ||
| it("should track allocated count correctly", () => { | ||
| const pool = new BufferPool(2, 3, 128); | ||
| expect(pool.allocatedBlockCount).toBe(0); | ||
| expect(pool.freeBlockCount).toBe(2); | ||
| const buffer1 = pool.request(); | ||
| expect(pool.allocatedBlockCount).toBe(1); | ||
| expect(pool.freeBlockCount).toBe(1); | ||
| const buffer2 = pool.request(); | ||
| expect(pool.allocatedBlockCount).toBe(2); | ||
| expect(pool.freeBlockCount).toBe(0); | ||
| expect(buffer1).not.toBeNull(); | ||
| expect(buffer2).not.toBeNull(); | ||
| expect(buffer1).not.toBe(buffer2); | ||
| }); | ||
| it("should update utilization ratio", () => { | ||
| const pool = new BufferPool(2, 4, 64); | ||
| expect(pool.utilizationRatio).toBe(0); | ||
| pool.request(); | ||
| expect(pool.utilizationRatio).toBe(0.5); // 1/2 | ||
| pool.request(); | ||
| expect(pool.utilizationRatio).toBe(1.0); // 2/2 | ||
| }); | ||
| it("should update hasAvailableBlocks correctly", () => { | ||
| const pool = new BufferPool(1, 2, 64); | ||
| expect(pool.hasAvailableBlocks).toBe(true); | ||
| pool.request(); | ||
| expect(pool.hasAvailableBlocks).toBe(true); | ||
| pool.request(); | ||
| expect(pool.hasAvailableBlocks).toBe(false); | ||
| }); | ||
| it("should expand pool when running out of blocks", () => { | ||
| const pool = new BufferPool(1, 4, 128); | ||
| expect(pool.totalBlockCount).toBe(1); | ||
| expect(pool.freeBlockCount).toBe(1); | ||
| const buffer1 = pool.request(); | ||
| expect(pool.allocatedBlockCount).toBe(1); | ||
| expect(pool.freeBlockCount).toBe(0); | ||
| expect(pool.totalBlockCount).toBe(1); | ||
| const buffer2 = pool.request(); | ||
| expect(pool.allocatedBlockCount).toBe(2); | ||
| expect(pool.totalBlockCount).toBe(2); | ||
| expect(pool.freeBlockCount).toBe(0); | ||
| expect(buffer1).not.toBeNull(); | ||
| expect(buffer2).not.toBeNull(); | ||
| expect(buffer1).not.toBe(buffer2); | ||
| }); | ||
| it("should throw when reaching max capacity", () => { | ||
| const pool = new BufferPool(1, 2, 128); | ||
| const { buffer: buffer1 } = pool.request(); | ||
| const { buffer: buffer2 } = pool.request(); | ||
| expect(buffer1).not.toBeNull(); | ||
| expect(buffer2).not.toBeNull(); | ||
| expect(pool.totalBlockCount).toBe(2); | ||
| expect(() => pool.request()).toThrow('BufferPool out of memory: no free blocks available'); | ||
| expect(pool.allocatedBlockCount).toBe(2); | ||
| expect(pool.freeBlockCount).toBe(0); | ||
| expect(pool.hasAvailableBlocks).toBe(false); | ||
| }); | ||
| it("should return different buffer instances", () => { | ||
| const pool = new BufferPool(2, 3, 256); | ||
| const { buffer: buffer1 } = pool.request(); | ||
| const { buffer: buffer2 } = pool.request(); | ||
| expect(buffer1).not.toBe(buffer2); | ||
| }); | ||
| it("should handle single block pool", () => { | ||
| const pool = new BufferPool(1, 1, 512); | ||
| const { buffer: buffer1, index } = pool.request(); | ||
| expect(buffer1).not.toBeNull(); | ||
| expect(buffer1.byteLength).toBe(512); | ||
| expect(index).toBe(0); | ||
| expect(() => pool.request()).toThrow('BufferPool out of memory: no free blocks available'); | ||
| expect(pool.utilizationRatio).toBe(1.0); | ||
| expect(pool.hasAvailableBlocks).toBe(false); | ||
| }); | ||
| }); | ||
| describe("release", () => { | ||
| it("should release buffer correctly", () => { | ||
| const pool = new BufferPool(2, 3, 128); | ||
| const { buffer } = pool.request(); | ||
| expect(pool.allocatedBlockCount).toBe(1); | ||
| expect(pool.freeBlockCount).toBe(1); | ||
| pool.release(buffer); | ||
| expect(pool.allocatedBlockCount).toBe(0); | ||
| expect(pool.freeBlockCount).toBe(2); | ||
| expect(pool.hasAvailableBlocks).toBe(true); | ||
| }); | ||
| it("should allow reuse of released buffer slot", () => { | ||
| const pool = new BufferPool(1, 2, 256); | ||
| const { buffer: buffer1 } = pool.request(); | ||
| pool.request(); | ||
| expect(pool.totalBlockCount).toBe(2); | ||
| expect(() => pool.request()).toThrow('BufferPool out of memory: no free blocks available'); | ||
| pool.release(buffer1); | ||
| const { buffer: buffer4 } = pool.request(); | ||
| expect(buffer4).not.toBeNull(); | ||
| expect(buffer4.byteLength).toBe(256); | ||
| expect(pool.allocatedBlockCount).toBe(2); | ||
| }); | ||
| it("should update utilization ratio after release", () => { | ||
| const pool = new BufferPool(2, 4, 64); | ||
| const buffers: ArrayBuffer[] = []; | ||
| buffers.push(pool.request().buffer); | ||
| buffers.push(pool.request().buffer); | ||
| expect(pool.utilizationRatio).toBe(1.0); | ||
| pool.release(buffers[0]!); | ||
| expect(pool.utilizationRatio).toBe(0.5); | ||
| pool.release(buffers[1]!); | ||
| expect(pool.utilizationRatio).toBe(0); | ||
| }); | ||
| it("should throw on buffer not from this pool", () => { | ||
| const pool1 = new BufferPool(2, 2, 128); | ||
| const pool2 = new BufferPool(2, 2, 128); | ||
| const { buffer: buffer1 } = pool1.request(); | ||
| const { buffer: buffer2 } = pool2.request(); | ||
| expect(() => pool1.release(buffer2)).toThrow('ArrayBuffer was not allocated from this allocator or already freed'); | ||
| expect(() => pool2.release(buffer1)).toThrow('ArrayBuffer was not allocated from this allocator or already freed'); | ||
| }); | ||
| it("should throw on already released buffer", () => { | ||
| const pool = new BufferPool(2, 2, 128); | ||
| const { buffer } = pool.request(); | ||
| pool.release(buffer); | ||
| expect(() => pool.release(buffer)).toThrow('ArrayBuffer was not allocated from this allocator or already freed'); | ||
| }); | ||
| it("should throw on arbitrary ArrayBuffer", () => { | ||
| const pool = new BufferPool(2, 2, 128); | ||
| const arbitraryBuffer = new ArrayBuffer(128); | ||
| expect(() => pool.release(arbitraryBuffer)).toThrow('ArrayBuffer was not allocated from this allocator or already freed'); | ||
| }); | ||
| it("should handle multiple releases correctly", () => { | ||
| const pool = new BufferPool(3, 5, 256); | ||
| const buffers = [ | ||
| pool.request().buffer, | ||
| pool.request().buffer, | ||
| pool.request().buffer, | ||
| ]; | ||
| expect(pool.allocatedBlockCount).toBe(3); | ||
| pool.release(buffers[1]!); | ||
| expect(pool.allocatedBlockCount).toBe(2); | ||
| pool.release(buffers[0]!); | ||
| expect(pool.allocatedBlockCount).toBe(1); | ||
| pool.release(buffers[2]!); | ||
| expect(pool.allocatedBlockCount).toBe(0); | ||
| expect(pool.freeBlockCount).toBe(3); | ||
| }); | ||
| }); | ||
| describe("releaseBlock", () => { | ||
| it("should release buffer correctly by index", () => { | ||
| const pool = new BufferPool(2, 3, 128); | ||
| const { index } = pool.request(); | ||
| expect(pool.allocatedBlockCount).toBe(1); | ||
| expect(pool.freeBlockCount).toBe(1); | ||
| pool.releaseBlock(index); | ||
| expect(pool.allocatedBlockCount).toBe(0); | ||
| expect(pool.freeBlockCount).toBe(2); | ||
| expect(pool.hasAvailableBlocks).toBe(true); | ||
| }); | ||
| it("should allow reuse of a block released by index", () => { | ||
| const pool = new BufferPool(1, 1, 256); | ||
| const { buffer: buffer1, index } = pool.request(); | ||
| expect(pool.allocatedBlockCount).toBe(1); | ||
| pool.releaseBlock(index); | ||
| expect(pool.allocatedBlockCount).toBe(0); | ||
| const { buffer: buffer2, index: index2 } = pool.request(); | ||
| expect(pool.allocatedBlockCount).toBe(1); | ||
| expect(buffer2).toBe(buffer1); | ||
| expect(index2).toBe(index); | ||
| }); | ||
| it("should throw on releasing a free block", () => { | ||
| const pool = new BufferPool(2, 2, 128); | ||
| expect(() => pool.releaseBlock(0)).toThrow('Block was not allocated or already freed'); | ||
| expect(() => pool.releaseBlock(1)).toThrow('Block was not allocated or already freed'); | ||
| }); | ||
| it("should throw on already released block", () => { | ||
| const pool = new BufferPool(2, 2, 128); | ||
| const { index } = pool.request(); | ||
| pool.releaseBlock(index); | ||
| expect(() => pool.releaseBlock(index)).toThrow('Block was not allocated or already freed'); | ||
| }); | ||
| it("should throw on negative index", () => { | ||
| const pool = new BufferPool(3, 3, 128); | ||
| expect(() => pool.releaseBlock(-1)).toThrow('Block index out of range'); | ||
| }); | ||
| it("should throw on index too large", () => { | ||
| const pool = new BufferPool(3, 3, 128); | ||
| expect(() => pool.releaseBlock(3)).toThrow('Block index out of range'); | ||
| }); | ||
| }); | ||
| describe("getBuffer", () => { | ||
| it("should return buffer by index", () => { | ||
| const pool = new BufferPool(3, 3, 512); | ||
| const buffer0 = pool.getBuffer(0); | ||
| const buffer1 = pool.getBuffer(1); | ||
| const buffer2 = pool.getBuffer(2); | ||
| expect(buffer0.byteLength).toBe(512); | ||
| expect(buffer1.byteLength).toBe(512); | ||
| expect(buffer2.byteLength).toBe(512); | ||
| expect(buffer0).not.toBe(buffer1); | ||
| expect(buffer1).not.toBe(buffer2); | ||
| }); | ||
| it("should return same instance for same index", () => { | ||
| const pool = new BufferPool(2, 2, 256); | ||
| const buffer1 = pool.getBuffer(0); | ||
| const buffer2 = pool.getBuffer(0); | ||
| expect(buffer1).toBe(buffer2); | ||
| }); | ||
| it("should throw on negative index", () => { | ||
| const pool = new BufferPool(3, 3, 128); | ||
| expect(() => pool.getBuffer(-1)).toThrow('Block index out of range'); | ||
| expect(() => pool.getBuffer(-10)).toThrow('Block index out of range'); | ||
| }); | ||
| it("should throw on index too large", () => { | ||
| const pool = new BufferPool(3, 3, 128); | ||
| expect(() => pool.getBuffer(3)).toThrow('Block index out of range'); | ||
| expect(() => pool.getBuffer(10)).toThrow('Block index out of range'); | ||
| }); | ||
| it("should work with boundary indices", () => { | ||
| const pool = new BufferPool(5, 5, 64); | ||
| const firstBuffer = pool.getBuffer(0); | ||
| const lastBuffer = pool.getBuffer(4); | ||
| expect(firstBuffer.byteLength).toBe(64); | ||
| expect(lastBuffer.byteLength).toBe(64); | ||
| expect(firstBuffer).not.toBe(lastBuffer); | ||
| }); | ||
| it("should only allow access to current blocks, not max", () => { | ||
| const pool = new BufferPool(2, 5, 64); | ||
| expect(() => pool.getBuffer(0)).not.toThrow(); | ||
| expect(() => pool.getBuffer(1)).not.toThrow(); | ||
| expect(() => pool.getBuffer(2)).toThrow('Block index out of range'); | ||
| }); | ||
| }); | ||
| describe("reset", () => { | ||
| it("should reset pool to initial state", () => { | ||
| const pool = new BufferPool(2, 4, 256); | ||
| const buffer1 = pool.request(); | ||
| const buffer2 = pool.request(); | ||
| const buffer3 = pool.request(); | ||
| expect(pool.allocatedBlockCount).toBe(3); | ||
| expect(pool.totalBlockCount).toBe(4); | ||
| expect(pool.utilizationRatio).toBe(0.75); | ||
| pool.reset(); | ||
| expect(pool.allocatedBlockCount).toBe(0); | ||
| expect(pool.totalBlockCount).toBe(2); | ||
| expect(pool.freeBlockCount).toBe(2); | ||
| expect(pool.utilizationRatio).toBe(0); | ||
| expect(pool.hasAvailableBlocks).toBe(true); | ||
| }); | ||
| it("should allow full reallocation after reset", () => { | ||
| const pool = new BufferPool(1, 3, 128); | ||
| pool.request(); | ||
| pool.request(); | ||
| pool.request(); | ||
| expect(pool.totalBlockCount).toBe(3); | ||
| expect(() => pool.request()).toThrow('BufferPool out of memory: no free blocks available'); | ||
| pool.reset(); | ||
| expect(pool.totalBlockCount).toBe(1); | ||
| const newBuffers = [ | ||
| pool.request(), | ||
| pool.request(), | ||
| pool.request() | ||
| ]; | ||
| expect(newBuffers[0]).toBeDefined(); | ||
| expect(newBuffers[1]).toBeDefined(); | ||
| expect(newBuffers[2]).toBeDefined(); | ||
| expect(pool.totalBlockCount).toBe(3); | ||
| expect(() => pool.request()).toThrow('BufferPool out of memory: no free blocks available'); | ||
| }); | ||
| it("should invalidate old buffer references", () => { | ||
| const pool = new BufferPool(2, 2, 256); | ||
| const { buffer } = pool.request(); | ||
| pool.reset(); | ||
| expect(() => pool.release(buffer)).toThrow('ArrayBuffer was not allocated from this allocator or already freed'); | ||
| }); | ||
| it("should reset empty pool correctly", () => { | ||
| const pool = new BufferPool(3, 5, 512); | ||
| pool.reset(); | ||
| expect(pool.allocatedBlockCount).toBe(0); | ||
| expect(pool.totalBlockCount).toBe(3); | ||
| expect(pool.freeBlockCount).toBe(3); | ||
| expect(pool.utilizationRatio).toBe(0); | ||
| expect(pool.hasAvailableBlocks).toBe(true); | ||
| }); | ||
| it("should work multiple times", () => { | ||
| const pool = new BufferPool(1, 2, 64); | ||
| for (let i = 0; i < 3; i++) { | ||
| pool.request(); | ||
| expect(pool.allocatedBlockCount).toBe(1); | ||
| pool.reset(); | ||
| expect(pool.allocatedBlockCount).toBe(0); | ||
| expect(pool.totalBlockCount).toBe(1); | ||
| expect(pool.freeBlockCount).toBe(1); | ||
| } | ||
| }); | ||
| }); | ||
| describe("properties", () => { | ||
| it("should return correct totalBlockCount", () => { | ||
| const pools: [BufferPool, BufferPool, BufferPool] = [ | ||
| new BufferPool(1, 1, 64), | ||
| new BufferPool(5, 10, 128), | ||
| new BufferPool(50, 100, 256) | ||
| ]; | ||
| expect(pools[0].totalBlockCount).toBe(1); | ||
| expect(pools[1].totalBlockCount).toBe(5); | ||
| expect(pools[2].totalBlockCount).toBe(50); | ||
| }); | ||
| it("should return correct blockSize", () => { | ||
| const pools: [BufferPool, BufferPool, BufferPool] = [ | ||
| new BufferPool(2, 5, 64), | ||
| new BufferPool(2, 5, 256), | ||
| new BufferPool(2, 5, 1024) | ||
| ]; | ||
| expect(pools[0].blockSize).toBe(64); | ||
| expect(pools[1].blockSize).toBe(256); | ||
| expect(pools[2].blockSize).toBe(1024); | ||
| }); | ||
| it("should track allocatedBlockCount accurately", () => { | ||
| const pool = new BufferPool(2, 4, 128); | ||
| const buffers: ArrayBuffer[] = []; | ||
| expect(pool.allocatedBlockCount).toBe(0); | ||
| buffers.push(pool.request().buffer); | ||
| expect(pool.allocatedBlockCount).toBe(1); | ||
| buffers.push(pool.request().buffer); | ||
| expect(pool.allocatedBlockCount).toBe(2); | ||
| pool.release(buffers[1]!); | ||
| expect(pool.allocatedBlockCount).toBe(1); | ||
| pool.release(buffers[0]!); | ||
| expect(pool.allocatedBlockCount).toBe(0); | ||
| }); | ||
| it("should track freeBlockCount accurately", () => { | ||
| const pool = new BufferPool(2, 3, 256); | ||
| expect(pool.freeBlockCount).toBe(2); | ||
| const buffer1 = pool.request().buffer; | ||
| expect(pool.freeBlockCount).toBe(1); | ||
| const buffer2 = pool.request().buffer; | ||
| expect(pool.freeBlockCount).toBe(0); | ||
| const buffer3 = pool.request().buffer; | ||
| expect(pool.freeBlockCount).toBe(0); | ||
| pool.release(buffer2); | ||
| expect(pool.freeBlockCount).toBe(1); | ||
| }); | ||
| it("should calculate utilizationRatio correctly for edge cases", () => { | ||
| const pool = new BufferPool(1, 1, 64); | ||
| expect(pool.utilizationRatio).toBe(0); | ||
| pool.request(); | ||
| expect(pool.utilizationRatio).toBe(1); | ||
| pool.reset(); | ||
| expect(pool.utilizationRatio).toBe(0); | ||
| }); | ||
| it("should update hasAvailableBlocks correctly in all scenarios", () => { | ||
| const pool = new BufferPool(1, 2, 128); | ||
| expect(pool.hasAvailableBlocks).toBe(true); | ||
| const { buffer: buffer1 } = pool.request(); | ||
| expect(pool.hasAvailableBlocks).toBe(true); | ||
| const { buffer: buffer2 } = pool.request(); | ||
| expect(pool.hasAvailableBlocks).toBe(false); | ||
| pool.release(buffer1); | ||
| expect(pool.hasAvailableBlocks).toBe(true); | ||
| pool.release(buffer2); | ||
| expect(pool.hasAvailableBlocks).toBe(true); | ||
| pool.reset(); | ||
| expect(pool.hasAvailableBlocks).toBe(true); | ||
| }); | ||
| }); | ||
| describe("dynamic expansion", () => { | ||
| it("should expand pool when needed", () => { | ||
| const pool = new BufferPool(1, 8, 256); | ||
| expect(pool.totalBlockCount).toBe(1); | ||
| pool.request(); | ||
| expect(pool.totalBlockCount).toBe(1); | ||
| pool.request(); | ||
| expect(pool.totalBlockCount).toBe(2); | ||
| pool.request(); | ||
| pool.request(); | ||
| expect(pool.totalBlockCount).toBe(4); | ||
| }); | ||
| it("should expand by doubling until max is reached", () => { | ||
| const pool = new BufferPool(1, 7, 128); | ||
| const buffers: ArrayBuffer[] = []; | ||
| expect(pool.totalBlockCount).toBe(1); | ||
| buffers.push(pool.request().buffer); | ||
| buffers.push(pool.request().buffer); | ||
| expect(pool.totalBlockCount).toBe(2); | ||
| buffers.push(pool.request().buffer); | ||
| buffers.push(pool.request().buffer); | ||
| expect(pool.totalBlockCount).toBe(4); | ||
| buffers.push(pool.request().buffer); | ||
| buffers.push(pool.request().buffer); | ||
| buffers.push(pool.request().buffer); | ||
| expect(pool.totalBlockCount).toBe(7); | ||
| expect(() => pool.request()).toThrow('BufferPool out of memory: no free blocks available'); | ||
| }); | ||
| it("should not expand beyond max blocks", () => { | ||
| const pool = new BufferPool(2, 3, 128); | ||
| pool.request(); | ||
| pool.request(); | ||
| pool.request(); | ||
| expect(pool.totalBlockCount).toBe(3); | ||
| expect(() => pool.request()).toThrow('BufferPool out of memory: no free blocks available'); | ||
| }); | ||
| it("should handle released blocks in expanded pool", () => { | ||
| const pool = new BufferPool(1, 4, 256); | ||
| const { buffer: buffer1 } = pool.request(); | ||
| const { buffer: buffer2 } = pool.request(); | ||
| const { buffer: buffer3 } = pool.request(); | ||
| const { buffer: buffer4 } = pool.request(); | ||
| expect(pool.totalBlockCount).toBe(4); | ||
| expect(pool.allocatedBlockCount).toBe(4); | ||
| pool.release(buffer2); | ||
| expect(pool.allocatedBlockCount).toBe(3); | ||
| expect(pool.freeBlockCount).toBe(1); | ||
| pool.request(); | ||
| expect(pool.allocatedBlockCount).toBe(4); | ||
| expect(pool.totalBlockCount).toBe(4); | ||
| }); | ||
| }); | ||
| describe("edge cases", () => { | ||
| it("should handle large pool sizes", () => { | ||
| const pool = new BufferPool(100, 1000, 4096); | ||
| expect(pool.totalBlockCount).toBe(100); | ||
| expect(pool.maxBlockCount).toBe(1000); | ||
| expect(pool.blockSize).toBe(4096); | ||
| expect(pool.freeBlockCount).toBe(100); | ||
| const { buffer } = pool.request(); | ||
| expect(buffer.byteLength).toBe(4096); | ||
| expect(pool.allocatedBlockCount).toBe(1); | ||
| }); | ||
| it("should handle small block sizes", () => { | ||
| const pool = new BufferPool(5, 10, 1); | ||
| expect(pool.blockSize).toBe(1); | ||
| const { buffer } = pool.request(); | ||
| expect(buffer.byteLength).toBe(1); | ||
| }); | ||
| it("should handle rapid allocation and deallocation", () => { | ||
| const pool = new BufferPool(2, 5, 256); | ||
| for (let cycle = 0; cycle < 10; cycle++) { | ||
| const buffers: ArrayBuffer[] = []; | ||
| // Allocate up to max | ||
| for (let i = 0; i < 5; i++) { | ||
| buffers.push(pool.request().buffer); | ||
| } | ||
| expect(pool.allocatedBlockCount).toBe(5); | ||
| expect(pool.hasAvailableBlocks).toBe(false); | ||
| // Release all | ||
| for (const buffer of buffers) { | ||
| pool.release(buffer); | ||
| } | ||
| expect(pool.allocatedBlockCount).toBe(0); | ||
| expect(pool.hasAvailableBlocks).toBe(true); | ||
| } | ||
| }); | ||
| it("should maintain consistency during interleaved operations", () => { | ||
| const pool = new BufferPool(1, 3, 128); | ||
| const { buffer: buffer1 } = pool.request(); | ||
| const { buffer: buffer2 } = pool.request(); | ||
| expect(pool.allocatedBlockCount).toBe(2); | ||
| expect(pool.totalBlockCount).toBe(2); | ||
| expect(pool.freeBlockCount).toBe(0); | ||
| pool.release(buffer1); | ||
| expect(pool.allocatedBlockCount).toBe(1); | ||
| expect(pool.freeBlockCount).toBe(1); | ||
| const { buffer: buffer3 } = pool.request(); | ||
| const { buffer: buffer4 } = pool.request(); | ||
| expect(pool.allocatedBlockCount).toBe(3); | ||
| expect(pool.totalBlockCount).toBe(3); | ||
| expect(pool.freeBlockCount).toBe(0); | ||
| pool.release(buffer2); | ||
| pool.release(buffer3); | ||
| pool.release(buffer4); | ||
| expect(pool.allocatedBlockCount).toBe(0); | ||
| expect(pool.freeBlockCount).toBe(3); | ||
| }); | ||
| }); | ||
| describe("memory management", () => { | ||
| it("should not allocate new buffers on request when available", () => { | ||
| const pool = new BufferPool(2, 2, 256); | ||
| const internalBuffer0 = pool.getBuffer(0); | ||
| const internalBuffer1 = pool.getBuffer(1); | ||
| const { buffer: requestedBuffer1 } = pool.request(); | ||
| const { buffer: requestedBuffer2 } = pool.request(); | ||
| expect([internalBuffer0, internalBuffer1]).toContain(requestedBuffer1); | ||
| expect([internalBuffer0, internalBuffer1]).toContain(requestedBuffer2); | ||
| expect(requestedBuffer1).not.toBe(requestedBuffer2); | ||
| }); | ||
| it("should reuse buffer instances after release", () => { | ||
| const pool = new BufferPool(1, 1, 512); | ||
| const { buffer: buffer1 } = pool.request(); | ||
| pool.release(buffer1); | ||
| const { buffer: buffer2 } = pool.request(); | ||
| expect(buffer2).toBe(buffer1); | ||
| }); | ||
| it("should maintain buffer contents after release and reallocation", () => { | ||
| const pool = new BufferPool(1, 1, 256); | ||
| const { buffer } = pool.request(); | ||
| const view = new Uint8Array(buffer); | ||
| view[0] = 42; | ||
| view[100] = 123; | ||
| view[255] = 200; | ||
| pool.release(buffer); | ||
| const { buffer: reusedBuffer } = pool.request(); | ||
| const reusedView = new Uint8Array(reusedBuffer); | ||
| expect(reusedView[0]).toBe(42); | ||
| expect(reusedView[100]).toBe(123); | ||
| expect(reusedView[255]).toBe(200); | ||
| }); | ||
| }); | ||
| describe("concurrent operations simulation", () => { | ||
| it("should handle multiple operations in sequence", () => { | ||
| const pool = new BufferPool(1, 3, 128); | ||
| const operations: string[] = []; | ||
| operations.push('request'); | ||
| const { buffer: buffer1 } = pool.request(); | ||
| expect(pool.allocatedBlockCount).toBe(1); | ||
| operations.push('request'); | ||
| const { buffer: buffer2 } = pool.request(); | ||
| expect(pool.allocatedBlockCount).toBe(2); | ||
| expect(pool.totalBlockCount).toBe(2); | ||
| operations.push('release'); | ||
| pool.release(buffer1); | ||
| expect(pool.allocatedBlockCount).toBe(1); | ||
| operations.push('request'); | ||
| const { buffer: buffer3 } = pool.request(); | ||
| expect(pool.allocatedBlockCount).toBe(2); | ||
| operations.push('reset'); | ||
| pool.reset(); | ||
| expect(pool.allocatedBlockCount).toBe(0); | ||
| expect(pool.totalBlockCount).toBe(1); | ||
| operations.push('request'); | ||
| const { buffer: buffer4 } = pool.request(); | ||
| expect(buffer4).not.toBeNull(); | ||
| expect(pool.allocatedBlockCount).toBe(1); | ||
| }); | ||
| }); | ||
| }); |
| export interface BlockBuffer { | ||
| __type: "BlockBuffer"; | ||
| buffer: ArrayBuffer; | ||
| index: number; | ||
| } | ||
| /** | ||
| * Buffer pool with minimal overhead. | ||
| * To control when ArrayBuffers are allocated and freed | ||
| * and to avoid some gc runs. | ||
| */ | ||
| export class BufferPool { | ||
| private buffers: ArrayBuffer[]; | ||
| public readonly blockSize: number; | ||
| private freeBlocks: number[] = []; | ||
| private readonly minBlocks: number; | ||
| private readonly maxBlocks: number; | ||
| private currentBlocks: number; | ||
| private allocatedCount: number = 0; | ||
| private bufferToBlockIndex = new WeakMap<ArrayBuffer, number>(); | ||
| constructor(minBlocks: number, maxBlocks: number, blockSize: number) { | ||
| if (minBlocks <= 0 || maxBlocks <= 0 || blockSize <= 0) { | ||
| throw new Error('Min blocks, max blocks, and block size must be positive'); | ||
| } | ||
| if (minBlocks > maxBlocks) { | ||
| throw new Error('Min blocks cannot be greater than max blocks'); | ||
| } | ||
| this.minBlocks = minBlocks; | ||
| this.maxBlocks = maxBlocks; | ||
| this.blockSize = blockSize; | ||
| this.currentBlocks = minBlocks; | ||
| this.buffers = []; | ||
| this.initializePool(); | ||
| } | ||
| private initializePool(): void { | ||
| this.freeBlocks = []; | ||
| for (let i = 0; i < this.minBlocks; i++) { | ||
| this.buffers.push(new ArrayBuffer(this.blockSize)); | ||
| this.freeBlocks.push(i); | ||
| } | ||
| } | ||
| private expandPool(): boolean { | ||
| if (this.currentBlocks >= this.maxBlocks) { | ||
| return false; | ||
| } | ||
| const oldBlocks = this.currentBlocks; | ||
| const newBlocks = Math.min(this.maxBlocks, this.currentBlocks * 2); | ||
| for (let i = oldBlocks; i < newBlocks; i++) { | ||
| this.buffers.push(new ArrayBuffer(this.blockSize)); | ||
| this.freeBlocks.push(i); | ||
| } | ||
| this.currentBlocks = newBlocks; | ||
| return true; | ||
| } | ||
| /** | ||
| * Request a block. Returns an object with the pre-allocated ArrayBuffer and its index, or throws if out of memory. | ||
| */ | ||
| request(): BlockBuffer { | ||
| if (this.freeBlocks.length === 0) { | ||
| if (!this.expandPool()) { | ||
| throw new Error('BufferPool out of memory: no free blocks available'); | ||
| } | ||
| } | ||
| const blockIndex = this.freeBlocks.pop()!; | ||
| if (blockIndex < 0 || blockIndex >= this.buffers.length) { | ||
| throw new Error('Invalid block index'); | ||
| } | ||
| this.allocatedCount++; | ||
| const buffer = this.buffers[blockIndex]!; | ||
| this.bufferToBlockIndex.set(buffer, blockIndex); | ||
| return { __type: "BlockBuffer", buffer, index: blockIndex }; | ||
| } | ||
| /** | ||
| * Release a block using the ArrayBuffer returned from request(). | ||
| */ | ||
| release(buffer: ArrayBuffer): void { | ||
| const blockIndex = this.bufferToBlockIndex.get(buffer); | ||
| if (blockIndex === undefined) { | ||
| throw new Error('ArrayBuffer was not allocated from this allocator or already freed'); | ||
| } | ||
| this.bufferToBlockIndex.delete(buffer); | ||
| this.freeBlocks.push(blockIndex); | ||
| this.allocatedCount--; | ||
| } | ||
| /** | ||
| * Release a block by its index. | ||
| */ | ||
| releaseBlock(blockIndex: number): void { | ||
| if (blockIndex < 0 || blockIndex >= this.currentBlocks) { | ||
| throw new Error('Block index out of range'); | ||
| } | ||
| const buffer = this.buffers[blockIndex]!; | ||
| if (!this.bufferToBlockIndex.has(buffer)) { | ||
| throw new Error(`Block ${blockIndex} was not allocated or already freed`); | ||
| } | ||
| this.bufferToBlockIndex.delete(buffer); | ||
| this.freeBlocks.push(blockIndex); | ||
| this.allocatedCount--; | ||
| } | ||
| /** | ||
| * Get the ArrayBuffer for a specific block index. | ||
| */ | ||
| getBuffer(blockIndex: number): ArrayBuffer { | ||
| if (blockIndex < 0 || blockIndex >= this.currentBlocks) { | ||
| throw new Error('Block index out of range'); | ||
| } | ||
| return this.buffers[blockIndex]!; | ||
| } | ||
| reset(): void { | ||
| this.allocatedCount = 0; | ||
| this.bufferToBlockIndex = new WeakMap<ArrayBuffer, number>(); | ||
| this.currentBlocks = this.minBlocks; | ||
| this.buffers = []; | ||
| this.initializePool(); | ||
| } | ||
| get totalBlockCount(): number { | ||
| return this.currentBlocks; | ||
| } | ||
| get maxBlockCount(): number { | ||
| return this.maxBlocks; | ||
| } | ||
| get minBlockCount(): number { | ||
| return this.minBlocks; | ||
| } | ||
| get allocatedBlockCount(): number { | ||
| return this.allocatedCount; | ||
| } | ||
| get freeBlockCount(): number { | ||
| return this.freeBlocks.length; | ||
| } | ||
| get hasAvailableBlocks(): boolean { | ||
| return this.freeBlocks.length > 0 || this.currentBlocks < this.maxBlocks; | ||
| } | ||
| get utilizationRatio(): number { | ||
| return this.currentBlocks > 0 ? this.allocatedCount / this.currentBlocks : 0; | ||
| } | ||
| } |
| export const TextureUsageFlags = { | ||
| COPY_SRC: 1 << 0, | ||
| COPY_DST: 1 << 1, | ||
| TEXTURE_BINDING: 1 << 2, | ||
| STORAGE_BINDING: 1 << 3, | ||
| RENDER_ATTACHMENT: 1 << 4, | ||
| TRANSIENT_ATTACHMENT: 1 << 5, | ||
| } as const; | ||
| export const BufferUsageFlags = { | ||
| MAP_READ: 1 << 0, | ||
| MAP_WRITE: 1 << 1, | ||
| COPY_SRC: 1 << 2, | ||
| COPY_DST: 1 << 3, | ||
| INDEX: 1 << 4, | ||
| VERTEX: 1 << 5, | ||
| UNIFORM: 1 << 6, | ||
| STORAGE: 1 << 7, | ||
| INDIRECT: 1 << 8, | ||
| QUERY_RESOLVE: 1 << 9, | ||
| } as const; | ||
| export const ShaderStageFlags = { | ||
| VERTEX: 1 << 0, | ||
| FRAGMENT: 1 << 1, | ||
| COMPUTE: 1 << 2, | ||
| } as const; | ||
| export const MapModeFlags = { | ||
| READ: 1 << 0, | ||
| WRITE: 1 << 1, | ||
| } as const; |
| /** | ||
| * Just for testing some performance and memory behavior. | ||
| */ | ||
| import { type Pointer } from "bun:ffi"; | ||
| import { | ||
| createGPUInstance, | ||
| } from '..'; | ||
| import type { GPUImpl } from "../GPU"; | ||
| import { setupGlobals } from "../index"; | ||
| setupGlobals(); | ||
| export async function runTriangleToPngExample(filename: string = "triangle.png") { | ||
| console.log("\n--- Running Triangle to PNG Example ---"); | ||
| let gpu: GPUImpl | null = null; | ||
| let adapter: GPUAdapter | null = null; | ||
| let device: GPUDevice | null = null; | ||
| let queue: GPUQueue | null = null; | ||
| let queuePtr: Pointer | null = null; | ||
| // Resources | ||
| let vsModule: GPUShaderModule | null = null; | ||
| let fsModule: GPUShaderModule | null = null; | ||
| let bgl: GPUBindGroupLayout | null = null; | ||
| let pll: GPUPipelineLayout | null = null; | ||
| let pipeline: GPURenderPipeline | null = null; | ||
| let vertexBuffer: GPUBuffer | null = null; | ||
| let indexBuffer: GPUBuffer | null = null; | ||
| let renderTargetTexture: GPUTexture | null = null; | ||
| let renderTargetView: GPUTextureView | null = null; | ||
| let readbackBuffer: GPUBuffer | null = null; | ||
| let commandEncoder: GPUCommandEncoder | null = null; | ||
| let commandBuffer: GPUCommandBuffer | null = null; | ||
| let emptyBindGroup: GPUBindGroup | null = null; // NEW | ||
| const width = 128; | ||
| const height = 128; | ||
| const textureFormat = "rgba8unorm"; // Format needs to be easy for sharp | ||
| try { | ||
| // 1. Setup Context | ||
| gpu = createGPUInstance(); | ||
| adapter = await gpu.requestAdapter(); | ||
| if (!adapter) { | ||
| throw new Error("Example setup failed: Could not get WGPU context."); | ||
| } | ||
| device = await adapter.requestDevice(); | ||
| if (!device) { | ||
| throw new Error("Example setup failed: Could not get WGPU context."); | ||
| } | ||
| // @ts-ignore testing | ||
| const devicePtr = device.devicePtr; | ||
| if (!devicePtr) { | ||
| throw new Error("Example setup failed: Could not get WGPU context."); | ||
| } | ||
| queue = device.queue; | ||
| if (!gpu || !adapter || !device || !queue) { | ||
| throw new Error("Example setup failed: Could not get WGPU context."); | ||
| } | ||
| queuePtr = queue.ptr; | ||
| if (!queuePtr) { | ||
| throw new Error("Example setup failed: Could not get WGPU context."); | ||
| } | ||
| device.onuncapturederror = (ev) => { | ||
| console.error("[Example] Uncaptured Error: Type={}, Message={}", ev.error.message); | ||
| }; | ||
| // 2. Resources (Shaders, Buffers, Layouts, Pipeline) | ||
| const vertexData = new Float32Array([-0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.0, 0.5, 0.0]); | ||
| const indexData = new Uint16Array([0, 1, 2, 3]); | ||
| const vertexBufferStride = 3 * Float32Array.BYTES_PER_ELEMENT; | ||
| const wgslVS = /* wgsl */ `@vertex fn main(@location(0) pos: vec3f) -> @builtin(position) vec4f { return vec4f(pos, 1.0); }`; | ||
| const wgslFS = /* wgsl */ `@fragment fn main() -> @location(0) vec4f { return vec4f(0.0, 1.0, 0.0, 1.0); }`; // Green triangle | ||
| vsModule = device!.createShaderModule({ code: wgslVS }); | ||
| fsModule = device!.createShaderModule({ code: wgslFS }); | ||
| // Final buffers (Ensure COPY_DST usage) | ||
| vertexBuffer = device!.createBuffer({ size: vertexData.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST }); | ||
| indexBuffer = device!.createBuffer({ size: indexData.byteLength, usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST }); | ||
| // Use queueWriteBuffer to upload data directly | ||
| console.log("Uploading vertex/index data using queueWriteBuffer..."); | ||
| queue!.writeBuffer(vertexBuffer!, 0, vertexData.buffer); | ||
| queue!.writeBuffer(indexBuffer!, 0, indexData.buffer); | ||
| // We might need to wait for these writes if subsequent commands depend on them immediately, | ||
| // but the render pass doesn't start until after command encoding, so it should be fine. | ||
| // Layouts & Pipeline | ||
| bgl = device!.createBindGroupLayout({ entries: [] }); | ||
| pll = device!.createPipelineLayout({ bindGroupLayouts: [bgl!] }); | ||
| // Create the empty bind group (NEW) | ||
| emptyBindGroup = device!.createBindGroup({ | ||
| label: "Example Empty BG", | ||
| layout: bgl!, | ||
| entries: [], | ||
| }); | ||
| const vertexBuffersLayout: GPUVertexBufferLayout[] = [{ | ||
| arrayStride: vertexBufferStride, | ||
| attributes: [{ format: "float32x3", offset: 0, shaderLocation: 0 }] | ||
| }]; | ||
| pipeline = device!.createRenderPipeline({ | ||
| layout: pll, | ||
| vertex: { module: vsModule, buffers: vertexBuffersLayout }, | ||
| fragment: { module: fsModule, targets: [{ format: textureFormat }] }, | ||
| primitive: { topology: "triangle-list" } | ||
| }); | ||
| // Render Target | ||
| renderTargetTexture = device!.createTexture({ | ||
| size: [width, height], | ||
| format: textureFormat, | ||
| usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, | ||
| }); | ||
| renderTargetView = renderTargetTexture!.createView({ label: "Triangle Render Target View" }); | ||
| // Readback Buffer | ||
| const bytesPerRow = width * 4; // RGBA8 | ||
| const readbackBufferSize = bytesPerRow * height; | ||
| readbackBuffer = device!.createBuffer({ | ||
| label: "Triangle Readback Buffer", | ||
| size: readbackBufferSize, | ||
| usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, | ||
| }); | ||
| if (!vsModule || !fsModule || !vertexBuffer || !indexBuffer || !bgl || !pll || !pipeline || !renderTargetTexture || !renderTargetView || !readbackBuffer) { | ||
| throw new Error("Example setup failed: Resource creation failed."); | ||
| } | ||
| // 3. Command Encoding | ||
| commandEncoder = device.createCommandEncoder({ label: "Triangle Command Encoder" }); | ||
| // Render Pass | ||
| const passEncoder = commandEncoder!.beginRenderPass({ | ||
| colorAttachments: [{ | ||
| view: renderTargetView!, | ||
| loadOp: "clear", | ||
| storeOp: "store", | ||
| clearValue: { r: 0.1, g: 0.1, b: 0.1, a: 1.0 }, // Dark background | ||
| }] | ||
| }); | ||
| passEncoder!.setPipeline(pipeline); | ||
| passEncoder!.setViewport(0, 0, width, height, 0, 1); | ||
| passEncoder!.setScissorRect(0, 0, width, height); | ||
| passEncoder!.setVertexBuffer(0, vertexBuffer!); | ||
| passEncoder!.setIndexBuffer(indexBuffer!, "uint16"); | ||
| passEncoder!.setBindGroup(0, emptyBindGroup!); | ||
| passEncoder!.drawIndexed(3); | ||
| passEncoder!.end(); | ||
| let count = 0; | ||
| while (true) { | ||
| console.log(`Loop ${count}...`); | ||
| count++; | ||
| const copyCommandEncoder = device.createCommandEncoder({ label: "Triangle Copy Command Encoder" }); | ||
| // Copy Texture to Readback Buffer | ||
| copyCommandEncoder!.copyTextureToBuffer( | ||
| { texture: renderTargetTexture! }, // source | ||
| { buffer: readbackBuffer!, bytesPerRow, rowsPerImage: height }, // destination | ||
| { width, height } // copySize | ||
| ); | ||
| commandBuffer = copyCommandEncoder!.finish(); | ||
| queue.submit([commandBuffer!]); | ||
| await queue!.onSubmittedWorkDone(); | ||
| await readbackBuffer!.mapAsync(GPUMapMode.READ, 0, readbackBufferSize); | ||
| // const mappedData = readbackBuffer!.getMappedRange(0, readbackBufferSize); | ||
| // if (!mappedData) { | ||
| // throw new Error("Failed to map readback buffer."); | ||
| // } | ||
| // // 6. Save to PNG using Sharp | ||
| // console.log(`Attempting to save ${width}x${height} texture to ${filename}...`); | ||
| // try { | ||
| // await sharp(Buffer.from(mappedData), { // Use Buffer.from to copy | ||
| // raw: { | ||
| // width: width, | ||
| // height: height, | ||
| // channels: 4, // RGBA | ||
| // }, | ||
| // }) | ||
| // .png() | ||
| // .toFile(filename); | ||
| // console.log(`Successfully saved image to ${filename}`); | ||
| // } catch(sharpError) { | ||
| // console.error("Failed to save PNG with sharp:", sharpError); | ||
| // console.error("Please ensure sharp is installed ('bun add sharp') and native dependencies are met."); | ||
| // } | ||
| readbackBuffer!.unmap(); // Unmap after sharp has copied the data | ||
| await Bun.sleep(100); | ||
| } | ||
| } catch (e) { | ||
| console.error("Error in Triangle to PNG Example:", e); | ||
| } finally { | ||
| console.log("Example Cleanup..."); | ||
| // Cleanup resources (order matters roughly reverse of creation/use) | ||
| if (readbackBuffer) readbackBuffer.destroy(); | ||
| // CommandEncoder is consumed by finish | ||
| // RenderPassEncoder is consumed by end | ||
| if (renderTargetView) renderTargetView.destroy(); | ||
| if (renderTargetTexture) renderTargetTexture.destroy(); | ||
| if (pipeline) pipeline.destroy(); | ||
| if (indexBuffer) indexBuffer.destroy(); | ||
| if (vertexBuffer) vertexBuffer.destroy(); | ||
| if (pll) pll.destroy(); | ||
| if (bgl) bgl.destroy(); | ||
| if (emptyBindGroup) emptyBindGroup.destroy(); // NEW Cleanup | ||
| if (fsModule) fsModule.destroy(); | ||
| if (vsModule) vsModule.destroy(); | ||
| if (device) device.destroy(); | ||
| if (gpu) gpu.destroy(); | ||
| console.log("--- Example Finished ---"); | ||
| } | ||
| } | ||
| // --- Call the example --- | ||
| runTriangleToPngExample(); // Uncomment to run |
| import sharp from 'sharp'; | ||
| import { type Pointer } from "bun:ffi"; | ||
| import { | ||
| createGPUInstance, | ||
| setupGlobals, | ||
| } from '..'; | ||
| import type { GPUImpl } from "../GPU"; | ||
| setupGlobals(); | ||
| export async function runTriangleToPngExample(filename: string = "triangle.png") { | ||
| console.log("\n--- Running Triangle to PNG Example ---"); | ||
| let gpu: GPUImpl | null = null; | ||
| let adapter: GPUAdapter | null = null; | ||
| let device: GPUDevice | null = null; | ||
| let queue: GPUQueue | null = null; | ||
| let queuePtr: Pointer | null = null; | ||
| // Resources | ||
| let vsModule: GPUShaderModule | null = null; | ||
| let fsModule: GPUShaderModule | null = null; | ||
| let bgl: GPUBindGroupLayout | null = null; | ||
| let pll: GPUPipelineLayout | null = null; | ||
| let pipeline: GPURenderPipeline | null = null; | ||
| let vertexBuffer: GPUBuffer | null = null; | ||
| let indexBuffer: GPUBuffer | null = null; | ||
| let renderTargetTexture: GPUTexture | null = null; | ||
| let renderTargetView: GPUTextureView | null = null; | ||
| let readbackBuffer: GPUBuffer | null = null; | ||
| let commandEncoder: GPUCommandEncoder | null = null; | ||
| let commandBuffer: GPUCommandBuffer | null = null; | ||
| let emptyBindGroup: GPUBindGroup | null = null; // NEW | ||
| const width = 128; | ||
| const height = 128; | ||
| const textureFormat = "rgba8unorm"; // Format needs to be easy for sharp | ||
| try { | ||
| // 1. Setup Context | ||
| gpu = createGPUInstance(); | ||
| adapter = await gpu.requestAdapter(); | ||
| if (!adapter) { | ||
| throw new Error("Example setup failed: Could not get WGPU context."); | ||
| } | ||
| device = await adapter.requestDevice(); | ||
| if (!device) { | ||
| throw new Error("Example setup failed: Could not get WGPU context."); | ||
| } | ||
| // @ts-ignore testing | ||
| const devicePtr = device.devicePtr; | ||
| if (!devicePtr) { | ||
| throw new Error("Example setup failed: Could not get WGPU context."); | ||
| } | ||
| queue = device.queue; | ||
| if (!gpu || !adapter || !device || !queue) { | ||
| throw new Error("Example setup failed: Could not get WGPU context."); | ||
| } | ||
| queuePtr = queue.ptr; | ||
| if (!queuePtr) { | ||
| throw new Error("Example setup failed: Could not get WGPU context."); | ||
| } | ||
| // Setup error handler for debugging | ||
| device.onuncapturederror = (ev) => { | ||
| console.error("[Example] Uncaptured Error: Type={}, Message={}", ev.error.message); | ||
| }; | ||
| // 2. Resources (Shaders, Buffers, Layouts, Pipeline) | ||
| const vertexData = new Float32Array([-0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.0, 0.5, 0.0]); | ||
| const indexData = new Uint16Array([0, 1, 2, 3]); | ||
| const vertexBufferStride = 3 * Float32Array.BYTES_PER_ELEMENT; | ||
| const wgslVS = /* wgsl */ `@vertex fn main(@location(0) pos: vec3f) -> @builtin(position) vec4f { return vec4f(pos, 1.0); }`; | ||
| const wgslFS = /* wgsl */ `@fragment fn main() -> @location(0) vec4f { return vec4f(0.0, 1.0, 0.0, 1.0); }`; // Green triangle | ||
| vsModule = device!.createShaderModule({ code: wgslVS }); | ||
| fsModule = device!.createShaderModule({ code: wgslFS }); | ||
| // Final buffers (Ensure COPY_DST usage) | ||
| vertexBuffer = device!.createBuffer({ size: vertexData.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST }); | ||
| indexBuffer = device!.createBuffer({ size: indexData.byteLength, usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST }); | ||
| // Use queueWriteBuffer to upload data directly | ||
| console.log("Uploading vertex/index data using queueWriteBuffer..."); | ||
| queue!.writeBuffer(vertexBuffer!, 0, vertexData.buffer); | ||
| queue!.writeBuffer(indexBuffer!, 0, indexData.buffer); | ||
| // We might need to wait for these writes if subsequent commands depend on them immediately, | ||
| // but the render pass doesn't start until after command encoding, so it should be fine. | ||
| // Layouts & Pipeline | ||
| bgl = device!.createBindGroupLayout({ entries: [] }); | ||
| pll = device!.createPipelineLayout({ bindGroupLayouts: [bgl!] }); | ||
| // Create the empty bind group (NEW) | ||
| emptyBindGroup = device!.createBindGroup({ | ||
| label: "Example Empty BG", | ||
| layout: bgl!, | ||
| entries: [], | ||
| }); | ||
| const vertexBuffersLayout: GPUVertexBufferLayout[] = [{ | ||
| arrayStride: vertexBufferStride, | ||
| attributes: [{ format: "float32x3", offset: 0, shaderLocation: 0 }] | ||
| }]; | ||
| pipeline = device!.createRenderPipeline({ | ||
| layout: pll, | ||
| vertex: { module: vsModule, buffers: vertexBuffersLayout }, | ||
| fragment: { module: fsModule, targets: [{ format: textureFormat }] }, | ||
| primitive: { topology: "triangle-list" } | ||
| }); | ||
| // Render Target | ||
| renderTargetTexture = device!.createTexture({ | ||
| size: [width, height], | ||
| format: textureFormat, | ||
| usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, | ||
| }); | ||
| renderTargetView = renderTargetTexture!.createView({ label: "Triangle Render Target View" }); | ||
| // Readback Buffer | ||
| const bytesPerRow = width * 4; // RGBA8 | ||
| const readbackBufferSize = bytesPerRow * height; | ||
| readbackBuffer = device!.createBuffer({ | ||
| label: "Triangle Readback Buffer", | ||
| size: readbackBufferSize, | ||
| usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, | ||
| }); | ||
| if (!vsModule || !fsModule || !vertexBuffer || !indexBuffer || !bgl || !pll || !pipeline || !renderTargetTexture || !renderTargetView || !readbackBuffer) { | ||
| throw new Error("Example setup failed: Resource creation failed."); | ||
| } | ||
| // 3. Command Encoding | ||
| commandEncoder = device.createCommandEncoder({ label: "Triangle Command Encoder" }); | ||
| // Render Pass | ||
| const passEncoder = commandEncoder!.beginRenderPass({ | ||
| colorAttachments: [{ | ||
| view: renderTargetView!, | ||
| loadOp: "clear", | ||
| storeOp: "store", | ||
| clearValue: { r: 0.1, g: 0.1, b: 0.1, a: 1.0 }, // Dark background | ||
| }] | ||
| }); | ||
| passEncoder!.setPipeline(pipeline); | ||
| passEncoder!.setViewport(0, 0, width, height, 0, 1); | ||
| passEncoder!.setScissorRect(0, 0, width, height); | ||
| passEncoder!.setVertexBuffer(0, vertexBuffer!); | ||
| passEncoder!.setIndexBuffer(indexBuffer!, "uint16"); | ||
| passEncoder!.setBindGroup(0, emptyBindGroup!); | ||
| passEncoder!.drawIndexed(3); | ||
| passEncoder!.end(); | ||
| // Copy Texture to Readback Buffer | ||
| commandEncoder!.copyTextureToBuffer( | ||
| { texture: renderTargetTexture! }, // source | ||
| { buffer: readbackBuffer!, bytesPerRow, rowsPerImage: height }, // destination | ||
| { width, height } // copySize | ||
| ); | ||
| commandBuffer = commandEncoder!.finish(); | ||
| // 4. Submit and Wait | ||
| queue.submit([commandBuffer!]); | ||
| await queue!.onSubmittedWorkDone(); | ||
| // 5. Map Readback Buffer | ||
| await readbackBuffer!.mapAsync(GPUMapMode.READ, 0, readbackBufferSize); | ||
| const mappedData = readbackBuffer!.getMappedRange(0, readbackBufferSize); | ||
| if (!mappedData) { | ||
| throw new Error("Failed to map readback buffer."); | ||
| } | ||
| // 6. Save to PNG using Sharp | ||
| console.log(`Attempting to save ${width}x${height} texture to ${filename}...`); | ||
| try { | ||
| await sharp(Buffer.from(mappedData), { | ||
| raw: { | ||
| width: width, | ||
| height: height, | ||
| channels: 4, // RGBA | ||
| }, | ||
| }) | ||
| .png() | ||
| .toFile(filename); | ||
| console.log(`Successfully saved image to ${filename}`); | ||
| } catch(sharpError) { | ||
| console.error("Failed to save PNG with sharp:", sharpError); | ||
| console.error("Please ensure sharp is installed ('bun add sharp') and native dependencies are met."); | ||
| } | ||
| readbackBuffer!.unmap(); // Unmap after sharp has copied the data | ||
| // --- SECOND COPY TEST --- | ||
| console.log("Starting second copy test..."); | ||
| const readbackBuffer2 = device!.createBuffer({ | ||
| label: "Triangle Readback Buffer 2", | ||
| size: readbackBufferSize, // Reuse size | ||
| usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, | ||
| }); | ||
| const commandEncoder2 = device.createCommandEncoder({ label: "Triangle Command Encoder 2" }); | ||
| commandEncoder2!.copyTextureToBuffer( | ||
| { texture: renderTargetTexture! }, // source (original texture) | ||
| { buffer: readbackBuffer2!, bytesPerRow, rowsPerImage: height }, // destination (new buffer) | ||
| { width, height } // copySize | ||
| ); | ||
| const commandBuffer2 = commandEncoder2!.finish(); | ||
| console.log("Submitting second copy command..."); | ||
| queue.submit([commandBuffer2!]); | ||
| await queue!.onSubmittedWorkDone(); | ||
| console.log("Second copy finished."); | ||
| console.log("Mapping second readback buffer..."); | ||
| await readbackBuffer2!.mapAsync(GPUMapMode.READ, 0, readbackBufferSize); | ||
| const mappedData2 = readbackBuffer2!.getMappedRange(0, readbackBufferSize); | ||
| if (!mappedData2) { | ||
| console.error("Failed to map second readback buffer."); | ||
| } else { | ||
| // Optional: Verify a pixel or just confirm mapping worked | ||
| console.log("Second readback buffer mapped successfully."); | ||
| // Example: Check first pixel's green value (should be ~255) | ||
| const pixelView = new Uint8Array(mappedData2); | ||
| if (pixelView.length >= 4) { | ||
| console.log(`Second copy, pixel (0,0) G value: ${pixelView[1]}`); | ||
| } else { | ||
| console.warn("Second mapped data too small to check pixel."); | ||
| } | ||
| } | ||
| readbackBuffer2!.unmap(); | ||
| readbackBuffer2.destroy(); // Clean up second buffer immediately | ||
| // --- END SECOND COPY TEST --- | ||
| } catch (e) { | ||
| console.error("Error in Triangle to PNG Example:", e); | ||
| } finally { | ||
| console.log("Example Cleanup..."); | ||
| if (readbackBuffer) readbackBuffer.destroy(); | ||
| if (renderTargetView) renderTargetView.destroy(); | ||
| if (renderTargetTexture) renderTargetTexture.destroy(); | ||
| if (pipeline) pipeline.destroy(); | ||
| if (indexBuffer) indexBuffer.destroy(); | ||
| if (vertexBuffer) vertexBuffer.destroy(); | ||
| if (pll) pll.destroy(); | ||
| if (bgl) bgl.destroy(); | ||
| if (emptyBindGroup) emptyBindGroup.destroy(); // NEW Cleanup | ||
| if (fsModule) fsModule.destroy(); | ||
| if (vsModule) vsModule.destroy(); | ||
| if (device) device.destroy(); | ||
| if (gpu) gpu.destroy(); | ||
| console.log("--- Example Finished ---"); | ||
| } | ||
| } | ||
| runTriangleToPngExample(); |
-1038
| import { dlopen, suffix, FFIType } from "bun:ffi"; | ||
| import { join } from "path"; | ||
| import { fileURLToPath } from "url"; | ||
| import { existsSync } from "fs"; | ||
| import os from "os"; | ||
| // Get the directory of the current module | ||
| const __dirname = fileURLToPath(new URL(".", import.meta.url)); | ||
| // const DEFAULT_PATH = join(__dirname, "../dawn/"); | ||
| const DEFAULT_PATH = join(__dirname, "lib/"); | ||
| // const LIB_NAME = "webgpu_dawn"; | ||
| const LIB_NAME = "webgpu_wrapper"; | ||
| // Map platform and architecture to the target name format used in our build | ||
| function getPlatformTarget(): string { | ||
| const platform = os.platform(); | ||
| const arch = os.arch(); | ||
| // Convert Bun/Node.js platform names to Zig platform names | ||
| const platformMap: Record<string, string> = { | ||
| 'darwin': 'macos', | ||
| 'win32': 'windows', | ||
| 'linux': 'linux', | ||
| }; | ||
| const archMap: Record<string, string> = { | ||
| 'x64': 'x86_64', | ||
| 'arm64': 'aarch64', | ||
| }; | ||
| const zigPlatform = platformMap[platform] || platform; | ||
| const zigArch = archMap[arch] || arch; | ||
| return `${zigArch}-${zigPlatform}`; | ||
| } | ||
| function findLibrary(): string { | ||
| const target = getPlatformTarget(); | ||
| const libDir = DEFAULT_PATH; | ||
| // First try target-specific directory | ||
| const [arch, osName] = target.split('-'); | ||
| const isWindows = osName === 'windows'; | ||
| const libraryName = isWindows ? LIB_NAME : `lib${LIB_NAME}`; | ||
| const targetLibPath = join(libDir, target, `${libraryName}.${suffix}`); | ||
| if (existsSync(targetLibPath)) { | ||
| return targetLibPath; | ||
| } | ||
| throw new Error(`Could not find dawn library for platform: ${target} at ${targetLibPath}`); | ||
| } | ||
| function _loadLibrary(libPath?: string) { | ||
| const resolvedPath = libPath || findLibrary(); | ||
| // Define the FFI interface based on webgpu.h functions | ||
| const { symbols } = dlopen(resolvedPath, { | ||
| // --- Core API Functions --- | ||
| zwgpuCreateInstance: { | ||
| args: [FFIType.pointer], // descriptor: *const WGPUInstanceDescriptor (nullable) | ||
| returns: FFIType.pointer, // -> WGPUInstance | ||
| }, | ||
| // --- Instance Functions --- | ||
| zwgpuInstanceCreateSurface: { | ||
| args: [FFIType.pointer, FFIType.pointer], // instance: WGPUInstance, descriptor: *const WGPUSurfaceDescriptor | ||
| returns: FFIType.pointer, // -> WGPUSurface | ||
| }, | ||
| zwgpuInstanceProcessEvents: { | ||
| args: [FFIType.pointer], // instance: WGPUInstance | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuInstanceRequestAdapter: { | ||
| args: [ | ||
| FFIType.pointer, // instance: WGPUInstance | ||
| FFIType.pointer, // options: *const WGPURequestAdapterOptions (nullable) | ||
| FFIType.pointer, // callbackInfo: WGPURequestAdapterCallbackInfo | ||
| ], | ||
| returns: FFIType.u64, // -> WGPUFuture (id) | ||
| }, | ||
| zwgpuInstanceWaitAny: { | ||
| args: [ | ||
| FFIType.pointer, // instance: WGPUInstance | ||
| FFIType.u64, // futureCount: size_t (using u64) | ||
| FFIType.pointer, // futures: *WGPUFutureWaitInfo | ||
| FFIType.u64, // timeoutNS: uint64_t | ||
| ], | ||
| returns: FFIType.u32, // -> WGPUWaitStatus (enum) | ||
| }, | ||
| zwgpuInstanceGetWGSLLanguageFeatures: { | ||
| args: [FFIType.pointer, FFIType.pointer], // instance: WGPUInstance, features: *mut WGPUSupportedWGSLLanguageFeatures | ||
| returns: FFIType.u32, // -> WGPUStatus | ||
| }, | ||
| zwgpuInstanceRelease: { | ||
| args: [FFIType.pointer], // instance: WGPUInstance | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuInstanceAddRef: { // Typically not needed from JS due to GC, but included for completeness | ||
| args: [FFIType.pointer], // instance: WGPUInstance | ||
| returns: FFIType.void, | ||
| }, | ||
| // --- Adapter Functions --- | ||
| zwgpuAdapterCreateDevice: { | ||
| args: [ | ||
| FFIType.pointer, // adapter: WGPUAdapter | ||
| FFIType.pointer, // descriptor: *const WGPUDeviceDescriptor (nullable) | ||
| ], | ||
| returns: FFIType.pointer, // -> WGPUDevice | ||
| }, | ||
| zwgpuAdapterGetInfo: { | ||
| args: [FFIType.pointer, FFIType.pointer], // adapter: WGPUAdapter, info: *mut WGPUAdapterInfo | ||
| returns: FFIType.u32, // WGPUStatus | ||
| }, | ||
| zwgpuAdapterRequestDevice: { | ||
| args: [ | ||
| FFIType.pointer, // adapter: WGPUAdapter | ||
| FFIType.pointer, // options: *const WGPUDeviceDescriptor (nullable) | ||
| FFIType.pointer, // callbackInfo: WGPURequestDeviceCallbackInfo | ||
| ], | ||
| returns: FFIType.u64, // -> WGPUFuture (id) | ||
| }, | ||
| zwgpuAdapterRelease: { | ||
| args: [FFIType.pointer], // adapter: WGPUAdapter | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuAdapterGetFeatures: { // Added for getting adapter features | ||
| args: [FFIType.pointer, FFIType.pointer], // adapter: WGPUAdapter, features: *WGPUSupportedFeatures | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuAdapterGetLimits: { | ||
| args: [FFIType.pointer, FFIType.pointer], // adapter: WGPUAdapter, limits: *mut WGPULimits | ||
| returns: FFIType.u32, // WGPUStatus | ||
| }, | ||
| // Add other adapter functions like GetFeatures, GetLimits, HasFeature etc. | ||
| // --- Device Functions --- | ||
| zwgpuDeviceGetAdapterInfo: { | ||
| args: [FFIType.pointer, FFIType.pointer], // device: WGPUDevice, info: *mut WGPUAdapterInfo | ||
| returns: FFIType.u32, // WGPUStatus | ||
| }, | ||
| zwgpuDeviceCreateBuffer: { | ||
| args: [FFIType.pointer, FFIType.pointer], // device: WGPUDevice, descriptor: *const WGPUBufferDescriptor | ||
| returns: FFIType.pointer, // -> WGPUBuffer | ||
| }, | ||
| zwgpuDeviceCreateTexture: { | ||
| args: [FFIType.pointer, FFIType.pointer], // device: WGPUDevice, descriptor: *const WGPUTextureDescriptor | ||
| returns: FFIType.pointer, // -> WGPUTexture | ||
| }, | ||
| zwgpuDeviceCreateSampler: { | ||
| args: [FFIType.pointer, FFIType.pointer], // device: WGPUDevice, descriptor: *const WGPUSamplerDescriptor (nullable) | ||
| returns: FFIType.pointer, // -> WGPUSampler | ||
| }, | ||
| zwgpuDeviceCreateShaderModule: { | ||
| args: [FFIType.pointer, FFIType.pointer], // device: WGPUDevice, descriptor: *const WGPUShaderModuleDescriptor | ||
| returns: FFIType.pointer, // -> WGPUShaderModule | ||
| }, | ||
| zwgpuDeviceCreateBindGroupLayout: { | ||
| args: [FFIType.pointer, FFIType.pointer], // device: WGPUDevice, descriptor: *const WGPUBindGroupLayoutDescriptor | ||
| returns: FFIType.pointer, // -> WGPUBindGroupLayout | ||
| }, | ||
| zwgpuDeviceCreateBindGroup: { | ||
| args: [FFIType.pointer, FFIType.pointer], // device: WGPUDevice, descriptor: *const WGPUBindGroupDescriptor | ||
| returns: FFIType.pointer, // -> WGPUBindGroup | ||
| }, | ||
| zwgpuDeviceCreatePipelineLayout: { | ||
| args: [FFIType.pointer, FFIType.pointer], // device: WGPUDevice, descriptor: *const WGPUPipelineLayoutDescriptor | ||
| returns: FFIType.pointer, // -> WGPUPipelineLayout | ||
| }, | ||
| zwgpuDeviceCreateRenderPipeline: { | ||
| args: [FFIType.pointer, FFIType.pointer], // device: WGPUDevice, descriptor: *const WGPURenderPipelineDescriptor | ||
| returns: FFIType.pointer, // -> WGPURenderPipeline | ||
| }, | ||
| zwgpuDeviceCreateComputePipeline: { | ||
| args: [FFIType.pointer, FFIType.pointer], // device: WGPUDevice, descriptor: *const WGPUComputePipelineDescriptor | ||
| returns: FFIType.pointer, // -> WGPUComputePipeline | ||
| }, | ||
| zwgpuDeviceCreateRenderBundleEncoder: { | ||
| args: [FFIType.pointer, FFIType.pointer], // device: WGPUDevice, descriptor: *const WGPURenderBundleEncoderDescriptor | ||
| returns: FFIType.pointer, // -> WGPURenderBundleEncoder | ||
| }, | ||
| zwgpuDeviceCreateCommandEncoder: { | ||
| args: [FFIType.pointer, FFIType.pointer], // device: WGPUDevice, descriptor: *const WGPUCommandEncoderDescriptor (nullable) | ||
| returns: FFIType.pointer, // -> WGPUCommandEncoder | ||
| }, | ||
| zwgpuDeviceCreateQuerySet: { | ||
| args: [FFIType.pointer, FFIType.pointer], // device: WGPUDevice, descriptor: *const WGPUQuerySetDescriptor | ||
| returns: FFIType.pointer, // -> WGPUQuerySet | ||
| }, | ||
| zwgpuDeviceGetQueue: { | ||
| args: [FFIType.pointer], // device: WGPUDevice | ||
| returns: FFIType.pointer, // -> WGPUQueue | ||
| }, | ||
| zwgpuDeviceGetLimits: { | ||
| args: [FFIType.pointer, FFIType.pointer], // device: WGPUDevice, limits: *mut WGPULimits | ||
| returns: FFIType.u32, // WGPUStatus | ||
| }, | ||
| zwgpuDeviceHasFeature: { | ||
| args: [FFIType.pointer, FFIType.u32], // device: WGPUDevice, feature: WGPUFeatureName (enum) | ||
| returns: FFIType.bool, // WGPUBool (use bool for direct mapping) | ||
| }, | ||
| zwgpuDeviceGetFeatures: { | ||
| args: [FFIType.pointer, FFIType.pointer], // device: WGPUDevice, features: *mut WGPUSupportedFeatures | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuDevicePushErrorScope: { | ||
| args: [FFIType.pointer, FFIType.u32], // device: WGPUDevice, filter: WGPUErrorFilter (enum) | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuDevicePopErrorScope: { | ||
| args: [FFIType.pointer, FFIType.pointer], // device: WGPUDevice, callbackInfo: WGPUPopErrorScopeCallbackInfo | ||
| returns: FFIType.u64, // WGPUFuture (id) | ||
| }, | ||
| zwgpuDeviceTick: { | ||
| args: [FFIType.pointer], // device: WGPUDevice | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuDeviceInjectError: { | ||
| args: [FFIType.pointer, FFIType.u32, FFIType.pointer], // device: WGPUDevice, type: WGPUErrorType, message: *const WGPUStringView | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuDeviceCreateComputePipelineAsync: { | ||
| args: [ | ||
| FFIType.pointer, // device: WGPUDevice | ||
| FFIType.pointer, // descriptor: *const WGPUComputePipelineDescriptor | ||
| FFIType.pointer, // callbackInfo: *const WGPUCreateComputePipelineAsyncCallbackInfo | ||
| ], | ||
| returns: FFIType.u64, // -> WGPUFuture (id) | ||
| }, | ||
| zwgpuDeviceCreateRenderPipelineAsync: { | ||
| args: [ | ||
| FFIType.pointer, // device: WGPUDevice | ||
| FFIType.pointer, // descriptor: *const WGPURenderPipelineDescriptor | ||
| FFIType.pointer, // callbackInfo: *const WGPUCreateRenderPipelineAsyncCallbackInfo | ||
| ], | ||
| returns: FFIType.u64, // -> WGPUFuture (id) | ||
| }, | ||
| zwgpuDeviceDestroy: { | ||
| args: [FFIType.pointer], // device: WGPUDevice | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuDeviceRelease: { | ||
| args: [FFIType.pointer], // device: WGPUDevice | ||
| returns: FFIType.void, | ||
| }, | ||
| // Add other device functions... | ||
| // --- Buffer Functions --- | ||
| zwgpuBufferGetMappedRange: { | ||
| args: [FFIType.pointer, FFIType.u64, FFIType.u64], // buffer: WGPUBuffer, offset: size_t, size: size_t | ||
| returns: FFIType.ptr, // void* | ||
| }, | ||
| zwgpuBufferGetConstMappedRange: { | ||
| args: [FFIType.pointer, FFIType.u64, FFIType.u64], // buffer: WGPUBuffer, offset: size_t, size: size_t | ||
| returns: FFIType.ptr, // const void* | ||
| }, | ||
| zwgpuBufferUnmap: { | ||
| args: [FFIType.pointer], // buffer: WGPUBuffer | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuBufferMapAsync: { | ||
| args: [ | ||
| FFIType.pointer, // buffer: WGPUBuffer | ||
| FFIType.u64, // mode: WGPUMapMode (flags) | ||
| FFIType.u64, // offset: size_t | ||
| FFIType.u64, // size: size_t | ||
| FFIType.pointer, // callbackInfo: WGPUBufferMapCallbackInfo | ||
| ], | ||
| returns: FFIType.u64, // WGPUFuture (id) | ||
| }, | ||
| zwgpuBufferDestroy: { | ||
| args: [FFIType.pointer], // buffer: WGPUBuffer | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuBufferRelease: { | ||
| args: [FFIType.pointer], // buffer: WGPUBuffer | ||
| returns: FFIType.void, | ||
| }, | ||
| // Add other buffer functions... | ||
| // --- Texture Functions --- | ||
| zwgpuTextureCreateView: { | ||
| args: [FFIType.pointer, FFIType.pointer], // texture: WGPUTexture, descriptor: *const WGPUTextureViewDescriptor (nullable) | ||
| returns: FFIType.pointer, // -> WGPUTextureView | ||
| }, | ||
| zwgpuTextureDestroy: { | ||
| args: [FFIType.pointer], // texture: WGPUTexture | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuTextureRelease: { | ||
| args: [FFIType.pointer], // texture: WGPUTexture | ||
| returns: FFIType.void, | ||
| }, | ||
| // Add other texture functions... | ||
| // --- TextureView Functions --- | ||
| zwgpuTextureViewRelease: { | ||
| args: [FFIType.pointer], // textureView: WGPUTextureView | ||
| returns: FFIType.void, | ||
| }, | ||
| // Add other texture view functions... | ||
| // --- Sampler Functions --- | ||
| zwgpuSamplerRelease: { | ||
| args: [FFIType.pointer], // sampler: WGPUSampler | ||
| returns: FFIType.void, | ||
| }, | ||
| // Add other sampler functions... | ||
| // --- ShaderModule Functions --- | ||
| zwgpuShaderModuleGetCompilationInfo: { | ||
| args: [FFIType.pointer, FFIType.pointer], // shaderModule: WGPUShaderModule, callbackInfo: WGPUCompilationInfoCallbackInfo | ||
| returns: FFIType.u64, // WGPUFuture (id) | ||
| }, | ||
| zwgpuShaderModuleRelease: { | ||
| args: [FFIType.pointer], // shaderModule: WGPUShaderModule | ||
| returns: FFIType.void, | ||
| }, | ||
| // Add other shader module functions... | ||
| // --- BindGroupLayout Functions --- | ||
| zwgpuBindGroupLayoutRelease: { | ||
| args: [FFIType.pointer], // bindGroupLayout: WGPUBindGroupLayout | ||
| returns: FFIType.void, | ||
| }, | ||
| // --- BindGroup Functions --- | ||
| zwgpuBindGroupRelease: { | ||
| args: [FFIType.pointer], // bindGroup: WGPUBindGroup | ||
| returns: FFIType.void, | ||
| }, | ||
| // --- PipelineLayout Functions --- | ||
| zwgpuPipelineLayoutRelease: { | ||
| args: [FFIType.pointer], // pipelineLayout: WGPUPipelineLayout | ||
| returns: FFIType.void, | ||
| }, | ||
| // --- QuerySet Functions --- | ||
| zwgpuQuerySetDestroy: { | ||
| args: [FFIType.pointer], // querySet: WGPUQuerySet | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuQuerySetRelease: { | ||
| args: [FFIType.pointer], // querySet: WGPUQuerySet | ||
| returns: FFIType.void, | ||
| }, | ||
| // --- RenderPipeline Functions --- | ||
| zwgpuRenderPipelineRelease: { | ||
| args: [FFIType.pointer], // renderPipeline: WGPURenderPipeline | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPipelineGetBindGroupLayout: { | ||
| args: [FFIType.pointer, FFIType.u32], // renderPipeline: WGPURenderPipeline, groupIndex: uint32_t | ||
| returns: FFIType.pointer, // -> WGPUBindGroupLayout | ||
| }, | ||
| // --- ComputePipeline Functions --- | ||
| zwgpuComputePipelineRelease: { | ||
| args: [FFIType.pointer], // computePipeline: WGPUComputePipeline | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuComputePipelineGetBindGroupLayout: { | ||
| args: [FFIType.pointer, FFIType.u32], // computePipeline: WGPUComputePipeline, groupIndex: uint32_t | ||
| returns: FFIType.pointer, // -> WGPUBindGroupLayout | ||
| }, | ||
| // --- CommandEncoder Functions --- | ||
| zwgpuCommandEncoderBeginRenderPass: { | ||
| args: [FFIType.pointer, FFIType.pointer], // encoder: WGPUCommandEncoder, descriptor: *const WGPURenderPassDescriptor | ||
| returns: FFIType.pointer, // -> WGPURenderPassEncoder | ||
| }, | ||
| zwgpuCommandEncoderBeginComputePass: { | ||
| args: [FFIType.pointer, FFIType.pointer], // encoder: WGPUCommandEncoder, descriptor: *const WGPUComputePassDescriptor (nullable) | ||
| returns: FFIType.pointer, // -> WGPUComputePassEncoder | ||
| }, | ||
| zwgpuCommandEncoderClearBuffer: { | ||
| args: [ | ||
| FFIType.pointer, // commandEncoder: WGPUCommandEncoder | ||
| FFIType.pointer, // buffer: WGPUBuffer | ||
| FFIType.u64, // offset: uint64_t | ||
| FFIType.u64, // size: uint64_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuCommandEncoderCopyBufferToBuffer: { | ||
| args: [ | ||
| FFIType.pointer, // commandEncoder: WGPUCommandEncoder | ||
| FFIType.pointer, // source: WGPUBuffer | ||
| FFIType.u64, // sourceOffset: uint64_t | ||
| FFIType.pointer, // destination: WGPUBuffer | ||
| FFIType.u64, // destinationOffset: uint64_t | ||
| FFIType.u64, // size: uint64_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuCommandEncoderCopyBufferToTexture: { | ||
| args: [ | ||
| FFIType.pointer, // commandEncoder | ||
| FFIType.pointer, // source: *const WGPUTexelCopyBufferInfo | ||
| FFIType.pointer, // destination: *const WGPUTexelCopyTextureInfo | ||
| FFIType.pointer, // copySize: *const WGPUExtent3D | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuCommandEncoderCopyTextureToBuffer: { | ||
| args: [ | ||
| FFIType.pointer, // commandEncoder | ||
| FFIType.pointer, // source: *const WGPUTexelCopyTextureInfo | ||
| FFIType.pointer, // destination: *const WGPUTexelCopyBufferInfo | ||
| FFIType.pointer, // copySize: *const WGPUExtent3D | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuCommandEncoderCopyTextureToTexture: { | ||
| args: [ | ||
| FFIType.pointer, // commandEncoder | ||
| FFIType.pointer, // source: *const WGPUTexelCopyTextureInfo | ||
| FFIType.pointer, // destination: *const WGPUTexelCopyTextureInfo | ||
| FFIType.pointer, // copySize: *const WGPUExtent3D | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuCommandEncoderResolveQuerySet: { | ||
| args: [ | ||
| FFIType.pointer, // commandEncoder | ||
| FFIType.pointer, // querySet | ||
| FFIType.u32, // firstQuery | ||
| FFIType.u32, // queryCount | ||
| FFIType.pointer, // destination | ||
| FFIType.u64, // destinationOffset | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuCommandEncoderFinish: { | ||
| args: [FFIType.pointer, FFIType.pointer], // encoder: WGPUCommandEncoder, descriptor: *const WGPUCommandBufferDescriptor (nullable) | ||
| returns: FFIType.pointer, // -> WGPUCommandBuffer | ||
| }, | ||
| zwgpuCommandEncoderRelease: { | ||
| args: [FFIType.pointer], // commandEncoder: WGPUCommandEncoder | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuCommandEncoderPushDebugGroup: { | ||
| args: [FFIType.pointer, FFIType.pointer], // commandEncoder: WGPUCommandEncoder, groupLabel: *const WGPUStringView | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuCommandEncoderPopDebugGroup: { | ||
| args: [FFIType.pointer], // commandEncoder: WGPUCommandEncoder | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuCommandEncoderInsertDebugMarker: { | ||
| args: [FFIType.pointer, FFIType.pointer], // commandEncoder: WGPUCommandEncoder, markerLabel: *const WGPUStringView | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderSetScissorRect: { | ||
| args: [ | ||
| FFIType.pointer, // renderPassEncoder: WGPURenderPassEncoder | ||
| FFIType.u32, // x: uint32_t | ||
| FFIType.u32, // y: uint32_t | ||
| FFIType.u32, // width: uint32_t | ||
| FFIType.u32, // height: uint32_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderSetViewport: { | ||
| args: [ | ||
| FFIType.pointer, // renderPassEncoder: WGPURenderPassEncoder | ||
| FFIType.f32, // x: float | ||
| FFIType.f32, // y: float | ||
| FFIType.f32, // width: float | ||
| FFIType.f32, // height: float | ||
| FFIType.f32, // minDepth: float | ||
| FFIType.f32, // maxDepth: float | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderSetBlendConstant: { | ||
| args: [ | ||
| FFIType.pointer, // renderPassEncoder: WGPURenderPassEncoder | ||
| FFIType.pointer, // color: *const WGPUColor | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderSetStencilReference: { | ||
| args: [ | ||
| FFIType.pointer, // renderPassEncoder: WGPURenderPassEncoder | ||
| FFIType.u32, // reference: uint32_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderSetPipeline: { | ||
| args: [FFIType.pointer, FFIType.pointer], // encoder: WGPURenderPassEncoder, pipeline: WGPURenderPipeline | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderSetBindGroup: { | ||
| args: [ | ||
| FFIType.pointer, // encoder: WGPURenderPassEncoder | ||
| FFIType.u32, // groupIndex: uint32_t | ||
| FFIType.pointer, // group: WGPUBindGroup (nullable) | ||
| FFIType.u64, // dynamicOffsetCount: size_t | ||
| FFIType.pointer, // dynamicOffsets: *const uint32_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderSetVertexBuffer: { | ||
| args: [ | ||
| FFIType.pointer, // encoder: WGPURenderPassEncoder | ||
| FFIType.u32, // slot: uint32_t | ||
| FFIType.pointer, // buffer: WGPUBuffer (nullable) | ||
| FFIType.u64, // offset: uint64_t | ||
| FFIType.u64, // size: uint64_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderSetIndexBuffer: { | ||
| args: [ | ||
| FFIType.pointer, // encoder: WGPURenderPassEncoder | ||
| FFIType.pointer, // buffer: WGPUBuffer | ||
| FFIType.u32, // format: WGPUIndexFormat (enum) | ||
| FFIType.u64, // offset: uint64_t | ||
| FFIType.u64, // size: uint64_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderDraw: { | ||
| args: [ | ||
| FFIType.pointer, // encoder: WGPURenderPassEncoder | ||
| FFIType.u32, // vertexCount: uint32_t | ||
| FFIType.u32, // instanceCount: uint32_t | ||
| FFIType.u32, // firstVertex: uint32_t | ||
| FFIType.u32, // firstInstance: uint32_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderDrawIndexed: { | ||
| args: [ | ||
| FFIType.pointer, // encoder: WGPURenderPassEncoder | ||
| FFIType.u32, // indexCount: uint32_t | ||
| FFIType.u32, // instanceCount: uint32_t | ||
| FFIType.u32, // firstIndex: uint32_t | ||
| FFIType.i32, // baseVertex: int32_t | ||
| FFIType.u32, // firstInstance: uint32_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderDrawIndirect: { | ||
| args: [ | ||
| FFIType.pointer, // encoder: WGPURenderPassEncoder | ||
| FFIType.pointer, // indirectBuffer: WGPUBuffer | ||
| FFIType.u64, // indirectOffset: uint64_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderDrawIndexedIndirect: { | ||
| args: [ | ||
| FFIType.pointer, // encoder: WGPURenderPassEncoder | ||
| FFIType.pointer, // indirectBuffer: WGPUBuffer | ||
| FFIType.u64, // indirectOffset: uint64_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderExecuteBundles: { | ||
| args: [ | ||
| FFIType.pointer, // encoder: WGPURenderPassEncoder | ||
| FFIType.u64, // bundleCount: size_t | ||
| FFIType.pointer, // bundles: *const WGPURenderBundle | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderEnd: { | ||
| args: [FFIType.pointer], // encoder: WGPURenderPassEncoder | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderRelease: { | ||
| args: [FFIType.pointer], // encoder: WGPURenderPassEncoder | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderPushDebugGroup: { | ||
| args: [FFIType.pointer, FFIType.pointer], // encoder: WGPURenderPassEncoder, groupLabel: *const WGPUStringView | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderPopDebugGroup: { | ||
| args: [FFIType.pointer], // encoder: WGPURenderPassEncoder | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderInsertDebugMarker: { | ||
| args: [FFIType.pointer, FFIType.pointer], // encoder: WGPURenderPassEncoder, markerLabel: *const WGPUStringView | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderBeginOcclusionQuery: { | ||
| args: [ | ||
| FFIType.pointer, // renderPassEncoder | ||
| FFIType.u32, // queryIndex | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderPassEncoderEndOcclusionQuery: { | ||
| args: [ | ||
| FFIType.pointer, // renderPassEncoder | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| // --- ComputePassEncoder Functions --- | ||
| zwgpuComputePassEncoderSetPipeline: { | ||
| args: [FFIType.pointer, FFIType.pointer], // encoder: WGPUComputePassEncoder, pipeline: WGPUComputePipeline | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuComputePassEncoderSetBindGroup: { | ||
| args: [ | ||
| FFIType.pointer, // encoder: WGPUComputePassEncoder | ||
| FFIType.u32, // groupIndex: uint32_t | ||
| FFIType.pointer, // group: WGPUBindGroup (nullable) | ||
| FFIType.u64, // dynamicOffsetCount: size_t | ||
| FFIType.pointer, // dynamicOffsets: *const uint32_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuComputePassEncoderDispatchWorkgroups: { | ||
| args: [ | ||
| FFIType.pointer, // encoder: WGPUComputePassEncoder | ||
| FFIType.u32, // workgroupCountX: uint32_t | ||
| FFIType.u32, // workgroupCountY: uint32_t | ||
| FFIType.u32, // workgroupCountZ: uint32_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuComputePassEncoderDispatchWorkgroupsIndirect: { | ||
| args: [ | ||
| FFIType.pointer, // encoder: WGPUComputePassEncoder | ||
| FFIType.pointer, // indirectBuffer: WGPUBuffer | ||
| FFIType.u64, // indirectOffset: uint64_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuComputePassEncoderEnd: { | ||
| args: [FFIType.pointer], // encoder: WGPUComputePassEncoder | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuComputePassEncoderRelease: { | ||
| args: [FFIType.pointer], // encoder: WGPUComputePassEncoder | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuComputePassEncoderPushDebugGroup: { | ||
| args: [FFIType.pointer, FFIType.pointer], // encoder: WGPUComputePassEncoder, groupLabel: *const WGPUStringView | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuComputePassEncoderPopDebugGroup: { | ||
| args: [FFIType.pointer], // encoder: WGPUComputePassEncoder | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuComputePassEncoderInsertDebugMarker: { | ||
| args: [FFIType.pointer, FFIType.pointer], // encoder: WGPUComputePassEncoder, markerLabel: *const WGPUStringView | ||
| returns: FFIType.void, | ||
| }, | ||
| // Add other compute pass functions... | ||
| // --- CommandBuffer Functions --- | ||
| zwgpuCommandBufferRelease: { | ||
| args: [FFIType.pointer], // commandBuffer: WGPUCommandBuffer | ||
| returns: FFIType.void, | ||
| }, | ||
| // --- Queue Functions --- | ||
| zwgpuQueueSubmit: { | ||
| args: [ | ||
| FFIType.pointer, // queue: WGPUQueue | ||
| FFIType.u64, // commandCount: size_t | ||
| FFIType.pointer, // commands: *const WGPUCommandBuffer | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuQueueWriteBuffer: { | ||
| args: [ | ||
| FFIType.pointer, // queue: WGPUQueue | ||
| FFIType.pointer, // buffer: WGPUBuffer | ||
| FFIType.u64, // bufferOffset: uint64_t | ||
| FFIType.ptr, // data: *const void | ||
| FFIType.u64, // size: size_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuQueueWriteTexture: { | ||
| args: [ | ||
| FFIType.pointer, // queue: WGPUQueue | ||
| FFIType.pointer, // destination: *const WGPUTexelCopyTextureInfo | ||
| FFIType.ptr, // data: *const void | ||
| FFIType.u64, // dataSize: size_t | ||
| FFIType.pointer, // dataLayout: *const WGPUTexelCopyBufferLayout | ||
| FFIType.pointer, // writeSize: *const WGPUExtent3D | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuQueueOnSubmittedWorkDone: { | ||
| args: [ | ||
| FFIType.pointer, // queue: WGPUQueue | ||
| FFIType.pointer, // callbackInfo: WGPUQueueWorkDoneCallbackInfo | ||
| ], | ||
| returns: FFIType.u64, // WGPUFuture (id) | ||
| }, | ||
| zwgpuQueueRelease: { | ||
| args: [FFIType.pointer], // queue: WGPUQueue | ||
| returns: FFIType.void, | ||
| }, | ||
| // Add other queue functions... | ||
| // --- Surface Functions --- | ||
| zwgpuSurfaceConfigure: { | ||
| args: [FFIType.pointer, FFIType.pointer], // surface: WGPUSurface, config: *const WGPUSurfaceConfiguration | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuSurfaceUnconfigure: { | ||
| args: [FFIType.pointer], // surface: WGPUSurface | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuSurfaceGetCurrentTexture: { | ||
| args: [FFIType.pointer, FFIType.pointer], // surface: WGPUSurface, surfaceTexture: *mut WGPUSurfaceTexture | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuSurfacePresent: { | ||
| args: [FFIType.pointer], // surface: WGPUSurface | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuSurfaceRelease: { | ||
| args: [FFIType.pointer], // surface: WGPUSurface | ||
| returns: FFIType.void, | ||
| }, | ||
| // Add other surface functions... | ||
| // --- Freeing Functions (Important for structs returned by pointer) --- | ||
| zwgpuAdapterInfoFreeMembers: { | ||
| args: [FFIType.pointer], // value: WGPUAdapterInfo (passed by pointer but represents struct value) | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuSurfaceCapabilitiesFreeMembers: { | ||
| args: [FFIType.pointer], // value: WGPUSurfaceCapabilities (passed by pointer) | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuSupportedFeaturesFreeMembers: { // Added for freeing features array | ||
| args: [FFIType.pointer], // value: WGPUSupportedFeatures (passed by pointer) | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuSharedBufferMemoryEndAccessStateFreeMembers: { | ||
| args: [FFIType.pointer], // value: WGPUSharedBufferMemoryEndAccessState | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuSharedTextureMemoryEndAccessStateFreeMembers: { | ||
| args: [FFIType.pointer], // value: WGPUSharedTextureMemoryEndAccessState | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuSupportedWGSLLanguageFeaturesFreeMembers: { | ||
| args: [FFIType.pointer], // value: WGPUSupportedWGSLLanguageFeatures (passed by pointer) | ||
| returns: FFIType.void, | ||
| }, | ||
| // Add other FreeMembers functions as needed... | ||
| // NOTE: This is not exhaustive. Many more functions exist in webgpu.h | ||
| // Add more bindings here as needed, following the patterns above. | ||
| // --- RenderBundle Functions --- | ||
| zwgpuRenderBundleRelease: { | ||
| args: [FFIType.pointer], // bundle: WGPURenderBundle | ||
| returns: FFIType.void, | ||
| }, | ||
| // --- RenderBundleEncoder Functions --- | ||
| zwgpuRenderBundleEncoderDraw: { | ||
| args: [ | ||
| FFIType.pointer, // encoder: WGPURenderBundleEncoder | ||
| FFIType.u32, // vertexCount: uint32_t | ||
| FFIType.u32, // instanceCount: uint32_t | ||
| FFIType.u32, // firstVertex: uint32_t | ||
| FFIType.u32, // firstInstance: uint32_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderBundleEncoderDrawIndexed: { | ||
| args: [ | ||
| FFIType.pointer, // encoder: WGPURenderBundleEncoder | ||
| FFIType.u32, // indexCount: uint32_t | ||
| FFIType.u32, // instanceCount: uint32_t | ||
| FFIType.u32, // firstIndex: uint32_t | ||
| FFIType.i32, // baseVertex: int32_t | ||
| FFIType.u32, // firstInstance: uint32_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderBundleEncoderDrawIndirect: { | ||
| args: [ | ||
| FFIType.pointer, // encoder: WGPURenderBundleEncoder | ||
| FFIType.pointer, // indirectBuffer: WGPUBuffer | ||
| FFIType.u64, // indirectOffset: uint64_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderBundleEncoderDrawIndexedIndirect: { | ||
| args: [ | ||
| FFIType.pointer, // encoder: WGPURenderBundleEncoder | ||
| FFIType.pointer, // indirectBuffer: WGPUBuffer | ||
| FFIType.u64, // indirectOffset: uint64_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderBundleEncoderFinish: { | ||
| args: [FFIType.pointer, FFIType.pointer], // encoder: WGPURenderBundleEncoder, descriptor: *const WGPURenderBundleDescriptor (nullable) | ||
| returns: FFIType.pointer, // -> WGPURenderBundle | ||
| }, | ||
| zwgpuRenderBundleEncoderSetBindGroup: { | ||
| args: [ | ||
| FFIType.pointer, // encoder: WGPURenderBundleEncoder | ||
| FFIType.u32, // groupIndex: uint32_t | ||
| FFIType.pointer, // group: WGPUBindGroup (nullable) | ||
| FFIType.u64, // dynamicOffsetCount: size_t | ||
| FFIType.pointer, // dynamicOffsets: *const uint32_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderBundleEncoderSetIndexBuffer: { | ||
| args: [ | ||
| FFIType.pointer, // encoder: WGPURenderBundleEncoder | ||
| FFIType.pointer, // buffer: WGPUBuffer | ||
| FFIType.u32, // format: WGPUIndexFormat (enum) | ||
| FFIType.u64, // offset: uint64_t | ||
| FFIType.u64, // size: uint64_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderBundleEncoderSetPipeline: { | ||
| args: [FFIType.pointer, FFIType.pointer], // encoder: WGPURenderBundleEncoder, pipeline: WGPURenderPipeline | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderBundleEncoderSetVertexBuffer: { | ||
| args: [ | ||
| FFIType.pointer, // encoder: WGPURenderBundleEncoder | ||
| FFIType.u32, // slot: uint32_t | ||
| FFIType.pointer, // buffer: WGPUBuffer (nullable) | ||
| FFIType.u64, // offset: uint64_t | ||
| FFIType.u64, // size: uint64_t | ||
| ], | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderBundleEncoderRelease: { | ||
| args: [FFIType.pointer], // encoder: WGPURenderBundleEncoder | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderBundleEncoderPushDebugGroup: { | ||
| args: [FFIType.pointer, FFIType.pointer], // encoder: WGPURenderBundleEncoder, groupLabel: *const WGPUStringView | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderBundleEncoderPopDebugGroup: { | ||
| args: [FFIType.pointer], // encoder: WGPURenderBundleEncoder | ||
| returns: FFIType.void, | ||
| }, | ||
| zwgpuRenderBundleEncoderInsertDebugMarker: { | ||
| args: [FFIType.pointer, FFIType.pointer], // encoder: WGPURenderBundleEncoder, markerLabel: *const WGPUStringView | ||
| returns: FFIType.void, | ||
| }, | ||
| }); | ||
| return symbols; | ||
| } | ||
| type StripZWPrefix<KeyType extends string> = | ||
| KeyType extends `zw${infer Rest}` ? `w${Rest}` : KeyType; | ||
| type TransformedSymbolKeys<T extends object> = { | ||
| [K in keyof T as StripZWPrefix<K & string>]: T[K]; | ||
| }; | ||
| // The type of the final normalizedSymbols object | ||
| type NormalizedSymbolsType = TransformedSymbolKeys<ReturnType<typeof _loadLibrary>>; | ||
| export function loadLibrary(libPath?: string) { | ||
| const rawSymbols = _loadLibrary(libPath); | ||
| const normalizedSymbols = Object.keys(rawSymbols).reduce( | ||
| (acc, key) => { | ||
| const newKey = key.replace(/^zw/, 'w') as keyof NormalizedSymbolsType; // Assert the new key type | ||
| (acc as any)[newKey] = (rawSymbols as Record<string, any>)[key]; | ||
| return acc; | ||
| }, | ||
| {} as NormalizedSymbolsType // Crucially, type the initial accumulator | ||
| ); | ||
| const FFI_SYMBOLS = process.env.DEBUG === 'true' || process.env.TRACE_WEBGPU === 'true' ? convertToDebugSymbols(normalizedSymbols) : normalizedSymbols; | ||
| return FFI_SYMBOLS; | ||
| } | ||
| export type FFISymbols = ReturnType<typeof loadLibrary>; | ||
| function convertToDebugSymbols<T extends Record<string, any>>(symbols: T): T { | ||
| const debugSymbols: Record<string, any> = {}; | ||
| if (process.env.DEBUG === 'true') { | ||
| Object.entries(symbols).forEach(([key, value]) => { | ||
| if (typeof value === 'function') { | ||
| debugSymbols[key] = (...args: any[]) => { | ||
| console.log(`${key}(${args.map(arg => String(arg)).join(', ')})`); | ||
| const result = value(...args); | ||
| console.log(`${key} returned:`, String(result)); | ||
| return result; | ||
| }; | ||
| } else { | ||
| debugSymbols[key] = value; // Copy non-function properties as is | ||
| } | ||
| }); | ||
| } | ||
| if (process.env.TRACE_WEBGPU === 'true') { | ||
| const traceSymbols: Record<string, any> = {}; | ||
| Object.entries(symbols).forEach(([key, value]) => { | ||
| if (typeof value === 'function') { | ||
| traceSymbols[key] = []; | ||
| debugSymbols[key] = (...args: any[]) => { | ||
| const start = performance.now(); | ||
| const result = value(...args); | ||
| const end = performance.now(); | ||
| traceSymbols[key].push(end - start); | ||
| return result; | ||
| }; | ||
| } else { | ||
| debugSymbols[key] = value; // Copy non-function properties as is | ||
| } | ||
| }); | ||
| process.on('exit', () => { | ||
| const allStats: Array<{ | ||
| name: string; | ||
| count: number; | ||
| total: number; | ||
| average: number; | ||
| min: number; | ||
| max: number; | ||
| median: number; | ||
| p90: number; | ||
| p99: number; | ||
| }> = []; | ||
| for (const [key, timings] of Object.entries(traceSymbols)) { | ||
| if (!Array.isArray(timings) || timings.length === 0) { | ||
| continue; | ||
| } | ||
| const sortedTimings = [...timings].sort((a, b) => a - b); | ||
| const count = sortedTimings.length; | ||
| const total = sortedTimings.reduce((acc, t) => acc + t, 0); | ||
| const average = total / count; | ||
| const min = sortedTimings[0]; | ||
| const max = sortedTimings[count - 1]; | ||
| const medianIndex = Math.floor(count / 2); | ||
| const p90Index = Math.floor(count * 0.9); | ||
| const p99Index = Math.floor(count * 0.99); | ||
| const median = sortedTimings[medianIndex]; | ||
| const p90 = sortedTimings[Math.min(p90Index, count - 1)]; | ||
| const p99 = sortedTimings[Math.min(p99Index, count - 1)]; | ||
| allStats.push({ | ||
| name: key, | ||
| count, | ||
| total, | ||
| average, | ||
| min, | ||
| max, | ||
| median, | ||
| p90, | ||
| p99, | ||
| }); | ||
| } | ||
| allStats.sort((a, b) => b.total - a.total); | ||
| console.log("\n--- WebGPU FFI Call Performance ---"); | ||
| console.log("Sorted by total time spent (descending)"); | ||
| console.log("-------------------------------------------------------------------------------------------------------------------------"); | ||
| if (allStats.length === 0) { | ||
| console.log("No trace data collected or all symbols had zero calls."); | ||
| } else { | ||
| const nameHeader = "Symbol"; | ||
| const callsHeader = "Calls"; | ||
| const totalHeader = "Total (ms)"; | ||
| const avgHeader = "Avg (ms)"; | ||
| const minHeader = "Min (ms)"; | ||
| const maxHeader = "Max (ms)"; | ||
| const medHeader = "Med (ms)"; | ||
| const p90Header = "P90 (ms)"; | ||
| const p99Header = "P99 (ms)"; | ||
| const nameWidth = Math.max(nameHeader.length, ...allStats.map(s => s.name.length)); | ||
| const countWidth = Math.max(callsHeader.length, ...allStats.map(s => String(s.count).length)); | ||
| const totalWidth = Math.max(totalHeader.length, ...allStats.map(s => s.total.toFixed(2).length)); | ||
| const avgWidth = Math.max(avgHeader.length, ...allStats.map(s => s.average.toFixed(2).length)); | ||
| const minWidth = Math.max(minHeader.length, ...allStats.map(s => s.min.toFixed(2).length)); | ||
| const maxWidth = Math.max(maxHeader.length, ...allStats.map(s => s.max.toFixed(2).length)); | ||
| const medianWidth = Math.max(medHeader.length, ...allStats.map(s => s.median.toFixed(2).length)); | ||
| const p90Width = Math.max(p90Header.length, ...allStats.map(s => s.p90.toFixed(2).length)); | ||
| const p99Width = Math.max(p99Header.length, ...allStats.map(s => s.p99.toFixed(2).length)); | ||
| // Header | ||
| console.log( | ||
| `${nameHeader.padEnd(nameWidth)} | ` + | ||
| `${callsHeader.padStart(countWidth)} | ` + | ||
| `${totalHeader.padStart(totalWidth)} | ` + | ||
| `${avgHeader.padStart(avgWidth)} | ` + | ||
| `${minHeader.padStart(minWidth)} | ` + | ||
| `${maxHeader.padStart(maxWidth)} | ` + | ||
| `${medHeader.padStart(medianWidth)} | ` + | ||
| `${p90Header.padStart(p90Width)} | ` + | ||
| `${p99Header.padStart(p99Width)}` | ||
| ); | ||
| // Separator | ||
| console.log( | ||
| `${"-".repeat(nameWidth)}-+-${"-".repeat(countWidth)}-+-${"-".repeat(totalWidth)}-+-${"-".repeat(avgWidth)}-+-${"-".repeat(minWidth)}-+-${"-".repeat(maxWidth)}-+-${"-".repeat(medianWidth)}-+-${"-".repeat(p90Width)}-+-${"-".repeat(p99Width)}` | ||
| ); | ||
| allStats.forEach(stat => { | ||
| console.log( | ||
| `${stat.name.padEnd(nameWidth)} | ` + | ||
| `${String(stat.count).padStart(countWidth)} | ` + | ||
| `${stat.total.toFixed(2).padStart(totalWidth)} | ` + | ||
| `${stat.average.toFixed(2).padStart(avgWidth)} | ` + | ||
| `${stat.min.toFixed(2).padStart(minWidth)} | ` + | ||
| `${stat.max.toFixed(2).padStart(maxWidth)} | ` + | ||
| `${stat.median.toFixed(2).padStart(medianWidth)} | ` + | ||
| `${stat.p90.toFixed(2).padStart(p90Width)} | ` + | ||
| `${stat.p99.toFixed(2).padStart(p99Width)}` | ||
| ); | ||
| }); | ||
| } | ||
| console.log("-------------------------------------------------------------------------------------------------------------------------"); | ||
| }); | ||
| } | ||
| return debugSymbols as T; | ||
| } |
-204
| /// <reference types="@webgpu/types" /> | ||
| import { JSCallback, toArrayBuffer, type Pointer, ptr, FFIType } from "bun:ffi"; | ||
| import { type FFISymbols } from "./ffi"; | ||
| import { GPUAdapterImpl } from "./GPUAdapter"; | ||
| import { | ||
| WGPUCallbackInfoStruct, | ||
| WGPURequestAdapterOptionsStruct, | ||
| WGPUSupportedWGSLLanguageFeaturesStruct, | ||
| } from "./structs_def"; | ||
| import { fatalError } from "./utils/error"; | ||
| import { allocStruct } from "./structs_ffi"; | ||
| const RequestAdapterStatus = { | ||
| Success: 1, | ||
| CallbackCancelled: 2, // Not typically used directly from JS | ||
| Unavailable: 3, | ||
| Error: 4, | ||
| Unknown: 5, | ||
| } as const; | ||
| export class InstanceTicker { | ||
| private _waiting: number = 0; | ||
| private _ticking = false; | ||
| private _accTime: number = 0; | ||
| private _lastTime: number = performance.now(); | ||
| constructor(public readonly instancePtr: Pointer, private lib: FFISymbols) {} | ||
| register() { | ||
| this._lastTime = performance.now(); | ||
| this._accTime = 0; | ||
| this._waiting++; | ||
| this.scheduleTick(); | ||
| } | ||
| unregister() { | ||
| this._waiting--; | ||
| } | ||
| hasWaiting() { | ||
| return this._waiting > 0; | ||
| } | ||
| processEvents() { | ||
| this.lib.wgpuInstanceProcessEvents(this.instancePtr); | ||
| } | ||
| private scheduleTick() { | ||
| if (this._ticking) return; | ||
| this._ticking = true; | ||
| setImmediate(() => { | ||
| const now = performance.now(); | ||
| this._accTime += now - this._lastTime; | ||
| this._lastTime = now; | ||
| if (this._accTime > 0.05) { | ||
| this.lib.wgpuInstanceProcessEvents(this.instancePtr); | ||
| this._accTime = 0; | ||
| } | ||
| this._ticking = false; | ||
| if (this.hasWaiting()) { | ||
| this.scheduleTick(); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| export class GPUImpl implements GPU { | ||
| __brand: "GPU" = "GPU"; | ||
| private _destroyed = false; | ||
| private _ticker: InstanceTicker; | ||
| private _wgslLanguageFeatures: WGSLLanguageFeatures | null = null; | ||
| constructor(private instancePtr: Pointer, private lib: FFISymbols) { | ||
| this._ticker = new InstanceTicker(instancePtr, lib); | ||
| } | ||
| getPreferredCanvasFormat(): GPUTextureFormat { | ||
| return 'bgra8unorm'; | ||
| } | ||
| get wgslLanguageFeatures(): WGSLLanguageFeatures { | ||
| if (this._destroyed) { | ||
| console.warn("Accessing wgslLanguageFeatures on destroyed GPU instance"); | ||
| return Object.freeze(new Set<string>()); | ||
| } | ||
| if (this._wgslLanguageFeatures === null) { | ||
| try { | ||
| const { buffer: structBuffer } = allocStruct(WGPUSupportedWGSLLanguageFeaturesStruct, { | ||
| lengths: { | ||
| features: 32, | ||
| }, | ||
| }); | ||
| const status = this.lib.wgpuInstanceGetWGSLLanguageFeatures(this.instancePtr, ptr(structBuffer)); | ||
| if (status !== 1 /* WGPUStatus_Success */) { | ||
| console.error(`wgpuInstanceGetWGSLLanguageFeatures failed with status: ${status}`); | ||
| this._wgslLanguageFeatures = Object.freeze(new Set<string>()); | ||
| return this._wgslLanguageFeatures; | ||
| } | ||
| const unpacked = WGPUSupportedWGSLLanguageFeaturesStruct.unpack(structBuffer); | ||
| const supportedFeatures = new Set<string>(); | ||
| if (unpacked.features) { | ||
| for (const featureName of unpacked.features) { | ||
| supportedFeatures.add(featureName as string); | ||
| } | ||
| } | ||
| this._wgslLanguageFeatures = Object.freeze(supportedFeatures); | ||
| } catch (e) { | ||
| console.error("Error in wgslLanguageFeatures getter:", e); | ||
| this._wgslLanguageFeatures = Object.freeze(new Set<string>()); | ||
| } | ||
| } | ||
| return this._wgslLanguageFeatures; | ||
| } | ||
| requestAdapter(options?: GPURequestAdapterOptions & { featureLevel?: 'core' | 'compatibility' }): Promise<GPUAdapter | null> { | ||
| if (this._destroyed) { | ||
| return Promise.reject(new Error("GPU instance has been destroyed")); | ||
| } | ||
| return new Promise((resolve, reject) => { | ||
| let packedOptionsPtr: Pointer | null = null; | ||
| let jsCallback: JSCallback | null = null; | ||
| try { | ||
| if (options) { | ||
| try { | ||
| const buffer = WGPURequestAdapterOptionsStruct.pack(options); | ||
| packedOptionsPtr = ptr(buffer); | ||
| } catch (e) { | ||
| // console.error("Error packing WGPURequestAdapterOptionsStruct", e); | ||
| resolve(null); | ||
| return; | ||
| } | ||
| } | ||
| const callbackFn = (status: number, adapterPtr: Pointer | null, messagePtr: Pointer | null, messageSize: number, userdata1: Pointer | null, userdata2: Pointer | null) => { | ||
| this._ticker.unregister(); | ||
| const message = messagePtr ? Buffer.from(toArrayBuffer(messagePtr)).toString() : null; | ||
| if (status === RequestAdapterStatus.Success) { | ||
| if (adapterPtr) { | ||
| resolve(new GPUAdapterImpl(adapterPtr, this.instancePtr, this.lib, this._ticker)); | ||
| } else { | ||
| reject(new Error(`WGPU Error (Success but null adapter): ${message || 'No message.'}`)); | ||
| } | ||
| } else if (status === RequestAdapterStatus.Unavailable) { | ||
| resolve(null); | ||
| } else { | ||
| let statusName = Object.keys(RequestAdapterStatus).find(key => RequestAdapterStatus[key as keyof typeof RequestAdapterStatus] === status) || 'Unknown WGPU Error'; | ||
| reject(new Error(`WGPU Error (${statusName}): ${message || 'No message provided.'}`)); | ||
| } | ||
| if (jsCallback) { | ||
| jsCallback.close(); | ||
| } | ||
| }; | ||
| jsCallback = new JSCallback(callbackFn, { | ||
| args: [FFIType.u32, FFIType.pointer, FFIType.pointer, FFIType.u64, FFIType.pointer, FFIType.pointer ], returns: FFIType.void | ||
| }); | ||
| if (!jsCallback?.ptr) { | ||
| fatalError("Failed to create JSCallback"); | ||
| } | ||
| const buffer = WGPUCallbackInfoStruct.pack({ | ||
| nextInChain: null, | ||
| mode: "AllowProcessEvents", | ||
| callback: jsCallback?.ptr, | ||
| userdata1: null, | ||
| userdata2: null, | ||
| }); | ||
| const packedCallbackInfoPtr = ptr(buffer); | ||
| this.lib.wgpuInstanceRequestAdapter( | ||
| this.instancePtr, | ||
| packedOptionsPtr, | ||
| packedCallbackInfoPtr | ||
| ); | ||
| this._ticker.register(); | ||
| } catch (e) { | ||
| if (jsCallback) jsCallback.close(); | ||
| reject(e); | ||
| } | ||
| }); | ||
| } | ||
| destroy(): undefined { | ||
| if (this._destroyed) return; | ||
| this._destroyed = true; | ||
| try { | ||
| this.lib.wgpuInstanceRelease(this.instancePtr); | ||
| } catch(e) { | ||
| console.error("FFI Error: wgpuInstanceRelease", e); | ||
| } | ||
| return undefined; | ||
| } | ||
| } | ||
| import { expect, describe, it, beforeAll, afterAll } from "bun:test"; | ||
| import { createGPUInstance } from "./index"; | ||
| import type { GPUImpl } from "./GPU"; | ||
| import type { GPUAdapterImpl } from "./GPUAdapter"; // Import implementation for testing cache | ||
| // Global variables for the test suite | ||
| let gpu: GPUImpl | null = null; | ||
| let adapter: GPUAdapter | null = null; | ||
| describe("GPUAdapter", () => { | ||
| // --- Setup and Teardown --- | ||
| beforeAll(async () => { | ||
| try { | ||
| gpu = createGPUInstance(); | ||
| if (!gpu) throw new Error("Test Setup Failed: Could not create GPU instance."); | ||
| adapter = await gpu.requestAdapter(); | ||
| if (!adapter) throw new Error(`Test Setup Failed: Could not request adapter.`); | ||
| } catch (e) { | ||
| console.error("Error during GPUAdapter Test Setup:", e); | ||
| // Cleanup if setup fails partially | ||
| if (adapter && 'destroy' in adapter) (adapter as any).destroy(); // Assuming destroy exists | ||
| gpu?.destroy(); | ||
| throw e; | ||
| } | ||
| }); | ||
| afterAll(() => { | ||
| if (adapter && 'destroy' in adapter) { | ||
| (adapter as any).destroy(); | ||
| } | ||
| adapter = null; | ||
| gpu?.destroy(); | ||
| gpu = null; | ||
| }); | ||
| // --- Tests --- | ||
| describe("features", () => { | ||
| it("should return a non-empty set of features", () => { | ||
| expect(adapter).not.toBeNull(); | ||
| const features = adapter!.features; | ||
| expect(features).toBeInstanceOf(Set); | ||
| // Most adapters should have at least one feature. | ||
| // If running on a very minimal system (like CI with software rendering), | ||
| // this might need adjustment, but generally it's a good sanity check. | ||
| expect(features.size).toBeGreaterThan(0); | ||
| console.log("Adapter Features Found:", Array.from(features).join(', ')); | ||
| }); | ||
| it("should return the same cached set on subsequent calls", () => { | ||
| expect(adapter).not.toBeNull(); | ||
| const features1 = adapter!.features; | ||
| const features2 = adapter!.features; | ||
| expect(features1).toBe(features2); // Should be strictly equal due to caching | ||
| }); | ||
| it("should return an empty set after adapter is destroyed and features were previously accessed", () => { | ||
| expect(adapter).not.toBeNull(); | ||
| adapter!.features; // Populate cache | ||
| (adapter as GPUAdapterImpl).destroy(); | ||
| const featuresAfterDestroy = adapter!.features; | ||
| expect(featuresAfterDestroy).toBeInstanceOf(Set); | ||
| expect(featuresAfterDestroy.size).toBe(0); | ||
| }); | ||
| }); | ||
| describe("limits", () => { | ||
| it("should return a valid GPUSupportedLimits object", () => { | ||
| expect(adapter).not.toBeNull(); | ||
| const limits = adapter!.limits; | ||
| expect(limits).not.toBeNull(); | ||
| expect(limits.__brand).toBe("GPUSupportedLimits"); | ||
| // Check a few key properties to ensure it's populated | ||
| expect(typeof limits.maxTextureDimension2D).toBe('number'); | ||
| expect(limits.maxTextureDimension2D).toBeGreaterThanOrEqual(0); | ||
| expect(typeof limits.maxUniformBufferBindingSize).toBe('number'); | ||
| expect(limits.maxUniformBufferBindingSize).toBeGreaterThanOrEqual(0); | ||
| // console.log("Adapter Limits Found:", limits); | ||
| }); | ||
| it("should return the same cached object on subsequent calls", () => { | ||
| expect(adapter).not.toBeNull(); | ||
| const limits1 = adapter!.limits; | ||
| const limits2 = adapter!.limits; | ||
| expect(limits1).toBe(limits2); // Should be strictly equal due to caching | ||
| }); | ||
| it("should return default limits after adapter is destroyed and limits were previously accessed", () => { | ||
| expect(adapter).not.toBeNull(); | ||
| adapter!.limits; // Populate cache before destroying | ||
| // Temporarily store the adapter to destroy and then re-fetch to test destruction path | ||
| const adapterToDestroy = adapter as GPUAdapterImpl; | ||
| adapterToDestroy.destroy(); | ||
| const limitsAfterDestroy = adapterToDestroy.limits; | ||
| expect(limitsAfterDestroy).not.toBeNull(); | ||
| expect(limitsAfterDestroy.__brand).toBe("GPUSupportedLimits"); | ||
| // Check if it returns the default (e.g., 0 for many values) | ||
| expect(limitsAfterDestroy.maxTextureDimension2D).toBe(0); | ||
| }); | ||
| it("should return default limits after adapter is destroyed and limits were NOT previously accessed", () => { | ||
| expect(adapter).not.toBeNull(); | ||
| // Do not access adapter!.limits before destroy() here | ||
| const adapterToDestroy = adapter as GPUAdapterImpl; | ||
| adapterToDestroy.destroy(); | ||
| const limitsAfterDestroy = adapterToDestroy.limits; | ||
| expect(limitsAfterDestroy).not.toBeNull(); | ||
| expect(limitsAfterDestroy.__brand).toBe("GPUSupportedLimits"); | ||
| expect(limitsAfterDestroy.maxTextureDimension2D).toBe(0); | ||
| }); | ||
| }); | ||
| // TODO: Add tests for info, isFallbackAdapter | ||
| }); |
| /// <reference types="../index.d.ts" /> | ||
| import { type Pointer, FFIType, JSCallback, ptr, toArrayBuffer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| import { GPUDeviceImpl } from "./GPUDevice"; | ||
| import { | ||
| WGPUCallbackInfoStruct, | ||
| WGPUDeviceDescriptorStruct, | ||
| type WGPUUncapturedErrorCallbackInfo, | ||
| WGPULimitsStruct, | ||
| WGPUSupportedFeaturesStruct, | ||
| WGPUAdapterInfoStruct, | ||
| WGPUDeviceLostReasonDef, | ||
| } from "./structs_def"; | ||
| import { createWGPUError, fatalError, GPUErrorImpl, OperationError } from "./utils/error"; | ||
| import type { InstanceTicker } from "./GPU"; | ||
| import { allocStruct } from "./structs_ffi"; | ||
| import { GPUAdapterInfoImpl, normalizeIdentifier, DEFAULT_SUPPORTED_LIMITS, GPUSupportedLimitsImpl, decodeCallbackMessage } from "./shared"; | ||
| const RequestDeviceStatus = { | ||
| Success: 1, | ||
| CallbackCancelled: 2, | ||
| Error: 3, | ||
| Unknown: 4, | ||
| } as const; | ||
| const ReverseDeviceStatus = Object.fromEntries(Object.entries(RequestDeviceStatus).map(([key, value]) => [value, key])); | ||
| const EMPTY_ADAPTER_INFO: Readonly<GPUAdapterInfo> = Object.create(GPUAdapterInfoImpl.prototype); | ||
| const DEFAULT_LIMITS = Object.assign(Object.create(GPUSupportedLimitsImpl.prototype), DEFAULT_SUPPORTED_LIMITS); | ||
| export class GPUUncapturedErrorEventImpl extends Event implements GPUUncapturedErrorEvent { | ||
| __brand: "GPUUncapturedErrorEvent" = "GPUUncapturedErrorEvent"; | ||
| error: GPUError; | ||
| constructor(error: GPUError) { | ||
| super('uncapturederror', { bubbles: true, cancelable: true }); | ||
| this.error = error; | ||
| } | ||
| } | ||
| export class GPUAdapterImpl implements GPUAdapter { | ||
| __brand: "GPUAdapter" = "GPUAdapter"; | ||
| private _features: GPUSupportedFeatures | null = null; | ||
| private _limits: GPUSupportedLimits = DEFAULT_LIMITS; | ||
| private _info: GPUAdapterInfo = EMPTY_ADAPTER_INFO; | ||
| private _destroyed = false; | ||
| private _device: GPUDeviceImpl | null = null; | ||
| private _state: 'valid' | 'consumed' | 'invalid' = 'valid'; | ||
| constructor( | ||
| public readonly adapterPtr: Pointer, | ||
| private instancePtr: Pointer, | ||
| private lib: FFISymbols, | ||
| private instanceTicker: InstanceTicker, | ||
| ) {} | ||
| get info(): GPUAdapterInfo { | ||
| if (this._destroyed) { | ||
| return EMPTY_ADAPTER_INFO; | ||
| } | ||
| if (this._info === EMPTY_ADAPTER_INFO) { | ||
| let infoStructPtr: Pointer | null = null; | ||
| try { | ||
| const { buffer: structBuffer } = allocStruct(WGPUAdapterInfoStruct); | ||
| infoStructPtr = ptr(structBuffer); | ||
| const status = this.lib.wgpuAdapterGetInfo(this.adapterPtr, infoStructPtr); | ||
| if (status !== 1) { // WGPUStatus_Success = 1 | ||
| console.error(`wgpuAdapterGetInfo failed with status: ${status}`); | ||
| this._info = EMPTY_ADAPTER_INFO; | ||
| return this._info; | ||
| } | ||
| const rawInfo = WGPUAdapterInfoStruct.unpack(structBuffer); | ||
| this._info = Object.assign(Object.create(GPUAdapterInfoImpl.prototype), rawInfo, { | ||
| vendor: rawInfo.vendor, | ||
| architecture: rawInfo.architecture, | ||
| description: rawInfo.description, | ||
| device: normalizeIdentifier(rawInfo.device), | ||
| subgroupMinSize: rawInfo.subgroupMinSize, | ||
| subgroupMaxSize: rawInfo.subgroupMaxSize, | ||
| isFallbackAdapter: false, | ||
| }); | ||
| } catch (e) { | ||
| console.error("Error calling wgpuAdapterGetInfo or unpacking struct:", e); | ||
| this._info = EMPTY_ADAPTER_INFO; // Cache default on error | ||
| } finally { | ||
| if (infoStructPtr) { | ||
| this.lib.wgpuAdapterInfoFreeMembers(infoStructPtr); | ||
| } | ||
| } | ||
| } | ||
| return this._info; | ||
| } | ||
| get features(): GPUSupportedFeatures { | ||
| if (this._destroyed) { | ||
| console.warn("Accessing features on destroyed GPUAdapter"); | ||
| return Object.freeze(new Set<GPUFeatureName>()); | ||
| } | ||
| if (this._features === null) { | ||
| try { | ||
| const { buffer: featuresStructBuffer, subBuffers } = allocStruct(WGPUSupportedFeaturesStruct, { | ||
| lengths: { | ||
| features: 128, // 77 known features + some room for unknown features or future features | ||
| }, | ||
| }); | ||
| this.lib.wgpuAdapterGetFeatures(this.adapterPtr, ptr(featuresStructBuffer)); | ||
| const supportedFeatures = new Set<GPUFeatureName>(); | ||
| const unpacked = WGPUSupportedFeaturesStruct.unpack(featuresStructBuffer); | ||
| if (unpacked.features && unpacked.featureCount && unpacked.featureCount > 0) { | ||
| for (const feature of unpacked.features) { | ||
| supportedFeatures.add(feature as GPUFeatureName); | ||
| } | ||
| } | ||
| this._features = Object.freeze(supportedFeatures); | ||
| } catch (e) { | ||
| console.error("Error calling adapterGetFeatures FFI function:", e); | ||
| return Object.freeze(new Set<GPUFeatureName>()); | ||
| } | ||
| } | ||
| return this._features; | ||
| } | ||
| get limits(): GPUSupportedLimits { | ||
| if (this._destroyed) { | ||
| return this._limits; | ||
| } | ||
| if (this._limits === null) { | ||
| let limitsStructPtr: Pointer | null = null; | ||
| try { | ||
| const { buffer: structBuffer } = allocStruct(WGPULimitsStruct); | ||
| limitsStructPtr = ptr(structBuffer); | ||
| const status = this.lib.wgpuAdapterGetLimits(this.adapterPtr, limitsStructPtr); | ||
| if (status !== 1) { // WGPUStatus_Success = 1 | ||
| console.error(`wgpuAdapterGetLimits failed with status: ${status}`); | ||
| return this._limits; | ||
| } | ||
| const jsLimits = WGPULimitsStruct.unpack(structBuffer); | ||
| this._limits = Object.freeze(Object.assign(Object.create(GPUSupportedLimitsImpl.prototype), { | ||
| __brand: "GPUSupportedLimits" as const, | ||
| ...jsLimits, | ||
| maxUniformBufferBindingSize: Number(jsLimits.maxUniformBufferBindingSize), | ||
| maxStorageBufferBindingSize: Number(jsLimits.maxStorageBufferBindingSize), | ||
| maxBufferSize: Number(jsLimits.maxBufferSize), | ||
| })); | ||
| } catch (e) { | ||
| console.error("Error calling wgpuAdapterGetLimits or unpacking struct:", e); | ||
| } | ||
| } | ||
| return this._limits; | ||
| } | ||
| get isFallbackAdapter(): boolean { | ||
| console.error('get isFallbackAdapter', this.adapterPtr); | ||
| throw new Error("Not implemented"); | ||
| } | ||
| private handleUncapturedError(devicePtr: Pointer, typeInt: number, messagePtr: Pointer | null, messageSize: bigint, userdata1: Pointer | null, userdata2: Pointer | null) { | ||
| const message = decodeCallbackMessage(messagePtr, messageSize); | ||
| const error = createWGPUError(typeInt, message); | ||
| if (this._device) { | ||
| const event: GPUUncapturedErrorEvent = new GPUUncapturedErrorEventImpl(error); | ||
| this._device.handleUncapturedError(event); | ||
| this._device.dispatchEvent(event); | ||
| } else { | ||
| console.error(`Device not found for uncaptured error`); | ||
| } | ||
| } | ||
| private handleDeviceLost(devicePtr: Pointer | null, reason: number, messagePtr: Pointer | null, messageSize: bigint, userdata1: Pointer | null, userdata2: Pointer | null) { | ||
| const message = decodeCallbackMessage(messagePtr, messageSize); | ||
| this._state = 'invalid'; | ||
| if (this._device) { | ||
| this._device.handleDeviceLost(WGPUDeviceLostReasonDef.from(reason) as GPUDeviceLostReason, message); | ||
| } | ||
| } | ||
| requestDevice(descriptor?: GPUDeviceDescriptor): Promise<GPUDevice> { | ||
| if (this._destroyed) { | ||
| return Promise.reject(new Error("Adapter destroyed")); | ||
| } | ||
| if (this._state === 'invalid') { | ||
| this._device?.handleDeviceLost('unknown', 'Adapter already invalid', true); | ||
| return Promise.resolve(this._device as GPUDevice); | ||
| } | ||
| if (this._state === 'consumed') { | ||
| return Promise.reject(new OperationError("Adapter already consumed")); | ||
| } | ||
| this._state = 'consumed'; | ||
| return new Promise((resolve, reject) => { | ||
| let packedDescriptorPtr: Pointer | null = null; | ||
| let jsCallback: JSCallback | null = null; | ||
| try { | ||
| // --- 1. Pack Descriptor --- | ||
| const uncapturedErrorCallback = new JSCallback( | ||
| ( | ||
| devicePtr: Pointer, | ||
| typeInt: number, | ||
| messagePtr: Pointer | null, | ||
| messageSize: bigint, | ||
| userdata1: Pointer | null, | ||
| userdata2: Pointer | null | ||
| ) => { | ||
| this.handleUncapturedError(devicePtr, typeInt, messagePtr, messageSize, userdata1, userdata2); | ||
| }, | ||
| { | ||
| // NOTE: The message is a WGPUStringView, which is a struct with a pointer and a size. | ||
| // FFI cannot represent this directly, so it is passed in two arguments a pointer+size. | ||
| args: [FFIType.pointer, FFIType.u32, FFIType.pointer, FFIType.u64, FFIType.pointer, FFIType.pointer ], | ||
| returns: FFIType.void | ||
| } | ||
| ); | ||
| if (!uncapturedErrorCallback.ptr) { | ||
| fatalError("Failed to create uncapturedErrorCallback"); | ||
| } | ||
| const deviceLostCallback = new JSCallback( | ||
| ( | ||
| devicePtr: Pointer | null, | ||
| reason: number, | ||
| messagePtr: Pointer | null, | ||
| messageSize: bigint, | ||
| userdata1: Pointer | null, | ||
| userdata2: Pointer | null | ||
| ) => { | ||
| this.handleDeviceLost(devicePtr, reason, messagePtr, messageSize, userdata1, userdata2); | ||
| }, | ||
| { | ||
| args: [FFIType.pointer, FFIType.u32, FFIType.pointer, FFIType.u64, FFIType.pointer, FFIType.pointer ], | ||
| returns: FFIType.void, | ||
| } | ||
| ); | ||
| if (!deviceLostCallback.ptr) { | ||
| fatalError("Failed to create deviceLostCallback"); | ||
| } | ||
| const fullDescriptor: GPUDeviceDescriptor & { | ||
| uncapturedErrorCallbackInfo: WGPUUncapturedErrorCallbackInfo, | ||
| deviceLostCallbackInfo: ReturnType<typeof WGPUCallbackInfoStruct.unpack>, | ||
| defaultQueue: GPUQueueDescriptor, | ||
| } = { | ||
| ...descriptor, | ||
| uncapturedErrorCallbackInfo: { | ||
| callback: uncapturedErrorCallback.ptr, | ||
| userdata1: null, | ||
| userdata2: null, | ||
| }, | ||
| deviceLostCallbackInfo: { | ||
| nextInChain: null, | ||
| mode: 'AllowProcessEvents', | ||
| callback: deviceLostCallback.ptr, | ||
| userdata1: null, | ||
| userdata2: null, | ||
| }, | ||
| defaultQueue: { | ||
| label: 'default queue', | ||
| }, | ||
| } | ||
| try { | ||
| const limits = this.limits; | ||
| const features = this.features; | ||
| const descBuffer = WGPUDeviceDescriptorStruct.pack(fullDescriptor, { | ||
| validationHints: { | ||
| limits, | ||
| features, | ||
| } | ||
| }); | ||
| packedDescriptorPtr = ptr(descBuffer); | ||
| } catch (e) { | ||
| this._state = 'valid'; | ||
| reject(e); | ||
| return; | ||
| } | ||
| // --- 2. Create JSCallback --- | ||
| const callbackFn = (status: number, devicePtr: Pointer | null, messagePtr: Pointer | null, messageSize: bigint, userdata1: Pointer | null, userdata2: Pointer | null) => { | ||
| this.instanceTicker.unregister(); | ||
| const message = decodeCallbackMessage(messagePtr, messageSize); | ||
| if (status === RequestDeviceStatus.Success) { | ||
| if (devicePtr) { | ||
| const device = new GPUDeviceImpl(devicePtr, this.lib, this.instanceTicker); | ||
| this._device = device; | ||
| resolve(device); | ||
| } else { | ||
| console.error("WGPU Error: requestDevice Success but device pointer is null."); | ||
| reject(new Error(`WGPU Error (Success but null device): ${message || 'No message.'}`)); | ||
| } | ||
| } else { | ||
| this._state = 'valid'; | ||
| let statusName = ReverseDeviceStatus[status] || 'Unknown WGPU Error'; | ||
| reject(new OperationError(`WGPU Error (${statusName}): ${message || 'No message provided.'}`)); | ||
| } | ||
| if (jsCallback) { | ||
| jsCallback.close(); | ||
| } | ||
| }; | ||
| jsCallback = new JSCallback(callbackFn, { | ||
| args: [FFIType.u32, FFIType.pointer, FFIType.pointer, FFIType.u64, FFIType.pointer, FFIType.pointer ], returns: FFIType.void | ||
| }); | ||
| if (!jsCallback?.ptr) { | ||
| fatalError("Failed to create JSCallback"); | ||
| } | ||
| // --- 3. Pack CallbackInfo --- | ||
| const buffer = WGPUCallbackInfoStruct.pack({ | ||
| nextInChain: null, | ||
| mode: "AllowProcessEvents", | ||
| callback: jsCallback?.ptr, | ||
| userdata1: null, | ||
| userdata2: null, | ||
| }); | ||
| const packedCallbackInfoPtr = ptr(buffer); | ||
| // --- 4. Call FFI --- | ||
| this.lib.wgpuAdapterRequestDevice( | ||
| this.adapterPtr, | ||
| packedDescriptorPtr, | ||
| packedCallbackInfoPtr | ||
| ); | ||
| this.instanceTicker.register(); | ||
| } catch (e) { | ||
| console.error("Error during requestDevice:", e); | ||
| this._state = 'valid'; | ||
| if (jsCallback) jsCallback.close(); | ||
| reject(e); | ||
| } | ||
| }); | ||
| } | ||
| destroy(): undefined { | ||
| if (this._destroyed) return; | ||
| this._destroyed = true; | ||
| this._features = null; | ||
| try { | ||
| this.lib.wgpuAdapterRelease(this.adapterPtr); | ||
| } catch(e) { | ||
| console.error("FFI Error: wgpuAdapterRelease", e); | ||
| } | ||
| return undefined; | ||
| } | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| export class GPUBindGroupImpl implements GPUBindGroup { | ||
| __brand: "GPUBindGroup" = "GPUBindGroup"; | ||
| label: string; | ||
| // Store the FFI library and pointer | ||
| readonly ptr: Pointer; | ||
| private lib: FFISymbols; // Keep if needed for future methods | ||
| constructor(ptr: Pointer, lib: FFISymbols, label?: string) { | ||
| this.ptr = ptr; | ||
| this.lib = lib; | ||
| this.label = label || ''; | ||
| } | ||
| // Add destroy method (using bindGroupRelease from index.ts) | ||
| destroy(): undefined { | ||
| try { | ||
| this.lib.wgpuBindGroupRelease(this.ptr); | ||
| // TODO: Consider nullifying the ptr after release? | ||
| } catch(e) { | ||
| console.error("FFI Error: bindGroupRelease", e); | ||
| } | ||
| } | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| export class GPUBindGroupLayoutImpl implements GPUBindGroupLayout { | ||
| __brand: "GPUBindGroupLayout" = "GPUBindGroupLayout"; | ||
| label: string; | ||
| readonly ptr: Pointer; | ||
| private lib: FFISymbols; | ||
| constructor(ptr: Pointer, lib: FFISymbols, label?: string) { | ||
| this.ptr = ptr; | ||
| this.lib = lib; | ||
| this.label = label || ''; | ||
| } | ||
| // Add destroy method if needed (using bindGroupLayoutRelease from index.ts) | ||
| destroy(): undefined { | ||
| try { | ||
| this.lib.wgpuBindGroupLayoutRelease(this.ptr); | ||
| // TODO: Consider nullifying the ptr after release? | ||
| } catch(e) { | ||
| console.error("FFI Error: bindGroupLayoutRelease", e); | ||
| } | ||
| } | ||
| } |
-374
| import { FFIType, JSCallback, ptr, toArrayBuffer, type Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| import { BufferUsageFlags } from "./common"; | ||
| import { fatalError, OperationError } from "./utils/error"; | ||
| import { WGPUCallbackInfoStruct } from "./structs_def"; | ||
| import type { InstanceTicker } from "./GPU"; | ||
| import { AsyncStatus, decodeCallbackMessage, packUserDataId, unpackUserDataId } from "./shared"; | ||
| import type { GPUDeviceImpl } from "./GPUDevice"; | ||
| import { EventEmitter } from "events"; | ||
| export class GPUBufferImpl extends EventEmitter implements GPUBuffer { | ||
| private _size: GPUSize64; | ||
| private _descriptor: GPUBufferDescriptor; | ||
| private _mapState: GPUBufferMapState = 'unmapped'; | ||
| private _pendingMap: Promise<undefined> | null = null; | ||
| private _mapCallback: JSCallback; | ||
| private _mapCallbackPromiseData: { | ||
| resolve: (value: undefined) => void; | ||
| reject: (reason?: any) => void; | ||
| } | null = null; | ||
| private _destroyed = false; | ||
| private _mappedOffset: number = 0; | ||
| private _mappedSize: number = 0; | ||
| private _returnedRanges: Array<{offset: number, size: number}> = []; | ||
| private _detachableArrayBuffers: Array<ArrayBuffer> = []; | ||
| __brand: "GPUBuffer" = "GPUBuffer"; | ||
| label: string = ''; | ||
| ptr: Pointer; | ||
| constructor( | ||
| public readonly bufferPtr: Pointer, | ||
| private readonly device: GPUDeviceImpl, | ||
| private lib: FFISymbols, | ||
| descriptor: GPUBufferDescriptor, | ||
| private instanceTicker: InstanceTicker | ||
| ) { | ||
| super(); | ||
| this.ptr = bufferPtr; | ||
| this._size = descriptor.size; | ||
| this._descriptor = descriptor; | ||
| this._mapState = descriptor.mappedAtCreation ? 'mapped' : 'unmapped'; | ||
| if (descriptor.mappedAtCreation) { | ||
| this._mappedOffset = 0; | ||
| this._mappedSize = this._size; | ||
| } | ||
| this._mapCallback = new JSCallback( | ||
| (status: number, messagePtr: Pointer | null, messageSize: bigint, userdata1: Pointer, _userdata2: Pointer | null) => { | ||
| this.instanceTicker.unregister(); | ||
| this._pendingMap = null; | ||
| // Note: Workaround for windows, where the message is not spread across multiple arguments | ||
| const message = decodeCallbackMessage(messagePtr, process.platform === 'win32' ? undefined : messageSize); | ||
| let actualUserData: Pointer | number; | ||
| // TODO: The zig wrapper should probably wrap the callback as well and pass the arguemnts correctly | ||
| if (process.platform === 'win32') { | ||
| // On windows, the WGPUStringView as value is not spread across multiple arguments, for some reason, | ||
| // so we need to pass the messageSize as the userdata1 pointer | ||
| actualUserData = Number(messageSize as unknown as Pointer); | ||
| } else { | ||
| actualUserData = userdata1; | ||
| } | ||
| const userData = unpackUserDataId(actualUserData as Pointer); | ||
| if (status === AsyncStatus.Success) { | ||
| this._mapState = 'mapped'; | ||
| this._returnedRanges = []; | ||
| this._detachableArrayBuffers = []; | ||
| this._mapCallbackPromiseData?.resolve(undefined); | ||
| } else { | ||
| const statusName = Object.keys(AsyncStatus).find(key => AsyncStatus[key as keyof typeof AsyncStatus] === status) || 'Unknown Map Error'; | ||
| const errorMessage = `WGPU Buffer Map Error (${statusName}): ${message}`; | ||
| const wasAlreadyMapped = userData === 1; | ||
| const wasPending = this._mapState === 'pending'; | ||
| this._mapState = wasAlreadyMapped ? 'mapped' : 'unmapped'; | ||
| switch (status) { | ||
| case AsyncStatus.Error: | ||
| if (wasPending) { | ||
| this._mapCallbackPromiseData?.reject(new OperationError(errorMessage)); | ||
| } else { | ||
| this._mapCallbackPromiseData?.reject(new AbortError(errorMessage)); | ||
| } | ||
| break; | ||
| default: | ||
| this._mapCallbackPromiseData?.reject(new AbortError(errorMessage)); | ||
| } | ||
| } | ||
| this._mapCallbackPromiseData = null; | ||
| if (this._destroyed) { | ||
| this._mapCallback.close(); | ||
| } | ||
| }, | ||
| { | ||
| args: [FFIType.u32, FFIType.pointer, FFIType.u64, FFIType.pointer, FFIType.pointer], | ||
| returns: FFIType.void, | ||
| } | ||
| ); | ||
| } | ||
| private _checkRangeOverlap(newOffset: number, newSize: number): boolean { | ||
| const newEnd = newOffset + newSize; | ||
| for (const range of this._returnedRanges) { | ||
| const rangeEnd = range.offset + range.size; | ||
| const disjoint = (newOffset >= rangeEnd) || (range.offset >= newEnd); | ||
| if (!disjoint) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| private _createDetachableArrayBuffer(actualArrayBuffer: ArrayBuffer): ArrayBuffer { | ||
| this._detachableArrayBuffers.push(actualArrayBuffer); | ||
| return actualArrayBuffer; | ||
| } | ||
| get size(): GPUSize64 { | ||
| return this._size; | ||
| } | ||
| get usage(): GPUFlagsConstant { | ||
| return this._descriptor.usage; | ||
| } | ||
| get mapState(): GPUBufferMapState { | ||
| return this._mapState; | ||
| } | ||
| mapAsync(mode: GPUMapModeFlags, offset?: GPUSize64, size?: GPUSize64): Promise<undefined> { | ||
| if (this._destroyed) { | ||
| this.device.injectError('validation', 'Buffer is destroyed'); | ||
| return new Promise((_, reject) => { | ||
| process.nextTick(() => { | ||
| reject(new OperationError('Buffer is destroyed')); | ||
| }); | ||
| }); | ||
| } | ||
| if (this._pendingMap) { | ||
| return Promise.reject(new OperationError('Buffer mapping is already pending')); | ||
| } | ||
| const originalMapState = this._mapState; | ||
| this._mapState = 'pending'; | ||
| this._pendingMap = new Promise((resolve, reject) => { | ||
| const mapOffsetValue = offset ?? 0; | ||
| const mapSizeValue = size ?? (this._size - mapOffsetValue); | ||
| const mapOffset = BigInt(mapOffsetValue); | ||
| const mapSize = BigInt(mapSizeValue); | ||
| const userDataBuffer = packUserDataId(originalMapState === 'mapped' ? 1 : 0); | ||
| const userDataPtr = ptr(userDataBuffer); | ||
| this._mapCallbackPromiseData = { | ||
| resolve, | ||
| reject, | ||
| }; | ||
| if (!this._mapCallback.ptr) { | ||
| fatalError('Could not create buffer map callback'); | ||
| } | ||
| const callbackInfo = WGPUCallbackInfoStruct.pack({ | ||
| mode: 'AllowProcessEvents', | ||
| callback: this._mapCallback.ptr, | ||
| userdata1: userDataPtr, | ||
| }); | ||
| try { | ||
| this.lib.wgpuBufferMapAsync( | ||
| this.bufferPtr, | ||
| mode, | ||
| mapOffset, | ||
| mapSize, | ||
| ptr(callbackInfo) | ||
| ); | ||
| this._mappedOffset = mapOffsetValue; | ||
| this._mappedSize = mapSizeValue; | ||
| this.instanceTicker.register(); | ||
| } catch(e) { | ||
| this._pendingMap = null; | ||
| this._mapState = 'unmapped'; | ||
| this.instanceTicker.unregister(); | ||
| reject(e); | ||
| } | ||
| }); | ||
| return this._pendingMap; | ||
| } | ||
| private _validateAlignment(offset: GPUSize64, size: GPUSize64): void { | ||
| const kOffsetAlignment = 8; | ||
| const kSizeAlignment = 4; | ||
| if (offset % kOffsetAlignment !== 0) { | ||
| throw new OperationError(`offset (${offset}) is not aligned to ${kOffsetAlignment} bytes.`); | ||
| } | ||
| if (size % kSizeAlignment !== 0) { | ||
| throw new OperationError(`size (${size}) is not aligned to ${kSizeAlignment} bytes.`); | ||
| } | ||
| } | ||
| getMappedRangePtr(offset?: GPUSize64, size?: GPUSize64): Pointer { | ||
| if (this._destroyed) { | ||
| throw new OperationError('Buffer is destroyed'); | ||
| } | ||
| const mappedOffset = offset ?? 0; | ||
| const mappedSize = size ?? (this._size - mappedOffset); | ||
| this._validateAlignment(mappedOffset, mappedSize); | ||
| if (this._checkRangeOverlap(mappedOffset, mappedSize)) { | ||
| throw new OperationError("getMappedRangePtr: Requested range overlaps with an existing range."); | ||
| } | ||
| this._returnedRanges.push({ offset: mappedOffset, size: mappedSize }); | ||
| if (this._descriptor.usage & BufferUsageFlags.MAP_READ) { | ||
| return this._getConstMappedRangePtr(mappedOffset, mappedSize); | ||
| } | ||
| const readOffset = BigInt(mappedOffset); | ||
| const readSize = BigInt(mappedSize); | ||
| const dataPtr = this.lib.wgpuBufferGetMappedRange(this.bufferPtr, readOffset, readSize); | ||
| if (dataPtr === null || dataPtr.valueOf() === 0) { | ||
| throw new OperationError("getMappedRangePtr: Received null pointer (buffer likely not mapped or range invalid)."); | ||
| } | ||
| return dataPtr; | ||
| } | ||
| _getConstMappedRangePtr(offset: GPUSize64, size: GPUSize64): Pointer { | ||
| const readOffset = BigInt(offset); | ||
| const readSize = BigInt(size); | ||
| const dataPtr = this.lib.wgpuBufferGetConstMappedRange(this.bufferPtr, readOffset, readSize); | ||
| if (dataPtr === null || dataPtr.valueOf() === 0) { | ||
| throw new OperationError("getConstMappedRangePtr: Received null pointer (buffer likely not mapped or range invalid)."); | ||
| } | ||
| return dataPtr; | ||
| } | ||
| getMappedRange(offset?: GPUSize64, size?: GPUSize64): ArrayBuffer { | ||
| if (this._destroyed) { | ||
| throw new OperationError('Buffer is destroyed'); | ||
| } | ||
| if (this._mapState !== 'mapped') { | ||
| throw new OperationError("getMappedRange: Buffer is not in mapped state."); | ||
| } | ||
| const requestedOffset = offset ?? 0; | ||
| const requestedSize = size ?? (this._size - requestedOffset); | ||
| this._validateAlignment(requestedOffset, requestedSize); | ||
| if (requestedOffset < this._mappedOffset || | ||
| requestedOffset > this._size || | ||
| requestedOffset + requestedSize > this._mappedOffset + this._mappedSize) { | ||
| throw new OperationError("getMappedRange: Requested range is outside the mapped region."); | ||
| } | ||
| if (this._checkRangeOverlap(requestedOffset, requestedSize)) { | ||
| throw new OperationError("getMappedRange: Requested range overlaps with an existing range."); | ||
| } | ||
| this._returnedRanges.push({ offset: requestedOffset, size: requestedSize }); | ||
| if (requestedSize === 0) { | ||
| return new ArrayBuffer(0); | ||
| } | ||
| if (this._descriptor.usage & BufferUsageFlags.MAP_READ) { | ||
| const actualArrayBuffer = this._getConstMappedRange(requestedOffset, requestedSize); | ||
| return this._createDetachableArrayBuffer(actualArrayBuffer); | ||
| } | ||
| const readOffset = BigInt(requestedOffset); | ||
| const readSize = BigInt(requestedSize); | ||
| const dataPtr = this.lib.wgpuBufferGetMappedRange(this.bufferPtr, readOffset, readSize); | ||
| if (dataPtr === null || dataPtr.valueOf() === 0) { | ||
| throw new OperationError("getMappedRange: Received null pointer (buffer likely not mapped or range invalid)."); | ||
| } | ||
| const actualArrayBuffer = toArrayBuffer(dataPtr, 0, Number(readSize)); | ||
| return this._createDetachableArrayBuffer(actualArrayBuffer); | ||
| } | ||
| _getConstMappedRange(offset: GPUSize64, size: GPUSize64): ArrayBuffer { | ||
| const readOffset = BigInt(offset); | ||
| const readSize = BigInt(size); | ||
| const dataPtr = this.lib.wgpuBufferGetConstMappedRange(this.bufferPtr, readOffset, readSize); | ||
| if (dataPtr === null || dataPtr.valueOf() === 0) { | ||
| throw new OperationError("getConstMappedRange: Received null pointer (buffer likely not mapped or range invalid)."); | ||
| } | ||
| return toArrayBuffer(dataPtr, 0, Number(readSize)); | ||
| } | ||
| _detachBuffers(): void { | ||
| for (const buffer of this._detachableArrayBuffers) { | ||
| // Workaround to detach the ArrayBuffer | ||
| structuredClone(buffer, { transfer: [buffer] }); | ||
| } | ||
| this._detachableArrayBuffers = []; | ||
| } | ||
| unmap(): undefined { | ||
| // NOTE: It is valid to call unmap on a buffer that is destroyed | ||
| // (at creation, or after mappedAtCreation or mapAsync) | ||
| this._detachBuffers(); | ||
| this.lib.wgpuBufferUnmap(this.bufferPtr); | ||
| this._mapState = 'unmapped'; | ||
| this._mappedOffset = 0; | ||
| this._mappedSize = 0; | ||
| this._returnedRanges = []; | ||
| this.instanceTicker.processEvents(); | ||
| return undefined; | ||
| } | ||
| release(): undefined { | ||
| if (this._destroyed) { | ||
| throw new Error('Buffer is destroyed'); | ||
| } | ||
| try { | ||
| this.lib.wgpuBufferRelease(this.bufferPtr); | ||
| } catch(e) { | ||
| console.error("FFI Error: wgpuBufferRelease", e); | ||
| } | ||
| } | ||
| destroy(): undefined { | ||
| if (this._destroyed) { | ||
| return; | ||
| } | ||
| this._detachBuffers(); | ||
| try { | ||
| this.lib.wgpuBufferDestroy(this.bufferPtr); | ||
| this._destroyed = true; | ||
| this.emit('destroyed'); | ||
| this._mapState = 'unmapped'; | ||
| } catch (e) { | ||
| console.error("Error calling bufferDestroy FFI function:", e); | ||
| } | ||
| } | ||
| } |
| import type { Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| export class GPUCommandBufferImpl implements GPUCommandBuffer { | ||
| __brand: "GPUCommandBuffer" = "GPUCommandBuffer"; | ||
| label: string = 'Unnamed Command Buffer'; | ||
| readonly ptr: Pointer; | ||
| constructor(public readonly bufferPtr: Pointer, private lib: FFISymbols, label?: string) { | ||
| this.ptr = bufferPtr; | ||
| this.label = label || 'Unnamed Command Buffer'; | ||
| } | ||
| _destroy(): undefined { | ||
| try { | ||
| this.lib.wgpuCommandBufferRelease(this.bufferPtr); | ||
| } catch(e) { | ||
| console.error("FFI Error: commandBufferRelease", e); | ||
| } | ||
| } | ||
| } |
| import { type Pointer, ptr } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| import { GPUComputePassEncoderImpl } from "./GPUComputePassEncoder"; | ||
| import { GPURenderPassEncoderImpl } from "./GPURenderPassEncoder"; | ||
| import { GPUCommandBufferImpl } from "./GPUCommandBuffer"; | ||
| import { fatalError } from "./utils/error"; | ||
| import { | ||
| WGPURenderPassDescriptorStruct, | ||
| WGPUComputePassDescriptorStruct, | ||
| WGPUCommandBufferDescriptorStruct, | ||
| WGPUTexelCopyBufferInfoStruct, | ||
| WGPUTexelCopyTextureInfoStruct, | ||
| WGPUExtent3DStruct, | ||
| UINT64_MAX, | ||
| WGPUStringView, | ||
| } from "./structs_def"; | ||
| import { GPUBufferImpl } from "./GPUBuffer"; | ||
| export class GPUCommandEncoderImpl implements GPUCommandEncoder { | ||
| __brand: "GPUCommandEncoder" = "GPUCommandEncoder"; | ||
| label: string = 'Main Command Encoder'; | ||
| readonly ptr: Pointer; | ||
| private _destroyed = false; // Track destruction | ||
| constructor(public readonly encoderPtr: Pointer, private lib: FFISymbols) { | ||
| this.ptr = encoderPtr; | ||
| } | ||
| beginRenderPass(descriptor: GPURenderPassDescriptor): GPURenderPassEncoder { | ||
| if (this._destroyed) { | ||
| fatalError("Cannot call beginRenderPass on destroyed command encoder"); | ||
| } | ||
| const colorAttachments = Array.from(descriptor.colorAttachments ?? []).filter(ca => !!ca); | ||
| const packedDescriptorBuffer = WGPURenderPassDescriptorStruct.pack({ | ||
| ...descriptor, | ||
| colorAttachments | ||
| }); | ||
| const passEncoderPtr = this.lib.wgpuCommandEncoderBeginRenderPass( | ||
| this.encoderPtr, | ||
| ptr(packedDescriptorBuffer) | ||
| ); | ||
| if (!passEncoderPtr) { | ||
| fatalError("wgpuCommandEncoderBeginRenderPass returned null."); | ||
| } | ||
| return new GPURenderPassEncoderImpl(passEncoderPtr, this.lib); | ||
| } | ||
| beginComputePass(descriptor: GPUComputePassDescriptor = {}): GPUComputePassEncoder { | ||
| if (this._destroyed) { | ||
| fatalError("Cannot call beginComputePass on destroyed command encoder"); | ||
| } | ||
| const packedDescriptorBuffer = WGPUComputePassDescriptorStruct.pack(descriptor); | ||
| const passEncoderPtr = this.lib.wgpuCommandEncoderBeginComputePass( | ||
| this.encoderPtr, | ||
| ptr(packedDescriptorBuffer) | ||
| ); | ||
| if (!passEncoderPtr) { | ||
| fatalError("wgpuCommandEncoderBeginComputePass returned null."); | ||
| } | ||
| return new GPUComputePassEncoderImpl(passEncoderPtr, this.lib); | ||
| } | ||
| copyBufferToBuffer(source: GPUBuffer, destination: GPUBuffer, size?: number): undefined; | ||
| copyBufferToBuffer(source: GPUBuffer, sourceOffset: number, destination: GPUBuffer, destinationOffset: number, size: number): undefined; | ||
| copyBufferToBuffer( | ||
| source: GPUBuffer, | ||
| arg2: number | GPUBuffer, // sourceOffset or destination | ||
| arg3?: GPUBuffer | number, // destination or size | ||
| arg4?: number, // destinationOffset | ||
| arg5?: number // size | ||
| ): undefined { | ||
| if (this._destroyed) { | ||
| fatalError("Cannot call copyBufferToBuffer on destroyed command encoder"); | ||
| } | ||
| let sourceOffset = 0; | ||
| let destination: GPUBuffer; | ||
| let destinationOffset = 0; | ||
| let size: bigint; | ||
| if (typeof arg2 === 'number') { | ||
| sourceOffset = arg2; | ||
| destination = arg3 as GPUBuffer; | ||
| destinationOffset = arg4 ?? 0; | ||
| size = BigInt(arg5 ?? UINT64_MAX); | ||
| } else { | ||
| if (!(arg2 instanceof GPUBufferImpl)) { | ||
| fatalError("Invalid arguments for copyBufferToBuffer (expected destination buffer)"); | ||
| } | ||
| destination = arg2 as GPUBuffer; | ||
| if (typeof arg3 !== 'number') { | ||
| fatalError("Invalid arguments for copyBufferToBuffer (expected size)"); | ||
| } | ||
| size = BigInt(arg3); | ||
| } | ||
| try { | ||
| this.lib.wgpuCommandEncoderCopyBufferToBuffer( | ||
| this.encoderPtr, | ||
| source.ptr, | ||
| BigInt(sourceOffset), | ||
| destination.ptr, | ||
| BigInt(destinationOffset), | ||
| BigInt(size) | ||
| ); | ||
| } catch (e) { | ||
| console.error("FFI Error: wgpuCommandEncoderCopyBufferToBuffer", e); | ||
| } | ||
| return undefined; | ||
| } | ||
| copyBufferToTexture(source: GPUTexelCopyBufferInfo, destination: GPUTexelCopyTextureInfo, copySize: GPUExtent3DStrict): undefined { | ||
| if (this._destroyed) { | ||
| fatalError("Cannot call copyBufferToTexture on destroyed command encoder"); | ||
| } | ||
| const packedSourceBuffer = WGPUTexelCopyBufferInfoStruct.pack(source); | ||
| const packedDestinationBuffer = WGPUTexelCopyTextureInfoStruct.pack(destination); | ||
| const packedCopySizeBuffer = WGPUExtent3DStruct.pack(copySize); | ||
| try { | ||
| this.lib.wgpuCommandEncoderCopyBufferToTexture( | ||
| this.encoderPtr, | ||
| ptr(packedSourceBuffer), | ||
| ptr(packedDestinationBuffer), | ||
| ptr(packedCopySizeBuffer) | ||
| ); | ||
| } catch (e) { | ||
| console.error("FFI Error: wgpuCommandEncoderCopyBufferToTexture", e); | ||
| } | ||
| } | ||
| copyTextureToBuffer(source: GPUTexelCopyTextureInfo, destination: GPUTexelCopyBufferInfo, copySize: GPUExtent3DStrict): undefined { | ||
| if (this._destroyed) { | ||
| fatalError("Cannot call copyTextureToBuffer on destroyed command encoder"); | ||
| } | ||
| const packedSourceBuffer = WGPUTexelCopyTextureInfoStruct.pack(source); | ||
| const packedDestinationBuffer = WGPUTexelCopyBufferInfoStruct.pack(destination); | ||
| const packedCopySizeBuffer = WGPUExtent3DStruct.pack(copySize); | ||
| try { | ||
| this.lib.wgpuCommandEncoderCopyTextureToBuffer( | ||
| this.encoderPtr, | ||
| ptr(packedSourceBuffer), | ||
| ptr(packedDestinationBuffer), | ||
| ptr(packedCopySizeBuffer) | ||
| ); | ||
| } catch (e) { | ||
| console.error("FFI Error: wgpuCommandEncoderCopyTextureToBuffer", e); | ||
| } | ||
| } | ||
| copyTextureToTexture(source: GPUTexelCopyTextureInfo, destination: GPUTexelCopyTextureInfo, copySize: GPUExtent3DStrict): undefined { | ||
| if (this._destroyed) { | ||
| fatalError("Cannot call copyTextureToTexture on destroyed command encoder"); | ||
| } | ||
| const packedSourceBuffer = WGPUTexelCopyTextureInfoStruct.pack(source); | ||
| const packedDestinationBuffer = WGPUTexelCopyTextureInfoStruct.pack(destination); | ||
| const packedCopySizeBuffer = WGPUExtent3DStruct.pack(copySize); | ||
| try { | ||
| this.lib.wgpuCommandEncoderCopyTextureToTexture( | ||
| this.encoderPtr, | ||
| ptr(packedSourceBuffer), | ||
| ptr(packedDestinationBuffer), | ||
| ptr(packedCopySizeBuffer) | ||
| ); | ||
| } catch (e) { | ||
| console.error("FFI Error: wgpuCommandEncoderCopyTextureToTexture", e); | ||
| } | ||
| } | ||
| clearBuffer(buffer: GPUBuffer, offset?: GPUSize64, size?: GPUSize64): undefined { | ||
| if (this._destroyed) { | ||
| fatalError("Cannot call clearBuffer on destroyed command encoder"); | ||
| } | ||
| const offsetBigInt = offset !== undefined ? BigInt(offset) : 0n; | ||
| const sizeBigInt = size !== undefined ? BigInt(size) : UINT64_MAX; | ||
| try { | ||
| this.lib.wgpuCommandEncoderClearBuffer(this.encoderPtr, buffer.ptr, offsetBigInt, sizeBigInt); | ||
| } catch (e) { | ||
| console.error("FFI Error: wgpuCommandEncoderClearBuffer", e); | ||
| } | ||
| } | ||
| resolveQuerySet(querySet: GPUQuerySet, firstQuery: GPUSize32, queryCount: GPUSize32, destination: GPUBuffer, destinationOffset: GPUSize64): undefined { | ||
| if (this._destroyed) { | ||
| fatalError("Cannot call resolveQuerySet on destroyed command encoder"); | ||
| } | ||
| this.lib.wgpuCommandEncoderResolveQuerySet( | ||
| this.encoderPtr, | ||
| querySet.ptr, | ||
| firstQuery, | ||
| queryCount, | ||
| destination.ptr, | ||
| BigInt(destinationOffset) | ||
| ); | ||
| return undefined; | ||
| } | ||
| finish(descriptor?: GPUCommandBufferDescriptor): GPUCommandBuffer { | ||
| if (this._destroyed) { | ||
| fatalError("Cannot call finish on destroyed command encoder"); | ||
| } | ||
| const packedDescriptorBuffer = WGPUCommandBufferDescriptorStruct.pack(descriptor ?? {}); | ||
| const commandBufferPtr = this.lib.wgpuCommandEncoderFinish(this.encoderPtr, ptr(packedDescriptorBuffer)); | ||
| if (!commandBufferPtr) { | ||
| fatalError("wgpuCommandEncoderFinish returned null."); | ||
| } | ||
| // Seems like the spec (according to the CTS) says that command encoders are not destroyed automatically when finished? | ||
| this._destroy(); | ||
| return new GPUCommandBufferImpl(commandBufferPtr, this.lib, descriptor?.label); | ||
| } | ||
| pushDebugGroup(message: string): undefined { | ||
| if (this._destroyed) return; | ||
| const packedMessage = WGPUStringView.pack(message); | ||
| this.lib.wgpuCommandEncoderPushDebugGroup(this.encoderPtr, ptr(packedMessage)); | ||
| } | ||
| popDebugGroup(): undefined { | ||
| if (this._destroyed) return; | ||
| this.lib.wgpuCommandEncoderPopDebugGroup(this.encoderPtr); | ||
| } | ||
| insertDebugMarker(markerLabel: string): undefined { | ||
| if (this._destroyed) return; | ||
| const packedMarker = WGPUStringView.pack(markerLabel); | ||
| this.lib.wgpuCommandEncoderInsertDebugMarker(this.encoderPtr, ptr(packedMarker)); | ||
| } | ||
| /** | ||
| * Note: Command encoders are destroyed automatically when finished. | ||
| */ | ||
| _destroy(): undefined { | ||
| if (this._destroyed) return; | ||
| this._destroyed = true; | ||
| try { | ||
| this.lib.wgpuCommandEncoderRelease(this.encoderPtr); | ||
| } catch (e) { | ||
| console.error("FFI Error: wgpuCommandEncoderRelease", e); | ||
| } | ||
| } | ||
| } |
| import { type Pointer, ptr } from "bun:ffi"; | ||
| import { type FFISymbols } from "./ffi"; | ||
| import { WGPUStringView } from "./structs_def"; | ||
| export class GPUComputePassEncoderImpl implements GPUComputePassEncoder { | ||
| __brand: "GPUComputePassEncoder" = "GPUComputePassEncoder"; | ||
| private lib: FFISymbols; | ||
| label: string = ''; | ||
| constructor(public ptr: Pointer, lib: FFISymbols) { | ||
| this.lib = lib; | ||
| } | ||
| setPipeline(pipeline: GPUComputePipeline): undefined { | ||
| if (!pipeline || !pipeline.ptr) { | ||
| console.warn("ComputePassEncoder.setPipeline: null pipeline pointer."); | ||
| return; | ||
| } | ||
| this.lib.wgpuComputePassEncoderSetPipeline(this.ptr, pipeline.ptr); | ||
| } | ||
| setBindGroup(groupIndex: number, bindGroup: GPUBindGroup | null, dynamicOffsets?: Uint32Array | number[]): undefined { | ||
| if (!bindGroup || !bindGroup.ptr) { | ||
| console.warn("ComputePassEncoder.setBindGroup: null bindGroup pointer."); | ||
| return; | ||
| } | ||
| let offsetsBuffer: Uint32Array | undefined; | ||
| let offsetCount = 0; | ||
| let offsetPtr: Pointer | null = null; | ||
| if (dynamicOffsets) { | ||
| if (dynamicOffsets instanceof Uint32Array) { | ||
| offsetsBuffer = dynamicOffsets; | ||
| } else { | ||
| offsetsBuffer = new Uint32Array(dynamicOffsets); | ||
| } | ||
| offsetCount = offsetsBuffer.length; | ||
| if (offsetCount > 0) { | ||
| offsetPtr = ptr(offsetsBuffer.buffer, offsetsBuffer.byteOffset); | ||
| } | ||
| } | ||
| try { | ||
| // Pass BigUint64Array equivalent (Uint32Array pointer, u64 count) | ||
| this.lib.wgpuComputePassEncoderSetBindGroup(this.ptr, groupIndex, bindGroup.ptr, BigInt(offsetCount), offsetPtr); | ||
| } catch (e) { console.error("FFI Error: computePassEncoderSetBindGroup", e); } | ||
| } | ||
| dispatchWorkgroups(workgroupCountX: number, workgroupCountY: number = 1, workgroupCountZ: number = 1): undefined { | ||
| if (!this.ptr) { | ||
| console.warn("ComputePassEncoder.dispatchWorkgroups: null encoder pointer."); | ||
| return; | ||
| } | ||
| try { | ||
| this.lib.wgpuComputePassEncoderDispatchWorkgroups(this.ptr, workgroupCountX, workgroupCountY, workgroupCountZ); | ||
| } catch (e) { console.error("FFI Error: computePassEncoderDispatchWorkgroups", e); } | ||
| } | ||
| dispatchWorkgroupsIndirect(indirectBuffer: GPUBuffer, indirectOffset: number | bigint): undefined { | ||
| if (!indirectBuffer || !indirectBuffer.ptr) { | ||
| console.warn("ComputePassEncoder.dispatchWorkgroupsIndirect: null buffer pointer."); | ||
| return; | ||
| } | ||
| try { | ||
| this.lib.wgpuComputePassEncoderDispatchWorkgroupsIndirect(this.ptr, indirectBuffer.ptr, BigInt(indirectOffset)); | ||
| } catch (e) { console.error("FFI Error: computePassEncoderDispatchWorkgroupsIndirect", e); } | ||
| } | ||
| end(): undefined { | ||
| this.lib.wgpuComputePassEncoderEnd(this.ptr); | ||
| // TODO: Encoder is consumed after end, prevent reuse. | ||
| } | ||
| pushDebugGroup(message: string): undefined { | ||
| const packedMessage = WGPUStringView.pack(message); | ||
| this.lib.wgpuComputePassEncoderPushDebugGroup(this.ptr, ptr(packedMessage)); | ||
| } | ||
| popDebugGroup(): undefined { | ||
| this.lib.wgpuComputePassEncoderPopDebugGroup(this.ptr); | ||
| } | ||
| insertDebugMarker(markerLabel: string): undefined { | ||
| const packedMarker = WGPUStringView.pack(markerLabel); | ||
| this.lib.wgpuComputePassEncoderInsertDebugMarker(this.ptr, ptr(packedMarker)); | ||
| } | ||
| destroy(): undefined { | ||
| console.error('destroy', this.ptr); | ||
| throw new Error("Not implemented"); | ||
| } | ||
| } |
| import type { Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| import { GPUBindGroupLayoutImpl } from "./GPUBindGroupLayout"; | ||
| export class GPUComputePipelineImpl implements GPUComputePipeline { | ||
| __brand: "GPUComputePipeline" = "GPUComputePipeline"; | ||
| label: string; | ||
| readonly ptr: Pointer; | ||
| constructor(ptr: Pointer, private lib: FFISymbols, label?: string) { | ||
| this.ptr = ptr; | ||
| this.label = label || ''; | ||
| } | ||
| getBindGroupLayout(index: number): GPUBindGroupLayout { | ||
| const bindGroupLayoutPtr = this.lib.wgpuComputePipelineGetBindGroupLayout(this.ptr, index); | ||
| if (!bindGroupLayoutPtr) { | ||
| throw new Error(`Failed to get bind group layout for index ${index}. Pointer is null.`); | ||
| } | ||
| return new GPUBindGroupLayoutImpl(bindGroupLayoutPtr, this.lib); | ||
| } | ||
| destroy(): undefined { | ||
| try { | ||
| this.lib.wgpuComputePipelineRelease(this.ptr); | ||
| } catch (e) { | ||
| console.error("FFI Error: computePipelineRelease", e); | ||
| } | ||
| return undefined; | ||
| } | ||
| } |
| import { expect, describe, it, beforeAll, afterAll, beforeEach, afterEach } from "bun:test"; | ||
| import { | ||
| createGPUInstance, | ||
| } from "./index"; | ||
| import type { GPUImpl } from "./GPU"; | ||
| import { globals } from "./index"; | ||
| globals(); | ||
| // Global variables for the test suite | ||
| let gpu: GPUImpl | null = null; | ||
| let adapter: GPUAdapter | null = null; | ||
| let device: GPUDevice | null = null; | ||
| describe("GPUDevice", () => { | ||
| // --- Setup and Teardown --- | ||
| beforeAll(() => { | ||
| // Identical setup to index.test.ts | ||
| try { | ||
| gpu = createGPUInstance(); | ||
| } catch (e) { | ||
| console.error("Error during GPUDevice Test Setup:", e); | ||
| gpu?.destroy(); | ||
| throw e; | ||
| } | ||
| }); | ||
| afterAll(() => { | ||
| // Identical teardown to index.test.ts | ||
| device?.destroy(); // Releases adapter implicitly | ||
| device = null; | ||
| adapter = null; | ||
| if (gpu) { | ||
| gpu.destroy(); // Assuming GPUImpl has destroy | ||
| gpu = null; | ||
| } | ||
| }); | ||
| // Create fresh adapter and device for each test | ||
| beforeEach(async () => { | ||
| if (!gpu) throw new Error("GPU instance not created in beforeAll"); | ||
| try { | ||
| adapter = await gpu.requestAdapter(); | ||
| if (!adapter) throw new Error(`beforeEach Failed: Could not request adapter.`); | ||
| device = await adapter.requestDevice({ label: `Device for test` }); | ||
| if (!device) throw new Error(`beforeEach Failed: Could not request device.`); | ||
| } catch (e) { | ||
| console.error("Error during beforeEach setup:", e); | ||
| device?.destroy(); // Cleanup partial success | ||
| device = null; | ||
| adapter = null; | ||
| throw e; // Re-throw to fail the test | ||
| } | ||
| }); | ||
| // Destroy device after each test | ||
| afterEach(() => { | ||
| device?.destroy(); | ||
| device = null; | ||
| adapter = null; // Adapter is implicitly released by device destroy | ||
| }); | ||
| // --- Tests --- | ||
| describe("createBindGroupLayout", () => { | ||
| it("should create a layout with texture and sampler entries without errors", async () => { | ||
| let layout: GPUBindGroupLayout | null = null; | ||
| let errorOccurred = false; | ||
| let receivedError: GPUError | null = null; | ||
| const originalOnError = device!.onuncapturederror; // Store original handler | ||
| // Temporarily override error handler for this test | ||
| device!.onuncapturederror = (ev) => { | ||
| console.error("*** Test-specific uncaptured error caught! ***", ev.error.message); | ||
| errorOccurred = true; | ||
| receivedError = ev.error; | ||
| }; | ||
| const entries: GPUBindGroupLayoutEntry[] = [ | ||
| { // Texture entry (like MeshBasicMaterial map) | ||
| binding: 2, | ||
| visibility: GPUShaderStage.FRAGMENT, | ||
| texture: { | ||
| // Defaults: sampleType: 'float', viewDimension: '2d' | ||
| } | ||
| }, | ||
| { // Sampler entry (for the texture map) | ||
| binding: 1, | ||
| visibility: GPUShaderStage.FRAGMENT, | ||
| sampler: { | ||
| // Defaults: type: 'filtering' | ||
| } | ||
| } | ||
| ]; | ||
| const descriptor: GPUBindGroupLayoutDescriptor = { | ||
| label: "Test Basic Texture+Sampler BGL", | ||
| entries: entries | ||
| }; | ||
| // Expect this specific call NOT to throw the validation error | ||
| expect(() => { | ||
| layout = device!.createBindGroupLayout(descriptor); | ||
| }).not.toThrow(); | ||
| // Check if a valid layout object was returned | ||
| expect(layout).not.toBeNull(); | ||
| expect(layout!.ptr).toBeTruthy(); // Check if the native pointer exists | ||
| device!.tick(); | ||
| // Assert that the uncaptured error handler was NOT called | ||
| expect(errorOccurred).toBe(false); | ||
| if (errorOccurred && receivedError) { | ||
| // Cast to any to bypass strict type checking for .message | ||
| console.error("Received error message:", (receivedError as any).message); | ||
| } else if (errorOccurred) { | ||
| console.error("An uncaptured error occurred, but receivedError object was null or undefined"); | ||
| } | ||
| // Restore original error handler | ||
| if (device) { | ||
| device.onuncapturederror = originalOnError; | ||
| } | ||
| // Ensure cleanup even if expectations fail | ||
| if (layout && 'destroy' in layout) { | ||
| (layout as any).destroy(); // Use destroy if available (assuming GPUBindGroupLayoutImpl has it) | ||
| } else if (layout && 'release' in layout) { | ||
| (layout as any).release(); // Or use release if that's the method | ||
| } | ||
| }); | ||
| }); | ||
| describe("createRenderPipeline", () => { | ||
| it("should fail validation for attribute larger than stride (stride 8)", async () => { | ||
| let vsModule: GPUShaderModule | null = null; | ||
| let fsModule: GPUShaderModule | null = null; | ||
| let pipelineLayout: GPUPipelineLayout | null = null; | ||
| let pipeline: GPURenderPipeline | null = null; | ||
| let errorOccurred = false; | ||
| let errorMessage = ""; | ||
| const originalOnError = device!.onuncapturederror; | ||
| device!.onuncapturederror = (ev) => { | ||
| console.log("*** Test-specific uncaptured error caught (createRenderPipeline stride test)! ***", ev.error.message); | ||
| errorOccurred = true; | ||
| errorMessage = (ev.error as any).message || "Unknown error"; | ||
| }; | ||
| try { | ||
| // 1. Minimal Shaders | ||
| const wgslVS = /* wgsl */ ` | ||
| @vertex fn main(@location(0) p1: vec4f, @location(1) p2: vec3f) -> @builtin(position) vec4f { | ||
| return p1; | ||
| } | ||
| `; | ||
| const wgslFS = /* wgsl */ ` | ||
| @fragment fn main() -> @location(0) vec4f { | ||
| return vec4f(1.0, 0.0, 1.0, 1.0); | ||
| } | ||
| `; | ||
| vsModule = device!.createShaderModule({ code: wgslVS }); | ||
| fsModule = device!.createShaderModule({ code: wgslFS }); | ||
| console.log('got shader modules'); | ||
| // 2. Empty Pipeline Layout | ||
| pipelineLayout = device!.createPipelineLayout({ bindGroupLayouts: [] }); | ||
| console.log('pipelineLayout', pipelineLayout.ptr); | ||
| // 3. Faulty Vertex State | ||
| const vertexState: GPUVertexState = { | ||
| module: vsModule, | ||
| entryPoint: "main", | ||
| buffers: [ | ||
| { // Buffer layout 0 | ||
| arrayStride: 8, // <<<< Keep problematic stride | ||
| attributes: [ | ||
| { shaderLocation: 0, offset: 0, format: "float32" }, // OK (4 bytes) | ||
| { shaderLocation: 1, offset: 4, format: "float32x3" } // ERROR (12 bytes > stride 8) | ||
| ] | ||
| } | ||
| ] | ||
| }; | ||
| // 4. Minimal Fragment State | ||
| const fragmentState: GPUFragmentState = { | ||
| module: fsModule, | ||
| entryPoint: "main", | ||
| targets: [{ format: "bgra8unorm" }] // Example format | ||
| }; | ||
| // 5. Pipeline Descriptor | ||
| const descriptor: GPURenderPipelineDescriptor = { | ||
| label: "Test Stride Error Pipeline", | ||
| layout: pipelineLayout, | ||
| vertex: vertexState, | ||
| fragment: fragmentState, | ||
| primitive: { topology: "triangle-list" } // Need a topology | ||
| }; | ||
| // 6. Create Pipeline (expect internal error) | ||
| console.log('Create render pipeline') | ||
| pipeline = device!.createRenderPipeline(descriptor); | ||
| console.log('Got render pipeline', pipeline.ptr) | ||
| // 7. Process potentially async errors | ||
| device!.tick(); | ||
| // await Bun.sleep(10); // Sleep likely not needed if tick processes sync errors | ||
| // 8. Assert that the specific error occurred | ||
| expect(errorOccurred).toBe(true); // <<<< REVERTED: Expect error | ||
| if (errorOccurred) { | ||
| expect(errorMessage).toContain("must be <= the vertex buffer stride (8)"); | ||
| expect(errorMessage).toContain("VertexFormat::Float32x3"); | ||
| } else { | ||
| // Fail test explicitly if error didn't occur when expected | ||
| expect("Validation error did not occur").toBe("Error expected"); | ||
| } | ||
| // Pipeline might be null or invalid if error occurred, so don't assert on it here. | ||
| } catch(e) { | ||
| console.error("createRenderPipeline threw unexpected synchronous error:", e); | ||
| expect(e).toBeNull(); | ||
| } finally { | ||
| // Restore error handler | ||
| if (device) { | ||
| device.onuncapturederror = originalOnError; | ||
| } | ||
| // Cleanup | ||
| if (pipeline && 'release' in pipeline) (pipeline as any).release(); | ||
| if (pipelineLayout && 'release' in pipelineLayout) (pipelineLayout as any).release(); | ||
| if (vsModule && 'release' in vsModule) (vsModule as any).release(); | ||
| if (fsModule && 'release' in fsModule) (fsModule as any).release(); | ||
| } | ||
| }); | ||
| }); | ||
| // --- Tests for Features --- NEW | ||
| describe("features", () => { | ||
| it("should return a non-empty set of features", async () => { | ||
| expect(device).not.toBeNull(); | ||
| const features = device!.features; | ||
| expect(features).toBeInstanceOf(Set); | ||
| // A device should always have some features enabled | ||
| expect(features.size).toBeGreaterThan(0); | ||
| console.log("Device Features Found:", Array.from(features).join(', ')); | ||
| }); | ||
| it("should return the same cached set on subsequent calls", () => { | ||
| expect(device).not.toBeNull(); | ||
| const features1 = device!.features; | ||
| const features2 = device!.features; | ||
| expect(features1).toBe(features2); // Strict equality check for cache | ||
| }); | ||
| it("should return an empty set after device is destroyed", () => { | ||
| expect(device).not.toBeNull(); | ||
| const featuresBeforeDestroy = device!.features; // Populate cache | ||
| expect(featuresBeforeDestroy).not.toBeNull(); | ||
| expect(featuresBeforeDestroy!.size).toBeGreaterThan(0); | ||
| // Access internal cache for verification (relies on GPUDeviceImpl structure) | ||
| expect((device as any)['_features']).not.toBeNull(); | ||
| // Destroy the device | ||
| device!.destroy(); | ||
| // Verify internal cache is cleared | ||
| expect((device as any)['_features']).toBeNull(); | ||
| // Verify public getter now returns an empty set | ||
| const featuresAfterDestroy = device!.features; | ||
| expect(featuresAfterDestroy).toBeInstanceOf(Set); | ||
| expect(featuresAfterDestroy.size).toBe(0); | ||
| }); | ||
| }); | ||
| // --- Tests for Limits --- NEW | ||
| describe("limits", () => { | ||
| it("should return an object with expected limit properties", () => { | ||
| expect(device).not.toBeNull(); | ||
| const limits = device!.limits; | ||
| expect(limits).toBeInstanceOf(Object); | ||
| // Spot check some key limits - exact values depend on hardware/driver | ||
| expect(limits.maxTextureDimension2D).toBeGreaterThanOrEqual(1024); // Common minimum | ||
| expect(limits.maxBindGroups).toBeGreaterThanOrEqual(4); // WebGPU minimum | ||
| expect(limits.minUniformBufferOffsetAlignment).toBeGreaterThanOrEqual(16); // Common alignment | ||
| expect(limits.maxUniformBufferBindingSize).toBeGreaterThanOrEqual(16 * 1024); // WebGPU minimum | ||
| expect(limits.maxComputeWorkgroupSizeX).toBeGreaterThanOrEqual(64); // Common minimum | ||
| }); | ||
| it("should return the same cached object on subsequent calls", () => { | ||
| expect(device).not.toBeNull(); | ||
| const limits1 = device!.limits; | ||
| const limits2 = device!.limits; | ||
| expect(limits1).toBe(limits2); // Strict equality check for cache | ||
| }); | ||
| it("should return default limits object after device is destroyed", () => { | ||
| expect(device).not.toBeNull(); | ||
| const limitsBeforeDestroy = device!.limits; // Populate cache | ||
| expect(limitsBeforeDestroy).not.toBeNull(); | ||
| // Basic check that some limit was likely non-zero before destroy | ||
| expect(limitsBeforeDestroy!.maxTextureDimension2D).toBeGreaterThan(0); | ||
| // Verify internal cache exists before destroy | ||
| expect((device as any)['_limits']).not.toBeNull(); | ||
| // Destroy the device | ||
| device!.destroy(); | ||
| // Verify public getter now returns the default limits object | ||
| const limitsAfterDestroy = device!.limits; | ||
| expect(limitsAfterDestroy.maxTextureDimension2D).toBe(8192); | ||
| }); | ||
| }); | ||
| }); |
-965
| import { FFIType, JSCallback, type Pointer, ptr } from "bun:ffi"; | ||
| import { | ||
| WGPUSupportedFeaturesStruct, | ||
| WGPUFragmentStateStruct, | ||
| WGPUBindGroupLayoutDescriptorStruct, | ||
| WGPUShaderModuleDescriptorStruct, | ||
| WGPUSType, | ||
| WGPUShaderSourceWGSLStruct, | ||
| WGPUPipelineLayoutDescriptorStruct, | ||
| WGPUBindGroupDescriptorStruct, | ||
| WGPURenderPipelineDescriptorStruct, | ||
| WGPUVertexStateStruct, | ||
| WGPUComputeStateStruct, | ||
| UINT64_MAX, | ||
| WGPUCommandEncoderDescriptorStruct, | ||
| WGPUQuerySetDescriptorStruct, | ||
| WGPUAdapterInfoStruct, | ||
| WGPUErrorFilter, | ||
| WGPUCallbackInfoStruct, | ||
| WGPUExternalTextureBindingLayoutStruct, | ||
| normalizeGPUExtent3DStrict, | ||
| WGPUStringView, | ||
| } from "./structs_def"; | ||
| import { WGPUComputePipelineDescriptorStruct } from "./structs_def"; | ||
| import { allocStruct } from "./structs_ffi"; | ||
| import { type FFISymbols } from "./ffi"; | ||
| import { GPUQueueImpl } from "./GPUQueue"; | ||
| import { GPUCommandEncoderImpl } from "./GPUCommandEncoder"; | ||
| import { GPUTextureImpl } from "./GPUTexture"; | ||
| import { GPUBufferImpl } from "./GPUBuffer"; | ||
| import { GPUSamplerImpl } from "./GPUSampler"; | ||
| import { GPUBindGroupImpl } from "./GPUBindGroup"; | ||
| import { GPUBindGroupLayoutImpl } from "./GPUBindGroupLayout"; | ||
| import { GPUQuerySetImpl } from "./GPUQuerySet"; | ||
| import { GPUShaderModuleImpl } from "./GPUShaderModule"; | ||
| import { GPUPipelineLayoutImpl } from "./GPUPipelineLayout"; | ||
| import { GPUComputePipelineImpl } from "./GPUComputePipeline"; | ||
| import { GPURenderPipelineImpl } from "./GPURenderPipeline"; | ||
| import { createWGPUError, fatalError, GPUPipelineErrorImpl, OperationError } from "./utils/error"; | ||
| import { WGPULimitsStruct } from "./structs_def"; | ||
| import { WGPUBufferDescriptorStruct, WGPUTextureDescriptorStruct, WGPUSamplerDescriptorStruct, WGPURenderBundleEncoderDescriptorStruct } from "./structs_def"; | ||
| import type { InstanceTicker } from "./GPU"; | ||
| import { normalizeIdentifier, DEFAULT_SUPPORTED_LIMITS, GPUSupportedLimitsImpl, decodeCallbackMessage, AsyncStatus, unpackUserDataId, packUserDataId } from "./shared"; | ||
| import { GPUAdapterInfoImpl, WGPUErrorType } from "./shared"; | ||
| import { EventEmitter } from "events"; | ||
| import { GPUTextureViewImpl } from "./GPUTextureView"; | ||
| import { GPURenderBundleEncoderImpl } from "./GPURenderBundleEncoder"; | ||
| type EventListenerOptions = any; | ||
| export type DeviceErrorCallback = (this: GPUDevice, ev: GPUUncapturedErrorEvent) => any; | ||
| const PopErrorScopeStatus = { | ||
| Success: 1, | ||
| CallbackCancelled: 2, | ||
| Error: 3, | ||
| } as const; | ||
| function isDepthTextureFormat(format: string): boolean { | ||
| return format === 'depth16unorm' || | ||
| format === 'depth24plus' || | ||
| format === 'depth24plus-stencil8' || | ||
| format === 'depth32float' || | ||
| format === 'depth32float-stencil8'; | ||
| } | ||
| export class DeviceTicker { | ||
| private _waiting: number = 0; | ||
| private _ticking = false; | ||
| constructor(public readonly devicePtr: Pointer, private lib: FFISymbols) {} | ||
| register() { | ||
| this._waiting++; | ||
| this.scheduleTick(); | ||
| } | ||
| unregister() { | ||
| this._waiting--; | ||
| } | ||
| hasWaiting() { | ||
| return this._waiting > 0; | ||
| } | ||
| private scheduleTick() { | ||
| if (this._ticking) return; | ||
| this._ticking = true; | ||
| queueMicrotask(() => { | ||
| this.lib.wgpuDeviceTick(this.devicePtr); | ||
| this._ticking = false; | ||
| if (this.hasWaiting()) { | ||
| this.scheduleTick(); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| const EMPTY_ADAPTER_INFO: Readonly<GPUAdapterInfo> = Object.create(GPUAdapterInfoImpl.prototype); | ||
| const DEFAULT_LIMITS = Object.assign(Object.create(GPUSupportedLimitsImpl.prototype), DEFAULT_SUPPORTED_LIMITS); | ||
| let createComputePipelineAsyncId = 0; | ||
| let createRenderPipelineAsyncId = 0; | ||
| export class GPUDeviceImpl extends EventEmitter implements GPUDevice { | ||
| readonly ptr: Pointer; | ||
| readonly queuePtr: Pointer; | ||
| private _queue: GPUQueue | null = null; | ||
| private _userUncapturedErrorCallback: DeviceErrorCallback | null = null; | ||
| private _ticker: DeviceTicker; | ||
| private _lost: Promise<GPUDeviceLostInfo>; | ||
| private _lostPromiseResolve: ((value: GPUDeviceLostInfo) => void) | null = null; | ||
| private _features: GPUSupportedFeatures | null = null; | ||
| private _limits: GPUSupportedLimits = DEFAULT_LIMITS; | ||
| private _info: GPUAdapterInfo = EMPTY_ADAPTER_INFO; | ||
| private _destroyed = false; | ||
| private _errorScopePopId = 0; | ||
| private _popErrorScopeCallback: JSCallback; | ||
| private _popErrorScopePromises: Map<number, { | ||
| resolve: (value: GPUError | null) => void, | ||
| reject: (reason?: any) => void, | ||
| }> = new Map(); | ||
| private _createComputePipelineAsyncCallback: JSCallback; | ||
| private _createComputePipelineAsyncPromises: Map<number, { | ||
| resolve: (value: GPUComputePipeline) => void, | ||
| reject: (reason?: any) => void, | ||
| }> = new Map(); | ||
| private _createRenderPipelineAsyncCallback: JSCallback; | ||
| private _createRenderPipelineAsyncPromises: Map<number, { | ||
| resolve: (value: GPURenderPipeline) => void, | ||
| reject: (reason?: any) => void, | ||
| }> = new Map(); | ||
| private _buffers: Set<GPUBufferImpl> = new Set(); | ||
| __brand: "GPUDevice" = "GPUDevice"; | ||
| label: string = ''; | ||
| constructor(public readonly devicePtr: Pointer, private lib: FFISymbols, private instanceTicker: InstanceTicker) { | ||
| super(); | ||
| this.ptr = devicePtr; | ||
| const queuePtr = this.lib.wgpuDeviceGetQueue(this.devicePtr); | ||
| if (!queuePtr) { | ||
| fatalError("Failed to get device queue"); | ||
| } | ||
| this.queuePtr = queuePtr; | ||
| // TODO: When are device ticks still needed? | ||
| this._ticker = new DeviceTicker(this.devicePtr, this.lib); | ||
| this._queue = new GPUQueueImpl(this.queuePtr, this.lib, this.instanceTicker); | ||
| this._lost = new Promise((resolve) => { | ||
| this._lostPromiseResolve = resolve; | ||
| }); | ||
| this._popErrorScopeCallback = new JSCallback( | ||
| (status: number, errorType: number, messagePtr: Pointer | null, messageSize: bigint, userdata1: Pointer, userdata2: Pointer | null) => { | ||
| this.instanceTicker.unregister(); | ||
| const popId = unpackUserDataId(userdata1); | ||
| const promiseData = this._popErrorScopePromises.get(popId); | ||
| this._popErrorScopePromises.delete(popId); | ||
| if (promiseData) { | ||
| if (messageSize === 0n) { | ||
| promiseData.resolve(null); | ||
| } else if (status === PopErrorScopeStatus.Error) { | ||
| const message = decodeCallbackMessage(messagePtr, messageSize); | ||
| promiseData.reject(new OperationError(message)); | ||
| } else { | ||
| const message = decodeCallbackMessage(messagePtr, messageSize); | ||
| const error = createWGPUError(errorType, message); | ||
| promiseData.resolve(error); | ||
| } | ||
| } else { | ||
| console.error('[POP ERROR SCOPE CALLBACK] promise not found for ID:', popId, 'Map size:', this._popErrorScopePromises.size); | ||
| } | ||
| }, | ||
| { | ||
| args: [FFIType.u32, FFIType.u32, FFIType.pointer, FFIType.u64, FFIType.pointer, FFIType.pointer], | ||
| } | ||
| ); | ||
| this._createComputePipelineAsyncCallback = new JSCallback( | ||
| (status: number, pipeline: Pointer | null, messagePtr: Pointer | null, messageSize: bigint, userdata1: Pointer, userdata2: Pointer | null) => { | ||
| this.instanceTicker.unregister(); | ||
| const asyncId = unpackUserDataId(userdata1); | ||
| const promiseData = this._createComputePipelineAsyncPromises.get(asyncId); | ||
| this._createComputePipelineAsyncPromises.delete(asyncId); | ||
| if (promiseData) { | ||
| if (status === AsyncStatus.Success) { | ||
| if (pipeline) { | ||
| const computePipeline = new GPUComputePipelineImpl(pipeline, this.lib, "async-compute-pipeline"); | ||
| promiseData.resolve(computePipeline); | ||
| } else { | ||
| promiseData.reject(new Error("Pipeline creation succeeded but pipeline is null")); | ||
| } | ||
| } else { | ||
| const message = messagePtr ? decodeCallbackMessage(messagePtr, messageSize) : "Unknown error"; | ||
| promiseData.reject(new GPUPipelineErrorImpl(message, { reason: 'validation' })); | ||
| } | ||
| } else { | ||
| console.error('[CREATE COMPUTE PIPELINE ASYNC CALLBACK] promise not found'); | ||
| } | ||
| }, | ||
| { | ||
| args: [FFIType.u32, FFIType.pointer, FFIType.pointer, FFIType.u64, FFIType.pointer, FFIType.pointer], | ||
| } | ||
| ); | ||
| this._createRenderPipelineAsyncCallback = new JSCallback( | ||
| (status: number, pipeline: Pointer | null, messagePtr: Pointer | null, messageSize: bigint, userdata1: Pointer, userdata2: Pointer | null) => { | ||
| this.instanceTicker.unregister(); | ||
| const asyncId = unpackUserDataId(userdata1); | ||
| const promiseData = this._createRenderPipelineAsyncPromises.get(asyncId); | ||
| this._createRenderPipelineAsyncPromises.delete(asyncId); | ||
| if (promiseData) { | ||
| if (status === AsyncStatus.Success) { | ||
| if (pipeline) { | ||
| const renderPipeline = new GPURenderPipelineImpl(pipeline, this.lib, "async-render-pipeline"); | ||
| promiseData.resolve(renderPipeline); | ||
| } else { | ||
| promiseData.reject(new Error("Pipeline creation succeeded but pipeline is null")); | ||
| } | ||
| } else { | ||
| const message = messagePtr ? decodeCallbackMessage(messagePtr, messageSize) : "Unknown error"; | ||
| promiseData.reject(new GPUPipelineErrorImpl(message, { reason: 'validation' })); | ||
| } | ||
| } else { | ||
| console.error('[CREATE RENDER PIPELINE ASYNC CALLBACK] promise not found'); | ||
| } | ||
| }, | ||
| { | ||
| args: [FFIType.u32, FFIType.pointer, FFIType.pointer, FFIType.u64, FFIType.pointer, FFIType.pointer], | ||
| } | ||
| ); | ||
| } | ||
| tick(): undefined { | ||
| this.lib.wgpuDeviceTick(this.devicePtr); | ||
| return undefined; | ||
| } | ||
| addEventListener<K extends keyof __GPUDeviceEventMap>(type: K, listener: (this: GPUDevice, ev: __GPUDeviceEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void { | ||
| if (typeof options === 'object' && options !== null && options.once) { | ||
| this.once(type, listener); | ||
| } else { | ||
| this.on(type, listener); | ||
| } | ||
| } | ||
| removeEventListener<K extends keyof __GPUDeviceEventMap>(type: K, listener: (this: GPUDevice, ev: __GPUDeviceEventMap[K]) => any, options?: boolean | EventListenerOptions): void { | ||
| this.off(type, listener); | ||
| } | ||
| handleUncapturedError(event: GPUUncapturedErrorEvent) { | ||
| this.emit('uncapturederror', event); | ||
| if (this._userUncapturedErrorCallback) { | ||
| this._userUncapturedErrorCallback.call(this, event); | ||
| } else { | ||
| console.error(`>>> JS Device Error Callback <<< Type: ${event.error.message}`); | ||
| } | ||
| } | ||
| handleDeviceLost(reason: GPUDeviceLostReason, message: string, override: boolean = false) { | ||
| if (override) { | ||
| this._lost = Promise.resolve({ | ||
| reason, | ||
| message, | ||
| __brand: "GPUDeviceLostInfo" as const, | ||
| }); | ||
| return; | ||
| } | ||
| if (this._lostPromiseResolve) { | ||
| this._lostPromiseResolve({ | ||
| reason, | ||
| message, | ||
| __brand: "GPUDeviceLostInfo" as const, | ||
| }); | ||
| } | ||
| } | ||
| dispatchEvent(event: Event): boolean { | ||
| this.emit(event.type, event); | ||
| return true; | ||
| } | ||
| pushErrorScope(filter: GPUErrorFilter): undefined { | ||
| if (this._destroyed) { | ||
| fatalError('pushErrorScope on destroyed GPUDevice'); | ||
| } | ||
| this.lib.wgpuDevicePushErrorScope(this.devicePtr, WGPUErrorFilter.to(filter)); | ||
| return undefined; | ||
| } | ||
| popErrorScope(): Promise<GPUError | null> { | ||
| if (this._destroyed) { | ||
| fatalError('popErrorScope on destroyed GPUDevice'); | ||
| } | ||
| return new Promise((resolve, reject) => { | ||
| const id = this._errorScopePopId++; | ||
| const userDataBuffer = packUserDataId(id); | ||
| const userDataPtr = ptr(userDataBuffer); | ||
| this._popErrorScopePromises.set(id, { resolve, reject }); | ||
| const callbackInfo = WGPUCallbackInfoStruct.pack({ | ||
| mode: 'AllowProcessEvents', | ||
| callback: this._popErrorScopeCallback.ptr!, | ||
| userdata1: userDataPtr, | ||
| }); | ||
| this.lib.wgpuDevicePopErrorScope(this.devicePtr, ptr(callbackInfo)); | ||
| this.instanceTicker.register(); | ||
| }); | ||
| } | ||
| injectError(type: 'validation' | 'out-of-memory' | 'internal', message: string): undefined { | ||
| if (this._destroyed) { | ||
| fatalError('injectError on destroyed GPUDevice'); | ||
| } | ||
| const errorType = WGPUErrorType[type]; | ||
| if (errorType === undefined) { | ||
| fatalError(`Invalid error type for injectError: ${type}`); | ||
| } | ||
| const messageView = WGPUStringView.pack(message); | ||
| this.lib.wgpuDeviceInjectError(this.devicePtr, errorType, ptr(messageView)); | ||
| return undefined; | ||
| } | ||
| set onuncapturederror(listener: DeviceErrorCallback | null) { | ||
| this._userUncapturedErrorCallback = listener; | ||
| } | ||
| get lost(): Promise<GPUDeviceLostInfo> { | ||
| return this._lost; | ||
| } | ||
| get features(): GPUSupportedFeatures { | ||
| if (this._destroyed) { | ||
| console.warn("Accessing features on destroyed GPUDevice"); | ||
| return Object.freeze(new Set<string>()); | ||
| } | ||
| if (this._features === null) { | ||
| let supportedFeaturesStructPtr: Pointer | null = null; | ||
| try { | ||
| const { buffer: featuresStructBuffer } = allocStruct(WGPUSupportedFeaturesStruct, { | ||
| lengths: { | ||
| features: 128, // 77 known features + some room for unknown features or future features | ||
| } | ||
| }); | ||
| this.lib.wgpuDeviceGetFeatures(this.devicePtr, ptr(featuresStructBuffer)); | ||
| const supportedFeaturesData = WGPUSupportedFeaturesStruct.unpack(featuresStructBuffer); | ||
| const features = supportedFeaturesData.features; | ||
| const supportedFeatures = new Set<string>(features); | ||
| this._features = Object.freeze(supportedFeatures); | ||
| } catch (e) { | ||
| console.error("Error getting device features via wgpuDeviceGetFeatures:", e); | ||
| this._features = Object.freeze(new Set<string>()); // Set empty on error | ||
| } finally { | ||
| if (supportedFeaturesStructPtr) { | ||
| try { | ||
| this.lib.wgpuSupportedFeaturesFreeMembers(supportedFeaturesStructPtr); | ||
| } catch(freeError) { | ||
| console.error("Error calling wgpuSupportedFeaturesFreeMembers:", freeError); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return this._features; | ||
| } | ||
| get limits(): GPUSupportedLimits { | ||
| if (this._destroyed) { | ||
| console.warn("Accessing limits on destroyed GPUDevice"); | ||
| return this._limits; | ||
| } | ||
| if (this._limits === null) { | ||
| let limitsStructPtr: Pointer | null = null; | ||
| try { | ||
| const { buffer: structBuffer } = allocStruct(WGPULimitsStruct); | ||
| limitsStructPtr = ptr(structBuffer); | ||
| const status = this.lib.wgpuDeviceGetLimits(this.devicePtr, limitsStructPtr); | ||
| if (status !== 1) { // WGPUStatus_Success = 1 (assuming, needs verification or enum import) | ||
| console.error(`wgpuDeviceGetLimits failed with status: ${status}`); | ||
| return this._limits; | ||
| } | ||
| const jsLimits = WGPULimitsStruct.unpack(structBuffer); | ||
| this._limits = Object.freeze(Object.assign(Object.create(GPUSupportedLimitsImpl.prototype), { | ||
| __brand: "GPUSupportedLimits" as const, | ||
| ...jsLimits, | ||
| maxUniformBufferBindingSize: Number(jsLimits.maxUniformBufferBindingSize), | ||
| maxStorageBufferBindingSize: Number(jsLimits.maxStorageBufferBindingSize), | ||
| maxBufferSize: Number(jsLimits.maxBufferSize), | ||
| })); | ||
| } catch (e) { | ||
| console.error("Error calling wgpuDeviceGetLimits or unpacking struct:", e); | ||
| limitsStructPtr = null; | ||
| return this._limits; | ||
| } | ||
| } | ||
| return this._limits; | ||
| } | ||
| get adapterInfo(): GPUAdapterInfo { | ||
| if (this._destroyed) { | ||
| return EMPTY_ADAPTER_INFO; | ||
| } | ||
| if (this._info === EMPTY_ADAPTER_INFO) { | ||
| let infoStructPtr: Pointer | null = null; | ||
| try { | ||
| const { buffer: structBuffer } = allocStruct(WGPUAdapterInfoStruct); | ||
| infoStructPtr = ptr(structBuffer); | ||
| const status = this.lib.wgpuDeviceGetAdapterInfo(this.devicePtr, infoStructPtr); | ||
| if (status !== 1) { // WGPUStatus_Success = 1 | ||
| console.error(`wgpuAdapterGetInfo failed with status: ${status}`); | ||
| this._info = EMPTY_ADAPTER_INFO; | ||
| return this._info; | ||
| } | ||
| const rawInfo = WGPUAdapterInfoStruct.unpack(structBuffer); | ||
| this._info = Object.assign(Object.create(GPUAdapterInfoImpl.prototype), rawInfo, { | ||
| vendor: rawInfo.vendor, | ||
| architecture: rawInfo.architecture, | ||
| description: rawInfo.description, | ||
| device: normalizeIdentifier(rawInfo.device), | ||
| subgroupMinSize: rawInfo.subgroupMinSize, | ||
| subgroupMaxSize: rawInfo.subgroupMaxSize, | ||
| isFallbackAdapter: false, | ||
| }); | ||
| } catch (e) { | ||
| console.error("Error calling wgpuAdapterGetInfo or unpacking struct:", e); | ||
| this._info = EMPTY_ADAPTER_INFO; | ||
| } finally { | ||
| if (infoStructPtr) { | ||
| this.lib.wgpuAdapterInfoFreeMembers(infoStructPtr); | ||
| } | ||
| } | ||
| } | ||
| return this._info; | ||
| } | ||
| get queue(): GPUQueue { | ||
| if (!this._queue) { | ||
| throw new Error("Queue not initialized"); | ||
| } | ||
| return this._queue; | ||
| } | ||
| destroy() { | ||
| if (this._destroyed) return; | ||
| this._destroyed = true; | ||
| // Invalidate caches | ||
| this._features = null; | ||
| this._queue?.destroy(); | ||
| this._queue = null; | ||
| for (const buffer of this._buffers) { | ||
| buffer.destroy(); | ||
| } | ||
| this._buffers.clear(); | ||
| this.lib.wgpuDeviceDestroy(this.devicePtr); | ||
| this.instanceTicker.processEvents(); | ||
| return undefined; | ||
| } | ||
| createBuffer(descriptor: GPUBufferDescriptor): GPUBuffer { | ||
| if (descriptor.size < 0) { | ||
| fatalError("Buffer size must be greater than or equal to 0"); | ||
| } | ||
| if (descriptor.mappedAtCreation && descriptor.size % 4 !== 0) { | ||
| throw new RangeError("Buffer size must be a multiple of 4"); | ||
| } | ||
| if (descriptor.size > this.limits.maxBufferSize) { | ||
| throw new RangeError(`Buffer size must be less than or equal to ${this.limits.maxBufferSize}`); | ||
| } | ||
| const packedDescriptor = WGPUBufferDescriptorStruct.pack(descriptor); | ||
| const bufferPtr = this.lib.wgpuDeviceCreateBuffer( | ||
| this.devicePtr, | ||
| ptr(packedDescriptor) | ||
| ); | ||
| if (!bufferPtr) { | ||
| fatalError("Failed to create buffer"); | ||
| } | ||
| const buffer = new GPUBufferImpl(bufferPtr, this, this.lib, descriptor, this.instanceTicker); | ||
| this._buffers.add(buffer); | ||
| buffer.on('destroyed', () => this._buffers.delete(buffer)); | ||
| return buffer; | ||
| } | ||
| createTexture(descriptor: GPUTextureDescriptor): GPUTexture { | ||
| const packedDescriptor = WGPUTextureDescriptorStruct.pack(descriptor); | ||
| try { | ||
| const texturePtr = this.lib.wgpuDeviceCreateTexture( | ||
| this.devicePtr, | ||
| ptr(packedDescriptor) | ||
| ); | ||
| if (!texturePtr) { | ||
| fatalError("Failed to create texture"); | ||
| } | ||
| const { width, height = 1, depthOrArrayLayers = 1 } = normalizeGPUExtent3DStrict(descriptor.size); | ||
| const dimension = descriptor.dimension || '2d'; | ||
| const mipLevelCount = descriptor.mipLevelCount || 1; | ||
| const sampleCount = descriptor.sampleCount || 1; | ||
| return new GPUTextureImpl( | ||
| texturePtr, | ||
| this.lib, | ||
| width, | ||
| height, | ||
| depthOrArrayLayers, | ||
| descriptor.format, | ||
| dimension, | ||
| mipLevelCount, | ||
| sampleCount, | ||
| descriptor.usage | ||
| ); | ||
| } catch (e) { | ||
| fatalError("Error creating texture:", e); | ||
| } | ||
| } | ||
| createSampler(descriptor?: GPUSamplerDescriptor): GPUSampler { | ||
| const samplerDescriptor = descriptor || {}; | ||
| const packedDescriptor = WGPUSamplerDescriptorStruct.pack(samplerDescriptor); | ||
| try { | ||
| const samplerPtr = this.lib.wgpuDeviceCreateSampler( | ||
| this.devicePtr, | ||
| ptr(packedDescriptor) | ||
| ); | ||
| if (!samplerPtr) { | ||
| fatalError("Failed to create sampler"); | ||
| } | ||
| return new GPUSamplerImpl( | ||
| samplerPtr, | ||
| this.lib, | ||
| descriptor?.label | ||
| ); | ||
| } catch (e) { | ||
| fatalError("Error creating sampler:", e); | ||
| } | ||
| } | ||
| importExternalTexture(descriptor: GPUExternalTextureDescriptor): GPUExternalTexture { | ||
| fatalError('importExternalTexture not implemented', descriptor); | ||
| } | ||
| createBindGroupLayout(descriptor: GPUBindGroupLayoutDescriptor): GPUBindGroupLayout { | ||
| if (!descriptor.entries) { // || Array.from(descriptor.entries).length === 0) { | ||
| fatalError('createBindGroupLayout: descriptor.entries is missing'); | ||
| } | ||
| const entries = Array.from(descriptor.entries).map((e) => { | ||
| if (e.externalTexture) { | ||
| const chainedStruct = WGPUExternalTextureBindingLayoutStruct.pack({ | ||
| chain: { | ||
| next: null, | ||
| sType: WGPUSType.ExternalTextureBindingLayout | ||
| } | ||
| }) | ||
| return { | ||
| ...e, | ||
| nextInChain: ptr(chainedStruct) | ||
| } | ||
| } | ||
| return e; | ||
| }); | ||
| const packedDescriptorBuffer = WGPUBindGroupLayoutDescriptorStruct.pack({ | ||
| ...descriptor, | ||
| entries | ||
| }); | ||
| const packedDescriptorPtr = ptr(packedDescriptorBuffer); | ||
| const layoutPtr = this.lib.wgpuDeviceCreateBindGroupLayout( | ||
| this.devicePtr, | ||
| packedDescriptorPtr | ||
| ); | ||
| if (!layoutPtr) { | ||
| fatalError("Failed to create bind group layout (FFI returned null)"); | ||
| } | ||
| return new GPUBindGroupLayoutImpl(layoutPtr, this.lib, descriptor.label); | ||
| } | ||
| createPipelineLayout(descriptor: GPUPipelineLayoutDescriptor): GPUPipelineLayout { | ||
| const bgls = Array.from(descriptor.bindGroupLayouts).map((bgl) => bgl?.ptr).filter((bgl) => bgl !== undefined); | ||
| const descriptorBuffer = WGPUPipelineLayoutDescriptorStruct.pack({ | ||
| label: descriptor.label, | ||
| bindGroupLayouts: bgls | ||
| }); | ||
| const layoutPtr = this.lib.wgpuDeviceCreatePipelineLayout( | ||
| this.devicePtr, | ||
| ptr(descriptorBuffer) | ||
| ); | ||
| if (!layoutPtr) { | ||
| fatalError("Failed to create pipeline layout (FFI returned null)"); | ||
| } | ||
| return new GPUPipelineLayoutImpl(layoutPtr, this.lib, descriptor.label); | ||
| } | ||
| createShaderModule(descriptor: GPUShaderModuleDescriptor): GPUShaderModule { | ||
| if (!descriptor.code) { | ||
| fatalError("descriptor.code is missing"); | ||
| } | ||
| const codeStruct = WGPUShaderSourceWGSLStruct.pack({ | ||
| chain: { | ||
| next: null, | ||
| sType: WGPUSType.ShaderSourceWGSL | ||
| }, | ||
| code: descriptor.code | ||
| }); | ||
| const packedDescriptor = WGPUShaderModuleDescriptorStruct.pack({ | ||
| nextInChain: ptr(codeStruct), | ||
| label: descriptor.label | ||
| }); | ||
| const modulePtr = this.lib.wgpuDeviceCreateShaderModule( | ||
| this.devicePtr, | ||
| ptr(packedDescriptor) | ||
| ); | ||
| if (!modulePtr) { | ||
| fatalError("Failed to create shader module (FFI returned null)"); | ||
| } | ||
| return new GPUShaderModuleImpl(modulePtr, this.lib, descriptor.label || 'no-label'); | ||
| } | ||
| createBindGroup(descriptor: GPUBindGroupDescriptor): GPUBindGroup { | ||
| const entries = Array.from(descriptor.entries) | ||
| for (let i = 0; i < entries.length; i++) { | ||
| const e = entries[i]!; | ||
| if (e.resource instanceof GPUBufferImpl) { | ||
| entries[i] = { | ||
| ...e, | ||
| buffer: e.resource, | ||
| offset: e.resource.offset ?? 0n, | ||
| size: e.resource.size ?? UINT64_MAX | ||
| } as GPUBindGroupEntry; | ||
| } else if (e.resource instanceof GPUTextureViewImpl) { | ||
| entries[i] = { | ||
| ...e, | ||
| textureView: e.resource | ||
| } as GPUBindGroupEntry; | ||
| } else if (isBufferBinding(e.resource)) { | ||
| entries[i] = { | ||
| ...e, | ||
| buffer: e.resource.buffer, | ||
| offset: e.resource.offset ?? 0n, | ||
| size: e.resource.size ?? UINT64_MAX | ||
| } as GPUBindGroupEntry; | ||
| } else if (isSampler(e.resource)) { | ||
| entries[i] = { | ||
| ...e, | ||
| sampler: e.resource | ||
| } as GPUBindGroupEntry; | ||
| } else if (isTextureView(e.resource)) { | ||
| entries[i] = { | ||
| ...e, | ||
| textureView: e.resource | ||
| } as GPUBindGroupEntry; | ||
| } | ||
| } | ||
| const descriptorBuffer = WGPUBindGroupDescriptorStruct.pack({ ...descriptor, entries }); | ||
| try { | ||
| const groupPtr = this.lib.wgpuDeviceCreateBindGroup( | ||
| this.devicePtr, | ||
| ptr(descriptorBuffer) | ||
| ); | ||
| if (!groupPtr) { | ||
| fatalError("Failed to create bind group (FFI returned null)"); | ||
| } | ||
| return new GPUBindGroupImpl(groupPtr, this.lib, descriptor.label); | ||
| } catch (e) { | ||
| fatalError("Error calling deviceCreateBindGroupFromBuffer FFI function:", e); | ||
| } | ||
| } | ||
| _prepareComputePipelineDescriptor(descriptor: GPUComputePipelineDescriptor): ArrayBuffer { | ||
| let compute: ReturnType<typeof WGPUComputeStateStruct.unpack> | undefined = undefined; | ||
| if (descriptor.compute) { | ||
| const constants = descriptor.compute.constants ? Object.entries(descriptor.compute.constants).map(([key, value]) => ({ key, value })) : []; | ||
| compute = { | ||
| ...descriptor.compute, | ||
| constants | ||
| } | ||
| } else { | ||
| fatalError("GPUComputePipelineDescriptor.compute is required."); | ||
| } | ||
| const layoutForPacking = (descriptor.layout && descriptor.layout !== "auto") | ||
| ? descriptor.layout | ||
| : null; | ||
| const packedPipelineDescriptor = WGPUComputePipelineDescriptorStruct.pack({ | ||
| ...descriptor, | ||
| compute, | ||
| layout: layoutForPacking | ||
| }); | ||
| return packedPipelineDescriptor; | ||
| } | ||
| createComputePipeline(descriptor: GPUComputePipelineDescriptor): GPUComputePipeline { | ||
| const packedPipelineDescriptor = this._prepareComputePipelineDescriptor(descriptor); | ||
| let pipelinePtr: Pointer | null = null; | ||
| pipelinePtr = this.lib.wgpuDeviceCreateComputePipeline( | ||
| this.devicePtr, | ||
| ptr(packedPipelineDescriptor) | ||
| ); | ||
| if (!pipelinePtr) { | ||
| fatalError("Failed to create compute pipeline (FFI returned null)"); | ||
| } | ||
| return new GPUComputePipelineImpl(pipelinePtr, this.lib, descriptor.label); | ||
| } | ||
| _prepareRenderPipelineDescriptor(descriptor: GPURenderPipelineDescriptor): ArrayBuffer { | ||
| let fragment: ReturnType<typeof WGPUFragmentStateStruct.unpack> | undefined = undefined; | ||
| if (descriptor.fragment) { | ||
| const constants = descriptor.fragment.constants ? Object.entries(descriptor.fragment.constants).map(([key, value]) => ({ key, value })) : []; | ||
| fragment = { | ||
| ...descriptor.fragment, | ||
| constants, | ||
| targets: Array.from(descriptor.fragment.targets ?? []).filter((t) => t !== null && t !== undefined) | ||
| } | ||
| } | ||
| let vertex: ReturnType<typeof WGPUVertexStateStruct.unpack> | undefined = undefined; | ||
| if (descriptor.vertex) { | ||
| const constants = descriptor.vertex.constants ? Object.entries(descriptor.vertex.constants).map(([key, value]) => ({ key, value })) : []; | ||
| vertex = { | ||
| ...descriptor.vertex, | ||
| constants, | ||
| buffers: Array.from(descriptor.vertex.buffers ?? []).filter((t) => t !== null && t !== undefined) | ||
| } | ||
| } else { | ||
| fatalError("GPURenderPipelineDescriptor.vertex is required."); | ||
| } | ||
| const layoutForPacking = (descriptor.layout && descriptor.layout !== "auto") | ||
| ? descriptor.layout | ||
| : null; | ||
| const packedPipelineDescriptor = WGPURenderPipelineDescriptorStruct.pack({ | ||
| ...descriptor, | ||
| fragment, | ||
| vertex, | ||
| layout: layoutForPacking | ||
| }); | ||
| return packedPipelineDescriptor; | ||
| } | ||
| createRenderPipeline(descriptor: GPURenderPipelineDescriptor): GPURenderPipeline { | ||
| // Validate depthStencil state | ||
| if (descriptor.depthStencil) { | ||
| const format = descriptor.depthStencil.format; | ||
| const isDepthFormat = isDepthTextureFormat(format); | ||
| if (isDepthFormat && descriptor.depthStencil.depthWriteEnabled === undefined) { | ||
| this.injectError('validation', 'depthWriteEnabled is required for depth formats'); | ||
| } | ||
| // Check if depthCompare is required | ||
| if (isDepthFormat) { | ||
| const depthWriteEnabled = descriptor.depthStencil.depthWriteEnabled; | ||
| const stencilFront = descriptor.depthStencil.stencilFront; | ||
| const stencilBack = descriptor.depthStencil.stencilBack; | ||
| const frontDepthFailOp = stencilFront?.depthFailOp || 'keep'; | ||
| const backDepthFailOp = stencilBack?.depthFailOp || 'keep'; | ||
| const depthFailOpsAreKeep = frontDepthFailOp === 'keep' && backDepthFailOp === 'keep'; | ||
| if ((depthWriteEnabled || !depthFailOpsAreKeep) && descriptor.depthStencil.depthCompare === undefined) { | ||
| this.injectError('validation', 'depthCompare is required when depthWriteEnabled is true or stencil depth fail operations are not keep'); | ||
| } | ||
| } | ||
| } | ||
| const packedPipelineDescriptor = this._prepareRenderPipelineDescriptor(descriptor); | ||
| let pipelinePtr: Pointer | null = null; | ||
| pipelinePtr = this.lib.wgpuDeviceCreateRenderPipeline( | ||
| this.devicePtr, | ||
| ptr(packedPipelineDescriptor) | ||
| ); | ||
| if (!pipelinePtr) { | ||
| fatalError("Failed to create render pipeline (FFI returned null)"); | ||
| } | ||
| return new GPURenderPipelineImpl(pipelinePtr, this.lib, descriptor.label); | ||
| } | ||
| createCommandEncoder(descriptor?: GPUCommandEncoderDescriptor): GPUCommandEncoder { | ||
| const packedDescriptor = WGPUCommandEncoderDescriptorStruct.pack(descriptor ?? {}); | ||
| const encoderPtr = this.lib.wgpuDeviceCreateCommandEncoder(this.devicePtr, ptr(packedDescriptor)); | ||
| if (!encoderPtr) { | ||
| fatalError("Failed to create command encoder"); | ||
| } | ||
| return new GPUCommandEncoderImpl(encoderPtr, this.lib); | ||
| } | ||
| createComputePipelineAsync(descriptor: GPUComputePipelineDescriptor): Promise<GPUComputePipeline> { | ||
| if (this._destroyed) { | ||
| return Promise.reject(new Error('createComputePipelineAsync on destroyed GPUDevice')); | ||
| } | ||
| return new Promise((resolve, reject) => { | ||
| const id = createComputePipelineAsyncId++; | ||
| const userDataBuffer = packUserDataId(id); | ||
| this._createComputePipelineAsyncPromises.set(id, { resolve, reject }); | ||
| const userDataPtr = ptr(userDataBuffer); | ||
| const packedPipelineDescriptor = this._prepareComputePipelineDescriptor(descriptor); | ||
| const callbackInfo = WGPUCallbackInfoStruct.pack({ | ||
| mode: 'AllowProcessEvents', | ||
| callback: this._createComputePipelineAsyncCallback.ptr!, | ||
| userdata1: userDataPtr, | ||
| }); | ||
| this.lib.wgpuDeviceCreateComputePipelineAsync( | ||
| this.devicePtr, | ||
| ptr(packedPipelineDescriptor), | ||
| ptr(callbackInfo) | ||
| ); | ||
| this.instanceTicker.register(); | ||
| }); | ||
| } | ||
| createRenderPipelineAsync(descriptor: GPURenderPipelineDescriptor): Promise<GPURenderPipeline> { | ||
| if (this._destroyed) { | ||
| return Promise.reject(new Error('createRenderPipelineAsync on destroyed GPUDevice')); | ||
| } | ||
| // Validate depthStencil state | ||
| if (descriptor.depthStencil) { | ||
| const format = descriptor.depthStencil.format; | ||
| const isDepthFormat = isDepthTextureFormat(format); | ||
| if (isDepthFormat && descriptor.depthStencil.depthWriteEnabled === undefined) { | ||
| return Promise.reject(new GPUPipelineErrorImpl('depthWriteEnabled is required for depth formats', { reason: 'validation' })); | ||
| } | ||
| // Check if depthCompare is required | ||
| if (isDepthFormat) { | ||
| const depthWriteEnabled = descriptor.depthStencil.depthWriteEnabled; | ||
| const stencilFront = descriptor.depthStencil.stencilFront; | ||
| const stencilBack = descriptor.depthStencil.stencilBack; | ||
| const frontDepthFailOp = stencilFront?.depthFailOp || 'keep'; | ||
| const backDepthFailOp = stencilBack?.depthFailOp || 'keep'; | ||
| const depthFailOpsAreKeep = frontDepthFailOp === 'keep' && backDepthFailOp === 'keep'; | ||
| if ((depthWriteEnabled || !depthFailOpsAreKeep) && descriptor.depthStencil.depthCompare === undefined) { | ||
| return Promise.reject(new GPUPipelineErrorImpl('depthCompare is required when depthWriteEnabled is true or stencil depth fail operations are not keep', { reason: 'validation' })); | ||
| } | ||
| } | ||
| } | ||
| return new Promise((resolve, reject) => { | ||
| const id = createRenderPipelineAsyncId++; | ||
| const userDataBuffer = packUserDataId(id); | ||
| this._createRenderPipelineAsyncPromises.set(id, { resolve, reject }); | ||
| const userDataPtr = ptr(userDataBuffer); | ||
| const packedPipelineDescriptor = this._prepareRenderPipelineDescriptor(descriptor); | ||
| const callbackInfo = WGPUCallbackInfoStruct.pack({ | ||
| mode: 'AllowProcessEvents', | ||
| callback: this._createRenderPipelineAsyncCallback.ptr!, | ||
| userdata1: userDataPtr, | ||
| }); | ||
| this.lib.wgpuDeviceCreateRenderPipelineAsync( | ||
| this.devicePtr, | ||
| ptr(packedPipelineDescriptor), | ||
| ptr(callbackInfo) | ||
| ); | ||
| this.instanceTicker.register(); | ||
| }); | ||
| } | ||
| createRenderBundleEncoder(descriptor: GPURenderBundleEncoderDescriptor): GPURenderBundleEncoder { | ||
| if (this._destroyed) { | ||
| fatalError("Cannot call createRenderBundleEncoder on a destroyed GPUDevice"); | ||
| } | ||
| const colorFormats = Array.from(descriptor.colorFormats).filter(f => f !== null && f !== undefined) as GPUTextureFormat[]; | ||
| const packedDescriptor = WGPURenderBundleEncoderDescriptorStruct.pack({ | ||
| ...descriptor, | ||
| colorFormats, | ||
| }); | ||
| const encoderPtr = this.lib.wgpuDeviceCreateRenderBundleEncoder(this.devicePtr, ptr(packedDescriptor)); | ||
| if (!encoderPtr) { | ||
| fatalError("Failed to create render bundle encoder"); | ||
| } | ||
| return new GPURenderBundleEncoderImpl(encoderPtr, this.lib, descriptor); | ||
| } | ||
| createQuerySet(descriptor: GPUQuerySetDescriptor): GPUQuerySet { | ||
| const packedDescriptor = WGPUQuerySetDescriptorStruct.pack(descriptor); | ||
| let querySetPtr: Pointer | null = null; | ||
| querySetPtr = this.lib.wgpuDeviceCreateQuerySet( | ||
| this.devicePtr, | ||
| ptr(packedDescriptor) | ||
| ); | ||
| if (!querySetPtr) { | ||
| fatalError("Failed to create query set (FFI returned null)"); | ||
| } | ||
| return new GPUQuerySetImpl(querySetPtr, this.lib, descriptor.type, descriptor.count, descriptor.label); | ||
| } | ||
| } | ||
| function isBufferBinding(resource: GPUBindingResource): resource is GPUBufferBinding { | ||
| return 'buffer' in resource; | ||
| } | ||
| function isSampler(resource: GPUBindingResource): resource is GPUSampler { | ||
| // @ts-ignore | ||
| return resource.__brand === 'GPUSampler'; | ||
| } | ||
| function isTextureView(resource: GPUBindingResource): resource is GPUTextureView { | ||
| // @ts-ignore | ||
| return resource.__brand === 'GPUTextureView'; | ||
| } |
| import type { Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| export class GPUPipelineLayoutImpl implements GPUPipelineLayout { | ||
| __brand: "GPUPipelineLayout" = "GPUPipelineLayout"; | ||
| label: string; | ||
| readonly ptr: Pointer; | ||
| constructor(ptr: Pointer, private lib: FFISymbols, label?: string) { | ||
| this.ptr = ptr; | ||
| this.label = label || ''; | ||
| } | ||
| destroy(): undefined { | ||
| try { | ||
| this.lib.wgpuPipelineLayoutRelease(this.ptr); | ||
| } catch(e) { | ||
| console.error("FFI Error: pipelineLayoutRelease", e); | ||
| } | ||
| return undefined; | ||
| } | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import { type FFISymbols } from "./ffi"; | ||
| export class GPUQuerySetImpl implements GPUQuerySet { | ||
| __brand: "GPUQuerySet" = "GPUQuerySet"; | ||
| label: string; | ||
| constructor(public readonly ptr: Pointer, private lib: FFISymbols, public readonly type: GPUQueryType, public readonly count: number, label?: string) { | ||
| this.label = label || ''; | ||
| } | ||
| destroy(): undefined { | ||
| this.lib.wgpuQuerySetDestroy(this.ptr); | ||
| } | ||
| } | ||
| import { expect, describe, it, beforeAll, afterAll } from "bun:test"; | ||
| import { | ||
| createGPUInstance, | ||
| } from "./index"; | ||
| import type { GPUImpl } from "./GPU"; | ||
| import { TextureUsageFlags } from "./common"; | ||
| // Global variables for the test suite | ||
| let gpu: GPUImpl | null = null; | ||
| let adapter: GPUAdapter | null = null; | ||
| let device: GPUDevice | null = null; | ||
| let queue: GPUQueue | null = null; | ||
| describe("GPUQueue", () => { | ||
| // --- Setup and Teardown --- | ||
| beforeAll(async () => { | ||
| try { | ||
| gpu = createGPUInstance(); | ||
| adapter = await gpu.requestAdapter(); | ||
| if (!adapter) throw new Error(`Test Setup Failed: Could not request adapter.`); | ||
| device = await adapter.requestDevice({ label: "GPUQueue Test Device" }); | ||
| if (!device) throw new Error(`Test Setup Failed: Could not request device.`); | ||
| queue = device.queue; | ||
| if (!queue) throw new Error(`Test Setup Failed: Could not get queue.`); | ||
| device.onuncapturederror = (ev) => { | ||
| console.error(`>>> GPUQueue Test Uncaptured Error <<< Type: ${ev.error.message}`); | ||
| // Allow tests to handle expected errors, don't throw here generally | ||
| }; | ||
| } catch (e) { | ||
| console.error("Error during GPUQueue Test Setup:", e); | ||
| device?.destroy(); | ||
| gpu?.destroy(); | ||
| throw e; | ||
| } | ||
| }); | ||
| afterAll(() => { | ||
| device?.destroy(); | ||
| device = null; | ||
| adapter = null; | ||
| queue = null; | ||
| if (gpu) { | ||
| gpu.destroy(); | ||
| gpu = null; | ||
| } | ||
| }); | ||
| // --- Tests --- | ||
| describe("writeTexture", () => { | ||
| it("should succeed writing a 2D texture even if dataLayout omits rowsPerImage", async () => { | ||
| let texture: GPUTexture | null = null; | ||
| let errorOccurred = false; | ||
| const originalOnError = device!.onuncapturederror; | ||
| const textureSize = { width: 4, height: 4 }; | ||
| const textureFormat = "rgba8unorm"; | ||
| const bytesPerPixel = 4; | ||
| const bytesPerRow = textureSize.width * bytesPerPixel; | ||
| const textureData = new Uint8Array(textureSize.width * textureSize.height * bytesPerPixel); | ||
| textureData.fill(128); // Fill with gray | ||
| device!.onuncapturederror = (ev) => { | ||
| console.error("*** Test writeTexture uncaptured error caught! ***", ev.error.message); | ||
| errorOccurred = true; | ||
| }; | ||
| try { | ||
| texture = device!.createTexture({ | ||
| label: "Test WriteTexture No RowsPerImage", | ||
| size: [textureSize.width, textureSize.height], | ||
| format: textureFormat, | ||
| usage: TextureUsageFlags.COPY_DST | TextureUsageFlags.COPY_SRC, // Need COPY_SRC for potential verification | ||
| }); | ||
| expect(texture).not.toBeNull(); | ||
| const destination: GPUTexelCopyTextureInfo = { | ||
| texture: texture!, | ||
| }; | ||
| // CRITICAL: Layout MISSING rowsPerImage | ||
| const dataLayout: GPUTexelCopyBufferLayout = { | ||
| offset: 0, | ||
| bytesPerRow: bytesPerRow, | ||
| // rowsPerImage: textureSize.height // Omitted! | ||
| }; | ||
| const writeSize: GPUExtent3DStrict = { | ||
| width: textureSize.width, | ||
| height: textureSize.height, | ||
| depthOrArrayLayers: 1, | ||
| }; | ||
| // Expect this call NOT to throw an error | ||
| expect(() => { | ||
| queue!.writeTexture(destination, textureData.buffer, dataLayout, writeSize); | ||
| }).not.toThrow(); | ||
| // Wait for queue completion to catch potential async validation errors | ||
| await queue!.onSubmittedWorkDone(); | ||
| // Assert no uncaptured error occurred | ||
| expect(errorOccurred).toBe(false); | ||
| // Optional: Add verification step (copyTextureToBuffer, map, check pixel) if needed | ||
| // For now, just ensuring no error is thrown is the main goal. | ||
| } catch (e) { | ||
| console.error("Unexpected error during writeTexture test:", e); | ||
| expect(e).toBeNull(); // Force failure if synchronous error occurs | ||
| } finally { | ||
| if (device) device!.onuncapturederror = originalOnError; | ||
| if (texture) texture.destroy(); | ||
| } | ||
| }); | ||
| }); | ||
| }); |
-252
| import { FFIType, JSCallback, type Pointer, ptr } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| import { fatalError } from "./utils/error"; | ||
| import { packObjectArray } from "./structs_ffi"; | ||
| import { | ||
| normalizeGPUExtent3DStrict, | ||
| WGPUCallbackInfoStruct, | ||
| WGPUExtent3DStruct, | ||
| WGPUTexelCopyBufferLayoutStruct, | ||
| WGPUTexelCopyTextureInfoStruct | ||
| } from "./structs_def"; | ||
| import { InstanceTicker } from "./GPU"; | ||
| // Type alias for buffer sources compatible with bun:ffi ptr() | ||
| export type PtrSource = ArrayBuffer | ArrayBufferView; | ||
| export const QueueWorkDoneStatus = { | ||
| Success: 1, | ||
| CallbackCancelled: 2, | ||
| Error: 3, | ||
| Force32: 0x7FFFFFFF, | ||
| } as const; | ||
| export class GPUQueueImpl implements GPUQueue { | ||
| __brand: "GPUQueue" = "GPUQueue"; | ||
| label: string = 'Main Device Queue'; | ||
| private _onSubmittedWorkDoneCallback: JSCallback; | ||
| private _onSubmittedWorkDoneResolves: ((value: undefined) => void)[] = []; | ||
| private _onSubmittedWorkDoneRejects: ((reason?: any) => void)[] = []; | ||
| constructor(public readonly ptr: Pointer, private lib: FFISymbols, private instanceTicker: InstanceTicker) { | ||
| this._onSubmittedWorkDoneCallback = new JSCallback( | ||
| (status: number, _userdata1: Pointer | null, _userdata2: Pointer | null) => { | ||
| this.instanceTicker.unregister(); | ||
| if (status === QueueWorkDoneStatus.Success) { | ||
| this._onSubmittedWorkDoneResolves.forEach(r => r(undefined)); | ||
| } else { | ||
| const statusName = Object.keys(QueueWorkDoneStatus).find(key => QueueWorkDoneStatus[key as keyof typeof QueueWorkDoneStatus] === status) || 'Unknown Status'; | ||
| const error = new Error(`Queue work done failed with status: ${statusName}(${status})`); | ||
| this._onSubmittedWorkDoneRejects.forEach(r => r(error)); | ||
| } | ||
| this._onSubmittedWorkDoneResolves = []; | ||
| this._onSubmittedWorkDoneRejects = []; | ||
| }, | ||
| { | ||
| args: [FFIType.u32, FFIType.pointer, FFIType.pointer], | ||
| returns: FFIType.void, | ||
| } | ||
| ) | ||
| } | ||
| submit(commandBuffers: Iterable<GPUCommandBuffer>): undefined { | ||
| const commandBuffersArray = Array.from(commandBuffers); | ||
| if (!commandBuffersArray || commandBuffersArray.length === 0) { | ||
| // console.warn("queueSubmit: no command buffers provided"); | ||
| // The CTS test suite calls .submit([]) to work around a bug in chromium to flush the queue. | ||
| return; | ||
| } | ||
| const handleView = packObjectArray(commandBuffersArray) | ||
| this.lib.wgpuQueueSubmit(this.ptr, commandBuffersArray.length, ptr(handleView.buffer)); | ||
| } | ||
| onSubmittedWorkDone(): Promise<undefined> { | ||
| return new Promise((resolve, reject) => { | ||
| if (!this._onSubmittedWorkDoneCallback.ptr) { | ||
| fatalError('Could not create queue done callback') | ||
| } | ||
| this._onSubmittedWorkDoneResolves.push(resolve); | ||
| this._onSubmittedWorkDoneRejects.push(reject); | ||
| // if (this._onSubmittedWorkDoneResolves.length > 1) { | ||
| // return; | ||
| // } | ||
| const callbackInfo = WGPUCallbackInfoStruct.pack({ | ||
| mode: 'AllowProcessEvents', | ||
| callback: this._onSubmittedWorkDoneCallback.ptr, | ||
| }); | ||
| try { | ||
| this.lib.wgpuQueueOnSubmittedWorkDone( | ||
| this.ptr, | ||
| ptr(callbackInfo), | ||
| ); | ||
| this.instanceTicker.register(); | ||
| } catch (e) { | ||
| reject(e); | ||
| } | ||
| }); | ||
| } | ||
| writeBuffer( | ||
| buffer: GPUBuffer, | ||
| bufferOffset: number, | ||
| data: PtrSource, | ||
| dataOffset?: number, | ||
| size?: number | ||
| ): undefined { | ||
| let arrayBuffer: ArrayBuffer; | ||
| let byteOffsetInData: number; | ||
| let byteLengthInData: number; | ||
| let bytesPerElement: number = 1; | ||
| if (data instanceof ArrayBuffer) { | ||
| arrayBuffer = data; | ||
| byteOffsetInData = 0; | ||
| byteLengthInData = data.byteLength; | ||
| bytesPerElement = 1; | ||
| } else if (ArrayBuffer.isView(data)) { | ||
| if (!(data.buffer instanceof ArrayBuffer)) { | ||
| fatalError("queueWriteBuffer: Data view's underlying buffer is not an ArrayBuffer."); | ||
| } | ||
| arrayBuffer = data.buffer; | ||
| byteOffsetInData = data.byteOffset; | ||
| byteLengthInData = data.byteLength; | ||
| if ('BYTES_PER_ELEMENT' in data && typeof data.BYTES_PER_ELEMENT === 'number') { | ||
| bytesPerElement = data.BYTES_PER_ELEMENT; | ||
| } | ||
| } else { | ||
| fatalError("queueWriteBuffer: Invalid data type. Must be ArrayBuffer or ArrayBufferView."); | ||
| } | ||
| const dataOffsetElements = dataOffset ?? 0; | ||
| if (dataOffsetElements > Math.floor(byteLengthInData / bytesPerElement)) { | ||
| fatalError("queueWriteBuffer: dataOffset is larger than data's element count."); | ||
| } | ||
| const dataOffsetBytes = dataOffsetElements * bytesPerElement; | ||
| const finalDataOffset = byteOffsetInData + dataOffsetBytes; | ||
| const remainingDataSize = byteLengthInData - dataOffsetBytes; | ||
| let finalSize: number; | ||
| if (size !== undefined) { | ||
| if (size > Number.MAX_SAFE_INTEGER / bytesPerElement) { | ||
| fatalError("queueWriteBuffer: size overflows."); | ||
| } | ||
| finalSize = size * bytesPerElement; | ||
| } else { | ||
| finalSize = remainingDataSize; | ||
| } | ||
| if (finalSize > remainingDataSize) { | ||
| fatalError("queueWriteBuffer: size + dataOffset is larger than data's size."); | ||
| } | ||
| if (finalSize <= 0) { | ||
| console.warn("queueWriteBuffer: Calculated dataSize is 0 or negative, nothing to write."); | ||
| return; | ||
| } | ||
| if (finalSize % 4 !== 0) { | ||
| fatalError("queueWriteBuffer: size is not a multiple of 4 bytes."); | ||
| } | ||
| const dataPtr = ptr(arrayBuffer, finalDataOffset); | ||
| try { | ||
| this.lib.wgpuQueueWriteBuffer( | ||
| this.ptr, | ||
| buffer.ptr, | ||
| BigInt(bufferOffset), | ||
| dataPtr, | ||
| BigInt(finalSize) | ||
| ); | ||
| } catch (e) { | ||
| console.error("FFI Error: queueWriteBuffer", e); | ||
| } | ||
| } | ||
| writeTexture( | ||
| destination: GPUTexelCopyTextureInfo, | ||
| data: PtrSource, | ||
| dataLayout: GPUTexelCopyBufferLayout, | ||
| writeSize: GPUExtent3DStrict | ||
| ): undefined { | ||
| if (!this.ptr) { | ||
| fatalError("queueWriteTexture: Invalid queue pointer"); | ||
| } | ||
| let arrayBuffer: ArrayBuffer; | ||
| let byteOffsetInData: number; | ||
| let byteLengthInData: number; | ||
| if (data instanceof ArrayBuffer) { | ||
| arrayBuffer = data; | ||
| byteOffsetInData = 0; | ||
| byteLengthInData = data.byteLength; | ||
| } else if (ArrayBuffer.isView(data)) { | ||
| if (!(data.buffer instanceof ArrayBuffer)) { | ||
| fatalError("queueWriteTexture: Data view's underlying buffer is not an ArrayBuffer."); | ||
| } | ||
| arrayBuffer = data.buffer; | ||
| byteOffsetInData = data.byteOffset; | ||
| byteLengthInData = data.byteLength; | ||
| } else { | ||
| fatalError("queueWriteTexture: Invalid data type. Must be ArrayBuffer or ArrayBufferView."); | ||
| } | ||
| if (byteLengthInData <= 0) { | ||
| console.warn("queueWriteTexture: data size is 0 or negative, nothing to write."); | ||
| return; | ||
| } | ||
| if (!dataLayout.bytesPerRow) { | ||
| fatalError("queueWriteTexture: dataLayout.bytesPerRow is required."); | ||
| } | ||
| const normalizedWriteSize = normalizeGPUExtent3DStrict(writeSize); | ||
| const packedDestination = WGPUTexelCopyTextureInfoStruct.pack(destination); | ||
| const layoutForPacking: GPUTexelCopyBufferLayout = { | ||
| offset: dataLayout.offset ?? 0, | ||
| bytesPerRow: dataLayout.bytesPerRow, | ||
| rowsPerImage: dataLayout.rowsPerImage ?? normalizedWriteSize.height, | ||
| }; | ||
| const packedLayout = WGPUTexelCopyBufferLayoutStruct.pack(layoutForPacking); | ||
| const packedWriteSize = WGPUExtent3DStruct.pack(normalizedWriteSize); | ||
| const dataPtr = ptr(arrayBuffer, byteOffsetInData); | ||
| try { | ||
| this.lib.wgpuQueueWriteTexture( | ||
| this.ptr, | ||
| ptr(packedDestination), | ||
| dataPtr, | ||
| BigInt(byteLengthInData), | ||
| ptr(packedLayout), | ||
| ptr(packedWriteSize) | ||
| ); | ||
| } catch (e) { | ||
| console.error("FFI Error: queueWriteTexture", e); | ||
| } | ||
| } | ||
| copyBufferToBuffer(source: GPUTexelCopyBufferInfo, destination: GPUTexelCopyBufferInfo, size: number): undefined { | ||
| fatalError('copyBufferToBuffer not implemented', this.ptr, source, destination, size); | ||
| } | ||
| copyBufferToTexture(source: GPUTexelCopyBufferInfo, destination: GPUTexelCopyTextureInfo, size: GPUExtent3D): undefined { | ||
| fatalError('copyBufferToTexture not implemented', this.ptr, source, destination, size); | ||
| } | ||
| copyExternalImageToTexture(source: GPUCopyExternalImageSourceInfo, destination: GPUCopyExternalImageDestInfo, copySize: GPUExtent3DStrict): undefined { | ||
| fatalError('copyExternalImageToTexture not implemented', this.ptr, source, destination, copySize); | ||
| } | ||
| destroy(): undefined { | ||
| this._onSubmittedWorkDoneCallback.close(); | ||
| this.lib.wgpuQueueRelease(this.ptr); | ||
| } | ||
| } |
| import type { Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| export class GPURenderBundleImpl implements GPURenderBundle { | ||
| __brand: "GPURenderBundle" = "GPURenderBundle"; | ||
| label: string = ''; | ||
| readonly ptr: Pointer; | ||
| private lib: FFISymbols; | ||
| private _destroyed: boolean = false; | ||
| constructor(ptr: Pointer, lib: FFISymbols, label?: string) { | ||
| this.ptr = ptr; | ||
| this.lib = lib; | ||
| if (label) { | ||
| this.label = label; | ||
| } | ||
| } | ||
| destroy(): undefined { | ||
| if (this._destroyed) return; | ||
| this._destroyed = true; | ||
| this.lib.wgpuRenderBundleRelease(this.ptr); | ||
| } | ||
| } |
| import { type Pointer, ptr } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| import { GPURenderBundleImpl } from "./GPURenderBundle"; | ||
| import { WGPURenderBundleDescriptorStruct, WGPUIndexFormat, WGPUStringView } from "./structs_def"; | ||
| import { fatalError } from "./utils/error"; | ||
| import { GPUBufferImpl } from "./GPUBuffer"; | ||
| import { GPURenderPipelineImpl } from "./GPURenderPipeline"; | ||
| import { GPUBindGroupImpl } from "./GPUBindGroup"; | ||
| export class GPURenderBundleEncoderImpl implements GPURenderBundleEncoder { | ||
| __brand: "GPURenderBundleEncoder" = "GPURenderBundleEncoder"; | ||
| private _lib: FFISymbols; | ||
| private _destroyed: boolean = false; | ||
| label: string; | ||
| public ptr: Pointer; | ||
| constructor(ptr: Pointer, lib: FFISymbols, descriptor: GPURenderBundleEncoderDescriptor) { | ||
| this.ptr = ptr; | ||
| this._lib = lib; | ||
| this.label = descriptor.label ?? ''; | ||
| } | ||
| setBindGroup(groupIndex: number, bindGroup: GPUBindGroup | null, dynamicOffsets?: Uint32Array | number[]): undefined { | ||
| if (!bindGroup) return; | ||
| let offsetsBuffer: Uint32Array | undefined; | ||
| let offsetCount = 0; | ||
| let offsetPtr: Pointer | null = null; | ||
| if (dynamicOffsets) { | ||
| if (dynamicOffsets instanceof Uint32Array) { | ||
| offsetsBuffer = dynamicOffsets; | ||
| } else { | ||
| offsetsBuffer = new Uint32Array(dynamicOffsets); | ||
| } | ||
| offsetCount = offsetsBuffer.length; | ||
| if (offsetCount > 0) { | ||
| offsetPtr = ptr(offsetsBuffer); | ||
| } | ||
| } | ||
| this._lib.wgpuRenderBundleEncoderSetBindGroup(this.ptr, groupIndex, (bindGroup as GPUBindGroupImpl).ptr, BigInt(offsetCount), offsetPtr); | ||
| return undefined; | ||
| } | ||
| setPipeline(pipeline: GPURenderPipeline): undefined { | ||
| this._lib.wgpuRenderBundleEncoderSetPipeline(this.ptr, (pipeline as GPURenderPipelineImpl).ptr); | ||
| return undefined; | ||
| } | ||
| setIndexBuffer(buffer: GPUBuffer, indexFormat: GPUIndexFormat, offset?: number, size?: number): undefined { | ||
| this._lib.wgpuRenderBundleEncoderSetIndexBuffer(this.ptr, (buffer as GPUBufferImpl).ptr, WGPUIndexFormat.to(indexFormat), BigInt(offset ?? 0), BigInt(size ?? buffer.size)); | ||
| return undefined; | ||
| } | ||
| setVertexBuffer(slot: number, buffer: GPUBuffer | null, offset?: number, size?: number): undefined { | ||
| if (!buffer) return; | ||
| this._lib.wgpuRenderBundleEncoderSetVertexBuffer(this.ptr, slot, (buffer as GPUBufferImpl).ptr, BigInt(offset ?? 0), BigInt(size ?? buffer.size)); | ||
| return undefined; | ||
| } | ||
| draw(vertexCount: number, instanceCount?: number, firstVertex?: number, firstInstance?: number): undefined { | ||
| if (this._destroyed) { | ||
| fatalError("Cannot call draw on a destroyed GPURenderBundleEncoder"); | ||
| } | ||
| this._lib.wgpuRenderBundleEncoderDraw(this.ptr, vertexCount, instanceCount ?? 1, firstVertex ?? 0, firstInstance ?? 0); | ||
| return undefined; | ||
| } | ||
| drawIndexed(indexCount: number, instanceCount?: number, firstIndex?: number, baseVertex?: number, firstInstance?: number): undefined { | ||
| if (this._destroyed) { | ||
| fatalError("Cannot call drawIndexed on a destroyed GPURenderBundleEncoder"); | ||
| } | ||
| this._lib.wgpuRenderBundleEncoderDrawIndexed(this.ptr, indexCount, instanceCount ?? 1, firstIndex ?? 0, baseVertex ?? 0, firstInstance ?? 0); | ||
| return undefined; | ||
| } | ||
| drawIndirect(indirectBuffer: GPUBuffer, indirectOffset: number): undefined { | ||
| if (this._destroyed) { | ||
| fatalError("Cannot call drawIndirect on a destroyed GPURenderBundleEncoder"); | ||
| } | ||
| this._lib.wgpuRenderBundleEncoderDrawIndirect(this.ptr, (indirectBuffer as GPUBufferImpl).ptr, BigInt(indirectOffset)); | ||
| return undefined; | ||
| } | ||
| drawIndexedIndirect(indirectBuffer: GPUBuffer, indirectOffset: number): undefined { | ||
| if (this._destroyed) { | ||
| fatalError("Cannot call drawIndexedIndirect on a destroyed GPURenderBundleEncoder"); | ||
| } | ||
| this._lib.wgpuRenderBundleEncoderDrawIndexedIndirect(this.ptr, (indirectBuffer as GPUBufferImpl).ptr, BigInt(indirectOffset)); | ||
| return undefined; | ||
| } | ||
| finish(descriptor?: GPURenderBundleDescriptor): GPURenderBundle { | ||
| if (this._destroyed) { | ||
| fatalError("Cannot call finish on a destroyed GPURenderBundleEncoder"); | ||
| } | ||
| const packedDescriptor = WGPURenderBundleDescriptorStruct.pack(descriptor ?? {}); | ||
| const bundlePtr = this._lib.wgpuRenderBundleEncoderFinish(this.ptr, ptr(packedDescriptor)); | ||
| if (!bundlePtr) { | ||
| fatalError("wgpuRenderBundleEncoderFinish returned a null pointer"); | ||
| } | ||
| // Apparently render bundle encoders are not destroyed automatically like command encoders on finish? | ||
| // this._destroy(); | ||
| return new GPURenderBundleImpl(bundlePtr, this._lib, descriptor?.label); | ||
| } | ||
| _destroy() { | ||
| if (this._destroyed) return; | ||
| this._destroyed = true; | ||
| this._lib.wgpuRenderBundleEncoderRelease(this.ptr); | ||
| } | ||
| pushDebugGroup(groupLabel: string): undefined { | ||
| const packedLabel = WGPUStringView.pack(groupLabel); | ||
| this._lib.wgpuRenderBundleEncoderPushDebugGroup(this.ptr, ptr(packedLabel)); | ||
| } | ||
| popDebugGroup(): undefined { | ||
| this._lib.wgpuRenderBundleEncoderPopDebugGroup(this.ptr); | ||
| } | ||
| insertDebugMarker(markerLabel: string): undefined { | ||
| const packedLabel = WGPUStringView.pack(markerLabel); | ||
| this._lib.wgpuRenderBundleEncoderInsertDebugMarker(this.ptr, ptr(packedLabel)); | ||
| } | ||
| } |
| import { type Pointer, ptr } from "bun:ffi"; | ||
| import { type FFISymbols } from "./ffi"; | ||
| import { fatalError } from "./utils/error"; | ||
| import { WGPU_WHOLE_SIZE, WGPUIndexFormat, WGPUStringView, WGPUColorStruct } from "./structs_def"; | ||
| export class GPURenderPassEncoderImpl implements GPURenderPassEncoder { | ||
| __brand: "GPURenderPassEncoder" = "GPURenderPassEncoder"; | ||
| label: string = ''; | ||
| private lib: FFISymbols; | ||
| constructor(public ptr: Pointer, lib: FFISymbols) { | ||
| this.lib = lib; | ||
| } | ||
| setBlendConstant(color: GPUColor): undefined { | ||
| const packedColor = WGPUColorStruct.pack(color); | ||
| this.lib.wgpuRenderPassEncoderSetBlendConstant(this.ptr, ptr(packedColor)); | ||
| return undefined; | ||
| } | ||
| setStencilReference(reference: GPUStencilValue): undefined { | ||
| this.lib.wgpuRenderPassEncoderSetStencilReference(this.ptr, reference); | ||
| return undefined; | ||
| } | ||
| beginOcclusionQuery(queryIndex: GPUSize32): undefined { | ||
| this.lib.wgpuRenderPassEncoderBeginOcclusionQuery(this.ptr, queryIndex); | ||
| return undefined; | ||
| } | ||
| endOcclusionQuery(): undefined { | ||
| this.lib.wgpuRenderPassEncoderEndOcclusionQuery(this.ptr); | ||
| return undefined; | ||
| } | ||
| executeBundles(bundles: Iterable<GPURenderBundle>): undefined { | ||
| const bundleArray = Array.from(bundles); | ||
| const bundlePtrs = bundleArray.map(b => b.ptr); | ||
| const bundlesBuffer = new BigUint64Array(bundlePtrs.map(p => BigInt(p))); | ||
| this.lib.wgpuRenderPassEncoderExecuteBundles(this.ptr, BigInt(bundleArray.length), ptr(bundlesBuffer)); | ||
| return undefined; | ||
| } | ||
| setPipeline(pipeline: GPURenderPipeline): undefined { | ||
| this.lib.wgpuRenderPassEncoderSetPipeline(this.ptr, pipeline.ptr); | ||
| } | ||
| setBindGroup(index: GPUIndex32, bindGroup: GPUBindGroup | null | undefined, dynamicOffsets?: Uint32Array | number[]): undefined { | ||
| if (!bindGroup) { | ||
| console.warn("RenderPassEncoder.setBindGroup: null bindGroup pointer."); | ||
| return; | ||
| } | ||
| let offsetsBuffer: Uint32Array | undefined; | ||
| let offsetCount = 0; | ||
| let offsetPtr: Pointer | null = null; | ||
| if (dynamicOffsets) { | ||
| if (dynamicOffsets instanceof Uint32Array) { | ||
| offsetsBuffer = dynamicOffsets; | ||
| } else { | ||
| offsetsBuffer = new Uint32Array(dynamicOffsets); | ||
| } | ||
| offsetCount = offsetsBuffer.length; | ||
| if (offsetCount > 0) { | ||
| offsetPtr = ptr(offsetsBuffer.buffer, offsetsBuffer.byteOffset); | ||
| } | ||
| } | ||
| try { | ||
| this.lib.wgpuRenderPassEncoderSetBindGroup(this.ptr, index, bindGroup.ptr, BigInt(offsetCount), offsetPtr); | ||
| } catch (e) { console.error("FFI Error: renderPassEncoderSetBindGroup", e); } | ||
| } | ||
| setVertexBuffer(slot: number, buffer: GPUBuffer | null | undefined, offset: number | bigint = 0, size?: number | bigint): undefined { | ||
| if (!buffer) { | ||
| console.warn("RenderPassEncoder.setVertexBuffer: null buffer pointer."); | ||
| return; | ||
| } | ||
| // Use WGPU_WHOLE_SIZE equivalent if size is not provided | ||
| const bufferSize = size ?? WGPU_WHOLE_SIZE; | ||
| this.lib.wgpuRenderPassEncoderSetVertexBuffer(this.ptr, slot, buffer.ptr, BigInt(offset), BigInt(bufferSize)); | ||
| } | ||
| setIndexBuffer(buffer: GPUBuffer | null | undefined, format: GPUIndexFormat, offset: number | bigint = 0, size?: number | bigint): undefined { | ||
| if (!buffer) { | ||
| console.warn("RenderPassEncoder.setIndexBuffer: null buffer pointer."); | ||
| return; | ||
| } | ||
| const formatValue = WGPUIndexFormat.to(format); | ||
| // Use WGPU_WHOLE_SIZE equivalent if size is not provided | ||
| const bufferSize = size ?? WGPU_WHOLE_SIZE; | ||
| this.lib.wgpuRenderPassEncoderSetIndexBuffer(this.ptr, buffer.ptr, formatValue, BigInt(offset), BigInt(bufferSize)); | ||
| } | ||
| setViewport(x: number, y: number, width: number, height: number, minDepth: number, maxDepth: number): undefined { | ||
| this.lib.wgpuRenderPassEncoderSetViewport(this.ptr, x, y, width, height, minDepth, maxDepth); | ||
| } | ||
| setScissorRect(x: number, y: number, width: number, height: number): undefined { | ||
| const ux = Math.max(0, Math.floor(x)); | ||
| const uy = Math.max(0, Math.floor(y)); | ||
| const uwidth = Math.max(0, Math.floor(width)); | ||
| const uheight = Math.max(0, Math.floor(height)); | ||
| this.lib.wgpuRenderPassEncoderSetScissorRect(this.ptr, ux, uy, uwidth, uheight); | ||
| } | ||
| draw(vertexCount: number, instanceCount: number = 1, firstVertex: number = 0, firstInstance: number = 0): undefined { | ||
| if (!this.ptr) { console.warn("RenderPassEncoder.draw: null encoder pointer."); return; } | ||
| this.lib.wgpuRenderPassEncoderDraw(this.ptr, vertexCount, instanceCount, firstVertex, firstInstance); | ||
| } | ||
| drawIndexed(indexCount: number, instanceCount: number = 1, firstIndex: number = 0, baseVertex: number = 0, firstInstance: number = 0): undefined { | ||
| if (!this.ptr) { console.warn("RenderPassEncoder.drawIndexed: null encoder pointer."); return; } | ||
| this.lib.wgpuRenderPassEncoderDrawIndexed(this.ptr, indexCount, instanceCount, firstIndex, baseVertex, firstInstance); | ||
| } | ||
| drawIndirect(indirectBuffer: GPUBuffer, indirectOffset: number | bigint): undefined { | ||
| this.lib.wgpuRenderPassEncoderDrawIndirect(this.ptr, indirectBuffer.ptr, BigInt(indirectOffset)); | ||
| } | ||
| drawIndexedIndirect(indirectBuffer: GPUBuffer, indirectOffset: number): undefined { | ||
| this.lib.wgpuRenderPassEncoderDrawIndexedIndirect(this.ptr, indirectBuffer.ptr, BigInt(indirectOffset)); | ||
| return undefined; | ||
| } | ||
| end(): undefined { | ||
| this.lib.wgpuRenderPassEncoderEnd(this.ptr); | ||
| // TODO: Encoder is consumed after end, prevent reuse. | ||
| } | ||
| pushDebugGroup(message: string): undefined { | ||
| const packedMessage = WGPUStringView.pack(message); | ||
| this.lib.wgpuRenderPassEncoderPushDebugGroup(this.ptr, ptr(packedMessage)); | ||
| } | ||
| popDebugGroup(): undefined { | ||
| this.lib.wgpuRenderPassEncoderPopDebugGroup(this.ptr); | ||
| } | ||
| insertDebugMarker(markerLabel: string): undefined { | ||
| const packedMarker = WGPUStringView.pack(markerLabel); | ||
| this.lib.wgpuRenderPassEncoderInsertDebugMarker(this.ptr, ptr(packedMarker)); | ||
| } | ||
| destroy(): undefined { | ||
| try { | ||
| this.lib.wgpuRenderPassEncoderRelease(this.ptr); | ||
| } catch(e) { | ||
| console.error("FFI Error: renderPassEncoderRelease", e); | ||
| } | ||
| } | ||
| } |
| import type { Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| import { GPUBindGroupLayoutImpl } from "./GPUBindGroupLayout"; | ||
| import { fatalError } from "./utils/error"; | ||
| export class GPURenderPipelineImpl implements GPURenderPipeline { | ||
| __brand: "GPURenderPipeline" = "GPURenderPipeline"; | ||
| label: string; | ||
| readonly ptr: Pointer; | ||
| constructor(ptr: Pointer, private lib: FFISymbols, label?: string) { | ||
| this.ptr = ptr; | ||
| this.label = label || ''; | ||
| } | ||
| getBindGroupLayout(index: number): GPUBindGroupLayout { | ||
| const layoutPtr = this.lib.wgpuRenderPipelineGetBindGroupLayout(this.ptr, index); | ||
| if (!layoutPtr) { | ||
| fatalError('wgpuRenderPipelineGetBindGroupLayout returned null'); | ||
| } | ||
| // The returned layout is owned by the pipeline and is valid as long as the pipeline is. | ||
| return new GPUBindGroupLayoutImpl(layoutPtr, this.lib); | ||
| } | ||
| destroy(): undefined { | ||
| try { | ||
| this.lib.wgpuRenderPipelineRelease(this.ptr); | ||
| } catch(e) { | ||
| console.error("FFI Error: renderPipelineRelease", e); | ||
| } | ||
| } | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| export class GPUSamplerImpl implements GPUSampler { | ||
| __brand: "GPUSampler" = "GPUSampler"; | ||
| label: string = ''; | ||
| ptr: Pointer; | ||
| constructor( | ||
| public readonly samplerPtr: Pointer, | ||
| private lib: FFISymbols, | ||
| label?: string | ||
| ) { | ||
| this.ptr = samplerPtr; | ||
| if (label) this.label = label; | ||
| } | ||
| destroy(): undefined { | ||
| try { | ||
| this.lib.wgpuSamplerRelease(this.samplerPtr); | ||
| } catch(e) { | ||
| console.error("FFI Error: samplerRelease", e); | ||
| } | ||
| return undefined; | ||
| } | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import { type FFISymbols } from "./ffi"; | ||
| import { fatalError } from "./utils/error"; | ||
| export class GPUShaderModuleImpl implements GPUShaderModule { | ||
| __brand: "GPUShaderModule" = "GPUShaderModule"; | ||
| constructor(public readonly ptr: Pointer, private lib: FFISymbols, public readonly label: string) { | ||
| this.label = label || ''; | ||
| } | ||
| getCompilationInfo(): Promise<GPUCompilationInfo> { | ||
| return fatalError('getCompilationInfo not implemented'); | ||
| } | ||
| destroy(): undefined { | ||
| try { | ||
| this.lib.wgpuShaderModuleRelease(this.ptr); | ||
| } catch(e) { | ||
| console.error("FFI Error: shaderModuleRelease", e); | ||
| } | ||
| } | ||
| } |
| import { ptr, type Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| import { GPUTextureViewImpl } from "./GPUTextureView"; | ||
| import { fatalError } from "./utils/error"; | ||
| import { WGPUTextureViewDescriptorStruct } from "./structs_def"; | ||
| export class GPUTextureImpl implements GPUTexture { | ||
| __brand: "GPUTexture" = "GPUTexture"; | ||
| label: string = ''; | ||
| ptr: Pointer; | ||
| constructor( | ||
| public readonly texturePtr: Pointer, | ||
| private lib: FFISymbols, | ||
| private _width: number, | ||
| private _height: number, | ||
| private _depthOrArrayLayers: number, | ||
| private _format: GPUTextureFormat, | ||
| private _dimension: GPUTextureDimension, | ||
| private _mipLevelCount: number, | ||
| private _sampleCount: number, | ||
| private _usage: GPUTextureUsageFlags | ||
| ) { | ||
| this.ptr = texturePtr; | ||
| } | ||
| get width(): number { | ||
| return this._width; | ||
| } | ||
| get height(): number { | ||
| return this._height; | ||
| } | ||
| get depthOrArrayLayers(): number { | ||
| return this._depthOrArrayLayers; | ||
| } | ||
| get format(): GPUTextureFormat { | ||
| return this._format; | ||
| } | ||
| get dimension(): GPUTextureDimension { | ||
| return this._dimension; | ||
| } | ||
| get mipLevelCount(): number { | ||
| return this._mipLevelCount; | ||
| } | ||
| get sampleCount(): number { | ||
| return this._sampleCount; | ||
| } | ||
| get usage(): GPUFlagsConstant { | ||
| return this._usage; | ||
| } | ||
| createView(descriptor?: GPUTextureViewDescriptor): GPUTextureView { | ||
| const label = descriptor?.label || `View of ${this.label || 'Texture_'+this.texturePtr}`; | ||
| const mergedDescriptor = { | ||
| ...descriptor, | ||
| label | ||
| }; | ||
| // 3D textures always have arrayLayerCount = 1 since they use depth, not array layers. | ||
| // For 1D/2D textures, let native implementation handle defaults and validate view compatibility. | ||
| // https://github.com/gpuweb/cts/blob/main/src/webgpu/api/validation/createBindGroup.spec.ts#L1244 | ||
| if (descriptor?.arrayLayerCount !== undefined) { | ||
| mergedDescriptor.arrayLayerCount = descriptor.arrayLayerCount; | ||
| } else if (this.dimension === '3d') { | ||
| mergedDescriptor.arrayLayerCount = 1; | ||
| } | ||
| const packedDescriptorBuffer = WGPUTextureViewDescriptorStruct.pack(mergedDescriptor); | ||
| const viewPtr = this.lib.wgpuTextureCreateView(this.texturePtr, ptr(packedDescriptorBuffer)); | ||
| if (!viewPtr) { | ||
| fatalError("Failed to create texture view"); | ||
| } | ||
| return new GPUTextureViewImpl(viewPtr, this.lib, label); | ||
| } | ||
| destroy(): undefined { | ||
| try { | ||
| this.lib.wgpuTextureDestroy(this.texturePtr); | ||
| } catch(e) { | ||
| console.error("FFI Error: textureRelease", e); | ||
| } | ||
| return undefined; | ||
| } | ||
| } |
| import { type Pointer } from "bun:ffi"; | ||
| import type { FFISymbols } from "./ffi"; | ||
| export class GPUTextureViewImpl implements GPUTextureView { | ||
| __brand: "GPUTextureView" = "GPUTextureView"; | ||
| label: string = ''; | ||
| ptr: Pointer; | ||
| constructor( | ||
| public readonly viewPtr: Pointer, | ||
| private lib: FFISymbols, | ||
| label?: string | ||
| ) { | ||
| this.ptr = viewPtr; | ||
| if (label) this.label = label; | ||
| } | ||
| destroy(): undefined { | ||
| try { | ||
| this.lib.wgpuTextureViewRelease(this.viewPtr); | ||
| } catch(e) { | ||
| console.error("FFI Error: textureViewRelease", e); | ||
| } | ||
| return undefined; | ||
| } | ||
| } |
Sorry, the diff of this file is too big to display
-67
| /// <reference types="@webgpu/types" /> | ||
| /// <reference types="../index.d.ts" /> | ||
| import { type Pointer } from "bun:ffi"; | ||
| import { loadLibrary, type FFISymbols } from "./ffi"; | ||
| import { GPUImpl } from "./GPU"; | ||
| import { GPUDeviceImpl } from "./GPUDevice"; | ||
| import { GPUAdapterInfoImpl, GPUSupportedLimitsImpl } from "./shared"; | ||
| import { BufferUsageFlags, MapModeFlags, ShaderStageFlags, TextureUsageFlags } from "./common"; | ||
| import { | ||
| GPUOutOfMemoryError, GPUErrorImpl, GPUInternalError, GPUValidationError, AbortError, GPUPipelineErrorImpl | ||
| } from "./utils/error"; | ||
| export * from "./mocks/GPUCanvasContext"; | ||
| function createInstance(lib: FFISymbols): Pointer | null { | ||
| try { | ||
| return lib.wgpuCreateInstance(null); | ||
| } catch(e) { | ||
| console.error("FFI Error: createInstance", e); return null; | ||
| } | ||
| } | ||
| export function createGPUInstance(libPath?: string): GPUImpl { | ||
| const lib = loadLibrary(libPath); | ||
| const instancePtr = createInstance(lib); | ||
| if (!instancePtr) { | ||
| throw new Error("Failed to create GPU instance"); | ||
| } | ||
| return new GPUImpl(instancePtr, lib); | ||
| } | ||
| export const globals = { | ||
| GPUPipelineError: GPUPipelineErrorImpl as any, | ||
| AbortError: AbortError as any, | ||
| GPUError: GPUErrorImpl as any, | ||
| GPUOutOfMemoryError: GPUOutOfMemoryError as any, | ||
| GPUInternalError: GPUInternalError as any, | ||
| GPUValidationError: GPUValidationError as any, | ||
| GPUTextureUsage: TextureUsageFlags, | ||
| GPUBufferUsage: BufferUsageFlags, | ||
| GPUShaderStage: ShaderStageFlags, | ||
| GPUMapMode: MapModeFlags, | ||
| GPUDevice: GPUDeviceImpl as any, | ||
| GPUAdapterInfo: GPUAdapterInfoImpl as any, | ||
| GPUSupportedLimits: GPUSupportedLimitsImpl as any, | ||
| }; | ||
| export async function setupGlobals({ libPath }: { libPath?: string } = {}) { | ||
| if (!navigator.gpu) { | ||
| const gpuInstance = createGPUInstance(libPath); | ||
| global.navigator = { | ||
| ...(global.navigator ?? {}), | ||
| gpu: gpuInstance, | ||
| }; | ||
| } | ||
| Object.assign(globalThis, globals); | ||
| } | ||
| export async function createWebGPUDevice() { | ||
| const adapter = await navigator.gpu.requestAdapter(); | ||
| const device = await adapter?.requestDevice(); | ||
| if (!device) { | ||
| throw new Error('Failed to create WebGPU device'); | ||
| } | ||
| return device; | ||
| } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
| export class GPUCanvasContextMock implements GPUCanvasContext { | ||
| readonly __brand: "GPUCanvasContext" = "GPUCanvasContext"; | ||
| private _configuration: GPUCanvasConfiguration | null = null; | ||
| private _currentTexture: GPUTexture | null = null; | ||
| private _nextTexture: GPUTexture | null = null; | ||
| private width: number; | ||
| private height: number; | ||
| // Keep a reference to the associated device if needed for validation or texture creation | ||
| private _device: GPUDevice | null = null; | ||
| constructor( | ||
| public readonly canvas: HTMLCanvasElement | OffscreenCanvas, | ||
| width: number, | ||
| height: number | ||
| ) { | ||
| this.width = width; | ||
| this.height = height; | ||
| } | ||
| configure(descriptor: GPUCanvasConfiguration): undefined { | ||
| if (!descriptor || !descriptor.device) { | ||
| throw new Error("GPUCanvasContextMock.configure: Invalid descriptor or missing device."); | ||
| } | ||
| this._configuration = { | ||
| ...descriptor, | ||
| alphaMode: descriptor.alphaMode ?? 'premultiplied', | ||
| usage: descriptor.usage ? descriptor.usage | GPUTextureUsage.TEXTURE_BINDING : (GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.TEXTURE_BINDING), | ||
| colorSpace: descriptor.colorSpace ?? 'srgb', | ||
| }; | ||
| this._device = descriptor.device; | ||
| // Invalidate any previously vended texture | ||
| this._currentTexture?.destroy(); | ||
| this._currentTexture = null; | ||
| return undefined; | ||
| } | ||
| unconfigure(): undefined { | ||
| this._configuration = null; | ||
| this._currentTexture?.destroy(); | ||
| this._currentTexture = null; | ||
| this._device = null; // Or keep if it should persist? Depends on desired mock behavior. | ||
| return undefined; | ||
| } | ||
| getConfiguration(): GPUCanvasConfigurationOut | null { | ||
| if (!this._configuration) { | ||
| return null; | ||
| } | ||
| const configOut: GPUCanvasConfigurationOut = { | ||
| device: this._configuration.device, | ||
| format: this._configuration.format, | ||
| usage: this._configuration.usage ?? (GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC), | ||
| viewFormats: Array.from(this._configuration.viewFormats ?? []), | ||
| colorSpace: this._configuration.colorSpace ?? 'srgb', | ||
| alphaMode: this._configuration.alphaMode ?? 'premultiplied', | ||
| }; | ||
| if (this._configuration.toneMapping) { | ||
| configOut.toneMapping = this._configuration.toneMapping; | ||
| } | ||
| return configOut; | ||
| } | ||
| setSize(width: number, height: number): undefined { | ||
| this.width = width; | ||
| this.height = height; | ||
| if (this._configuration && this._device) { | ||
| this.createTextures(); | ||
| } | ||
| return undefined; | ||
| } | ||
| private createTextures(): undefined { | ||
| const currentTexture = this._currentTexture; | ||
| const nextTexture = this._nextTexture; | ||
| // defer destruction of textures as async methods might be using them | ||
| setTimeout(() => { | ||
| currentTexture?.destroy(); | ||
| nextTexture?.destroy(); | ||
| }, 1000); | ||
| this._currentTexture = this.createRenderTexture(this.width, this.height); | ||
| this._nextTexture = this.createRenderTexture(this.width, this.height); | ||
| } | ||
| private createRenderTexture(width: number, height: number): GPUTexture { | ||
| if (!this._configuration || !this._device) { | ||
| throw new Error("GPUCanvasContextMock.getCurrentTexture: Context is not configured."); | ||
| } | ||
| return this._device.createTexture({ | ||
| label: 'canvasCurrentTexture', | ||
| size: { | ||
| width: width, | ||
| height: height, | ||
| depthOrArrayLayers: 1, | ||
| }, | ||
| format: this._configuration.format, | ||
| usage: this._configuration.usage ?? (GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC), | ||
| dimension: '2d', | ||
| mipLevelCount: 1, | ||
| sampleCount: 1, | ||
| }); | ||
| } | ||
| getCurrentTexture(): GPUTexture { | ||
| if (!this._configuration || !this._device) { | ||
| throw new Error("GPUCanvasContextMock.getCurrentTexture: Context is not configured."); | ||
| } | ||
| if (!this._currentTexture) { | ||
| this.createTextures(); | ||
| } | ||
| return this._currentTexture!; | ||
| } | ||
| switchTextures(): GPUTexture { | ||
| const temp = this._currentTexture; | ||
| this._currentTexture = this._nextTexture; | ||
| this._nextTexture = temp; | ||
| return this._currentTexture!; | ||
| } | ||
| } |
-167
| import { type Pointer, toArrayBuffer } from "bun:ffi"; | ||
| import { BufferPool, type BlockBuffer } from "./buffer_pool"; | ||
| export const AsyncStatus = { | ||
| Success: 1, | ||
| CallbackCancelled: 2, | ||
| Error: 3, | ||
| Aborted: 4, | ||
| Force32: 0x7FFFFFFF, | ||
| } as const; | ||
| export const WGPUErrorType = { | ||
| "no-error": 1, | ||
| "validation": 2, | ||
| "out-of-memory": 3, | ||
| "internal": 4, | ||
| "unknown": 5, | ||
| // "device-lost": 6, | ||
| "force-32": 0x7FFFFFFF | ||
| } as const; | ||
| const idBufferPool = new BufferPool(64, 1024, 8); | ||
| export function packUserDataId(id: number): ArrayBuffer { | ||
| const blockBuffer = idBufferPool.request(); | ||
| const userDataBuffer = new Uint32Array(blockBuffer.buffer); | ||
| userDataBuffer[0] = id; | ||
| userDataBuffer[1] = blockBuffer.index; | ||
| return blockBuffer.buffer; | ||
| } | ||
| export function unpackUserDataId(userDataPtr: Pointer): number { | ||
| const userDataBuffer = toArrayBuffer(userDataPtr, 0, 8); | ||
| const userDataView = new Uint32Array(userDataBuffer); | ||
| const id = userDataView[0]; | ||
| const index = userDataView[1]; | ||
| idBufferPool.releaseBlock(index!); | ||
| return id!; | ||
| } | ||
| export class GPUAdapterInfoImpl implements GPUAdapterInfo { | ||
| __brand: "GPUAdapterInfo" = "GPUAdapterInfo"; | ||
| vendor: string = ""; | ||
| architecture: string = ""; | ||
| device: string = ""; | ||
| description: string = ""; | ||
| subgroupMinSize: number = 0; | ||
| subgroupMaxSize: number = 0; | ||
| isFallbackAdapter: boolean = false; | ||
| constructor() { | ||
| throw new TypeError('Illegal constructor'); | ||
| } | ||
| } | ||
| export function normalizeIdentifier(input: string): string { | ||
| if (!input || input.trim() === '') { | ||
| return ''; | ||
| } | ||
| return input | ||
| .toLowerCase() | ||
| .replace(/[^a-z0-9-]/g, '-') | ||
| .replace(/-+/g, '-') | ||
| .replace(/^-|-$/g, ''); | ||
| } | ||
| export function decodeCallbackMessage(messagePtr: Pointer | null, messageSize?: number | bigint): string { | ||
| if (!messagePtr || messageSize === 0n || messageSize === 0) { | ||
| return '[empty message]'; | ||
| } | ||
| let arrayBuffer: ArrayBuffer | null = null; | ||
| arrayBuffer = messageSize ? toArrayBuffer(messagePtr, 0, Number(messageSize)) : toArrayBuffer(messagePtr); | ||
| let message = 'Could not decode error message'; | ||
| if (arrayBuffer instanceof Error) { | ||
| message = arrayBuffer.message; | ||
| } else { | ||
| message = Buffer.from(arrayBuffer).toString(); | ||
| } | ||
| return message; | ||
| } | ||
| export const DEFAULT_SUPPORTED_LIMITS: Omit<GPUSupportedLimits, '__brand'> & { maxImmediateSize: number } = Object.freeze({ | ||
| maxTextureDimension1D: 8192, | ||
| maxTextureDimension2D: 8192, | ||
| maxTextureDimension3D: 2048, | ||
| maxTextureArrayLayers: 256, | ||
| maxBindGroups: 4, | ||
| maxBindGroupsPlusVertexBuffers: 24, | ||
| maxBindingsPerBindGroup: 1000, | ||
| maxStorageBuffersInFragmentStage: 8, | ||
| maxStorageBuffersInVertexStage: 8, | ||
| maxStorageTexturesInFragmentStage: 4, | ||
| maxStorageTexturesInVertexStage: 4, | ||
| maxDynamicUniformBuffersPerPipelineLayout: 8, | ||
| maxDynamicStorageBuffersPerPipelineLayout: 4, | ||
| maxSampledTexturesPerShaderStage: 16, | ||
| maxSamplersPerShaderStage: 16, | ||
| maxStorageBuffersPerShaderStage: 8, | ||
| maxStorageTexturesPerShaderStage: 4, | ||
| maxUniformBuffersPerShaderStage: 12, | ||
| maxUniformBufferBindingSize: 65536, | ||
| maxStorageBufferBindingSize: 134217728, | ||
| minUniformBufferOffsetAlignment: 256, | ||
| minStorageBufferOffsetAlignment: 256, | ||
| maxVertexBuffers: 8, | ||
| maxBufferSize: 268435456, | ||
| maxVertexAttributes: 16, | ||
| maxVertexBufferArrayStride: 2048, | ||
| maxInterStageShaderComponents: 4294967295, | ||
| maxInterStageShaderVariables: 16, | ||
| maxColorAttachments: 8, | ||
| maxColorAttachmentBytesPerSample: 32, | ||
| maxComputeWorkgroupStorageSize: 16384, | ||
| maxComputeInvocationsPerWorkgroup: 256, | ||
| maxComputeWorkgroupSizeX: 256, | ||
| maxComputeWorkgroupSizeY: 256, | ||
| maxComputeWorkgroupSizeZ: 64, | ||
| maxComputeWorkgroupsPerDimension: 65535, | ||
| maxImmediateSize: 0, | ||
| }); | ||
| export class GPUSupportedLimitsImpl implements GPUSupportedLimits { | ||
| __brand: "GPUSupportedLimits" = "GPUSupportedLimits"; | ||
| maxTextureDimension1D = 8192; | ||
| maxTextureDimension2D = 8192; | ||
| maxTextureDimension3D = 2048; | ||
| maxTextureArrayLayers = 256; | ||
| maxBindGroups = 4; | ||
| maxBindGroupsPlusVertexBuffers = 24; | ||
| maxBindingsPerBindGroup = 1000; | ||
| maxStorageBuffersInFragmentStage = 8; | ||
| maxStorageBuffersInVertexStage = 8; | ||
| maxStorageTexturesInFragmentStage = 4; | ||
| maxStorageTexturesInVertexStage = 4; | ||
| maxDynamicUniformBuffersPerPipelineLayout = 8; | ||
| maxDynamicStorageBuffersPerPipelineLayout = 4; | ||
| maxSampledTexturesPerShaderStage = 16; | ||
| maxSamplersPerShaderStage = 16; | ||
| maxStorageBuffersPerShaderStage = 8; | ||
| maxStorageTexturesPerShaderStage = 4; | ||
| maxUniformBuffersPerShaderStage = 12; | ||
| maxUniformBufferBindingSize = 65536; | ||
| maxStorageBufferBindingSize = 134217728; | ||
| minUniformBufferOffsetAlignment = 256; | ||
| minStorageBufferOffsetAlignment = 256; | ||
| maxVertexBuffers = 8; | ||
| maxBufferSize = 268435456; | ||
| maxVertexAttributes = 16; | ||
| maxVertexBufferArrayStride = 2048; | ||
| maxInterStageShaderComponents = 4294967295; | ||
| maxInterStageShaderVariables = 16; | ||
| maxColorAttachments = 8; | ||
| maxColorAttachmentBytesPerSample = 32; | ||
| maxComputeWorkgroupStorageSize = 16384; | ||
| maxComputeInvocationsPerWorkgroup = 256; | ||
| maxComputeWorkgroupSizeX = 256; | ||
| maxComputeWorkgroupSizeY = 256; | ||
| maxComputeWorkgroupSizeZ = 64; | ||
| maxComputeWorkgroupsPerDimension = 65535; | ||
| constructor() { | ||
| throw new TypeError('Illegal constructor'); | ||
| } | ||
| } |
-1301
| import { toArrayBuffer, type Pointer } from "bun:ffi"; | ||
| import { fatalError, OperationError } from "./utils/error"; | ||
| import { defineEnum, defineStruct, objectPtr } from "./structs_ffi"; | ||
| import { DEFAULT_SUPPORTED_LIMITS, WGPUErrorType } from "./shared"; | ||
| export const WGPUBool = 'bool_u32'; | ||
| export const UINT64_MAX = 0xFFFFFFFFFFFFFFFFn; | ||
| export const WGPU_WHOLE_SIZE = 0xFFFFFFFFFFFFFFFFn; | ||
| export const WGPU_STRLEN = UINT64_MAX; | ||
| export const WGPUCallbackMode = { | ||
| WaitAnyOnly: 0x00000001, | ||
| AllowProcessEvents: 0x00000002, | ||
| AllowSpontaneous: 0x00000003, | ||
| Force32: 0x7FFFFFFF | ||
| }; | ||
| export const WGPUCallbackModeDef = defineEnum(WGPUCallbackMode); | ||
| export const WGPUErrorTypeDef = defineEnum(WGPUErrorType); | ||
| export const WGPUDeviceLostReason = { | ||
| unknown: 0x00000001, | ||
| destroyed: 0x00000002, | ||
| "callback-cancelled": 0x00000003, | ||
| "failed-creation": 0x00000004, | ||
| "force-32": 0x7FFFFFFF | ||
| } as const; | ||
| export const WGPUDeviceLostReasonDef = defineEnum(WGPUDeviceLostReason); | ||
| export const WGPUSType = { | ||
| ShaderSourceSPIRV: 0x00000001, | ||
| ShaderSourceWGSL: 0x00000002, | ||
| RenderPassMaxDrawCount: 0x00000003, | ||
| SurfaceSourceMetalLayer: 0x00000004, | ||
| SurfaceSourceWindowsHWND: 0x00000005, | ||
| SurfaceSourceXlibWindow: 0x00000006, | ||
| SurfaceSourceWaylandSurface: 0x00000007, | ||
| SurfaceSourceAndroidNativeWindow: 0x00000008, | ||
| SurfaceSourceXCBWindow: 0x00000009, | ||
| SurfaceColorManagement: 0x0000000A, | ||
| RequestAdapterWebXROptions: 0x0000000B, | ||
| AdapterPropertiesSubgroups: 0x0000000C, | ||
| TextureBindingViewDimensionDescriptor: 0x00020000, | ||
| EmscriptenSurfaceSourceCanvasHTMLSelector: 0x00040000, | ||
| SurfaceDescriptorFromWindowsCoreWindow: 0x00050000, | ||
| ExternalTextureBindingEntry: 0x00050001, | ||
| ExternalTextureBindingLayout: 0x00050002, | ||
| SurfaceDescriptorFromWindowsUWPSwapChainPanel: 0x00050003, | ||
| DawnTextureInternalUsageDescriptor: 0x00050004, | ||
| DawnEncoderInternalUsageDescriptor: 0x00050005, | ||
| DawnInstanceDescriptor: 0x00050006, | ||
| DawnCacheDeviceDescriptor: 0x00050007, | ||
| DawnAdapterPropertiesPowerPreference: 0x00050008, | ||
| DawnBufferDescriptorErrorInfoFromWireClient: 0x00050009, | ||
| DawnTogglesDescriptor: 0x0005000A, | ||
| DawnShaderModuleSPIRVOptionsDescriptor: 0x0005000B, | ||
| RequestAdapterOptionsLUID: 0x0005000C, | ||
| RequestAdapterOptionsGetGLProc: 0x0005000D, | ||
| RequestAdapterOptionsD3D11Device: 0x0005000E, | ||
| DawnRenderPassColorAttachmentRenderToSingleSampled: 0x0005000F, | ||
| RenderPassPixelLocalStorage: 0x00050010, | ||
| PipelineLayoutPixelLocalStorage: 0x00050011, | ||
| BufferHostMappedPointer: 0x00050012, | ||
| AdapterPropertiesMemoryHeaps: 0x00050013, | ||
| AdapterPropertiesD3D: 0x00050014, | ||
| AdapterPropertiesVk: 0x00050015, | ||
| DawnWireWGSLControl: 0x00050016, | ||
| DawnWGSLBlocklist: 0x00050017, | ||
| DawnDrmFormatCapabilities: 0x00050018, | ||
| ShaderModuleCompilationOptions: 0x00050019, | ||
| ColorTargetStateExpandResolveTextureDawn: 0x0005001A, | ||
| RenderPassDescriptorExpandResolveRect: 0x0005001B, | ||
| SharedTextureMemoryVkDedicatedAllocationDescriptor: 0x0005001C, | ||
| SharedTextureMemoryAHardwareBufferDescriptor: 0x0005001D, | ||
| SharedTextureMemoryDmaBufDescriptor: 0x0005001E, | ||
| SharedTextureMemoryOpaqueFDDescriptor: 0x0005001F, | ||
| SharedTextureMemoryZirconHandleDescriptor: 0x00050020, | ||
| SharedTextureMemoryDXGISharedHandleDescriptor: 0x00050021, | ||
| SharedTextureMemoryD3D11Texture2DDescriptor: 0x00050022, | ||
| SharedTextureMemoryIOSurfaceDescriptor: 0x00050023, | ||
| SharedTextureMemoryEGLImageDescriptor: 0x00050024, | ||
| SharedTextureMemoryInitializedBeginState: 0x00050025, | ||
| SharedTextureMemoryInitializedEndState: 0x00050026, | ||
| SharedTextureMemoryVkImageLayoutBeginState: 0x00050027, | ||
| SharedTextureMemoryVkImageLayoutEndState: 0x00050028, | ||
| SharedTextureMemoryD3DSwapchainBeginState: 0x00050029, | ||
| SharedFenceVkSemaphoreOpaqueFDDescriptor: 0x0005002A, | ||
| SharedFenceVkSemaphoreOpaqueFDExportInfo: 0x0005002B, | ||
| SharedFenceSyncFDDescriptor: 0x0005002C, | ||
| SharedFenceSyncFDExportInfo: 0x0005002D, | ||
| SharedFenceVkSemaphoreZirconHandleDescriptor: 0x0005002E, | ||
| SharedFenceVkSemaphoreZirconHandleExportInfo: 0x0005002F, | ||
| SharedFenceDXGISharedHandleDescriptor: 0x00050030, | ||
| SharedFenceDXGISharedHandleExportInfo: 0x00050031, | ||
| SharedFenceMTLSharedEventDescriptor: 0x00050032, | ||
| SharedFenceMTLSharedEventExportInfo: 0x00050033, | ||
| SharedBufferMemoryD3D12ResourceDescriptor: 0x00050034, | ||
| StaticSamplerBindingLayout: 0x00050035, | ||
| YCbCrVkDescriptor: 0x00050036, | ||
| SharedTextureMemoryAHardwareBufferProperties: 0x00050037, | ||
| AHardwareBufferProperties: 0x00050038, | ||
| DawnExperimentalImmediateDataLimits: 0x00050039, | ||
| DawnTexelCopyBufferRowAlignmentLimits: 0x0005003A, | ||
| AdapterPropertiesSubgroupMatrixConfigs: 0x0005003B, | ||
| SharedFenceEGLSyncDescriptor: 0x0005003C, | ||
| SharedFenceEGLSyncExportInfo: 0x0005003D, | ||
| DawnInjectedInvalidSType: 0x0005003E, | ||
| DawnCompilationMessageUtf16: 0x0005003F, | ||
| DawnFakeBufferOOMForTesting: 0x00050040, | ||
| SurfaceDescriptorFromWindowsWinUISwapChainPanel: 0x00050041, | ||
| DawnDeviceAllocatorControl: 0x00050042, | ||
| Force32: 0x7FFFFFFF | ||
| } as const; | ||
| export const WGPUCompareFunction = defineEnum({ | ||
| undefined: 0, | ||
| never: 1, | ||
| less: 2, | ||
| equal: 3, | ||
| "less-equal": 4, | ||
| greater: 5, | ||
| "not-equal": 6, | ||
| "greater-equal": 7, | ||
| always: 8, | ||
| "force-32": 0x7FFFFFFF | ||
| }); | ||
| export const WGPUErrorFilter = defineEnum({ | ||
| validation: 0x00000001, | ||
| "out-of-memory": 0x00000002, | ||
| internal: 0x00000003, | ||
| "force-32": 0x7FFFFFFF | ||
| }); | ||
| export const WGPUStringView = defineStruct([ | ||
| ['data', 'char*', { optional: true }], | ||
| ['length', 'u64'], | ||
| ], { | ||
| mapValue: (v: string | null | undefined) => { | ||
| if (!v) { | ||
| return { | ||
| data: null, | ||
| length: WGPU_STRLEN, | ||
| }; | ||
| } | ||
| return { | ||
| data: v, | ||
| length: Buffer.byteLength(v), | ||
| }; | ||
| }, | ||
| reduceValue: (v: { data: Pointer | null; length: bigint }) => { | ||
| if (v.data === null || v.length === 0n) { | ||
| return ''; | ||
| } | ||
| const buffer = toArrayBuffer(v.data, 0, Number(v.length) || 0); | ||
| return new TextDecoder().decode(buffer); | ||
| }, | ||
| }); | ||
| export function pointerValue(ptr: Pointer | null): bigint { | ||
| const value = ptr ? BigInt(ptr.valueOf()) : 0n; | ||
| if (value === 0n) { | ||
| fatalError("Pointer is null before FFI call!"); | ||
| } | ||
| return value; | ||
| } | ||
| export const PowerPreference = defineEnum({ | ||
| undefined: 0, | ||
| 'low-power': 1, | ||
| 'high-performance': 2, | ||
| }); | ||
| export const WGPUBackendType = defineEnum({ | ||
| Undefined: 0x00000000, | ||
| Null: 0x00000001, | ||
| WebGPU: 0x00000002, | ||
| D3D11: 0x00000003, | ||
| D3D12: 0x00000004, | ||
| Metal: 0x00000005, | ||
| Vulkan: 0x00000006, | ||
| OpenGL: 0x00000007, | ||
| OpenGLES: 0x00000008, | ||
| Force32: 0x7FFFFFFF | ||
| }); | ||
| export const WGPUFeatureLevel = defineEnum({ | ||
| undefined: 0x00000000, | ||
| compatibility: 0x00000001, | ||
| core: 0x00000002, | ||
| force32: 0x7FFFFFFF | ||
| }); | ||
| export const WGPURequestAdapterOptionsStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['featureLevel', WGPUFeatureLevel, { optional: true }], | ||
| ['powerPreference', PowerPreference, { optional: true }], | ||
| ['forceFallbackAdapter', WGPUBool, { optional: true }], | ||
| ['backendType', WGPUBackendType, { optional: true }], | ||
| ['compatibleSurface', 'pointer', { optional: true }], | ||
| ]); | ||
| export const WGPUCallbackInfoStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['mode', WGPUCallbackModeDef], | ||
| ['callback', 'pointer'], | ||
| ['userdata1', 'pointer', { optional: true }], | ||
| ['userdata2', 'pointer', { optional: true }], | ||
| ]); | ||
| export const WGPUChainedStructStruct = defineStruct([ | ||
| ['next', 'pointer', { optional: true }], | ||
| ['sType', 'u32'] | ||
| ]); | ||
| // TODO: Align with https://gpuweb.github.io/gpuweb/#enumdef-gpufeaturename | ||
| export const WGPUFeatureNameDef = defineEnum({ | ||
| 'depth-clip-control': 0x00000001, | ||
| 'depth32float-stencil8': 0x00000002, | ||
| 'timestamp-query': 0x00000003, | ||
| 'texture-compression-bc': 0x00000004, | ||
| 'texture-compression-bc-sliced-3d': 0x00000005, | ||
| 'texture-compression-etc2': 0x00000006, | ||
| 'texture-compression-astc': 0x00000007, | ||
| 'texture-compression-astc-sliced-3d': 0x00000008, | ||
| 'indirect-first-instance': 0x00000009, | ||
| 'shader-f16': 0x0000000A, | ||
| 'rg11b10ufloat-renderable': 0x0000000B, | ||
| 'bgra8unorm-storage': 0x0000000C, | ||
| 'float32-filterable': 0x0000000D, | ||
| 'float32-blendable': 0x0000000E, | ||
| 'clip-distances': 0x0000000F, | ||
| 'dual-source-blending': 0x00000010, | ||
| 'subgroups': 0x00000011, | ||
| 'core-features-and-limits': 0x00000012, | ||
| 'dawn-internal-usages': 0x00050000, | ||
| 'dawn-multi-planar-formats': 0x00050001, | ||
| 'dawn-native': 0x00050002, | ||
| 'chromium-experimental-timestamp-query-inside-passes': 0x00050003, | ||
| 'implicit-device-synchronization': 0x00050004, | ||
| 'chromium-experimental-immediate-data': 0x00050005, | ||
| 'transient-attachments': 0x00050006, | ||
| 'msaa-render-to-single-sampled': 0x00050007, | ||
| 'subgroups-f16': 0x00050008, | ||
| 'd3d11-multithread-protected': 0x00050009, | ||
| 'angle-texture-sharing': 0x0005000A, | ||
| 'pixel-local-storage-coherent': 0x0005000B, | ||
| 'pixel-local-storage-non-coherent': 0x0005000C, | ||
| 'unorm16-texture-formats': 0x0005000D, | ||
| 'snorm16-texture-formats': 0x0005000E, | ||
| 'multi-planar-format-extended-usages': 0x0005000F, | ||
| 'multi-planar-format-p010': 0x00050010, | ||
| 'host-mapped-pointer': 0x00050011, | ||
| 'multi-planar-render-targets': 0x00050012, | ||
| 'multi-planar-format-nv12a': 0x00050013, | ||
| 'framebuffer-fetch': 0x00050014, | ||
| 'buffer-map-extended-usages': 0x00050015, | ||
| 'adapter-properties-memory-heaps': 0x00050016, | ||
| 'adapter-properties-d3d': 0x00050017, | ||
| 'adapter-properties-vk': 0x00050018, | ||
| 'r8-unorm-storage': 0x00050019, | ||
| 'dawn-format-capabilities': 0x0005001A, | ||
| 'dawn-drm-format-capabilities': 0x0005001B, | ||
| 'norm16-texture-formats': 0x0005001C, | ||
| 'multi-planar-format-nv16': 0x0005001D, | ||
| 'multi-planar-format-nv24': 0x0005001E, | ||
| 'multi-planar-format-p210': 0x0005001F, | ||
| 'multi-planar-format-p410': 0x00050020, | ||
| 'shared-texture-memory-vk-dedicated-allocation': 0x00050021, | ||
| 'shared-texture-memory-a-hardware-buffer': 0x00050022, | ||
| 'shared-texture-memory-dma-buf': 0x00050023, | ||
| 'shared-texture-memory-opaque-fd': 0x00050024, | ||
| 'shared-texture-memory-zircon-handle': 0x00050025, | ||
| 'shared-texture-memory-dxgi-shared-handle': 0x00050026, | ||
| 'shared-texture-memory-d3d11-texture2d': 0x00050027, | ||
| 'shared-texture-memory-iosurface': 0x00050028, | ||
| 'shared-texture-memory-egl-image': 0x00050029, | ||
| 'shared-fence-vk-semaphore-opaque-fd': 0x0005002A, | ||
| 'shared-fence-sync-fd': 0x0005002B, | ||
| 'shared-fence-vk-semaphore-zircon-handle': 0x0005002C, | ||
| 'shared-fence-dxgi-shared-handle': 0x0005002D, | ||
| 'shared-fence-mtl-shared-event': 0x0005002E, | ||
| 'shared-buffer-memory-d3d12-resource': 0x0005002F, | ||
| 'static-samplers': 0x00050030, | ||
| 'ycbcr-vulkan-samplers': 0x00050031, | ||
| 'shader-module-compilation-options': 0x00050032, | ||
| 'dawn-load-resolve-texture': 0x00050033, | ||
| 'dawn-partial-load-resolve-texture': 0x00050034, | ||
| 'multi-draw-indirect': 0x00050035, | ||
| 'dawn-texel-copy-buffer-row-alignment': 0x00050036, | ||
| 'flexible-texture-views': 0x00050037, | ||
| 'chromium-experimental-subgroup-matrix': 0x00050038, | ||
| 'shared-fence-egl-sync': 0x00050039, | ||
| 'dawn-device-allocator-control': 0x0005003A, | ||
| 'force-32': 0x7FFFFFFF | ||
| }, 'u32'); | ||
| export const WGPUTextureFormat = defineEnum({ | ||
| // Basic formats | ||
| // Used for testing, causes uncaptured validation errors | ||
| undefined: 0x00000000, | ||
| r8unorm: 0x00000001, | ||
| r8snorm: 0x00000002, | ||
| r8uint: 0x00000003, | ||
| r8sint: 0x00000004, | ||
| r16uint: 0x00000005, | ||
| r16sint: 0x00000006, | ||
| r16float: 0x00000007, | ||
| rg8unorm: 0x00000008, | ||
| rg8snorm: 0x00000009, | ||
| rg8uint: 0x0000000A, | ||
| rg8sint: 0x0000000B, | ||
| r32float: 0x0000000C, | ||
| r32uint: 0x0000000D, | ||
| r32sint: 0x0000000E, | ||
| rg16uint: 0x0000000F, | ||
| rg16sint: 0x00000010, | ||
| rg16float: 0x00000011, | ||
| // RGBA formats | ||
| rgba8unorm: 0x00000012, | ||
| "rgba8unorm-srgb": 0x00000013, | ||
| rgba8snorm: 0x00000014, | ||
| rgba8uint: 0x00000015, | ||
| rgba8sint: 0x00000016, | ||
| bgra8unorm: 0x00000017, | ||
| "bgra8unorm-srgb": 0x00000018, | ||
| rgb10a2uint: 0x00000019, | ||
| rgb10a2unorm: 0x0000001A, | ||
| rg11b10ufloat: 0x0000001B, | ||
| rgb9e5ufloat: 0x0000001C, | ||
| // High precision formats | ||
| rg32float: 0x0000001D, | ||
| rg32uint: 0x0000001E, | ||
| rg32sint: 0x0000001F, | ||
| rgba16uint: 0x00000020, | ||
| rgba16sint: 0x00000021, | ||
| rgba16float: 0x00000022, | ||
| rgba32float: 0x00000023, | ||
| rgba32uint: 0x00000024, | ||
| rgba32sint: 0x00000025, | ||
| // Depth/stencil formats | ||
| stencil8: 0x00000026, | ||
| depth16unorm: 0x00000027, | ||
| depth24plus: 0x00000028, | ||
| "depth24plus-stencil8": 0x00000029, | ||
| depth32float: 0x0000002A, | ||
| "depth32float-stencil8": 0x0000002B, | ||
| // BC compressed formats | ||
| "bc1-rgba-unorm": 0x0000002C, | ||
| "bc1-rgba-unorm-srgb": 0x0000002D, | ||
| "bc2-rgba-unorm": 0x0000002E, | ||
| "bc2-rgba-unorm-srgb": 0x0000002F, | ||
| "bc3-rgba-unorm": 0x00000030, | ||
| "bc3-rgba-unorm-srgb": 0x00000031, | ||
| "bc4-r-unorm": 0x00000032, | ||
| "bc4-r-snorm": 0x00000033, | ||
| "bc5-rg-unorm": 0x00000034, | ||
| "bc5-rg-snorm": 0x00000035, | ||
| "bc6h-rgb-ufloat": 0x00000036, | ||
| "bc6h-rgb-float": 0x00000037, | ||
| "bc7-rgba-unorm": 0x00000038, | ||
| "bc7-rgba-unorm-srgb": 0x00000039, | ||
| // ETC2/EAC compressed formats | ||
| "etc2-rgb8unorm": 0x0000003A, | ||
| "etc2-rgb8unorm-srgb": 0x0000003B, | ||
| "etc2-rgb8a1unorm": 0x0000003C, | ||
| "etc2-rgb8a1unorm-srgb": 0x0000003D, | ||
| "etc2-rgba8unorm": 0x0000003E, | ||
| "etc2-rgba8unorm-srgb": 0x0000003F, | ||
| "eac-r11unorm": 0x00000040, | ||
| "eac-r11snorm": 0x00000041, | ||
| "eac-rg11unorm": 0x00000042, | ||
| "eac-rg11snorm": 0x00000043, | ||
| // ASTC compressed formats | ||
| "astc-4x4-unorm": 0x00000044, | ||
| "astc-4x4-unorm-srgb": 0x00000045, | ||
| "astc-5x4-unorm": 0x00000046, | ||
| "astc-5x4-unorm-srgb": 0x00000047, | ||
| "astc-5x5-unorm": 0x00000048, | ||
| "astc-5x5-unorm-srgb": 0x00000049, | ||
| "astc-6x5-unorm": 0x0000004A, | ||
| "astc-6x5-unorm-srgb": 0x0000004B, | ||
| "astc-6x6-unorm": 0x0000004C, | ||
| "astc-6x6-unorm-srgb": 0x0000004D, | ||
| "astc-8x5-unorm": 0x0000004E, | ||
| "astc-8x5-unorm-srgb": 0x0000004F, | ||
| "astc-8x6-unorm": 0x00000050, | ||
| "astc-8x6-unorm-srgb": 0x00000051, | ||
| "astc-8x8-unorm": 0x00000052, | ||
| "astc-8x8-unorm-srgb": 0x00000053, | ||
| "astc-10x5-unorm": 0x00000054, | ||
| "astc-10x5-unorm-srgb": 0x00000055, | ||
| "astc-10x6-unorm": 0x00000056, | ||
| "astc-10x6-unorm-srgb": 0x00000057, | ||
| "astc-10x8-unorm": 0x00000058, | ||
| "astc-10x8-unorm-srgb": 0x00000059, | ||
| "astc-10x10-unorm": 0x0000005A, | ||
| "astc-10x10-unorm-srgb": 0x0000005B, | ||
| "astc-12x10-unorm": 0x0000005C, | ||
| "astc-12x10-unorm-srgb": 0x0000005D, | ||
| "astc-12x12-unorm": 0x0000005E, | ||
| "astc-12x12-unorm-srgb": 0x0000005F, | ||
| // Dawn-specific formats (0x00050000 range) | ||
| r16unorm: 0x00050000, | ||
| rg16unorm: 0x00050001, | ||
| rgba16unorm: 0x00050002, | ||
| r16snorm: 0x00050003, | ||
| rg16snorm: 0x00050004, | ||
| rgba16snorm: 0x00050005, | ||
| "r8bg8-biplanar-420unorm": 0x00050006, | ||
| "r10x6bg10x6-biplanar-420unorm": 0x00050007, | ||
| "r8bg8a8-triplanar-420unorm": 0x00050008, | ||
| "r8bg8-biplanar-422unorm": 0x00050009, | ||
| "r8bg8-biplanar-444unorm": 0x0005000A, | ||
| "r10x6bg10x6-biplanar-422unorm": 0x0005000B, | ||
| "r10x6bg10x6-biplanar-444unorm": 0x0005000C, | ||
| external: 0x0005000D, | ||
| } as const, 'u32'); | ||
| export const WGPUWGSLLanguageFeatureNameDef = defineEnum({ | ||
| readonly_and_readwrite_storage_textures: 0x00000001, | ||
| packed_4x8_integer_dot_product: 0x00000002, | ||
| unrestricted_pointer_parameters: 0x00000003, | ||
| pointer_composite_access: 0x00000004, | ||
| sized_binding_array: 0x00000005, | ||
| chromium_testing_unimplemented: 0x00050000, | ||
| chromium_testing_unsafe_experimental: 0x00050001, | ||
| chromium_testing_experimental: 0x00050002, | ||
| chromium_testing_shipped_with_killswitch: 0x00050003, | ||
| chromium_testing_shipped: 0x00050004, | ||
| force_32: 0x7FFFFFFF | ||
| }, 'u32'); | ||
| export const WGPUSupportedFeaturesStruct = defineStruct([ | ||
| ['featureCount', 'u64', { unpackTransform: (val: bigint) => Number(val), lengthOf: 'features' }], | ||
| ['features', [WGPUFeatureNameDef]], | ||
| ]); | ||
| export const WGPUSupportedWGSLLanguageFeaturesStruct = defineStruct([ | ||
| ['featureCount', 'u64', { unpackTransform: (val: bigint) => Number(val), lengthOf: 'features' }], | ||
| ['features', [WGPUWGSLLanguageFeatureNameDef]], | ||
| ]); | ||
| function validateMutipleOf(val: number, multipleOf: number) { | ||
| const mod = val % multipleOf; | ||
| if (mod !== 0) { | ||
| throw new OperationError(`Value must be a multiple of ${multipleOf}, got ${val}`); | ||
| } | ||
| } | ||
| function validateRange(val: number, min: number, max: number) { | ||
| if (val < 0 || val > Number.MAX_SAFE_INTEGER) { | ||
| throw new TypeError(`Value must be between 0 and ${Number.MAX_SAFE_INTEGER}, got ${val}`); | ||
| } | ||
| if (val < min || val > max) { | ||
| throw new OperationError(`Value must be between ${min} and ${max}, got ${val}`); | ||
| } | ||
| } | ||
| function minValidator(val: number, fieldName: string, { hints }: { hints?: { limits: GPUSupportedLimits, features: GPUSupportedFeatures } } = {}) { | ||
| if (val < 0 || val > Number.MAX_SAFE_INTEGER) { | ||
| throw new TypeError(`Value must be between 0 and ${Number.MAX_SAFE_INTEGER}, got ${val}`); | ||
| } | ||
| if (hints && fieldName in hints.limits) { | ||
| const minValue = hints.limits[fieldName as keyof GPUSupportedLimits] as number; | ||
| if (val < minValue) { | ||
| throw new OperationError(`Value must be >= ${minValue}, got ${val}`); | ||
| } | ||
| } | ||
| } | ||
| function validateLimitField(val: number, fieldName: string, { hints }: { hints?: { limits: GPUSupportedLimits, features: GPUSupportedFeatures } } = {}) { | ||
| if (hints && fieldName in hints.limits) { | ||
| const maxValue = hints.limits[fieldName as keyof GPUSupportedLimits] as number; | ||
| validateRange(val, 0, maxValue); | ||
| } | ||
| } | ||
| // WGPULimits struct mirroring C layout | ||
| export const WGPULimitsStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['maxTextureDimension1D', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxTextureDimension1D, validate: validateLimitField }], | ||
| ['maxTextureDimension2D', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxTextureDimension2D, validate: validateLimitField }], | ||
| ['maxTextureDimension3D', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxTextureDimension3D, validate: validateLimitField }], | ||
| ['maxTextureArrayLayers', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxTextureArrayLayers, validate: validateLimitField }], | ||
| ['maxBindGroups', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxBindGroups, validate: validateLimitField }], | ||
| ['maxBindGroupsPlusVertexBuffers', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxBindGroupsPlusVertexBuffers, validate: validateLimitField }], | ||
| ['maxBindingsPerBindGroup', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxBindingsPerBindGroup, validate: validateLimitField }], | ||
| ['maxDynamicUniformBuffersPerPipelineLayout', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxDynamicUniformBuffersPerPipelineLayout, validate: validateLimitField }], | ||
| ['maxDynamicStorageBuffersPerPipelineLayout', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxDynamicStorageBuffersPerPipelineLayout, validate: validateLimitField }], | ||
| ['maxSampledTexturesPerShaderStage', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxSampledTexturesPerShaderStage, validate: validateLimitField }], | ||
| ['maxSamplersPerShaderStage', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxSamplersPerShaderStage, validate: validateLimitField }], | ||
| ['maxStorageBuffersPerShaderStage', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxStorageBuffersPerShaderStage, validate: validateLimitField }], | ||
| ['maxStorageTexturesPerShaderStage', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxStorageTexturesPerShaderStage, validate: validateLimitField }], | ||
| ['maxUniformBuffersPerShaderStage', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxUniformBuffersPerShaderStage, validate: validateLimitField }], | ||
| ['maxUniformBufferBindingSize', 'u64', { default: DEFAULT_SUPPORTED_LIMITS.maxUniformBufferBindingSize, validate: validateLimitField }], | ||
| ['maxStorageBufferBindingSize', 'u64', { default: DEFAULT_SUPPORTED_LIMITS.maxStorageBufferBindingSize, validate: validateLimitField }], | ||
| ['minUniformBufferOffsetAlignment', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.minUniformBufferOffsetAlignment, validate: [minValidator, (val: number) => validateMutipleOf(val, 2)] }], | ||
| ['minStorageBufferOffsetAlignment', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.minStorageBufferOffsetAlignment, validate: [minValidator, (val: number) => validateMutipleOf(val, 2)] }], | ||
| ['maxVertexBuffers', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxVertexBuffers, validate: validateLimitField }], | ||
| ['maxBufferSize', 'u64', { default: DEFAULT_SUPPORTED_LIMITS.maxBufferSize, validate: validateLimitField }], | ||
| ['maxVertexAttributes', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxVertexAttributes, validate: validateLimitField }], | ||
| ['maxVertexBufferArrayStride', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxVertexBufferArrayStride, validate: validateLimitField }], | ||
| ['maxInterStageShaderVariables', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxInterStageShaderVariables, validate: validateLimitField }], | ||
| ['maxColorAttachments', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxColorAttachments, validate: validateLimitField }], | ||
| ['maxColorAttachmentBytesPerSample', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxColorAttachmentBytesPerSample, validate: validateLimitField }], | ||
| ['maxComputeWorkgroupStorageSize', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxComputeWorkgroupStorageSize, validate: validateLimitField }], | ||
| ['maxComputeInvocationsPerWorkgroup', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxComputeInvocationsPerWorkgroup, validate: validateLimitField }], | ||
| ['maxComputeWorkgroupSizeX', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxComputeWorkgroupSizeX, validate: validateLimitField }], | ||
| ['maxComputeWorkgroupSizeY', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxComputeWorkgroupSizeY, validate: validateLimitField }], | ||
| ['maxComputeWorkgroupSizeZ', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxComputeWorkgroupSizeZ, validate: validateLimitField }], | ||
| ['maxComputeWorkgroupsPerDimension', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxComputeWorkgroupsPerDimension, validate: validateLimitField }], | ||
| ['maxImmediateSize', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxImmediateSize, validate: validateLimitField }], | ||
| ['maxStorageBuffersInVertexStage', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxStorageBuffersInVertexStage, validate: validateLimitField }], | ||
| ['maxStorageTexturesInVertexStage', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxStorageTexturesInVertexStage, validate: validateLimitField }], | ||
| ['maxStorageBuffersInFragmentStage', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxStorageBuffersInFragmentStage, validate: validateLimitField }], | ||
| ['maxStorageTexturesInFragmentStage', 'u32', { default: DEFAULT_SUPPORTED_LIMITS.maxStorageTexturesInFragmentStage, validate: validateLimitField }], | ||
| ], { | ||
| default: { | ||
| ...DEFAULT_SUPPORTED_LIMITS, | ||
| } | ||
| }); | ||
| // Corresponding TypeScript type for convenience | ||
| export type WGPULimits = GPUSupportedLimits & { | ||
| nextInChain?: Pointer | null; | ||
| }; | ||
| // WGPUQueueDescriptor struct mirroring C layout | ||
| export type WGPUQueueDescriptor = { | ||
| nextInChain?: Pointer | null; | ||
| label?: string; | ||
| }; | ||
| export const WGPUQueueDescriptorStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['label', WGPUStringView, { optional: true }], | ||
| ]); | ||
| // WGPUUncapturedErrorCallbackInfo + Callback FFI type | ||
| export type WGPUUncapturedErrorCallbackInfo = { | ||
| nextInChain?: Pointer | null; | ||
| callback: Pointer; | ||
| userdata1?: Pointer | null; | ||
| userdata2?: Pointer | null; | ||
| }; | ||
| export const WGPUUncapturedErrorCallbackInfoStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['callback', 'pointer'], | ||
| ['userdata1', 'pointer', { optional: true }], | ||
| ['userdata2', 'pointer', { optional: true }], | ||
| ]); | ||
| export const WGPUAdapterInfoStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['vendor', WGPUStringView], | ||
| ['architecture', WGPUStringView], | ||
| ['device', WGPUStringView], | ||
| ['description', WGPUStringView], | ||
| ['backendType', 'u32'], | ||
| ['adapterType', 'u32'], | ||
| ['vendorID', 'u32'], | ||
| ['deviceID', 'u32'], | ||
| ['subgroupMinSize', 'u32'], | ||
| ['subgroupMaxSize', 'u32'], | ||
| ]); | ||
| export const WGPUDeviceDescriptorStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['label', WGPUStringView, { optional: true }], | ||
| ['requiredFeatureCount', 'u64', { lengthOf: 'requiredFeatures' }], // Assuming 64-bit size_t | ||
| ['requiredFeatures', [WGPUFeatureNameDef], { optional: true, validate: (val: string[] | undefined, fieldName: string, { hints }: { hints?: { limits: GPUSupportedLimits, features: GPUSupportedFeatures } } = {}) => { | ||
| if (!val) { | ||
| return; | ||
| } | ||
| for (const feature of val) { | ||
| if (!hints?.features.has(feature)) { | ||
| throw new TypeError(`Invalid feature required: ${feature}`); | ||
| } | ||
| } | ||
| } }], | ||
| ['requiredLimits', WGPULimitsStruct, { optional: true, asPointer: true, validate: (val: WGPULimits | undefined, fieldName: string, { hints }: { hints?: { limits: GPUSupportedLimits, features: GPUSupportedFeatures } } = {}) => { | ||
| if (!val) { | ||
| return; | ||
| } | ||
| for (const key in val) { | ||
| if (hints?.limits && !(key in hints?.limits) && val[key as keyof WGPULimits] !== undefined) { | ||
| throw new OperationError(`Invalid limit required: ${key} ${val[key as keyof WGPULimits]}`); | ||
| } | ||
| } | ||
| } }], | ||
| ['defaultQueue', WGPUQueueDescriptorStruct], | ||
| ['deviceLostCallbackInfo', WGPUCallbackInfoStruct, { optional: true }], | ||
| ['uncapturedErrorCallbackInfo', WGPUUncapturedErrorCallbackInfoStruct], | ||
| ]); | ||
| export const WGPUBufferDescriptorStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['label', WGPUStringView, { optional: true }], | ||
| ['usage', 'u64'], | ||
| ['size', 'u64'], | ||
| ['mappedAtCreation', WGPUBool, { default: false }], | ||
| ]); | ||
| export function normalizeGPUExtent3DStrict(size: GPUExtent3DStrict): { width: number; height?: number; depthOrArrayLayers?: number } { | ||
| if (Symbol.iterator in size) { | ||
| const arr = Array.from(size); | ||
| return { | ||
| width: arr[0] ?? 1, | ||
| height: arr[1] ?? 1, | ||
| depthOrArrayLayers: arr[2] ?? 1 | ||
| }; | ||
| } | ||
| return size; | ||
| } | ||
| export const WGPUExtent3DStruct = defineStruct([ | ||
| ['width', 'u32'], | ||
| ['height', 'u32', { default: 1 }], | ||
| ['depthOrArrayLayers', 'u32', { default: 1 }], | ||
| ], { | ||
| mapValue: (v: GPUExtent3DStrict) => { | ||
| return normalizeGPUExtent3DStrict(v); | ||
| } | ||
| }); | ||
| export const WGPUTextureDimension = defineEnum({ | ||
| undefined: 0, | ||
| "1d": 1, | ||
| "2d": 2, | ||
| "3d": 3, | ||
| "force-32": 0x7FFFFFFF | ||
| }); | ||
| export const WGPUTextureDescriptorStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['label', WGPUStringView, { optional: true }], | ||
| ['usage', 'u64'], | ||
| ['dimension', WGPUTextureDimension, { default: '2d' }], | ||
| ['size', WGPUExtent3DStruct], | ||
| ['format', WGPUTextureFormat, { default: 'rgba8unorm' }], | ||
| ['mipLevelCount', 'u32', { default: 1 }], | ||
| ['sampleCount', 'u32', { default: 1 }], | ||
| ['viewFormatCount', 'u64', { lengthOf: 'viewFormats' }], | ||
| ['viewFormats', [WGPUTextureFormat], { optional: true }], | ||
| ]); | ||
| export const WGPUFilterMode = defineEnum({ | ||
| undefined: 0, | ||
| nearest: 1, | ||
| linear: 2, | ||
| "force-32": 0x7FFFFFFF | ||
| }); | ||
| export const WGPUMipmapFilterMode = defineEnum({ | ||
| undefined: 0, | ||
| nearest: 1, | ||
| linear: 2, | ||
| "force-32": 0x7FFFFFFF | ||
| }); | ||
| export const WGPUAddressMode = defineEnum({ | ||
| undefined: 0, | ||
| 'clamp-to-edge': 1, | ||
| repeat: 2, | ||
| 'mirror-repeat': 3, | ||
| "force-32": 0x7FFFFFFF | ||
| }); | ||
| export const WGPUSamplerDescriptorStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['label', WGPUStringView, { optional: true }], | ||
| ['addressModeU', WGPUAddressMode, { default: 'undefined' }], | ||
| ['addressModeV', WGPUAddressMode, { default: 'undefined' }], | ||
| ['addressModeW', WGPUAddressMode, { default: 'undefined' }], | ||
| ['magFilter', WGPUFilterMode, { default: 'undefined' }], | ||
| ['minFilter', WGPUFilterMode, { default: 'undefined' }], | ||
| ['mipmapFilter', WGPUMipmapFilterMode, { default: 'undefined' }], | ||
| ['lodMinClamp', 'f32', { default: 0.0 }], | ||
| ['lodMaxClamp', 'f32', { default: 32.0 }], | ||
| ['compare', WGPUCompareFunction, { default: 'undefined' }], | ||
| ['maxAnisotropy', 'u16', { default: 1, packTransform: (val: number) => val < 0 ? 0 : val }], | ||
| ]); | ||
| export const WGPUBufferBindingType = defineEnum({ | ||
| "binding-not-used": 0, | ||
| undefined: 1, | ||
| uniform: 2, | ||
| storage: 3, | ||
| "read-only-storage": 4, | ||
| }); | ||
| export const WGPUSamplerBindingType = defineEnum({ | ||
| "binding-not-used": 0, | ||
| undefined: 1, | ||
| filtering: 2, | ||
| "non-filtering": 3, | ||
| comparison: 4, | ||
| }); | ||
| export const WGPUTextureSampleType = defineEnum({ | ||
| "binding-not-used": 0, | ||
| undefined: 1, | ||
| float: 2, | ||
| "unfilterable-float": 3, | ||
| depth: 4, | ||
| sint: 5, | ||
| uint: 6, | ||
| }); | ||
| export const WGPUTextureViewDimension = defineEnum({ | ||
| "undefined": 0, | ||
| "1d": 1, | ||
| "2d": 2, | ||
| "2d-array": 3, | ||
| cube: 4, | ||
| "cube-array": 5, | ||
| "3d": 6, | ||
| }); | ||
| export const WGPUStorageTextureAccess = defineEnum({ | ||
| "binding-not-used": 0, | ||
| undefined: 1, | ||
| "write-only": 2, | ||
| "read-only": 3, | ||
| "read-write": 4, | ||
| }); | ||
| export const WGPUBufferBindingLayoutStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['type', WGPUBufferBindingType, { default: 'uniform' }], | ||
| ['hasDynamicOffset', WGPUBool, { default: false }], | ||
| ['minBindingSize', 'u64', { default: 0 }], | ||
| ]); | ||
| export const WGPUSamplerBindingLayoutStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['type', WGPUSamplerBindingType, { default: 'filtering' }], | ||
| ]); | ||
| export const WGPUTextureBindingLayoutStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['sampleType', WGPUTextureSampleType, { default: 'float' }], | ||
| ['viewDimension', WGPUTextureViewDimension, { default: '2d' }], | ||
| ['multisampled', WGPUBool, { default: false }], | ||
| ]); | ||
| export const WGPUStorageTextureBindingLayoutStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['access', WGPUStorageTextureAccess, { default: 'write-only' }], | ||
| ['format', WGPUTextureFormat, { default: 'rgba8unorm' }], | ||
| ['viewDimension', WGPUTextureViewDimension, { default: '2d' }], | ||
| ]); | ||
| export const WGPUTextureAspect = defineEnum({ | ||
| undefined: 0, | ||
| all: 1, | ||
| "stencil-only": 2, | ||
| "depth-only": 3, | ||
| "plane-0-only": 0x00050000, // Dawn specific? | ||
| "plane-1-only": 0x00050001, // Dawn specific? | ||
| "plane-2-only": 0x00050002, // Dawn specific? | ||
| "force-32": 0x7FFFFFFF // Ensure correct name 'force-32' if needed | ||
| }, 'u32'); | ||
| export const WGPUTextureViewDescriptorStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['label', WGPUStringView, { optional: true }], | ||
| ['format', WGPUTextureFormat, { default: 'undefined' }], | ||
| ['dimension', WGPUTextureViewDimension, { default: 'undefined' }], | ||
| ['baseMipLevel', 'u32', { default: 0 }], | ||
| ['mipLevelCount', 'u32', { default: 0xFFFFFFFF }], // WGPU_MIP_LEVEL_COUNT_UNDEFINED | ||
| ['baseArrayLayer', 'u32', { default: 0 }], | ||
| ['arrayLayerCount', 'u32', { default: 0xFFFFFFFF }], // WGPU_ARRAY_LAYER_COUNT_UNDEFINED | ||
| ['aspect', WGPUTextureAspect, { default: 'all' }], // Default 'all' is more useful than 'undefined' | ||
| ['usage', 'u64', { default: 0n }], // WGPUTextureUsage_None - C definition uses flags (u64) | ||
| ]); | ||
| export const WGPUExternalTextureBindingLayoutStruct = defineStruct([ | ||
| ['chain', WGPUChainedStructStruct], | ||
| ]); | ||
| export const WGPUBindGroupLayoutEntryStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['binding', 'u32'], | ||
| ['visibility', 'u64'], | ||
| // Weird | ||
| // With some builds this is needed, but others not, unclear when and why. | ||
| // Need to check the gcc/g++ versions and what causes the mismatch. | ||
| // Interestingly, it works with either u32 or u64. | ||
| // it then fails with: "Error: Unexpected validation error occurred: BindGroupLayoutEntry had none of buffer, sampler, texture, storageTexture, or externalTexture set" | ||
| ['_alignment0', 'u32', { default: 0, condition: () => process.platform !== 'win32' }], | ||
| ['buffer', WGPUBufferBindingLayoutStruct, { optional: true }], | ||
| ['sampler', WGPUSamplerBindingLayoutStruct, { optional: true }], | ||
| ['texture', WGPUTextureBindingLayoutStruct, { optional: true }], | ||
| ['storageTexture', WGPUStorageTextureBindingLayoutStruct, { optional: true }], | ||
| ]); | ||
| export const WGPUBindGroupLayoutDescriptorStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['label', WGPUStringView, { optional: true }], | ||
| ['entryCount', 'u64', { lengthOf: 'entries' }], | ||
| ['entries', [WGPUBindGroupLayoutEntryStruct]], | ||
| ]); | ||
| export const WGPUBindGroupEntryStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['binding', 'u32'], | ||
| ['buffer', objectPtr<GPUBuffer>(), { optional: true }], | ||
| ['offset', 'u64', { optional: true }], // Optional because only relevant for buffers | ||
| ['size', 'u64', { optional: true }], // Optional because only relevant for buffers | ||
| ['sampler', objectPtr<GPUSampler>(), { optional: true }], | ||
| ['textureView', objectPtr<GPUTextureView>(), { optional: true }], | ||
| ]); | ||
| export const WGPUBindGroupDescriptorStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['label', WGPUStringView, { optional: true }], | ||
| ['layout', objectPtr<GPUBindGroupLayout>()], | ||
| ['entryCount', 'u64', { lengthOf: 'entries' }], | ||
| ['entries', [WGPUBindGroupEntryStruct]], | ||
| ]); | ||
| export const WGPUPipelineLayoutDescriptorStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['label', WGPUStringView, { optional: true }], | ||
| ['bindGroupLayoutCount', 'u64', { lengthOf: 'bindGroupLayouts' }], | ||
| ['bindGroupLayouts', ['pointer']], | ||
| ['immediateSize', 'u32', { default: 0 }] | ||
| ]); | ||
| // Pack shader module descriptor | ||
| export const WGPUShaderModuleDescriptorStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['label', WGPUStringView, { optional: true }] | ||
| ]); | ||
| // For WGSL shader source | ||
| export const WGPUShaderSourceWGSLStruct = defineStruct([ | ||
| ['chain', WGPUChainedStructStruct], | ||
| ['code', WGPUStringView] | ||
| ]); | ||
| // --- Render Pipeline Structs --- | ||
| export const WGPUVertexStepMode = defineEnum({ | ||
| undefined: 0, | ||
| vertex: 1, | ||
| instance: 2, | ||
| }); | ||
| export const WGPUPrimitiveTopology = defineEnum({ | ||
| undefined: 0, | ||
| "point-list": 1, | ||
| "line-list": 2, | ||
| "line-strip": 3, | ||
| "triangle-list": 4, | ||
| "triangle-strip": 5, | ||
| }); | ||
| export const WGPUIndexFormat = defineEnum({ | ||
| undefined: 0, | ||
| uint16: 1, | ||
| uint32: 2, | ||
| }); | ||
| export const WGPUFrontFace = defineEnum({ | ||
| undefined: 0, | ||
| ccw: 1, | ||
| cw: 2, | ||
| }); | ||
| export const WGPUCullMode = defineEnum({ | ||
| undefined: 0, | ||
| none: 1, | ||
| front: 2, | ||
| back: 3, | ||
| }); | ||
| export const WGPUStencilOperation = defineEnum({ | ||
| undefined: 0, | ||
| keep: 1, | ||
| zero: 2, | ||
| replace: 3, | ||
| invert: 4, | ||
| "increment-clamp": 5, | ||
| "decrement-clamp": 6, | ||
| "increment-wrap": 7, | ||
| "decrement-wrap": 8, | ||
| }); | ||
| export const WGPUBlendOperation = defineEnum({ | ||
| undefined: 0, | ||
| add: 1, | ||
| subtract: 2, | ||
| "reverse-subtract": 3, | ||
| min: 4, | ||
| max: 5, | ||
| }); | ||
| export const WGPUBlendFactor = defineEnum({ | ||
| undefined: 0, | ||
| zero: 1, | ||
| one: 2, | ||
| src: 3, | ||
| "one-minus-src": 4, | ||
| "src-alpha": 5, | ||
| "one-minus-src-alpha": 6, | ||
| dst: 7, | ||
| "one-minus-dst": 8, | ||
| "dst-alpha": 9, | ||
| "one-minus-dst-alpha": 10, | ||
| "src-alpha-saturated": 11, | ||
| constant: 12, | ||
| "one-minus-constant": 13, | ||
| src1: 14, // Requires DualSourceBlending feature | ||
| "one-minus-src1": 15, // Requires DualSourceBlending feature | ||
| "src1-alpha": 16, // Requires DualSourceBlending feature | ||
| "one-minus-src1-alpha": 17, // Requires DualSourceBlending feature | ||
| }); | ||
| // WGPUColorWriteMask flags (using u64 for flags) | ||
| export const WGPUColorWriteMask = { | ||
| None: 0x0000000000000000n, | ||
| Red: 0x0000000000000001n, | ||
| Green: 0x0000000000000002n, | ||
| Blue: 0x0000000000000004n, | ||
| Alpha: 0x0000000000000008n, | ||
| All: 0x000000000000000Fn, | ||
| } as const; | ||
| // Note: This enum is not used in the C struct definition, but is part of the JS API | ||
| export const WGPUOptionalBool = defineEnum({ | ||
| False: 0, | ||
| True: 1, | ||
| Undefined: 2, // This might need mapping depending on how bools are handled (0/1 vs specific enum) | ||
| }); | ||
| // @ts-ignore TODO: Add missing formats from webgpu.h | ||
| export const WGPUVertexFormat = defineEnum({ | ||
| "uint8": 0x00000001, | ||
| "uint8x2": 0x00000002, | ||
| "uint8x4": 0x00000003, | ||
| "sint8": 0x00000004, | ||
| "sint8x2": 0x00000005, | ||
| "sint8x4": 0x00000006, | ||
| "unorm8": 0x00000007, | ||
| "unorm8x2": 0x00000008, | ||
| "unorm8x4": 0x00000009, | ||
| "snorm8": 0x0000000A, | ||
| "snorm8x2": 0x0000000B, | ||
| "snorm8x4": 0x0000000C, | ||
| "uint16": 0x0000000D, | ||
| "uint16x2": 0x0000000E, | ||
| "uint16x4": 0x0000000F, | ||
| "sint16": 0x00000010, | ||
| "sint16x2": 0x00000011, | ||
| "sint16x4": 0x00000012, | ||
| "unorm16": 0x00000013, | ||
| "unorm16x2": 0x00000014, | ||
| "unorm16x4": 0x00000015, | ||
| "snorm16": 0x00000016, | ||
| "snorm16x2": 0x00000017, | ||
| "snorm16x4": 0x00000018, | ||
| "float16": 0x00000019, | ||
| "float16x2": 0x0000001A, | ||
| "float16x4": 0x0000001B, | ||
| "float32": 0x0000001C, | ||
| "float32x2": 0x0000001D, | ||
| "float32x3": 0x0000001E, | ||
| "float32x4": 0x0000001F, | ||
| "uint32": 0x00000020, | ||
| "uint32x2": 0x00000021, | ||
| "uint32x3": 0x00000022, | ||
| "uint32x4": 0x00000023, | ||
| "sint32": 0x00000024, | ||
| "sint32x2": 0x00000025, | ||
| "sint32x3": 0x00000026, | ||
| "sint32x4": 0x00000027, | ||
| "unorm10-10-10-2": 0x00000028, | ||
| "unorm8x4-bgra": 0x00000029, | ||
| "force32": 0x7FFFFFFF | ||
| }, 'u32'); | ||
| // -- Nested Structs needed for Render Pipeline -- | ||
| export const WGPUConstantEntryStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['key', WGPUStringView], | ||
| ['value', 'f64', { validate: (val: number) => { | ||
| if (!Number.isFinite(val)) { | ||
| throw new TypeError(`Pipeline constant value must be finite, got ${val}`); | ||
| } | ||
| } }] | ||
| ]); | ||
| export const WGPUVertexAttributeStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['format', WGPUVertexFormat], | ||
| ['offset', 'u64'], | ||
| ['shaderLocation', 'u32'] | ||
| ]); | ||
| export const WGPUVertexBufferLayoutStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['stepMode', WGPUVertexStepMode, { default: 'vertex' }], | ||
| ['arrayStride', 'u64'], | ||
| ['attributeCount', 'u64', { lengthOf: 'attributes' }], | ||
| ['attributes', [WGPUVertexAttributeStruct]] // Pointer to array | ||
| ]); | ||
| export const WGPUVertexStateStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['module', objectPtr<GPUShaderModule>()], | ||
| ['entryPoint', WGPUStringView, { optional: true, mapOptionalInline: true }], | ||
| ['constantCount', 'u64', { lengthOf: 'constants' }], | ||
| ['constants', [WGPUConstantEntryStruct], { optional: true }], | ||
| ['bufferCount', 'u64', { lengthOf: 'buffers' }], | ||
| ['buffers', [WGPUVertexBufferLayoutStruct], { optional: true }] | ||
| ]); | ||
| export const WGPUPrimitiveStateStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['topology', WGPUPrimitiveTopology, { default: 'triangle-list' }], | ||
| ['stripIndexFormat', WGPUIndexFormat, { default: 'undefined' }], // Defaults to Undefined (0) | ||
| ['frontFace', WGPUFrontFace, { default: 'ccw' }], | ||
| ['cullMode', WGPUCullMode, { default: 'none' }], | ||
| ['unclippedDepth', WGPUBool, { optional: true }] | ||
| ]); | ||
| export const WGPUStencilFaceStateStruct = defineStruct([ | ||
| ['compare', WGPUCompareFunction, { default: 'always' }], | ||
| ['failOp', WGPUStencilOperation, { default: 'keep' }], | ||
| ['depthFailOp', WGPUStencilOperation, { default: 'keep' }], | ||
| ['passOp', WGPUStencilOperation, { default: 'keep' }] | ||
| ]); | ||
| export const WGPUDepthStencilStateStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['format', WGPUTextureFormat], | ||
| ['depthWriteEnabled', WGPUBool, { default: false }], | ||
| ['depthCompare', WGPUCompareFunction, { default: 'always' }], | ||
| ['stencilFront', WGPUStencilFaceStateStruct, { default: {} }], // Inline struct | ||
| ['stencilBack', WGPUStencilFaceStateStruct, { default: {} }], // Inline struct | ||
| ['stencilReadMask', 'u32', { default: 0xFFFFFFFF }], | ||
| ['stencilWriteMask', 'u32', { default: 0xFFFFFFFF }], | ||
| ['depthBias', 'i32', { default: 0 }], // Use i32 | ||
| ['depthBiasSlopeScale', 'f32', { default: 0.0 }], | ||
| ['depthBiasClamp', 'f32', { default: 0.0 }] | ||
| ]); | ||
| export const WGPUMultisampleStateStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['count', 'u32', { default: 1 }], | ||
| ['mask', 'u32', { default: 0xFFFFFFFF }], | ||
| ['alphaToCoverageEnabled', WGPUBool, { default: false }] // Use u8 for WGPUBool | ||
| ]); | ||
| export const WGPUBlendComponentStruct = defineStruct([ | ||
| ['operation', WGPUBlendOperation, { default: 'add' }], | ||
| ['srcFactor', WGPUBlendFactor, { default: 'one' }], | ||
| ['dstFactor', WGPUBlendFactor, { default: 'zero' }] | ||
| ]); | ||
| export const WGPUBlendStateStruct = defineStruct([ | ||
| ['color', WGPUBlendComponentStruct], // Inline struct | ||
| ['alpha', WGPUBlendComponentStruct] // Inline struct | ||
| ]); | ||
| export const WGPUColorTargetStateStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['format', WGPUTextureFormat], | ||
| ['blend', WGPUBlendStateStruct, { optional: true, asPointer: true }], | ||
| ['writeMask', 'u64', { default: WGPUColorWriteMask.All }] // Use u64 for flags | ||
| ]); | ||
| export const WGPUFragmentStateStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['module', objectPtr<GPUShaderModule>()], | ||
| ['entryPoint', WGPUStringView, { optional: true, mapOptionalInline: true }], | ||
| ['constantCount', 'u64', { lengthOf: 'constants' }], | ||
| ['constants', [WGPUConstantEntryStruct], { optional: true }], | ||
| ['targetCount', 'u64', { lengthOf: 'targets' }], | ||
| ['targets', [WGPUColorTargetStateStruct]] | ||
| ]); | ||
| // -- Finally, the Render Pipeline Descriptor -- | ||
| export const WGPURenderPipelineDescriptorStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['label', WGPUStringView, { optional: true }], | ||
| ['layout', objectPtr<GPUPipelineLayout>(), { optional: true }], | ||
| ['vertex', WGPUVertexStateStruct], // Inline struct | ||
| ['primitive', WGPUPrimitiveStateStruct, { default: {} }], // Inline struct | ||
| ['depthStencil', WGPUDepthStencilStateStruct, { optional: true, asPointer: true }], | ||
| ['multisample', WGPUMultisampleStateStruct, { default: {} }], // Inline struct | ||
| ['fragment', WGPUFragmentStateStruct, { optional: true, asPointer: true }] | ||
| ]); | ||
| // --- Compute Pipeline Structs --- | ||
| export const WGPUComputeStateStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['module', objectPtr<GPUShaderModule>()], | ||
| ['entryPoint', WGPUStringView, { optional: true, mapOptionalInline: true }], | ||
| ['constantCount', 'u64', { lengthOf: 'constants' }], | ||
| ['constants', [WGPUConstantEntryStruct], { optional: true }], | ||
| ]); | ||
| export const WGPUComputePipelineDescriptorStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['label', WGPUStringView, { optional: true }], | ||
| ['layout', objectPtr<GPUPipelineLayout>(), { optional: true }], | ||
| ['compute', WGPUComputeStateStruct], | ||
| ]); | ||
| // Structs for Command Encoder | ||
| export const WGPUCommandEncoderDescriptorStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['label', WGPUStringView, { optional: true }] | ||
| ]); | ||
| export const WGPULoadOp = defineEnum({ | ||
| undefined: 0, | ||
| load: 1, | ||
| clear: 2, | ||
| "expand-resolve-texture": 0x00050003 // Dawn specific? | ||
| }, 'u32'); | ||
| export const WGPUStoreOp = defineEnum({ | ||
| undefined: 0, | ||
| store: 1, | ||
| discard: 2, | ||
| }, 'u32'); | ||
| export const WGPUColorStruct = defineStruct([ | ||
| ['r', 'f64'], | ||
| ['g', 'f64'], | ||
| ['b', 'f64'], | ||
| ['a', 'f64'], | ||
| ], { | ||
| default: { r: 0, g: 0, b: 0, a: 0 }, | ||
| mapValue: (v?: GPUColor) => { | ||
| if (!v) return null; | ||
| const clearValue = v ?? { r: 0, g: 0, b: 0, a: 0 }; | ||
| let mappedClearValue = { r: 0, g: 0, b: 0, a: 0 }; | ||
| if (typeof clearValue === 'object' && 'r' in clearValue) { | ||
| mappedClearValue = clearValue as {r: number, g: number, b: number, a: number}; | ||
| } else if (Array.isArray(clearValue)) { | ||
| mappedClearValue = { r: clearValue[0]!, g: clearValue[1]!, b: clearValue[2]!, a: clearValue[3]! }; | ||
| } | ||
| return mappedClearValue; | ||
| } | ||
| }); | ||
| export const WGPUOrigin3DStruct = defineStruct([ | ||
| ['x', 'u32', { default: 0 }], | ||
| ['y', 'u32', { default: 0 }], | ||
| ['z', 'u32', { default: 0 }], | ||
| ], { | ||
| mapValue: (v: GPUOrigin3D) => { | ||
| if (Symbol.iterator in v) { | ||
| const arr = Array.from(v); | ||
| return { | ||
| x: arr[0] ?? 0, | ||
| y: arr[1] ?? 0, | ||
| z: arr[2] ?? 0 | ||
| }; | ||
| } | ||
| return v; | ||
| } | ||
| }); | ||
| export const WGPURenderPassColorAttachmentStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['view', objectPtr<GPUTextureView>()], // Can be null in JS API, but C expects a valid pointer or error | ||
| ['depthSlice', 'u32', { default: 0xFFFFFFFF }], // WGPU_DEPTH_SLICE_UNDEFINED | ||
| ['resolveTarget', objectPtr<GPUTextureView>(), { optional: true }], | ||
| ['loadOp', WGPULoadOp], | ||
| ['storeOp', WGPUStoreOp], | ||
| ['clearValue', WGPUColorStruct, { default: { r: 0, g: 0, b: 0, a: 0 } }], | ||
| ]); | ||
| export const WGPURenderPassDepthStencilAttachmentStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['view', objectPtr<GPUTextureView>()], | ||
| ['depthLoadOp', WGPULoadOp, { optional: true }], // Optional because only needed if format has depth | ||
| ['depthStoreOp', WGPUStoreOp, { optional: true }], // Optional | ||
| ['depthClearValue', 'f32', { default: NaN }], // WGPU_DEPTH_CLEAR_VALUE_UNDEFINED -> use NaN | ||
| ['depthReadOnly', WGPUBool, { default: false }], | ||
| ['stencilLoadOp', WGPULoadOp, { optional: true }], // Optional because only needed if format has stencil | ||
| ['stencilStoreOp', WGPUStoreOp, { optional: true }], // Optional | ||
| ['stencilClearValue', 'u32', { default: 0 }], | ||
| ['stencilReadOnly', WGPUBool, { default: false }], | ||
| ]); | ||
| export const WGPUPassTimestampWritesStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['querySet', objectPtr<GPUQuerySet>()], | ||
| ['beginningOfPassWriteIndex', 'u32', { default: 0xFFFFFFFF }], // WGPU_QUERY_SET_INDEX_UNDEFINED | ||
| ['endOfPassWriteIndex', 'u32', { default: 0xFFFFFFFF }], // WGPU_QUERY_SET_INDEX_UNDEFINED | ||
| ]); | ||
| export const WGPURenderPassDescriptorStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['label', WGPUStringView, { optional: true }], | ||
| ['colorAttachmentCount', 'u64', { lengthOf: 'colorAttachments' }], | ||
| ['colorAttachments', [WGPURenderPassColorAttachmentStruct], { optional: true }], | ||
| ['depthStencilAttachment', WGPURenderPassDepthStencilAttachmentStruct, { optional: true, asPointer: true }], | ||
| ['occlusionQuerySet', objectPtr<GPUQuerySet>(), { optional: true }], | ||
| ['timestampWrites', WGPUPassTimestampWritesStruct, { optional: true, asPointer: true }], | ||
| ]); | ||
| export const WGPUComputePassDescriptorStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['label', WGPUStringView, { optional: true }], | ||
| ['timestampWrites', WGPUPassTimestampWritesStruct, { optional: true, asPointer: true }], | ||
| ]); | ||
| export const WGPUTexelCopyBufferLayoutStruct = defineStruct([ | ||
| ['offset', 'u64', { default: 0 }], | ||
| ['bytesPerRow', 'u32', { default: 0xFFFFFFFF }], // WGPU_COPY_STRIDE_UNDEFINED | ||
| ['rowsPerImage', 'u32', { default: 0xFFFFFFFF }], // WGPU_COPY_STRIDE_UNDEFINED | ||
| ]); | ||
| export const WGPUTexelCopyBufferInfoStruct = defineStruct([ | ||
| ['layout', WGPUTexelCopyBufferLayoutStruct], // Nested struct | ||
| ['buffer', objectPtr<GPUBuffer>()], | ||
| ], { | ||
| // Map the JS GPUTexelCopyBufferInfo to the nested structure | ||
| mapValue: (v: GPUTexelCopyBufferInfo) => ({ | ||
| layout: { | ||
| offset: v.offset ?? 0, | ||
| bytesPerRow: v.bytesPerRow, | ||
| rowsPerImage: v.rowsPerImage | ||
| }, | ||
| buffer: v.buffer | ||
| }) | ||
| }); | ||
| export const WGPUTexelCopyTextureInfoStruct = defineStruct([ | ||
| ['texture', objectPtr<GPUTexture>()], | ||
| ['mipLevel', 'u32', { default: 0 }], | ||
| ['origin', WGPUOrigin3DStruct, { default: { x:0, y:0, z:0 } }], | ||
| ['aspect', WGPUTextureAspect, { default: 'all' }], | ||
| ]); | ||
| export const WGPUCommandBufferDescriptorStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['label', WGPUStringView, { optional: true }], | ||
| ]); | ||
| export const WGPUQueryType = defineEnum({ | ||
| occlusion: 1, | ||
| timestamp: 2, | ||
| }, 'u32'); | ||
| export const WGPUQuerySetDescriptorStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['label', WGPUStringView, { optional: true }], | ||
| ['type', WGPUQueryType], | ||
| ['count', 'u32'], | ||
| ]); | ||
| // --- Workaround Structs --- | ||
| export const ZWGPUWorkaroundCopyTextureAndMapStruct = defineStruct([ | ||
| ['device', 'pointer'], | ||
| ['queue', 'pointer'], | ||
| ['instance', 'pointer'], | ||
| ['render_texture', 'pointer'], | ||
| ['readback_buffer', 'pointer'], | ||
| ['bytes_per_row', 'u32'], | ||
| ['width', 'u32'], | ||
| ['height', 'u32'], | ||
| ['output_buffer', 'pointer'], | ||
| ['buffer_size', 'u64'], | ||
| ]); | ||
| export const WGPURenderBundleDescriptorStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['label', WGPUStringView, { optional: true }], | ||
| ]); | ||
| export const WGPURenderBundleEncoderDescriptorStruct = defineStruct([ | ||
| ['nextInChain', 'pointer', { optional: true }], | ||
| ['label', WGPUStringView, { optional: true }], | ||
| ['colorFormatCount', 'u64', { lengthOf: 'colorFormats' }], | ||
| ['colorFormats', [WGPUTextureFormat]], | ||
| ['depthStencilFormat', WGPUTextureFormat, { default: 'undefined' }], | ||
| ['sampleCount', 'u32', { default: 1 }], | ||
| ['depthReadOnly', WGPUBool, { default: false }], | ||
| ['stencilReadOnly', WGPUBool, { default: false }], | ||
| ]); |
Sorry, the diff of this file is too big to display
| import { ptr, toArrayBuffer, type Pointer } from "bun:ffi"; | ||
| import { fatalError } from "./utils/error"; | ||
| export const pointerSize = process.arch === 'x64' || process.arch === 'arm64' ? 8 : 4; | ||
| type PrimitiveType = | ||
| 'u8' | 'u16' | 'u32' | 'u64' | | ||
| 'f32' | 'f64' | | ||
| 'pointer' | | ||
| 'i32' | 'i16' | | ||
| 'bool_u8' | 'bool_u32'; | ||
| const typeSizes: Record<PrimitiveType, number> = { | ||
| u8: 1, | ||
| bool_u8: 1, | ||
| bool_u32: 4, | ||
| u16: 2, | ||
| i16: 2, | ||
| u32: 4, | ||
| u64: 8, | ||
| f32: 4, | ||
| f64: 8, | ||
| pointer: pointerSize, | ||
| i32: 4, | ||
| } as const; | ||
| const primitiveKeys = Object.keys(typeSizes); | ||
| function isPrimitiveType(type: any): type is PrimitiveType { | ||
| return typeof type === 'string' && primitiveKeys.includes(type); | ||
| } | ||
| const typeAlignments: Record<PrimitiveType, number> = { ...typeSizes }; | ||
| const typeGetters: Record<PrimitiveType, (view: DataView, offset: number) => any> = { | ||
| u8: (view: DataView, offset: number) => view.getUint8(offset), | ||
| bool_u8: (view: DataView, offset: number) => Boolean(view.getUint8(offset)), | ||
| bool_u32: (view: DataView, offset: number) => Boolean(view.getUint32(offset, true)), | ||
| u16: (view: DataView, offset: number) => view.getUint16(offset, true), | ||
| i16: (view: DataView, offset: number) => view.getInt16(offset, true), | ||
| u32: (view: DataView, offset: number) => view.getUint32(offset, true), | ||
| u64: (view: DataView, offset: number) => view.getBigUint64(offset, true), | ||
| f32: (view: DataView, offset: number) => view.getFloat32(offset, true), | ||
| f64: (view: DataView, offset: number) => view.getFloat64(offset, true), | ||
| i32: (view: DataView, offset: number) => view.getInt32(offset, true), | ||
| pointer: (view: DataView, offset: number) => | ||
| pointerSize === 8 | ||
| ? view.getBigUint64(offset, true) | ||
| : BigInt(view.getUint32(offset, true)), | ||
| }; | ||
| // --- Types --- | ||
| interface PointyObject { | ||
| ptr: Pointer | number | bigint | null | ||
| } | ||
| interface ObjectPointerDef<T extends PointyObject> { | ||
| __type: 'objectPointer'; | ||
| } | ||
| /** | ||
| * Type helper for creating object pointers for structs. | ||
| */ | ||
| export function objectPtr<T extends PointyObject>(): ObjectPointerDef<T> { | ||
| return { | ||
| __type: 'objectPointer', | ||
| }; | ||
| } | ||
| function isObjectPointerDef<T extends PointyObject>( | ||
| type: any | ||
| ): type is ObjectPointerDef<T> { | ||
| return typeof type === 'object' && type !== null && type.__type === 'objectPointer'; | ||
| } | ||
| // --- Types --- | ||
| type Prettify<T> = { | ||
| [K in keyof T]: T[K] | ||
| } & {}; | ||
| type Simplify<T> = T extends (...args: any[]) => any | ||
| ? T | ||
| : T extends object | ||
| ? Prettify<T> | ||
| : T; | ||
| type PrimitiveToTSType<T extends PrimitiveType> = | ||
| T extends 'u8' | 'u16' | 'u32' | 'i16' | 'i32' | 'f32' | 'f64' ? number : | ||
| T extends 'u64' ? bigint | number : // typescript webgpu types currently use numbers for u64 | ||
| T extends 'bool_u8' | 'bool_u32' ? boolean : | ||
| T extends 'pointer' ? number | bigint : // Represent pointers as numbers or bigints | ||
| never; | ||
| type FieldDefInputType<Def> = | ||
| Def extends PrimitiveType ? PrimitiveToTSType<Def> : | ||
| Def extends 'cstring' | 'char*' ? string | null : // Strings can be null | ||
| Def extends EnumDef<infer E> ? keyof E : // Enums map to their keys | ||
| Def extends StructDef<any, infer InputType> ? InputType : // Extract InputType for pack operations | ||
| Def extends ObjectPointerDef<infer T> ? T | null : | ||
| Def extends readonly [infer InnerDef] ? // Array check | ||
| InnerDef extends PrimitiveType ? Iterable<PrimitiveToTSType<InnerDef>> : | ||
| InnerDef extends EnumDef<infer E> ? Iterable<keyof E> : | ||
| InnerDef extends StructDef<any, infer InputType> ? Iterable<InputType> : // Extract InputType for struct arrays | ||
| InnerDef extends ObjectPointerDef<infer T> ? (T | null)[] : | ||
| never : | ||
| never; | ||
| type FieldDefOutputType<Def> = | ||
| Def extends PrimitiveType ? PrimitiveToTSType<Def> : | ||
| Def extends 'cstring' | 'char*' ? string | null : // Strings can be null | ||
| Def extends EnumDef<infer E> ? keyof E : // Enums map to their keys | ||
| Def extends StructDef<infer OutputType, any> ? OutputType : // Extract OutputType for unpack operations | ||
| Def extends ObjectPointerDef<infer T> ? T | null : | ||
| Def extends readonly [infer InnerDef] ? // Array check | ||
| InnerDef extends PrimitiveType ? Iterable<PrimitiveToTSType<InnerDef>> : | ||
| InnerDef extends EnumDef<infer E> ? Iterable<keyof E> : | ||
| InnerDef extends StructDef<infer OutputType, any> ? Iterable<OutputType> : // Extract OutputType for struct arrays | ||
| InnerDef extends ObjectPointerDef<infer T> ? (T | null)[] : | ||
| never : | ||
| never; | ||
| type IsOptional<Options extends StructFieldOptions | undefined> = | ||
| Options extends { optional: true } ? true : | ||
| Options extends { default: any } ? true : | ||
| Options extends { lengthOf: string } ? true : // lengthOf implies the field is derived/optional input | ||
| Options extends { condition: () => boolean } ? true : // condition implies the field might not exist | ||
| false; | ||
| // Constructs the TS object type from the struct field definitions for INPUT (pack operations) | ||
| export type StructObjectInputType<Fields extends readonly StructField[]> = { | ||
| [F in Fields[number] as IsOptional<F[2]> extends false ? F[0] : never]: FieldDefInputType<F[1]>; | ||
| } & { | ||
| [F in Fields[number] as IsOptional<F[2]> extends true ? F[0] : never]?: FieldDefInputType<F[1]> | null; | ||
| }; | ||
| // Constructs the TS object type from the struct field definitions for OUTPUT (unpack operations) | ||
| export type StructObjectOutputType<Fields extends readonly StructField[]> = { | ||
| [F in Fields[number] as IsOptional<F[2]> extends false ? F[0] : never]: FieldDefOutputType<F[1]>; | ||
| } & { | ||
| [F in Fields[number] as IsOptional<F[2]> extends true ? F[0] : never]?: FieldDefOutputType<F[1]> | null; | ||
| }; | ||
| type DefineStructReturnType<Fields extends readonly StructField[], Options extends StructDefOptions | undefined> = | ||
| StructDef< | ||
| Simplify<Options extends { reduceValue: (value: any) => infer R } | ||
| ? R | ||
| : StructObjectOutputType<Fields>>, | ||
| Simplify<Options extends { mapValue: (value: infer V) => any } | ||
| ? V | ||
| : StructObjectInputType<Fields>> | ||
| >; | ||
| // --- END: types --- | ||
| interface AllocStructOptions { | ||
| lengths?: Record<string, number>; | ||
| } | ||
| interface AllocStructResult { | ||
| buffer: ArrayBuffer; | ||
| view: DataView; | ||
| subBuffers?: Record<string, ArrayBuffer>; | ||
| } | ||
| export function allocStruct( | ||
| structDef: StructDef<any, any>, | ||
| options?: AllocStructOptions | ||
| ): AllocStructResult { | ||
| const buffer = new ArrayBuffer(structDef.size); | ||
| const view = new DataView(buffer); | ||
| const result: AllocStructResult = { buffer, view }; | ||
| const { pack: pointerPacker } = primitivePackers('pointer'); | ||
| if (options?.lengths) { | ||
| const subBuffers: Record<string, ArrayBuffer> = {}; | ||
| // Allocate sub-buffers | ||
| for (const [arrayFieldName, length] of Object.entries(options.lengths)) { | ||
| const arrayMeta = structDef.arrayFields.get(arrayFieldName); | ||
| if (!arrayMeta) { | ||
| throw new Error(`Field '${arrayFieldName}' is not an array field with a lengthOf field`); | ||
| } | ||
| const subBuffer = new ArrayBuffer(length * arrayMeta.elementSize); | ||
| subBuffers[arrayFieldName] = subBuffer; | ||
| const pointer = length > 0 ? ptr(subBuffer) : null; | ||
| pointerPacker(view, arrayMeta.arrayOffset, pointer); | ||
| arrayMeta.lengthPack(view, arrayMeta.lengthOffset, length); | ||
| } | ||
| if (Object.keys(subBuffers).length > 0) { | ||
| result.subBuffers = subBuffers; | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| function alignOffset(offset: number, align: number): number { | ||
| return (offset + (align - 1)) & ~(align - 1); | ||
| } | ||
| interface EnumDef<T extends Record<string, number>> { | ||
| __type: 'enum'; | ||
| type: Exclude<PrimitiveType, 'bool_u8' | 'bool_u32'>; | ||
| to(value: keyof T): number; | ||
| from(value: number | bigint): keyof T; | ||
| enum: T; | ||
| } | ||
| function enumTypeError(value: string): never { | ||
| throw new TypeError(`Invalid enum value: ${value}`); | ||
| } | ||
| // Enums | ||
| export function defineEnum<T extends Record<string, number>>(mapping: T, base: Exclude<PrimitiveType, 'bool_u8' | 'bool_u32'> = 'u32'): EnumDef<T> { | ||
| const reverse = Object.fromEntries(Object.entries(mapping).map(([k, v]) => [v, k])); | ||
| return { | ||
| __type: 'enum', | ||
| type: base, | ||
| to(value: keyof T): number { | ||
| return typeof value === 'number' ? value : mapping[value] ?? enumTypeError(String(value)); | ||
| }, | ||
| from(value: number): keyof T { | ||
| return reverse[value] ?? enumTypeError(String(value)); | ||
| }, | ||
| enum: mapping, | ||
| }; | ||
| } | ||
| function isEnum<T extends Record<string, number>>(type: any): type is EnumDef<T> { | ||
| return typeof type === 'object' && type.__type === 'enum'; | ||
| } | ||
| type ValidationFunction = (value: any, fieldName: string, options: { hints?: any, input?: any }) => void | never; | ||
| interface StructFieldOptions { | ||
| optional?: boolean, | ||
| // Call mapValue even if the value is undefined for inline structs. | ||
| mapOptionalInline?: boolean, | ||
| unpackTransform?: (value: any) => any, | ||
| packTransform?: (value: any) => any, | ||
| lengthOf?: string, | ||
| asPointer?: boolean, | ||
| default?: any, | ||
| condition?: () => boolean, | ||
| validate?: ValidationFunction | ValidationFunction[], | ||
| }; | ||
| type StructField = | ||
| | readonly [string, PrimitiveType, StructFieldOptions?] | ||
| | readonly [string, EnumDef<any>, StructFieldOptions?] | ||
| | readonly [string, StructDef<any>, StructFieldOptions?] | ||
| | readonly [string, 'cstring' | 'char*', StructFieldOptions?] | ||
| | readonly [string, ObjectPointerDef<any>, StructFieldOptions?] | ||
| | readonly [string, readonly [EnumDef<any> | StructDef<any> | PrimitiveType | ObjectPointerDef<any>], StructFieldOptions?]; | ||
| interface StructFieldPackOptions { | ||
| validationHints?: any; | ||
| } | ||
| interface StructLayoutField { | ||
| name: string; | ||
| offset: number; | ||
| size: number; | ||
| align: number; | ||
| optional: boolean; | ||
| default?: any; | ||
| validate?: ValidationFunction[]; | ||
| pack: (view: DataView, offset: number, value: any, obj: any, options?: StructFieldPackOptions) => void; | ||
| unpack: (view: DataView, offset: number) => any; | ||
| type: PrimitiveType | EnumDef<any> | StructDef<any> | 'cstring' | 'char*' | ObjectPointerDef<any> | readonly [any]; | ||
| lengthOf?: string; | ||
| } | ||
| interface StructFieldDescription { | ||
| name: string; | ||
| offset: number; | ||
| size: number; | ||
| align: number; | ||
| optional: boolean; | ||
| type: PrimitiveType | EnumDef<any> | StructDef<any> | 'cstring' | 'char*' | ObjectPointerDef<any> | readonly [any]; | ||
| lengthOf?: string | ||
| } | ||
| interface ArrayFieldMetadata { | ||
| elementSize: number; | ||
| arrayOffset: number; | ||
| lengthOffset: number; | ||
| lengthPack: (view: DataView, offset: number, value: number) => void; | ||
| } | ||
| interface StructDef<OutputType, InputType = OutputType> { | ||
| __type: 'struct'; | ||
| size: number; | ||
| align: number; | ||
| hasMapValue: boolean; | ||
| layoutByName: Map<string, StructFieldDescription>; | ||
| arrayFields: Map<string, ArrayFieldMetadata>; | ||
| pack(obj: Simplify<InputType>, options?: StructFieldPackOptions): ArrayBuffer; | ||
| packInto(obj: Simplify<InputType>, view: DataView, offset: number, options?: StructFieldPackOptions): void; | ||
| unpack(buf: ArrayBuffer | SharedArrayBuffer): Simplify<OutputType>; | ||
| describe(): StructFieldDescription[]; | ||
| } | ||
| function isStruct(type: any): type is StructDef<any> { | ||
| return typeof type === 'object' && type.__type === 'struct'; | ||
| } | ||
| function primitivePackers(type: PrimitiveType) { | ||
| let pack: (view: DataView, off: number, val: any) => void; | ||
| let unpack: (view: DataView, off: number) => any; | ||
| switch (type) { | ||
| case 'u8': | ||
| pack = (view: DataView, off: number, val: number) => view.setUint8(off, val); | ||
| unpack = (view: DataView, off: number) => view.getUint8(off); | ||
| break; | ||
| case 'bool_u8': | ||
| pack = (view: DataView, off: number, val: boolean) => view.setUint8(off, !!val ? 1 : 0); | ||
| unpack = (view: DataView, off: number) => Boolean(view.getUint8(off)); | ||
| break; | ||
| case 'bool_u32': | ||
| pack = (view: DataView, off: number, val: boolean) => view.setUint32(off, !!val ? 1 : 0, true); | ||
| unpack = (view: DataView, off: number) => Boolean(view.getUint32(off, true)); | ||
| break; | ||
| case 'u16': | ||
| pack = (view: DataView, off: number, val: number) => view.setUint16(off, val, true); | ||
| unpack = (view: DataView, off: number) => view.getUint16(off, true); | ||
| break; | ||
| case 'i16': | ||
| pack = (view: DataView, off: number, val: number) => view.setInt16(off, val, true); | ||
| unpack = (view: DataView, off: number) => view.getInt16(off, true); | ||
| break; | ||
| case 'u32': | ||
| pack = (view: DataView, off: number, val: number) => view.setUint32(off, val, true); | ||
| unpack = (view: DataView, off: number) => view.getUint32(off, true); | ||
| break; | ||
| case 'i32': | ||
| pack = (view: DataView, off: number, val: number) => view.setInt32(off, val, true); | ||
| unpack = (view: DataView, off: number) => view.getInt32(off, true); | ||
| break; | ||
| case 'u64': | ||
| pack = (view: DataView, off: number, val: bigint) => view.setBigUint64(off, BigInt(val), true); | ||
| unpack = (view: DataView, off: number) => view.getBigUint64(off, true); | ||
| break; | ||
| case 'f32': | ||
| pack = (view: DataView, off: number, val: number) => view.setFloat32(off, val, true); | ||
| unpack = (view: DataView, off: number) => view.getFloat32(off, true); | ||
| break; | ||
| case 'f64': | ||
| pack = (view: DataView, off: number, val: number) => view.setFloat64(off, val, true); | ||
| unpack = (view: DataView, off: number) => view.getFloat64(off, true); | ||
| break; | ||
| case 'pointer': | ||
| pack = (view: DataView, off: number, val: bigint | number) => { | ||
| pointerSize === 8 | ||
| ? view.setBigUint64(off, val ? BigInt(val) : 0n, true) | ||
| : view.setUint32(off, val ? Number(val) : 0, true); | ||
| }; | ||
| unpack = (view: DataView, off: number): number => { | ||
| const bint = pointerSize === 8 | ||
| ? view.getBigUint64(off, true) | ||
| : BigInt(view.getUint32(off, true)); | ||
| return Number(bint) | ||
| } | ||
| break; | ||
| default: | ||
| // This should be caught by PrimitiveType, but belts and suspenders | ||
| fatalError(`Unsupported primitive type: ${type}`); | ||
| } | ||
| return { pack, unpack }; | ||
| } | ||
| interface StructDefOptions { | ||
| default?: Record<string, any>; // Default values for the entire struct on unpack | ||
| mapValue?: (value: any) => any; // Map input object before packing | ||
| reduceValue?: (value: any) => any; // Transform unpacked object to different type | ||
| } | ||
| const { pack: pointerPacker, unpack: pointerUnpacker } = primitivePackers('pointer'); | ||
| export function packObjectArray(val: (PointyObject | null)[]) { | ||
| const buffer = new ArrayBuffer(val.length * pointerSize); | ||
| const bufferView = new DataView(buffer); | ||
| for (let i = 0; i < val.length; i++) { | ||
| const instance = val[i]; | ||
| const ptrValue = instance?.ptr ?? null; | ||
| pointerPacker(bufferView, i * pointerSize, ptrValue); | ||
| } | ||
| return bufferView; | ||
| } | ||
| const encoder = new TextEncoder(); | ||
| // Define Struct | ||
| export function defineStruct<const Fields extends readonly StructField[], const Opts extends StructDefOptions = {} >( | ||
| fields: Fields & StructField[], | ||
| structDefOptions?: Opts | ||
| ): DefineStructReturnType<Fields, Opts> { | ||
| let offset = 0; | ||
| let maxAlign = 1; | ||
| const layout: StructLayoutField[] = []; | ||
| const lengthOfFields: Record<string, StructLayoutField> = {}; | ||
| const lengthOfRequested: { requester: StructLayoutField, def: EnumDef<any> | PrimitiveType }[] = []; | ||
| const arrayFieldsMetadata: Record<string, ArrayFieldMetadata> = {}; | ||
| for (const [name, typeOrStruct, options = {}] of fields) { | ||
| if (options.condition && !options.condition()) { | ||
| continue; | ||
| } | ||
| let size = 0, align = 0; | ||
| let pack: (view: DataView, offset: number, value: any, obj: any, options?: StructFieldPackOptions) => void; | ||
| let unpack: (view: DataView, offset: number) => any; | ||
| let needsLengthOf = false; | ||
| let lengthOfDef: EnumDef<any> | null = null; | ||
| // Primitive | ||
| if (isPrimitiveType(typeOrStruct)) { | ||
| size = typeSizes[typeOrStruct]; | ||
| align = typeAlignments[typeOrStruct]; | ||
| ({ pack, unpack } = primitivePackers(typeOrStruct)); | ||
| // CString (null-terminated) | ||
| } else if (typeof typeOrStruct === 'string' && typeOrStruct === 'cstring') { | ||
| size = pointerSize; | ||
| align = pointerSize; | ||
| pack = (view: DataView, off: number, val: string | null) => { | ||
| const bufPtr = val ? ptr(encoder.encode(val + '\0')) : null; | ||
| pointerPacker(view, off, bufPtr); | ||
| }; | ||
| unpack = (view: DataView, off: number) => { | ||
| // TODO: Unpack CString from pointer | ||
| const ptrVal = pointerUnpacker(view, off); | ||
| return ptrVal; // Returning pointer for now | ||
| }; | ||
| // char* (raw string pointer, length usually external) | ||
| } else if (typeof typeOrStruct === 'string' && typeOrStruct === 'char*') { | ||
| size = pointerSize; | ||
| align = pointerSize; | ||
| pack = (view: DataView, off: number, val: string | null) => { | ||
| const bufPtr = val ? ptr(encoder.encode(val)) : null; // No null terminator | ||
| pointerPacker(view, off, bufPtr); | ||
| }; | ||
| unpack = (view: DataView, off: number) => { | ||
| // TODO: Unpack char* requires length info, typically from another field | ||
| const ptrVal = pointerUnpacker(view, off); | ||
| return ptrVal; // Returning pointer for now | ||
| } | ||
| // Enum | ||
| } else if (isEnum(typeOrStruct)) { | ||
| const base = typeOrStruct.type; | ||
| size = typeSizes[base]; | ||
| align = typeAlignments[base]; | ||
| const { pack: packEnum } = primitivePackers(base); | ||
| pack = (view, off, val) => { | ||
| const num = typeOrStruct.to(val); | ||
| packEnum(view, off, num); | ||
| }; | ||
| unpack = (view, off) => { | ||
| const raw = typeGetters[base](view, off); | ||
| return typeOrStruct.from(raw); | ||
| }; | ||
| // Struct | ||
| } else if (isStruct(typeOrStruct)) { | ||
| if (options.asPointer === true) { | ||
| size = pointerSize; | ||
| align = pointerSize; | ||
| pack = (view, off, val, obj, options) => { | ||
| if (!val) { | ||
| pointerPacker(view, off, null); | ||
| return; | ||
| } | ||
| const nestedBuf = typeOrStruct.pack(val, options); | ||
| pointerPacker(view, off, ptr(nestedBuf)); | ||
| }; | ||
| unpack = (view, off) => { | ||
| throw new Error('Not implemented yet'); | ||
| }; | ||
| } else { // Inline struct | ||
| size = typeOrStruct.size; | ||
| align = typeOrStruct.align; | ||
| pack = (view, off, val, obj, options) => { | ||
| const nestedBuf = typeOrStruct.pack(val, options); | ||
| const nestedView = new Uint8Array(nestedBuf); | ||
| const dView = new Uint8Array(view.buffer); | ||
| dView.set(nestedView, off); | ||
| }; | ||
| unpack = (view, off) => { | ||
| const slice = view.buffer.slice(off, off + size); | ||
| return typeOrStruct.unpack(slice); | ||
| }; | ||
| } | ||
| // Object Pointer | ||
| } else if (isObjectPointerDef(typeOrStruct)) { | ||
| size = pointerSize; | ||
| align = pointerSize; | ||
| pack = (view, off, value: PointyObject | null) => { | ||
| const ptrValue = value?.ptr ?? null; | ||
| // @ts-ignore | ||
| if (ptrValue === undefined) { | ||
| console.warn(`Field '${name}' expected object with '.ptr' property, but got undefined pointer value from:`, value); | ||
| pointerPacker(view, off, null); // Pack null if pointer is missing | ||
| } else { | ||
| pointerPacker(view, off, ptrValue); | ||
| } | ||
| }; | ||
| // Unpacking returns the raw pointer value, not the class instance | ||
| // TODO: objectPtr could take a reconstructor function to reconstruct the object from the pointer | ||
| unpack = (view, off) => { | ||
| return pointerUnpacker(view, off); | ||
| }; | ||
| // Array ([EnumType], [StructType], [PrimitiveType], ...) | ||
| } else if (Array.isArray(typeOrStruct) && typeOrStruct.length === 1 && typeOrStruct[0] !== undefined) { | ||
| const [def] = typeOrStruct; | ||
| size = pointerSize; // Arrays are always represented by a pointer to the data | ||
| align = pointerSize; | ||
| let arrayElementSize: number; | ||
| if (isEnum(def)) { | ||
| // Packing an array of enums | ||
| arrayElementSize = typeSizes[def.type]; | ||
| pack = (view, off, val: string[], obj) => { | ||
| if (!val || val.length === 0) { | ||
| pointerPacker(view, off, null); | ||
| return; | ||
| } | ||
| const buffer = new ArrayBuffer(val.length * arrayElementSize); | ||
| const bufferView = new DataView(buffer); | ||
| for (let i = 0; i < val.length; i++) { | ||
| const num = def.to(val[i]!); | ||
| bufferView.setUint32(i * arrayElementSize, num, true); | ||
| } | ||
| pointerPacker(view, off, ptr(buffer)); | ||
| }; | ||
| unpack = null!; | ||
| needsLengthOf = true; | ||
| lengthOfDef = def; | ||
| } else if (isStruct(def)) { | ||
| // Array of Structs | ||
| arrayElementSize = def.size; | ||
| pack = (view, off, val: any[], obj, options) => { | ||
| if (!val || val.length === 0) { | ||
| pointerPacker(view, off, null); | ||
| return; | ||
| } | ||
| const buffer = new ArrayBuffer(val.length * arrayElementSize); | ||
| const bufferView = new DataView(buffer); | ||
| for (let i = 0; i < val.length; i++) { | ||
| def.packInto(val[i], bufferView, i * arrayElementSize, options); | ||
| } | ||
| pointerPacker(view, off, ptr(buffer)); | ||
| }; | ||
| unpack = (view, off) => { | ||
| throw new Error('Not implemented yet'); | ||
| }; | ||
| } else if (isPrimitiveType(def)) { | ||
| // Array of Primitives | ||
| arrayElementSize = typeSizes[def]; | ||
| const { pack: primitivePack } = primitivePackers(def); | ||
| // Ensure 'val' type matches the expected primitive array type | ||
| pack = (view, off, val: PrimitiveToTSType<typeof def>[]) => { | ||
| if (!val || val.length === 0) { | ||
| pointerPacker(view, off, null); | ||
| return; | ||
| } | ||
| const buffer = new ArrayBuffer(val.length * arrayElementSize); | ||
| const bufferView = new DataView(buffer); | ||
| for (let i = 0; i < val.length; i++) { | ||
| primitivePack(bufferView, i * arrayElementSize, val[i]); | ||
| } | ||
| pointerPacker(view, off, ptr(buffer)); | ||
| }; | ||
| unpack = null!; | ||
| // TODO: Implement unpack for primitve array | ||
| } else if (isObjectPointerDef(def)) { | ||
| arrayElementSize = pointerSize; | ||
| pack = (view, off, val) => { | ||
| if (!val || val.length === 0) { | ||
| pointerPacker(view, off, null); | ||
| return; | ||
| } | ||
| const packedView = packObjectArray(val); | ||
| pointerPacker(view, off, ptr(packedView.buffer)); | ||
| } | ||
| unpack = () => { | ||
| // TODO: implement unpack for class pointers | ||
| throw new Error('not implemented yet'); | ||
| } | ||
| } else { | ||
| throw new Error(`Unsupported array element type for ${name}: ${JSON.stringify(def)}`); | ||
| } | ||
| // Used for allocStruct | ||
| const lengthOfField = Object.values(lengthOfFields).find(f => f.lengthOf === name); | ||
| if (lengthOfField && isPrimitiveType(lengthOfField.type)) { | ||
| const { pack: lengthPack } = primitivePackers(lengthOfField.type); | ||
| arrayFieldsMetadata[name] = { | ||
| elementSize: arrayElementSize, | ||
| arrayOffset: offset, | ||
| lengthOffset: lengthOfField.offset, | ||
| lengthPack | ||
| }; | ||
| } | ||
| } else { | ||
| throw new Error(`Unsupported field type for ${name}: ${JSON.stringify(typeOrStruct)}`); | ||
| } | ||
| offset = alignOffset(offset, align); | ||
| if (options.unpackTransform) { | ||
| const originalUnpack = unpack; | ||
| unpack = (view, off) => options.unpackTransform!(originalUnpack(view, off)); | ||
| } | ||
| if (options.packTransform) { | ||
| const originalPack = pack; | ||
| pack = (view, off, val, obj, packOptions) => originalPack(view, off, options.packTransform!(val), obj, packOptions); | ||
| } | ||
| if (options.optional) { | ||
| const originalPack = pack; | ||
| if (isStruct(typeOrStruct) && !options.asPointer) { | ||
| pack = (view, off, val, obj, packOptions) => { | ||
| // Given mapOptionalInline, we execute the pack even if the value is undefined, | ||
| // as the mapValue function can handle undefined values if needed. | ||
| if (val || options.mapOptionalInline) { | ||
| originalPack(view, off, val, obj, packOptions); | ||
| } | ||
| }; | ||
| } else { | ||
| pack = (view, off, val, obj, packOptions) => originalPack(view, off, val ?? 0, obj, packOptions); | ||
| } | ||
| } | ||
| if (options.lengthOf) { | ||
| const originalPack = pack; | ||
| pack = (view, off, val, obj, packOptions) => originalPack(view, off, obj[options.lengthOf!] ? obj[options.lengthOf!].length : 0, obj, packOptions); | ||
| } | ||
| // Normalize validation to always be an array | ||
| let validateFunctions: ValidationFunction[] | undefined; | ||
| if (options.validate) { | ||
| validateFunctions = Array.isArray(options.validate) ? options.validate : [options.validate]; | ||
| } | ||
| // LAYOUT FIELD | ||
| const layoutField: StructLayoutField = { | ||
| name, | ||
| offset, | ||
| size, | ||
| align, | ||
| validate: validateFunctions, | ||
| optional: !!options.optional || !!options.lengthOf || options.default !== undefined, | ||
| default: options.default, | ||
| pack, | ||
| unpack, | ||
| type: typeOrStruct, | ||
| lengthOf: options.lengthOf | ||
| }; | ||
| layout.push(layoutField); | ||
| if (options.lengthOf) { | ||
| lengthOfFields[options.lengthOf] = layoutField; | ||
| } | ||
| if (needsLengthOf) { | ||
| if (!lengthOfDef) fatalError(`Internal error: needsLengthOf=true but lengthOfDef is null for ${name}`); | ||
| lengthOfRequested.push({ requester: layoutField, def: lengthOfDef }); | ||
| } | ||
| offset += size; | ||
| maxAlign = Math.max(maxAlign, align); | ||
| } | ||
| // Resolve lengthOf fields | ||
| for (const { requester, def } of lengthOfRequested) { | ||
| if (isPrimitiveType(def)) { | ||
| // TODO: Implement lengthOf for primitive types | ||
| continue; | ||
| } | ||
| const lengthOfField = lengthOfFields[requester.name]; | ||
| if (!lengthOfField) { | ||
| throw new Error(`lengthOf field not found for array field ${requester.name}`); | ||
| } | ||
| const elemSize = def.type === 'u32' ? 4 : 8; | ||
| requester.unpack = (view, off) => { | ||
| const result = []; | ||
| const length = lengthOfField.unpack(view, lengthOfField.offset); | ||
| const ptrAddress = pointerUnpacker(view, off); | ||
| if (ptrAddress === 0n && length > 0) { | ||
| throw new Error(`Array field ${requester.name} has null pointer but length ${length}.`); | ||
| } | ||
| if (ptrAddress === 0n || length === 0) { | ||
| return []; | ||
| } | ||
| const buffer = toArrayBuffer(ptrAddress, 0, length * elemSize); | ||
| const bufferView = new DataView(buffer); | ||
| for (let i = 0; i < length; i++) { | ||
| result.push(def.from(bufferView.getUint32(i * elemSize, true))); | ||
| } | ||
| return result; | ||
| } | ||
| } | ||
| const totalSize = alignOffset(offset, maxAlign); | ||
| const description = layout.map(f => ({ | ||
| name: f.name, | ||
| offset: f.offset, | ||
| size: f.size, | ||
| align: f.align, | ||
| optional: f.optional, | ||
| type: f.type, | ||
| lengthOf: f.lengthOf, | ||
| })); | ||
| const layoutByName = new Map(description.map(f => [f.name, f])); | ||
| const arrayFields = new Map(Object.entries(arrayFieldsMetadata)); | ||
| return { | ||
| __type: 'struct', | ||
| size: totalSize, | ||
| align: maxAlign, | ||
| hasMapValue: !!structDefOptions?.mapValue, | ||
| layoutByName, | ||
| arrayFields, | ||
| pack(obj: Simplify<StructObjectInputType<Fields>>, options?: StructFieldPackOptions): ArrayBuffer { | ||
| const buf = new ArrayBuffer(totalSize); | ||
| const view = new DataView(buf); | ||
| let mappedObj: any = obj; | ||
| if (structDefOptions?.mapValue) { | ||
| mappedObj = structDefOptions.mapValue(obj); | ||
| } | ||
| for (const field of layout) { | ||
| const value = (mappedObj as any)[field.name] ?? field.default; | ||
| if (!field.optional && value === undefined) { | ||
| fatalError(`Packing non-optional field '${field.name}' but value is undefined (and no default provided)`); | ||
| } | ||
| if (field.validate) { | ||
| for (const validateFn of field.validate) { | ||
| validateFn(value, field.name, { hints: options?.validationHints, input: mappedObj }); | ||
| } | ||
| } | ||
| field.pack(view, field.offset, value, mappedObj, options); | ||
| } | ||
| return view.buffer; | ||
| }, | ||
| packInto(obj: Simplify<StructObjectInputType<Fields>>, view: DataView, offset: number, options?: StructFieldPackOptions): void { | ||
| let mappedObj: any = obj; | ||
| if (structDefOptions?.mapValue) { | ||
| mappedObj = structDefOptions.mapValue(obj); | ||
| } | ||
| for (const field of layout) { | ||
| const value = (mappedObj as any)[field.name] ?? field.default; | ||
| if (!field.optional && value === undefined) { | ||
| console.warn(`packInto missing value for non-optional field '${field.name}' at offset ${offset + field.offset}. Writing default or zero.`); | ||
| } | ||
| if (field.validate) { | ||
| for (const validateFn of field.validate) { | ||
| validateFn(value, field.name, { hints: options?.validationHints, input: mappedObj }); | ||
| } | ||
| } | ||
| field.pack(view, offset + field.offset, value, mappedObj, options); | ||
| } | ||
| }, | ||
| // unpack method now returns the specific inferred object type | ||
| unpack(buf: ArrayBuffer | SharedArrayBuffer): Simplify<any> { | ||
| if (buf.byteLength < totalSize) { | ||
| fatalError(`Buffer size (${buf.byteLength}) is smaller than struct size (${totalSize}) for unpacking.`); | ||
| } | ||
| const view = new DataView(buf); | ||
| // Start with struct-level defaults if provided | ||
| const result: any = structDefOptions?.default ? { ...structDefOptions.default } : {}; | ||
| for (const field of layout) { | ||
| // Skip fields that don't have an unpacker (e.g., write-only or complex cases not yet impl) | ||
| if (!field.unpack) { | ||
| // This could happen for lengthOf fields if unpack isn't needed, or unimplemented array types | ||
| // console.warn(`Field '${field.name}' has no unpacker defined.`); | ||
| continue; | ||
| } | ||
| try { | ||
| result[field.name] = field.unpack(view, field.offset); | ||
| } catch (e: any) { | ||
| console.error(`Error unpacking field '${field.name}' at offset ${field.offset}:`, e); | ||
| throw e; // Re-throw after logging context | ||
| } | ||
| } | ||
| if (structDefOptions?.reduceValue) { | ||
| return structDefOptions.reduceValue(result); | ||
| } | ||
| return result as StructObjectOutputType<Fields>; | ||
| }, | ||
| describe() { | ||
| return description; | ||
| } | ||
| } as DefineStructReturnType<Fields, Opts>; | ||
| } |
| import { WGPUErrorType } from "../shared"; | ||
| export function fatalError(...args: any[]): never { | ||
| const message = args.join(' '); | ||
| console.error('FATAL ERROR:', message); | ||
| throw new Error(message); | ||
| } | ||
| export class OperationError extends Error { | ||
| constructor(message: string) { | ||
| super(message); | ||
| this.name = 'OperationError'; | ||
| } | ||
| } | ||
| export class GPUErrorImpl extends Error implements GPUError { | ||
| constructor(message: string) { | ||
| super(message); | ||
| this.name = 'GPUError'; | ||
| } | ||
| } | ||
| export class GPUOutOfMemoryError extends Error { | ||
| constructor(message: string) { | ||
| super(message); | ||
| this.name = 'GPUOutOfMemoryError'; | ||
| } | ||
| } | ||
| export class GPUInternalError extends Error { | ||
| constructor(message: string) { | ||
| super(message); | ||
| this.name = 'GPUInternalError'; | ||
| } | ||
| } | ||
| export class GPUValidationError extends Error { | ||
| constructor(message: string) { | ||
| super(message); | ||
| this.name = 'GPUValidationError'; | ||
| } | ||
| } | ||
| export class GPUPipelineErrorImpl extends DOMException implements GPUPipelineError { | ||
| readonly reason: GPUPipelineErrorReason; | ||
| readonly __brand: 'GPUPipelineError' = 'GPUPipelineError'; | ||
| constructor(message: string, options: GPUPipelineErrorInit) { | ||
| const parts = message.split('\n'); | ||
| const errorMessage = parts[0]; | ||
| const stack = parts.slice(1).join('\n'); | ||
| // @ts-ignore | ||
| super(errorMessage, 'GPUPipelineError'); | ||
| this.reason = options.reason; | ||
| this.stack = stack; | ||
| } | ||
| } | ||
| export class AbortError extends Error { | ||
| constructor(message: string) { | ||
| super(message); | ||
| this.name = 'AbortError'; | ||
| } | ||
| } | ||
| export function createWGPUError(type: number, message: string) { | ||
| switch (type) { | ||
| case WGPUErrorType['out-of-memory']: | ||
| return new GPUOutOfMemoryError(message); | ||
| case WGPUErrorType.internal: | ||
| return new GPUInternalError(message); | ||
| case WGPUErrorType.validation: | ||
| return new GPUValidationError(message); | ||
| default: | ||
| return new GPUErrorImpl(message); | ||
| } | ||
| } |
| const std = @import("std"); | ||
| pub fn build(b: *std.Build) void { | ||
| // Get optimization level from command line or default to Debug | ||
| const optimize = b.option(std.builtin.OptimizeMode, "optimize", "Optimization level (Debug, ReleaseFast, ReleaseSafe, ReleaseSmall)") orelse .Debug; | ||
| const sdk_path_str = b.option([]const u8, "sdk-path", "Path to macOS SDK (optional)") orelse getDefaultMacOSSDKPath(); | ||
| const target_str_opt = b.option([]const u8, "target", "Specific target to build (e.g., x86_64-macos). If not given, builds all default targets (excluding Windows and aarch64-linux).") orelse null; | ||
| const lib_name = "webgpu_wrapper"; | ||
| const root_source_path = "lib.zig"; | ||
| const output_base_dir = "../../lib"; | ||
| var targets_to_build_slice: []const std.Target.Query = undefined; | ||
| var single_target_storage: [1]std.Target.Query = undefined; // Storage if a single target is specified | ||
| if (target_str_opt) |target_str| { | ||
| var parts = std.mem.splitScalar(u8, target_str, '-'); | ||
| const arch_str = parts.next() orelse { | ||
| std.log.err("Invalid target string: {s}. Expected format: arch-os. Missing arch part.", .{target_str}); | ||
| return; | ||
| }; | ||
| const os_str = parts.next() orelse { | ||
| std.log.err("Invalid target string: {s}. Expected format: arch-os. Missing os part after arch {s}.", .{ target_str, arch_str }); | ||
| return; | ||
| }; | ||
| if (parts.next() != null) { | ||
| std.log.err("Invalid target string: {s}. Expected format: arch-os. Too many parts.", .{target_str}); | ||
| return; | ||
| } | ||
| const arch = std.meta.stringToEnum(std.Target.Cpu.Arch, arch_str) orelse { | ||
| std.log.err("Unsupported architecture: {s} in target string {s}. Please use one of the standard architecture names (e.g., x86_64, aarch64).", .{ arch_str, target_str }); | ||
| return; | ||
| }; | ||
| const os = std.meta.stringToEnum(std.Target.Os.Tag, os_str) orelse { | ||
| std.log.err("Unsupported OS: {s} in target string {s}. Please use one of the standard OS names (e.g., linux, macos).", .{ os_str, target_str }); | ||
| return; | ||
| }; | ||
| if (arch == .aarch64 and os == .linux) { | ||
| std.log.warn("Building for aarch64-linux. Note: Dawn support for this target might be experimental or incomplete.", .{}); | ||
| } | ||
| single_target_storage[0] = .{ .cpu_arch = arch, .os_tag = os }; | ||
| targets_to_build_slice = &single_target_storage; | ||
| } else { | ||
| // Default targets (all currently supported, non-Windows, non-aarch64-linux due to Dawn limitations) | ||
| const default_targets = [_]std.Target.Query{ | ||
| .{ .cpu_arch = .x86_64, .os_tag = .linux }, | ||
| .{ .cpu_arch = .x86_64, .os_tag = .macos }, | ||
| .{ .cpu_arch = .aarch64, .os_tag = .macos }, | ||
| // .{ .cpu_arch = .x86_64, .os_tag = .windows }, // Excluded by default | ||
| // .{ .cpu_arch = .aarch64, .os_tag = .linux }, // Excluded by default (Dawn limitation) | ||
| }; | ||
| targets_to_build_slice = &default_targets; | ||
| } | ||
| // Generate library for each target | ||
| for (targets_to_build_slice) |target_query| { | ||
| const target = b.resolveTargetQuery(target_query); | ||
| // Generate target_name_str first as it's needed for Dawn paths | ||
| var target_name_buffer: [64]u8 = undefined; | ||
| const target_name_str = std.fmt.bufPrint( | ||
| &target_name_buffer, | ||
| "{s}-{s}", | ||
| .{ | ||
| @tagName(target.result.cpu.arch), | ||
| @tagName(target.result.os.tag), | ||
| }, | ||
| ) catch @panic("target_name_buffer too small"); | ||
| const dawn_platform_libs_dir_str = b.fmt("../../dawn/libs/{s}", .{target_name_str}); | ||
| const dawn_platform_libs_path = b.path(dawn_platform_libs_dir_str); // For RPath | ||
| const target_lib = b.addSharedLibrary(.{ | ||
| .name = lib_name, | ||
| .root_source_file = b.path(root_source_path), | ||
| .target = if (target.result.os.tag == .windows) b.resolveTargetQuery(.{ .cpu_arch = .x86_64, .os_tag = .windows, .abi = .msvc }) else target, | ||
| .optimize = optimize, | ||
| .link_libc = true, | ||
| }); | ||
| target_lib.addIncludePath(.{ .cwd_relative = b.fmt("{s}/include", .{dawn_platform_libs_dir_str}) }); | ||
| target_lib.linkSystemLibrary("webgpu_dawn"); | ||
| // The library path for the linker to find the dawn library during build | ||
| target_lib.addLibraryPath(.{ .cwd_relative = dawn_platform_libs_dir_str }); | ||
| target_lib.addLibraryPath(.{ .cwd_relative = "../../dawn/libs" }); | ||
| if (target.result.os.tag == .windows) { | ||
| const sdk_path = "C:/Program Files (x86)/Windows Kits/10"; | ||
| const sdk_version = "10.0.22621.0"; // Must be the same as the SDK version in the workflow | ||
| const ucrt_path_str = b.fmt("{s}/Lib/{s}/ucrt/x64", .{ sdk_path, sdk_version }); | ||
| const um_path_str = b.fmt("{s}/Lib/{s}/um/x64", .{ sdk_path, sdk_version }); | ||
| target_lib.addIncludePath(.{ .cwd_relative = "../../dawn/include" }); | ||
| target_lib.addLibraryPath(.{ .cwd_relative = ucrt_path_str }); | ||
| target_lib.addLibraryPath(.{ .cwd_relative = um_path_str }); | ||
| // Link core Windows system libraries | ||
| target_lib.linkSystemLibrary("user32"); | ||
| target_lib.linkSystemLibrary("kernel32"); | ||
| target_lib.linkSystemLibrary("gdi32"); | ||
| target_lib.linkSystemLibrary("ole32"); | ||
| target_lib.linkSystemLibrary("uuid"); | ||
| target_lib.linkSystemLibrary("d3d11"); | ||
| target_lib.linkSystemLibrary("d3d12"); | ||
| target_lib.linkSystemLibrary("dxgi"); | ||
| target_lib.linkSystemLibrary("dxguid"); | ||
| // Explicit MSVC runtime linking | ||
| // target_lib.linkSystemLibrary("libcmt"); | ||
| // target_lib.linkSystemLibrary("libvcruntime"); | ||
| // target_lib.linkSystemLibrary("libucrt"); | ||
| target_lib.linkSystemLibrary("msvcrt"); | ||
| target_lib.linkSystemLibrary("vcruntime"); | ||
| target_lib.linkSystemLibrary("ucrt"); | ||
| } else if (target.result.os.tag == .macos) { | ||
| target_lib.linkLibCpp(); | ||
| target_lib.linkFramework("Foundation"); | ||
| target_lib.linkFramework("CoreFoundation"); | ||
| target_lib.linkFramework("IOKit"); | ||
| target_lib.linkFramework("IOSurface"); | ||
| target_lib.linkFramework("Metal"); | ||
| target_lib.linkFramework("QuartzCore"); | ||
| // Add SDK paths for frameworks and libraries | ||
| target_lib.addFrameworkPath(.{ .cwd_relative = b.fmt("{s}/System/Library/Frameworks", .{sdk_path_str}) }); | ||
| target_lib.addLibraryPath(.{ .cwd_relative = b.fmt("{s}/usr/lib", .{sdk_path_str}) }); | ||
| } | ||
| const install_target_lib = b.addInstallArtifact(target_lib, .{ | ||
| .dest_dir = .{ | ||
| .override = .{ | ||
| .custom = b.fmt("{s}/{s}", .{ output_base_dir, target_name_str }), | ||
| }, | ||
| }, | ||
| }); | ||
| target_lib.addRPath(dawn_platform_libs_path); | ||
| const build_step_name = b.fmt("build-{s}", .{target_name_str}); | ||
| const build_step = b.step(build_step_name, b.fmt("Build for {s}", .{target_name_str})); | ||
| build_step.dependOn(&install_target_lib.step); | ||
| b.getInstallStep().dependOn(&install_target_lib.step); | ||
| } | ||
| } | ||
| fn getDefaultMacOSSDKPath() []const u8 { | ||
| // This is a common path; adjust if your SDK is elsewhere or prefer to always set via -Dsdk-path | ||
| // For a more robust solution, consider using `xcrun --sdk macosx --show-sdk-path` | ||
| // and passing it via -Dsdk-path, or detecting common Xcode/Command Line Tools locations. | ||
| return "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"; | ||
| } |
-668
| const std = @import("std"); | ||
| // Import C headers from webgpu.h | ||
| const c = @cImport({ | ||
| @cInclude("dawn/webgpu.h"); | ||
| }); | ||
| // --- Instance Functions --- | ||
| pub export fn zwgpuCreateInstance(descriptor: ?*const c.WGPUInstanceDescriptor) c.WGPUInstance { | ||
| return c.wgpuCreateInstance(descriptor); | ||
| } | ||
| pub export fn zwgpuInstanceRelease(instance: c.WGPUInstance) void { | ||
| c.wgpuInstanceRelease(instance); | ||
| } | ||
| pub export fn zwgpuInstanceRequestAdapter( | ||
| instance: c.WGPUInstance, | ||
| options: ?*const c.WGPURequestAdapterOptions, | ||
| callback_info_ptr: *const c.WGPURequestAdapterCallbackInfo, | ||
| ) u64 { | ||
| const future = c.wgpuInstanceRequestAdapter(instance, options, callback_info_ptr.*); | ||
| return future.id; | ||
| } | ||
| pub export fn zwgpuInstanceCreateSurface(instance: c.WGPUInstance, descriptor: *const c.WGPUSurfaceDescriptor) c.WGPUSurface { | ||
| return c.wgpuInstanceCreateSurface(instance, descriptor); | ||
| } | ||
| pub export fn zwgpuInstanceProcessEvents(instance: c.WGPUInstance) void { | ||
| c.wgpuInstanceProcessEvents(instance); | ||
| } | ||
| pub export fn zwgpuInstanceWaitAny(instance: c.WGPUInstance, future_count: u64, futures: [*]c.WGPUFutureWaitInfo, timeout_ns: u64) c.WGPUWaitStatus { | ||
| return c.wgpuInstanceWaitAny(instance, @intCast(future_count), futures, timeout_ns); | ||
| } | ||
| pub export fn zwgpuInstanceAddRef(instance: c.WGPUInstance) void { | ||
| c.wgpuInstanceAddRef(instance); | ||
| } | ||
| pub export fn zwgpuInstanceGetWGSLLanguageFeatures(instance: c.WGPUInstance, js_features_struct_ptr: *c.WGPUSupportedWGSLLanguageFeatures) c.WGPUStatus { | ||
| var temp_dawn_features_struct: c.WGPUSupportedWGSLLanguageFeatures = undefined; | ||
| const status = c.wgpuInstanceGetWGSLLanguageFeatures(instance, &temp_dawn_features_struct); | ||
| if (status != c.WGPUStatus_Success) { | ||
| return status; | ||
| } | ||
| if (temp_dawn_features_struct.featureCount > 0 and temp_dawn_features_struct.features != null) { | ||
| const dawn_features_slice = temp_dawn_features_struct.features[0..temp_dawn_features_struct.featureCount]; | ||
| js_features_struct_ptr.featureCount = temp_dawn_features_struct.featureCount; | ||
| const mutable_features: [*]c.WGPUWGSLLanguageFeatureName = @ptrCast(@constCast(js_features_struct_ptr.features)); | ||
| @memcpy(mutable_features[0..temp_dawn_features_struct.featureCount], dawn_features_slice); | ||
| } else { | ||
| js_features_struct_ptr.featureCount = 0; | ||
| js_features_struct_ptr.features = null; | ||
| } | ||
| c.wgpuSupportedWGSLLanguageFeaturesFreeMembers(temp_dawn_features_struct); | ||
| return c.WGPUStatus_Success; | ||
| } | ||
| // --- Adapter Functions --- | ||
| pub export fn zwgpuAdapterRelease(adapter: c.WGPUAdapter) void { | ||
| c.wgpuAdapterRelease(adapter); | ||
| } | ||
| pub export fn zwgpuAdapterGetInfo(adapter: c.WGPUAdapter, info_ptr: *c.WGPUAdapterInfo) c.WGPUStatus { | ||
| return c.wgpuAdapterGetInfo(adapter, info_ptr); | ||
| } | ||
| pub export fn zwgpuAdapterRequestDevice( | ||
| adapter: c.WGPUAdapter, | ||
| descriptor: ?*const c.WGPUDeviceDescriptor, | ||
| callback_info_ptr: *const c.WGPURequestDeviceCallbackInfo, | ||
| ) u64 { | ||
| const future = c.wgpuAdapterRequestDevice(adapter, descriptor, callback_info_ptr.*); | ||
| return future.id; | ||
| } | ||
| pub export fn zwgpuAdapterCreateDevice(adapter: c.WGPUAdapter, descriptor: ?*const c.WGPUDeviceDescriptor) c.WGPUDevice { | ||
| return c.wgpuAdapterCreateDevice(adapter, descriptor); | ||
| } | ||
| // Note: Same Linux issue as with device features - Dawn frees the features array prematurely | ||
| pub export fn zwgpuAdapterGetFeatures(adapter: c.WGPUAdapter, js_features_struct_ptr: *c.WGPUSupportedFeatures) void { | ||
| var temp_dawn_features_struct: c.WGPUSupportedFeatures = undefined; | ||
| c.wgpuAdapterGetFeatures(adapter, &temp_dawn_features_struct); | ||
| if (temp_dawn_features_struct.featureCount > 0 and temp_dawn_features_struct.features != null) { | ||
| const dawn_features_slice = temp_dawn_features_struct.features[0..temp_dawn_features_struct.featureCount]; | ||
| js_features_struct_ptr.featureCount = temp_dawn_features_struct.featureCount; | ||
| const mutable_features: [*]c.WGPUFeatureName = @ptrCast(@constCast(js_features_struct_ptr.features)); | ||
| @memcpy(mutable_features[0..temp_dawn_features_struct.featureCount], dawn_features_slice); | ||
| } else { | ||
| js_features_struct_ptr.featureCount = 0; | ||
| js_features_struct_ptr.features = null; | ||
| } | ||
| c.wgpuSupportedFeaturesFreeMembers(temp_dawn_features_struct); | ||
| } | ||
| pub export fn zwgpuAdapterGetLimits(adapter: c.WGPUAdapter, limits: *c.WGPULimits) c.WGPUStatus { | ||
| return c.wgpuAdapterGetLimits(adapter, limits); | ||
| } | ||
| // --- Device Functions --- | ||
| pub export fn zwgpuDeviceRelease(device: c.WGPUDevice) void { | ||
| c.wgpuDeviceRelease(device); | ||
| } | ||
| pub export fn zwgpuDeviceGetAdapterInfo(device: c.WGPUDevice, info_ptr: *c.WGPUAdapterInfo) c.WGPUStatus { | ||
| return c.wgpuDeviceGetAdapterInfo(device, info_ptr); | ||
| } | ||
| pub export fn zwgpuDeviceGetQueue(device: c.WGPUDevice) c.WGPUQueue { | ||
| return c.wgpuDeviceGetQueue(device); | ||
| } | ||
| pub export fn zwgpuDeviceCreateBuffer(device: c.WGPUDevice, descriptor: *const c.WGPUBufferDescriptor) c.WGPUBuffer { | ||
| return c.wgpuDeviceCreateBuffer(device, descriptor); | ||
| } | ||
| pub export fn zwgpuDeviceCreateTexture(device: c.WGPUDevice, descriptor: *const c.WGPUTextureDescriptor) c.WGPUTexture { | ||
| return c.wgpuDeviceCreateTexture(device, descriptor); | ||
| } | ||
| pub export fn zwgpuDeviceCreateSampler(device: c.WGPUDevice, descriptor: ?*const c.WGPUSamplerDescriptor) c.WGPUSampler { | ||
| return c.wgpuDeviceCreateSampler(device, descriptor); | ||
| } | ||
| pub export fn zwgpuDeviceCreateShaderModule(device: c.WGPUDevice, descriptor: *const c.WGPUShaderModuleDescriptor) c.WGPUShaderModule { | ||
| return c.wgpuDeviceCreateShaderModule(device, descriptor); | ||
| } | ||
| pub export fn zwgpuDeviceCreateBindGroupLayout(device: c.WGPUDevice, descriptor: *const c.WGPUBindGroupLayoutDescriptor) c.WGPUBindGroupLayout { | ||
| return c.wgpuDeviceCreateBindGroupLayout(device, descriptor); | ||
| } | ||
| pub export fn zwgpuDeviceCreateBindGroup(device: c.WGPUDevice, descriptor: *const c.WGPUBindGroupDescriptor) c.WGPUBindGroup { | ||
| return c.wgpuDeviceCreateBindGroup(device, descriptor); | ||
| } | ||
| pub export fn zwgpuDeviceCreatePipelineLayout(device: c.WGPUDevice, descriptor: *const c.WGPUPipelineLayoutDescriptor) c.WGPUPipelineLayout { | ||
| return c.wgpuDeviceCreatePipelineLayout(device, descriptor); | ||
| } | ||
| pub export fn zwgpuDeviceCreateRenderPipeline(device: c.WGPUDevice, descriptor: *const c.WGPURenderPipelineDescriptor) c.WGPURenderPipeline { | ||
| return c.wgpuDeviceCreateRenderPipeline(device, descriptor); | ||
| } | ||
| pub export fn zwgpuDeviceCreateComputePipeline(device: c.WGPUDevice, descriptor: *const c.WGPUComputePipelineDescriptor) c.WGPUComputePipeline { | ||
| return c.wgpuDeviceCreateComputePipeline(device, descriptor); | ||
| } | ||
| pub export fn zwgpuDeviceCreateRenderBundleEncoder(device: c.WGPUDevice, descriptor: *const c.WGPURenderBundleEncoderDescriptor) c.WGPURenderBundleEncoder { | ||
| return c.wgpuDeviceCreateRenderBundleEncoder(device, descriptor); | ||
| } | ||
| pub export fn zwgpuDeviceCreateCommandEncoder(device: c.WGPUDevice, descriptor: ?*const c.WGPUCommandEncoderDescriptor) c.WGPUCommandEncoder { | ||
| return c.wgpuDeviceCreateCommandEncoder(device, descriptor); | ||
| } | ||
| pub export fn zwgpuDeviceCreateQuerySet(device: c.WGPUDevice, descriptor: *const c.WGPUQuerySetDescriptor) c.WGPUQuerySet { | ||
| return c.wgpuDeviceCreateQuerySet(device, descriptor); | ||
| } | ||
| pub export fn zwgpuDeviceGetLimits(device: c.WGPUDevice, limits: *c.WGPULimits) c.WGPUStatus { | ||
| return c.wgpuDeviceGetLimits(device, limits); | ||
| } | ||
| pub export fn zwgpuDeviceHasFeature(device: c.WGPUDevice, feature: c.WGPUFeatureName) bool { | ||
| return c.wgpuDeviceHasFeature(device, feature) != 0; | ||
| } | ||
| // Note: On linux the u32 features array allocated by dawn is freed again when leaving the boundary, | ||
| // even though it should be kept until freed via wgpuSupportedFeaturesFreeMembers, | ||
| // that is why we copy it to the js heap. | ||
| pub export fn zwgpuDeviceGetFeatures(device: c.WGPUDevice, js_features_struct_ptr: *c.WGPUSupportedFeatures) void { | ||
| var temp_dawn_features_struct: c.WGPUSupportedFeatures = undefined; | ||
| c.wgpuDeviceGetFeatures(device, &temp_dawn_features_struct); | ||
| if (temp_dawn_features_struct.featureCount > 0 and temp_dawn_features_struct.features != null) { | ||
| const dawn_features_slice = temp_dawn_features_struct.features[0..temp_dawn_features_struct.featureCount]; | ||
| js_features_struct_ptr.featureCount = temp_dawn_features_struct.featureCount; | ||
| const mutable_features: [*]c.WGPUFeatureName = @ptrCast(@constCast(js_features_struct_ptr.features)); | ||
| @memcpy(mutable_features[0..temp_dawn_features_struct.featureCount], dawn_features_slice); | ||
| } else { | ||
| js_features_struct_ptr.featureCount = 0; | ||
| js_features_struct_ptr.features = null; | ||
| } | ||
| c.wgpuSupportedFeaturesFreeMembers(temp_dawn_features_struct); | ||
| } | ||
| pub export fn zwgpuDevicePushErrorScope(device: c.WGPUDevice, filter: c.WGPUErrorFilter) void { | ||
| c.wgpuDevicePushErrorScope(device, filter); | ||
| } | ||
| pub export fn zwgpuDevicePopErrorScope(device: c.WGPUDevice, callback_info_ptr: *const c.WGPUPopErrorScopeCallbackInfo) u64 { | ||
| const future = c.wgpuDevicePopErrorScope(device, callback_info_ptr.*); | ||
| return future.id; | ||
| } | ||
| pub export fn zwgpuDeviceTick(device: c.WGPUDevice) void { | ||
| c.wgpuDeviceTick(device); | ||
| } | ||
| pub export fn zwgpuDeviceDestroy(device: c.WGPUDevice) void { | ||
| c.wgpuDeviceDestroy(device); | ||
| } | ||
| pub export fn zwgpuDeviceInjectError(device: c.WGPUDevice, error_type: c.WGPUErrorType, message_view_ptr: *const c.WGPUStringView) void { | ||
| c.wgpuDeviceInjectError(device, error_type, message_view_ptr.*); | ||
| } | ||
| pub export fn zwgpuDeviceCreateComputePipelineAsync( | ||
| device: c.WGPUDevice, | ||
| descriptor: *const c.WGPUComputePipelineDescriptor, | ||
| callback_info_ptr: *const c.WGPUCreateComputePipelineAsyncCallbackInfo, | ||
| ) u64 { | ||
| const future = c.wgpuDeviceCreateComputePipelineAsync(device, descriptor, callback_info_ptr.*); | ||
| return future.id; | ||
| } | ||
| pub export fn zwgpuDeviceCreateRenderPipelineAsync( | ||
| device: c.WGPUDevice, | ||
| descriptor: *const c.WGPURenderPipelineDescriptor, | ||
| callback_info_ptr: *const c.WGPUCreateRenderPipelineAsyncCallbackInfo, | ||
| ) u64 { | ||
| const future = c.wgpuDeviceCreateRenderPipelineAsync(device, descriptor, callback_info_ptr.*); | ||
| return future.id; | ||
| } | ||
| // --- Queue Functions --- | ||
| pub export fn zwgpuQueueRelease(queue: c.WGPUQueue) void { | ||
| c.wgpuQueueRelease(queue); | ||
| } | ||
| pub export fn zwgpuQueueSubmit(queue: c.WGPUQueue, command_count: u64, commands: [*]const c.WGPUCommandBuffer) void { | ||
| c.wgpuQueueSubmit(queue, @intCast(command_count), commands); | ||
| } | ||
| pub export fn zwgpuQueueWriteBuffer(queue: c.WGPUQueue, buffer: c.WGPUBuffer, buffer_offset: u64, data: ?*const anyopaque, size: u64) void { | ||
| c.wgpuQueueWriteBuffer(queue, buffer, buffer_offset, data, @intCast(size)); | ||
| } | ||
| pub export fn zwgpuQueueWriteTexture(queue: c.WGPUQueue, destination: *const c.WGPUTexelCopyTextureInfo, data: ?*const anyopaque, data_size: u64, data_layout: *const c.WGPUTexelCopyBufferLayout, write_size: *const c.WGPUExtent3D) void { | ||
| c.wgpuQueueWriteTexture(queue, destination, data, @intCast(data_size), data_layout, write_size); | ||
| } | ||
| pub export fn zwgpuQueueOnSubmittedWorkDone(queue: c.WGPUQueue, callback_info_ptr: *const c.WGPUQueueWorkDoneCallbackInfo) u64 { | ||
| const future = c.wgpuQueueOnSubmittedWorkDone(queue, callback_info_ptr.*); | ||
| return future.id; | ||
| } | ||
| // --- Buffer Functions --- | ||
| pub export fn zwgpuBufferMapAsync( | ||
| buffer: c.WGPUBuffer, | ||
| mode: c.WGPUMapMode, | ||
| offset: u64, | ||
| size: u64, | ||
| callback_info_ptr: *const c.WGPUBufferMapCallbackInfo, | ||
| ) u64 { | ||
| const future = c.wgpuBufferMapAsync(buffer, mode, offset, size, callback_info_ptr.*); | ||
| return future.id; | ||
| } | ||
| pub export fn zwgpuBufferUnmap(buffer: c.WGPUBuffer) void { | ||
| c.wgpuBufferUnmap(buffer); | ||
| } | ||
| pub export fn zwgpuBufferRelease(buffer: c.WGPUBuffer) void { | ||
| c.wgpuBufferRelease(buffer); | ||
| } | ||
| pub export fn zwgpuBufferGetMappedRange(buffer: c.WGPUBuffer, offset: u64, size: u64) ?*anyopaque { | ||
| return c.wgpuBufferGetMappedRange(buffer, @intCast(offset), @intCast(size)); | ||
| } | ||
| pub export fn zwgpuBufferGetConstMappedRange(buffer: c.WGPUBuffer, offset: u64, size: u64) ?*const anyopaque { | ||
| return c.wgpuBufferGetConstMappedRange(buffer, @intCast(offset), @intCast(size)); | ||
| } | ||
| pub export fn zwgpuBufferDestroy(buffer: c.WGPUBuffer) void { | ||
| c.wgpuBufferDestroy(buffer); | ||
| } | ||
| // --- Texture Functions --- | ||
| pub export fn zwgpuTextureCreateView(texture: c.WGPUTexture, descriptor: ?*const c.WGPUTextureViewDescriptor) c.WGPUTextureView { | ||
| return c.wgpuTextureCreateView(texture, descriptor); | ||
| } | ||
| pub export fn zwgpuTextureDestroy(texture: c.WGPUTexture) void { | ||
| c.wgpuTextureDestroy(texture); | ||
| } | ||
| pub export fn zwgpuTextureRelease(texture: c.WGPUTexture) void { | ||
| c.wgpuTextureRelease(texture); | ||
| } | ||
| // --- TextureView Functions --- | ||
| pub export fn zwgpuTextureViewRelease(texture_view: c.WGPUTextureView) void { | ||
| c.wgpuTextureViewRelease(texture_view); | ||
| } | ||
| // --- Sampler Functions --- | ||
| pub export fn zwgpuSamplerRelease(sampler: c.WGPUSampler) void { | ||
| c.wgpuSamplerRelease(sampler); | ||
| } | ||
| // --- ShaderModule Functions --- | ||
| pub export fn zwgpuShaderModuleGetCompilationInfo(shader_module: c.WGPUShaderModule, callback_info_ptr: *const c.WGPUCompilationInfoCallbackInfo) u64 { | ||
| const future = c.wgpuShaderModuleGetCompilationInfo(shader_module, callback_info_ptr.*); | ||
| return future.id; | ||
| } | ||
| pub export fn zwgpuShaderModuleRelease(shader_module: c.WGPUShaderModule) void { | ||
| c.wgpuShaderModuleRelease(shader_module); | ||
| } | ||
| // --- BindGroupLayout Functions --- | ||
| pub export fn zwgpuBindGroupLayoutRelease(bind_group_layout: c.WGPUBindGroupLayout) void { | ||
| c.wgpuBindGroupLayoutRelease(bind_group_layout); | ||
| } | ||
| // --- BindGroup Functions --- | ||
| pub export fn zwgpuBindGroupRelease(bind_group: c.WGPUBindGroup) void { | ||
| c.wgpuBindGroupRelease(bind_group); | ||
| } | ||
| // --- PipelineLayout Functions --- | ||
| pub export fn zwgpuPipelineLayoutRelease(pipeline_layout: c.WGPUPipelineLayout) void { | ||
| c.wgpuPipelineLayoutRelease(pipeline_layout); | ||
| } | ||
| // --- QuerySet Functions --- | ||
| pub export fn zwgpuQuerySetDestroy(query_set: c.WGPUQuerySet) void { | ||
| c.wgpuQuerySetDestroy(query_set); | ||
| } | ||
| pub export fn zwgpuQuerySetRelease(query_set: c.WGPUQuerySet) void { | ||
| c.wgpuQuerySetRelease(query_set); | ||
| } | ||
| // --- RenderPipeline Functions --- | ||
| pub export fn zwgpuRenderPipelineRelease(render_pipeline: c.WGPURenderPipeline) void { | ||
| c.wgpuRenderPipelineRelease(render_pipeline); | ||
| } | ||
| pub export fn zwgpuRenderPipelineGetBindGroupLayout(render_pipeline: c.WGPURenderPipeline, group_index: u32) c.WGPUBindGroupLayout { | ||
| return c.wgpuRenderPipelineGetBindGroupLayout(render_pipeline, group_index); | ||
| } | ||
| // --- ComputePipeline Functions --- | ||
| pub export fn zwgpuComputePipelineRelease(compute_pipeline: c.WGPUComputePipeline) void { | ||
| c.wgpuComputePipelineRelease(compute_pipeline); | ||
| } | ||
| pub export fn zwgpuComputePipelineGetBindGroupLayout(compute_pipeline: c.WGPUComputePipeline, group_index: u32) c.WGPUBindGroupLayout { | ||
| return c.wgpuComputePipelineGetBindGroupLayout(compute_pipeline, group_index); | ||
| } | ||
| // --- CommandEncoder Functions --- | ||
| pub export fn zwgpuCommandEncoderBeginRenderPass(encoder: c.WGPUCommandEncoder, descriptor: *const c.WGPURenderPassDescriptor) c.WGPURenderPassEncoder { | ||
| return c.wgpuCommandEncoderBeginRenderPass(encoder, descriptor); | ||
| } | ||
| pub export fn zwgpuCommandEncoderBeginComputePass(encoder: c.WGPUCommandEncoder, descriptor: ?*const c.WGPUComputePassDescriptor) c.WGPUComputePassEncoder { | ||
| return c.wgpuCommandEncoderBeginComputePass(encoder, descriptor); | ||
| } | ||
| pub export fn zwgpuCommandEncoderClearBuffer(encoder: c.WGPUCommandEncoder, buffer: c.WGPUBuffer, offset: u64, size: u64) void { | ||
| c.wgpuCommandEncoderClearBuffer(encoder, buffer, offset, size); | ||
| } | ||
| pub export fn zwgpuCommandEncoderCopyBufferToBuffer(encoder: c.WGPUCommandEncoder, source: c.WGPUBuffer, source_offset: u64, destination: c.WGPUBuffer, destination_offset: u64, size: u64) void { | ||
| c.wgpuCommandEncoderCopyBufferToBuffer(encoder, source, source_offset, destination, destination_offset, size); | ||
| } | ||
| pub export fn zwgpuCommandEncoderCopyBufferToTexture(encoder: c.WGPUCommandEncoder, source: *const c.WGPUTexelCopyBufferInfo, destination: *const c.WGPUTexelCopyTextureInfo, copy_size: *const c.WGPUExtent3D) void { | ||
| c.wgpuCommandEncoderCopyBufferToTexture(encoder, source, destination, copy_size); | ||
| } | ||
| pub export fn zwgpuCommandEncoderCopyTextureToBuffer(encoder: c.WGPUCommandEncoder, source: *const c.WGPUTexelCopyTextureInfo, destination: *const c.WGPUTexelCopyBufferInfo, copy_size: *const c.WGPUExtent3D) void { | ||
| c.wgpuCommandEncoderCopyTextureToBuffer(encoder, source, destination, copy_size); | ||
| } | ||
| pub export fn zwgpuCommandEncoderCopyTextureToTexture(encoder: c.WGPUCommandEncoder, source: *const c.WGPUTexelCopyTextureInfo, destination: *const c.WGPUTexelCopyTextureInfo, copy_size: *const c.WGPUExtent3D) void { | ||
| c.wgpuCommandEncoderCopyTextureToTexture(encoder, source, destination, copy_size); | ||
| } | ||
| pub export fn zwgpuCommandEncoderResolveQuerySet(encoder: c.WGPUCommandEncoder, query_set: c.WGPUQuerySet, first_query: u32, query_count: u32, destination: c.WGPUBuffer, destination_offset: u64) void { | ||
| c.wgpuCommandEncoderResolveQuerySet(encoder, query_set, first_query, query_count, destination, destination_offset); | ||
| } | ||
| pub export fn zwgpuCommandEncoderFinish(encoder: c.WGPUCommandEncoder, descriptor: ?*const c.WGPUCommandBufferDescriptor) c.WGPUCommandBuffer { | ||
| return c.wgpuCommandEncoderFinish(encoder, descriptor); | ||
| } | ||
| pub export fn zwgpuCommandEncoderRelease(encoder: c.WGPUCommandEncoder) void { | ||
| c.wgpuCommandEncoderRelease(encoder); | ||
| } | ||
| pub export fn zwgpuCommandEncoderPushDebugGroup(encoder: c.WGPUCommandEncoder, group_label: *const c.WGPUStringView) void { | ||
| c.wgpuCommandEncoderPushDebugGroup(encoder, group_label.*); | ||
| } | ||
| pub export fn zwgpuCommandEncoderPopDebugGroup(encoder: c.WGPUCommandEncoder) void { | ||
| c.wgpuCommandEncoderPopDebugGroup(encoder); | ||
| } | ||
| pub export fn zwgpuCommandEncoderInsertDebugMarker(encoder: c.WGPUCommandEncoder, marker_label: *const c.WGPUStringView) void { | ||
| c.wgpuCommandEncoderInsertDebugMarker(encoder, marker_label.*); | ||
| } | ||
| // --- RenderPassEncoder Functions --- | ||
| pub export fn zwgpuRenderPassEncoderSetScissorRect(encoder: c.WGPURenderPassEncoder, x: u32, y: u32, width: u32, height: u32) void { | ||
| c.wgpuRenderPassEncoderSetScissorRect(encoder, x, y, width, height); | ||
| } | ||
| pub export fn zwgpuRenderPassEncoderSetViewport(encoder: c.WGPURenderPassEncoder, x: f32, y: f32, width: f32, height: f32, min_depth: f32, max_depth: f32) void { | ||
| c.wgpuRenderPassEncoderSetViewport(encoder, x, y, width, height, min_depth, max_depth); | ||
| } | ||
| pub export fn zwgpuRenderPassEncoderSetPipeline(encoder: c.WGPURenderPassEncoder, pipeline: c.WGPURenderPipeline) void { | ||
| c.wgpuRenderPassEncoderSetPipeline(encoder, pipeline); | ||
| } | ||
| pub export fn zwgpuRenderPassEncoderSetBindGroup(encoder: c.WGPURenderPassEncoder, group_index: u32, group: c.WGPUBindGroup, dynamic_offset_count: u64, dynamic_offsets: ?*const u32) void { | ||
| c.wgpuRenderPassEncoderSetBindGroup(encoder, group_index, group, @intCast(dynamic_offset_count), dynamic_offsets); | ||
| } | ||
| pub export fn zwgpuRenderPassEncoderSetVertexBuffer(encoder: c.WGPURenderPassEncoder, slot: u32, buffer: c.WGPUBuffer, offset: u64, size: u64) void { | ||
| c.wgpuRenderPassEncoderSetVertexBuffer(encoder, slot, buffer, offset, size); | ||
| } | ||
| pub export fn zwgpuRenderPassEncoderSetIndexBuffer(encoder: c.WGPURenderPassEncoder, buffer: c.WGPUBuffer, format: c.WGPUIndexFormat, offset: u64, size: u64) void { | ||
| c.wgpuRenderPassEncoderSetIndexBuffer(encoder, buffer, format, offset, size); | ||
| } | ||
| pub export fn zwgpuRenderPassEncoderDraw(encoder: c.WGPURenderPassEncoder, vertex_count: u32, instance_count: u32, first_vertex: u32, first_instance: u32) void { | ||
| c.wgpuRenderPassEncoderDraw(encoder, vertex_count, instance_count, first_vertex, first_instance); | ||
| } | ||
| pub export fn zwgpuRenderPassEncoderDrawIndexed(encoder: c.WGPURenderPassEncoder, index_count: u32, instance_count: u32, first_index: u32, base_vertex: i32, first_instance: u32) void { | ||
| c.wgpuRenderPassEncoderDrawIndexed(encoder, index_count, instance_count, first_index, base_vertex, first_instance); | ||
| } | ||
| pub export fn zwgpuRenderPassEncoderDrawIndirect(encoder: c.WGPURenderPassEncoder, indirect_buffer: c.WGPUBuffer, indirect_offset: u64) void { | ||
| c.wgpuRenderPassEncoderDrawIndirect(encoder, indirect_buffer, indirect_offset); | ||
| } | ||
| pub export fn zwgpuRenderPassEncoderDrawIndexedIndirect(encoder: c.WGPURenderPassEncoder, indirect_buffer: c.WGPUBuffer, indirect_offset: u64) void { | ||
| c.wgpuRenderPassEncoderDrawIndexedIndirect(encoder, indirect_buffer, indirect_offset); | ||
| } | ||
| pub export fn zwgpuRenderPassEncoderExecuteBundles(encoder: c.WGPURenderPassEncoder, bundle_count: u64, bundles: [*]const c.WGPURenderBundle) void { | ||
| c.wgpuRenderPassEncoderExecuteBundles(encoder, @intCast(bundle_count), bundles); | ||
| } | ||
| pub export fn zwgpuRenderPassEncoderEnd(encoder: c.WGPURenderPassEncoder) void { | ||
| c.wgpuRenderPassEncoderEnd(encoder); | ||
| } | ||
| pub export fn zwgpuRenderPassEncoderRelease(encoder: c.WGPURenderPassEncoder) void { | ||
| c.wgpuRenderPassEncoderRelease(encoder); | ||
| } | ||
| pub export fn zwgpuRenderPassEncoderPushDebugGroup(encoder: c.WGPURenderPassEncoder, group_label: *const c.WGPUStringView) void { | ||
| c.wgpuRenderPassEncoderPushDebugGroup(encoder, group_label.*); | ||
| } | ||
| pub export fn zwgpuRenderPassEncoderPopDebugGroup(encoder: c.WGPURenderPassEncoder) void { | ||
| c.wgpuRenderPassEncoderPopDebugGroup(encoder); | ||
| } | ||
| pub export fn zwgpuRenderPassEncoderInsertDebugMarker(encoder: c.WGPURenderPassEncoder, marker_label: *const c.WGPUStringView) void { | ||
| c.wgpuRenderPassEncoderInsertDebugMarker(encoder, marker_label.*); | ||
| } | ||
| pub export fn zwgpuRenderPassEncoderSetBlendConstant(encoder: c.WGPURenderPassEncoder, color: *const c.WGPUColor) void { | ||
| c.wgpuRenderPassEncoderSetBlendConstant(encoder, color); | ||
| } | ||
| pub export fn zwgpuRenderPassEncoderSetStencilReference(encoder: c.WGPURenderPassEncoder, reference: u32) void { | ||
| c.wgpuRenderPassEncoderSetStencilReference(encoder, reference); | ||
| } | ||
| pub export fn zwgpuRenderPassEncoderBeginOcclusionQuery(encoder: c.WGPURenderPassEncoder, query_index: u32) void { | ||
| c.wgpuRenderPassEncoderBeginOcclusionQuery(encoder, query_index); | ||
| } | ||
| pub export fn zwgpuRenderPassEncoderEndOcclusionQuery(encoder: c.WGPURenderPassEncoder) void { | ||
| c.wgpuRenderPassEncoderEndOcclusionQuery(encoder); | ||
| } | ||
| // --- ComputePassEncoder Functions --- | ||
| pub export fn zwgpuComputePassEncoderSetPipeline(encoder: c.WGPUComputePassEncoder, pipeline: c.WGPUComputePipeline) void { | ||
| c.wgpuComputePassEncoderSetPipeline(encoder, pipeline); | ||
| } | ||
| pub export fn zwgpuComputePassEncoderSetBindGroup(encoder: c.WGPUComputePassEncoder, group_index: u32, group: c.WGPUBindGroup, dynamic_offset_count: u64, dynamic_offsets: ?*const u32) void { | ||
| c.wgpuComputePassEncoderSetBindGroup(encoder, group_index, group, @intCast(dynamic_offset_count), dynamic_offsets); | ||
| } | ||
| pub export fn zwgpuComputePassEncoderDispatchWorkgroups(encoder: c.WGPUComputePassEncoder, count_x: u32, count_y: u32, count_z: u32) void { | ||
| c.wgpuComputePassEncoderDispatchWorkgroups(encoder, count_x, count_y, count_z); | ||
| } | ||
| pub export fn zwgpuComputePassEncoderDispatchWorkgroupsIndirect(encoder: c.WGPUComputePassEncoder, indirect_buffer: c.WGPUBuffer, indirect_offset: u64) void { | ||
| c.wgpuComputePassEncoderDispatchWorkgroupsIndirect(encoder, indirect_buffer, indirect_offset); | ||
| } | ||
| pub export fn zwgpuComputePassEncoderEnd(encoder: c.WGPUComputePassEncoder) void { | ||
| c.wgpuComputePassEncoderEnd(encoder); | ||
| } | ||
| pub export fn zwgpuComputePassEncoderRelease(encoder: c.WGPUComputePassEncoder) void { | ||
| c.wgpuComputePassEncoderRelease(encoder); | ||
| } | ||
| pub export fn zwgpuComputePassEncoderPushDebugGroup(encoder: c.WGPUComputePassEncoder, group_label: *const c.WGPUStringView) void { | ||
| c.wgpuComputePassEncoderPushDebugGroup(encoder, group_label.*); | ||
| } | ||
| pub export fn zwgpuComputePassEncoderPopDebugGroup(encoder: c.WGPUComputePassEncoder) void { | ||
| c.wgpuComputePassEncoderPopDebugGroup(encoder); | ||
| } | ||
| pub export fn zwgpuComputePassEncoderInsertDebugMarker(encoder: c.WGPUComputePassEncoder, marker_label: *const c.WGPUStringView) void { | ||
| c.wgpuComputePassEncoderInsertDebugMarker(encoder, marker_label.*); | ||
| } | ||
| // --- CommandBuffer Functions --- | ||
| pub export fn zwgpuCommandBufferRelease(command_buffer: c.WGPUCommandBuffer) void { | ||
| c.wgpuCommandBufferRelease(command_buffer); | ||
| } | ||
| // --- Surface Functions --- | ||
| pub export fn zwgpuSurfaceConfigure(surface: c.WGPUSurface, config: *const c.WGPUSurfaceConfiguration) void { | ||
| c.wgpuSurfaceConfigure(surface, config); | ||
| } | ||
| pub export fn zwgpuSurfaceUnconfigure(surface: c.WGPUSurface) void { | ||
| c.wgpuSurfaceUnconfigure(surface); | ||
| } | ||
| pub export fn zwgpuSurfaceGetCurrentTexture(surface: c.WGPUSurface, surface_texture: *c.WGPUSurfaceTexture) void { | ||
| c.wgpuSurfaceGetCurrentTexture(surface, surface_texture); | ||
| } | ||
| pub export fn zwgpuSurfacePresent(surface: c.WGPUSurface) void { | ||
| c.wgpuSurfacePresent(surface); | ||
| } | ||
| pub export fn zwgpuSurfaceRelease(surface: c.WGPUSurface) void { | ||
| c.wgpuSurfaceRelease(surface); | ||
| } | ||
| // --- Freeing Functions --- | ||
| pub export fn zwgpuAdapterInfoFreeMembers(value_ptr: *const c.WGPUAdapterInfo) void { | ||
| c.wgpuAdapterInfoFreeMembers(value_ptr.*); | ||
| } | ||
| pub export fn zwgpuSurfaceCapabilitiesFreeMembers(value_ptr: *const c.WGPUSurfaceCapabilities) void { | ||
| c.wgpuSurfaceCapabilitiesFreeMembers(value_ptr.*); | ||
| } | ||
| pub export fn zwgpuSupportedFeaturesFreeMembers(value_ptr: *const c.WGPUSupportedFeatures) void { | ||
| c.wgpuSupportedFeaturesFreeMembers(value_ptr.*); | ||
| } | ||
| pub export fn zwgpuSharedBufferMemoryEndAccessStateFreeMembers(value_ptr: *const c.WGPUSharedBufferMemoryEndAccessState) void { | ||
| c.wgpuSharedBufferMemoryEndAccessStateFreeMembers(value_ptr.*); | ||
| } | ||
| pub export fn zwgpuSharedTextureMemoryEndAccessStateFreeMembers(value_ptr: *const c.WGPUSharedTextureMemoryEndAccessState) void { | ||
| c.wgpuSharedTextureMemoryEndAccessStateFreeMembers(value_ptr.*); | ||
| } | ||
| pub export fn zwgpuSupportedWGSLLanguageFeaturesFreeMembers(value_ptr: *const c.WGPUSupportedWGSLLanguageFeatures) void { | ||
| c.wgpuSupportedWGSLLanguageFeaturesFreeMembers(value_ptr.*); | ||
| } | ||
| // --- RenderBundle Functions --- | ||
| pub export fn zwgpuRenderBundleRelease(bundle: c.WGPURenderBundle) void { | ||
| c.wgpuRenderBundleRelease(bundle); | ||
| } | ||
| // --- RenderBundleEncoder Functions --- | ||
| pub export fn zwgpuRenderBundleEncoderDraw(encoder: c.WGPURenderBundleEncoder, vertex_count: u32, instance_count: u32, first_vertex: u32, first_instance: u32) void { | ||
| c.wgpuRenderBundleEncoderDraw(encoder, vertex_count, instance_count, first_vertex, first_instance); | ||
| } | ||
| pub export fn zwgpuRenderBundleEncoderDrawIndexed(encoder: c.WGPURenderBundleEncoder, index_count: u32, instance_count: u32, first_index: u32, base_vertex: i32, first_instance: u32) void { | ||
| c.wgpuRenderBundleEncoderDrawIndexed(encoder, index_count, instance_count, first_index, base_vertex, first_instance); | ||
| } | ||
| pub export fn zwgpuRenderBundleEncoderDrawIndirect(encoder: c.WGPURenderBundleEncoder, indirect_buffer: c.WGPUBuffer, indirect_offset: u64) void { | ||
| c.wgpuRenderBundleEncoderDrawIndirect(encoder, indirect_buffer, indirect_offset); | ||
| } | ||
| pub export fn zwgpuRenderBundleEncoderDrawIndexedIndirect(encoder: c.WGPURenderBundleEncoder, indirect_buffer: c.WGPUBuffer, indirect_offset: u64) void { | ||
| c.wgpuRenderBundleEncoderDrawIndexedIndirect(encoder, indirect_buffer, indirect_offset); | ||
| } | ||
| pub export fn zwgpuRenderBundleEncoderFinish(encoder: c.WGPURenderBundleEncoder, descriptor: ?*const c.WGPURenderBundleDescriptor) c.WGPURenderBundle { | ||
| return c.wgpuRenderBundleEncoderFinish(encoder, descriptor); | ||
| } | ||
| pub export fn zwgpuRenderBundleEncoderSetBindGroup(encoder: c.WGPURenderBundleEncoder, group_index: u32, group: c.WGPUBindGroup, dynamic_offset_count: u64, dynamic_offsets: ?*const u32) void { | ||
| c.wgpuRenderBundleEncoderSetBindGroup(encoder, group_index, group, @intCast(dynamic_offset_count), dynamic_offsets); | ||
| } | ||
| pub export fn zwgpuRenderBundleEncoderSetIndexBuffer(encoder: c.WGPURenderBundleEncoder, buffer: c.WGPUBuffer, format: c.WGPUIndexFormat, offset: u64, size: u64) void { | ||
| c.wgpuRenderBundleEncoderSetIndexBuffer(encoder, buffer, format, offset, size); | ||
| } | ||
| pub export fn zwgpuRenderBundleEncoderSetPipeline(encoder: c.WGPURenderBundleEncoder, pipeline: c.WGPURenderPipeline) void { | ||
| c.wgpuRenderBundleEncoderSetPipeline(encoder, pipeline); | ||
| } | ||
| pub export fn zwgpuRenderBundleEncoderSetVertexBuffer(encoder: c.WGPURenderBundleEncoder, slot: u32, buffer: c.WGPUBuffer, offset: u64, size: u64) void { | ||
| c.wgpuRenderBundleEncoderSetVertexBuffer(encoder, slot, buffer, offset, size); | ||
| } | ||
| pub export fn zwgpuRenderBundleEncoderRelease(encoder: c.WGPURenderBundleEncoder) void { | ||
| c.wgpuRenderBundleEncoderRelease(encoder); | ||
| } | ||
| pub export fn zwgpuRenderBundleEncoderPushDebugGroup(encoder: c.WGPURenderBundleEncoder, group_label: *const c.WGPUStringView) void { | ||
| c.wgpuRenderBundleEncoderPushDebugGroup(encoder, group_label.*); | ||
| } | ||
| pub export fn zwgpuRenderBundleEncoderPopDebugGroup(encoder: c.WGPURenderBundleEncoder) void { | ||
| c.wgpuRenderBundleEncoderPopDebugGroup(encoder); | ||
| } | ||
| pub export fn zwgpuRenderBundleEncoderInsertDebugMarker(encoder: c.WGPURenderBundleEncoder, marker_label: *const c.WGPUStringView) void { | ||
| c.wgpuRenderBundleEncoderInsertDebugMarker(encoder, marker_label.*); | ||
| } |
| { | ||
| "compilerOptions": { | ||
| // Environment setup & latest features | ||
| "lib": ["ESNext"], | ||
| "target": "ESNext", | ||
| "module": "ESNext", | ||
| "moduleDetection": "force", | ||
| "jsx": "react-jsx", | ||
| "allowJs": true, | ||
| // Bundler mode | ||
| "moduleResolution": "bundler", | ||
| "allowImportingTsExtensions": true, | ||
| "verbatimModuleSyntax": true, | ||
| "noEmit": true, | ||
| // Best practices | ||
| "strict": true, | ||
| "skipLibCheck": true, | ||
| "noFallthroughCasesInSwitch": true, | ||
| "noUncheckedIndexedAccess": true, | ||
| // Some stricter flags (disabled by default) | ||
| "noUnusedLocals": false, | ||
| "noUnusedParameters": false, | ||
| "noPropertyAccessFromIndexSignature": false | ||
| } | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
No contributors or author data
MaintenancePackage does not specify a list of contributors or an author in package.json.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
0
-100%1
-50%7
-80.56%426098
-99.28%5
400%34
-38.18%9284
-23.75%7
16.67%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed