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
138 changes: 87 additions & 51 deletions src/lib/components/OnboardingModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import WebGPUInstructions from './WebGPUInstructions.svelte';
import PaperViewer from './PaperViewer.svelte';
import MobileWarning from './MobileWarning.svelte';
import { completeOnboarding, setConsent, getConsent } from '$lib/utils/consentStorage';
import { completeOnboarding, setConsent, getConsent, updateWebGPUAvailability } from '$lib/utils/consentStorage';
import { useWebGPUStatus } from '$lib/composables/useWebGPUStatus.svelte';

interface Props {
Expand All @@ -21,6 +21,16 @@

const webgpu = useWebGPUStatus();

// Gate: non-dismissable when WebGPU is unavailable
const gated = $derived(!webgpu.isDetecting && !webgpu.available);

// Update stored WebGPU availability when detection completes
$effect(() => {
if (!webgpu.isDetecting) {
updateWebGPUAvailability(webgpu.available);
}
});

// Track completed steps
let completedSteps = $state<Set<number>>(new Set());

Expand Down Expand Up @@ -48,6 +58,7 @@
}

function handleFinish() {
if (gated) return;
markStepCompleted(currentStep);
if (dontShowAgain) {
completeOnboarding();
Expand All @@ -61,13 +72,14 @@
}

function handleBackdropClick(event: MouseEvent) {
if (gated) return;
if (event.target === event.currentTarget) {
handleFinish();
}
}

function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
if (event.key === 'Escape' && !gated) {
handleFinish();
} else if (event.key === 'ArrowRight' || (event.key === 'Enter' && currentStep < TOTAL_STEPS)) {
nextStep();
Expand Down Expand Up @@ -128,16 +140,18 @@
Step {currentStep} of {TOTAL_STEPS}
</p>
</div>
<button
type="button"
class="rounded-lg p-2 text-surface-500-400 transition-colors hover:bg-surface-100-800"
onclick={handleFinish}
aria-label="Close"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
{#if !gated}
<button
type="button"
class="rounded-lg p-2 text-surface-500-400 transition-colors hover:bg-surface-100-800"
onclick={handleFinish}
aria-label="Close"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
{/if}
</div>

<!-- Progress Indicator -->
Expand Down Expand Up @@ -227,9 +241,9 @@
</div>
{/if}

{#if !webgpu.available && !webgpu.isDetecting}
<div class="rounded-lg bg-surface-100-800 p-3 text-sm text-surface-600-300">
You can continue without WebGPU - the demo will use CPU-based processing (Futhark WASM).
{#if gated}
<div class="rounded-lg border border-error-500/30 bg-error-500/10 p-3 text-sm text-error-700 dark:text-error-400">
WebGPU is required to use the Pixelwise demos. Please follow the instructions above to enable WebGPU in your browser, then click "Re-check".
</div>
{/if}
</div>
Expand Down Expand Up @@ -331,47 +345,61 @@
{:else if currentStep === 5}
<!-- Step 5: Ready -->
<div class="space-y-4">
<div class="rounded-lg border border-success-500/30 bg-success-500/10 p-4">
<h4 class="mb-3 flex items-center gap-2 font-semibold text-success-700 dark:text-success-400">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Setup Complete
</h4>
<div class="space-y-2 text-sm text-surface-700-200">
<div class="flex items-center justify-between">
<span>Backend:</span>
<span class="font-mono text-surface-900-50">{performanceTier}</span>
</div>
<div class="flex items-center justify-between">
<span>WebGPU:</span>
<span class="font-mono {webgpu.available ? 'text-success-500' : 'text-warning-500'}">
{webgpu.available ? 'Available' : 'Using fallback'}
</span>
</div>
{#if webgpu.adapter}
{#if gated}
<div class="rounded-lg border border-error-500/30 bg-error-500/10 p-4">
<h4 class="mb-3 flex items-center gap-2 font-semibold text-error-700 dark:text-error-400">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
WebGPU Required
</h4>
<p class="text-sm text-surface-700-200">
The Pixelwise demos require a WebGPU-capable browser. Please go back to Step 2 for browser-specific instructions on enabling WebGPU, then click "Re-check".
</p>
</div>
{:else}
<div class="rounded-lg border border-success-500/30 bg-success-500/10 p-4">
<h4 class="mb-3 flex items-center gap-2 font-semibold text-success-700 dark:text-success-400">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Setup Complete
</h4>
<div class="space-y-2 text-sm text-surface-700-200">
<div class="flex items-center justify-between">
<span>Backend:</span>
<span class="font-mono text-surface-900-50">{performanceTier}</span>
</div>
<div class="flex items-center justify-between">
<span>WebGPU:</span>
<span class="font-mono {webgpu.available ? 'text-success-500' : 'text-warning-500'}">
{webgpu.available ? 'Available' : 'Using fallback'}
</span>
</div>
{#if webgpu.adapter}
<div class="flex items-center justify-between">
<span>GPU:</span>
<span class="font-mono text-surface-900-50 text-xs">{webgpu.adapter}</span>
</div>
{/if}
<div class="flex items-center justify-between">
<span>GPU:</span>
<span class="font-mono text-surface-900-50 text-xs">{webgpu.adapter}</span>
<span>Screen Capture:</span>
<span class="font-mono {screenCaptureTestResult === 'granted' ? 'text-success-500' : 'text-surface-500-400'}">
{screenCaptureTestResult === 'granted' ? 'Granted' : 'Will prompt on start'}
</span>
</div>
{/if}
<div class="flex items-center justify-between">
<span>Screen Capture:</span>
<span class="font-mono {screenCaptureTestResult === 'granted' ? 'text-success-500' : 'text-surface-500-400'}">
{screenCaptureTestResult === 'granted' ? 'Granted' : 'Will prompt on start'}
</span>
</div>
</div>
</div>

<label class="flex items-center gap-2 text-sm text-surface-600-300">
<input
type="checkbox"
bind:checked={dontShowAgain}
class="h-4 w-4 rounded border-surface-400-500 text-primary-500 focus:ring-primary-500"
/>
Don't show this wizard again
</label>
<label class="flex items-center gap-2 text-sm text-surface-600-300">
<input
type="checkbox"
bind:checked={dontShowAgain}
class="h-4 w-4 rounded border-surface-400-500 text-primary-500 focus:ring-primary-500"
/>
Don't show this wizard again
</label>
{/if}
</div>
{/if}
</div>
Expand Down Expand Up @@ -418,6 +446,14 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
{:else if gated}
<button
type="button"
class="rounded-lg bg-surface-300-600 px-6 py-2 text-sm font-medium text-surface-500-400 cursor-not-allowed"
disabled
>
WebGPU Required
</button>
{:else}
<button
type="button"
Expand Down
30 changes: 22 additions & 8 deletions src/lib/core/ComputeDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,15 +354,24 @@ export function createComputeDispatcher() {
throw new Error('Futhark context not initialized');
}

const input2d = futharkContext.new_f32_2d(levels, width, height);

try {
const result = futharkContext.compute_esdt_2d(input2d, useRelaxation);
const data = await result.toTypedArray();
result.free();
return { data, width, height };
} finally {
input2d.free();
const input2d = futharkContext.new_f32_2d(levels, width, height);

try {
const result = futharkContext.compute_esdt_2d(input2d, useRelaxation);
const data = await result.toTypedArray();
result.free();
return { data, width, height };
} finally {
input2d.free();
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
if (msg.includes('OOM') || msg.includes('out of memory') || msg.includes('Aborted')) {
console.error('[ComputeDispatcher] WASM OOM — nulling futhark context');
futharkContext = null;
}
throw err;
}
}

Expand Down Expand Up @@ -730,6 +739,11 @@ export function createComputeDispatcher() {
try {
return await runFullPipelineCPU(rgbaData, width, height, fullConfig);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
if (msg.includes('OOM') || msg.includes('Aborted')) {
console.error('[ComputeDispatcher] WASM OOM in CPU pipeline, context destroyed');
futharkContext = null;
}
console.error('[ComputeDispatcher] CPU pipeline failed:', err);
// Last resort: return original data unmodified
recordMetrics({
Expand Down
9 changes: 9 additions & 0 deletions src/lib/futhark-webgpu/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,15 @@ export async function newFutharkWebGPUContext(): Promise<FutharkWebGPUContext> {
} catch (err: unknown) {
contextPromise = null; // Reset on failure to allow retry
const message = err instanceof Error ? err.message : String(err);

if (message.includes('assert') || message.includes('Assertion') || message.includes('abort')) {
throw new Error(
`Futhark WebGPU context creation failed (GPU assertion). ` +
`This usually means WebGPU is not fully supported by your browser/GPU driver. ` +
`Original error: ${message}`
);
}

throw new Error(
`Failed to initialize Futhark WebGPU context. ` +
`Ensure 'just futhark-webgpu-compile' has been run. ` +
Expand Down
23 changes: 23 additions & 0 deletions src/lib/utils/consentStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export interface ConsentData {
completedSteps: number[];
/** Detected compute backend at onboarding time */
detectedBackend: string;
/** Whether WebGPU was available at onboarding time */
webgpuAvailable: boolean;
/** Version of onboarding shown (for future migrations) */
version: number;
}
Expand All @@ -48,6 +50,7 @@ const DEFAULT_CONSENT: ConsentData = {
paperViewed: false,
completedSteps: [],
detectedBackend: '',
webgpuAvailable: false,
version: 2
};

Expand Down Expand Up @@ -165,11 +168,31 @@ export function getConsentAge(): number | null {
return Math.floor(ageMs / (1000 * 60 * 60 * 24));
}

/**
* Check if demos should be blocked (no WebGPU available)
* @returns true if WebGPU is not available and demos should be gated
*/
export function shouldBlockDemos(): boolean {
if (!browser) return false;
const consent = getConsent();
if (consent && !consent.webgpuAvailable) return true;
return false;
}

/**
* Update the stored WebGPU availability status
*/
export function updateWebGPUAvailability(available: boolean): void {
setConsent({ webgpuAvailable: available });
}

export default {
getConsent,
setConsent,
completeOnboarding,
shouldShowOnboarding,
shouldBlockDemos,
updateWebGPUAvailability,
resetConsent,
getConsentAge
};
11 changes: 10 additions & 1 deletion src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { onMount } from 'svelte';
import type { Snippet } from 'svelte';
import OnboardingModal from '$lib/components/OnboardingModal.svelte';
import { shouldShowOnboarding } from '$lib/utils/consentStorage';
import { shouldShowOnboarding, shouldBlockDemos } from '$lib/utils/consentStorage';

interface Props {
children: Snippet;
Expand Down Expand Up @@ -71,6 +71,15 @@
showOnboarding = shouldShowOnboarding();
});

// Re-gate on demo route navigation when WebGPU is unavailable
$effect(() => {
if (browser && initialized && $page.url.pathname.startsWith('/demo')) {
if (shouldBlockDemos()) {
showOnboarding = true;
}
}
});

// Reactive effect: Apply dark mode to document.documentElement when isDark changes
$effect(() => {
if (browser && initialized) {
Expand Down
32 changes: 31 additions & 1 deletion src/routes/demo/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<script lang="ts">
import DemoSidebar from '$lib/components/demo-layout/DemoSidebar.svelte';
import WebGPUInstructions from '$lib/components/WebGPUInstructions.svelte';
import { useWebGPUStatus } from '$lib/composables/useWebGPUStatus.svelte';
import '../../app.css';

import type { Snippet } from 'svelte';
Expand All @@ -9,6 +11,9 @@
}

const { children }: Props = $props();

const webgpu = useWebGPUStatus();
const blocked = $derived(!webgpu.isDetecting && !webgpu.available);
</script>

<div class="flex min-h-screen bg-surface-100-800 text-surface-900-50">
Expand All @@ -17,6 +22,31 @@

<!-- Main Content Area -->
<main class="flex-1 overflow-y-auto">
{@render children()}
{#if webgpu.isDetecting}
<div class="flex h-full items-center justify-center p-8">
<div class="flex items-center gap-3">
<svg class="h-5 w-5 animate-spin text-primary-500" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p class="text-surface-500-400">Detecting WebGPU capabilities...</p>
</div>
</div>
{:else if blocked}
<div class="flex h-full items-center justify-center p-8">
<div class="max-w-lg space-y-4 text-center">
<h2 class="text-xl font-bold text-error-500">WebGPU Required</h2>
<p class="text-sm text-surface-600-300">
The Pixelwise demos require WebGPU. Please enable WebGPU in your browser
or switch to a supported browser, then reload the page.
</p>
{#if webgpu.capabilities}
<WebGPUInstructions capabilities={webgpu.capabilities} onRecheck={webgpu.redetect} />
{/if}
</div>
</div>
{:else}
{@render children()}
{/if}
</main>
</div>
Loading
Loading