Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 2 additions & 12 deletions src/benchmark/Benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BitonicSorter } from '../sorting/BitonicSorter';
import { RadixSorter } from '../sorting/RadixSorter';
import { BenchmarkResult } from '../shared/types';
import { DEFAULT_BENCHMARK_SIZES } from '../shared/constants';
import { createRandomUint32Array } from '../shared/random';

/**
* Performance benchmark for sorting algorithms
Expand Down Expand Up @@ -37,18 +38,7 @@ export class Benchmark {
* Generate random test data using crypto for better randomness
*/
static generateRandomData(size: number): Uint32Array {
const data = new Uint32Array(size);
// Use crypto.getRandomValues for cryptographically secure random numbers
// Falls back to Math.random if crypto is not available (e.g., older Node.js)
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
crypto.getRandomValues(data);
} else {
// Fallback for environments without crypto.getRandomValues
for (let i = 0; i < size; i++) {
data[i] = Math.floor(Math.random() * 0xffffffff);
}
}
return data;
return createRandomUint32Array(size);
}

/**
Expand Down
22 changes: 14 additions & 8 deletions src/core/BufferManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BufferAllocationError, BufferMapError } from './errors';
import { BufferAllocationError, BufferMapError, GPUTimeoutError } from './errors';
import { withTimeout } from './timeout';

/**
* Formats an unknown error into a string for error messages
Expand Down Expand Up @@ -102,14 +103,16 @@ export class BufferManager {
const alignedSize = BufferManager.alignSize(size, 4);
const stagingBuffer = this.createStagingBuffer(alignedSize);

// Copy from source to staging
const commandEncoder = this.device.createCommandEncoder();
commandEncoder.copyBufferToBuffer(sourceBuffer, 0, stagingBuffer, 0, alignedSize);
this.device.queue.submit([commandEncoder.finish()]);

// Map and read
try {
await stagingBuffer.mapAsync(GPUMapMode.READ);
// Copy from source to staging
const commandEncoder = this.device.createCommandEncoder();
commandEncoder.copyBufferToBuffer(sourceBuffer, 0, stagingBuffer, 0, alignedSize);
this.device.queue.submit([commandEncoder.finish()]);

// Map and read
await withTimeout(stagingBuffer.mapAsync(GPUMapMode.READ), {
message: 'Buffer mapping timed out',
});
const mappedRange = stagingBuffer.getMappedRange();
const result = new Uint32Array(mappedRange.slice(0, size));
stagingBuffer.unmap();
Expand All @@ -120,6 +123,9 @@ export class BufferManager {
return result;
} catch (e) {
this.releaseBuffer(stagingBuffer);
if (e instanceof GPUTimeoutError) {
throw e;
}
throw new BufferMapError(`Failed to read buffer: ${formatError(e)}`);
}
}
Expand Down
38 changes: 38 additions & 0 deletions src/core/BufferScope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Tracks temporary GPU buffers for a single operation and releases them once.
*/
export class BufferScope {
private releasers = new Map<GPUBuffer, () => void>();

/**
* Track a buffer with an optional custom release strategy.
*/
track<T extends GPUBuffer>(buffer: T, release?: (buffer: T) => void): T {
this.releasers.set(buffer, () => {
if (release) {
release(buffer);
return;
}
buffer.destroy();
});

return buffer;
}

/**
* Stop tracking a buffer so later releaseAll() calls leave it alone.
*/
untrack(buffer: GPUBuffer): void {
this.releasers.delete(buffer);
}

/**
* Release every tracked buffer exactly once.
*/
releaseAll(): void {
for (const release of this.releasers.values()) {
release();
}
this.releasers.clear();
}
}
15 changes: 11 additions & 4 deletions src/core/GPUContext.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { GPUContextConfig, GPULimitsInfo } from '../shared/types';
import { WebGPUNotSupportedError, GPUAdapterError, GPUDeviceError } from './errors';
import { browserGPURuntime } from './runtime/browserGPURuntime';
import type { GPURuntime } from './runtime/GPURuntime';

/**
* Callback type for device loss events
Expand All @@ -15,12 +17,17 @@ export class GPUContext {
private initialized = false;
private deviceLossCallbacks: Set<DeviceLossCallback> = new Set();
private limitsInfo: GPULimitsInfo | null = null;
private runtime: GPURuntime;

constructor(runtime: GPURuntime = browserGPURuntime) {
this.runtime = runtime;
}

/**
* Check if WebGPU is supported in the current environment
*/
static isSupported(): boolean {
return typeof navigator !== 'undefined' && 'gpu' in navigator;
static isSupported(runtime: GPURuntime = browserGPURuntime): boolean {
return runtime.isSupported();
}

/**
Expand Down Expand Up @@ -69,12 +76,12 @@ export class GPUContext {
return;
}

if (!GPUContext.isSupported()) {
if (!this.runtime.isSupported()) {
throw new WebGPUNotSupportedError();
}

// Request adapter
this.adapter = await navigator.gpu.requestAdapter({
this.adapter = await this.runtime.requestAdapter({
powerPreference: config?.powerPreference ?? 'high-performance',
});

Expand Down
4 changes: 4 additions & 0 deletions src/core/runtime/GPURuntime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface GPURuntime {
isSupported(): boolean;
requestAdapter(options?: GPURequestAdapterOptions): Promise<GPUAdapter | null>;
}
19 changes: 19 additions & 0 deletions src/core/runtime/browserGPURuntime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { GPURuntime } from './GPURuntime';

function getBrowserGPU(): GPU | null {
if (typeof navigator === 'undefined' || !('gpu' in navigator)) {
return null;
}

return navigator.gpu;
}

export const browserGPURuntime: GPURuntime = {
isSupported(): boolean {
return getBrowserGPU() !== null;
},

async requestAdapter(options?: GPURequestAdapterOptions): Promise<GPUAdapter | null> {
return (await getBrowserGPU()?.requestAdapter(options)) ?? null;
},
};
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Core exports
export { GPUContext } from './core/GPUContext';
export type { DeviceLossCallback } from './core/GPUContext';
export { browserGPURuntime } from './core/runtime/browserGPURuntime';
export type { GPURuntime } from './core/runtime/GPURuntime';
export { BufferManager } from './core/BufferManager';
export { Validator } from './core/Validator';
export { withTimeout, createTimeoutWrapper } from './core/timeout';
Expand Down
32 changes: 32 additions & 0 deletions src/shared/random.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const MAX_CRYPTO_FILL_BYTES = 65536;
const MAX_CRYPTO_FILL_U32 = MAX_CRYPTO_FILL_BYTES / Uint32Array.BYTES_PER_ELEMENT;
const MAX_U32_EXCLUSIVE = 0x100000000;

/**
* Fill an existing Uint32Array with random data.
* Uses chunked crypto fills to stay within Web Crypto per-call quotas.
*/
export function fillRandomUint32Array(data: Uint32Array): Uint32Array {
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
for (let offset = 0; offset < data.length; offset += MAX_CRYPTO_FILL_U32) {
const chunkLength = Math.min(MAX_CRYPTO_FILL_U32, data.length - offset);
const chunk = new Uint32Array(chunkLength);
crypto.getRandomValues(chunk);
data.set(chunk, offset);
}
return data;
}

for (let i = 0; i < data.length; i++) {
data[i] = Math.floor(Math.random() * MAX_U32_EXCLUSIVE);
}

return data;
}

/**
* Create a random Uint32Array of the requested size.
*/
export function createRandomUint32Array(size: number): Uint32Array {
return fillRandomUint32Array(new Uint32Array(size));
}
Loading
Loading