Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ ProGPU release packages are built from `eng/progpu-package-list.sh` by the `Rele
| --- | --- | --- |
| `ProGPU.Backend` | WebGPU device, swapchain, Silk.NET windowing, and platform backend services. | [![NuGet](https://img.shields.io/nuget/vpre/ProGPU.Backend.svg)](https://www.nuget.org/packages/ProGPU.Backend/) |
| `ProGPU.Browser` | Batched .NET WebAssembly dispatcher and `navigator.gpu` browser host services. | [![NuGet](https://img.shields.io/nuget/vpre/ProGPU.Browser.svg)](https://www.nuget.org/packages/ProGPU.Browser/) |
| `ProGPU.iOS` | Native UIKit/`CAMetalLayer` host using statically linked WebGPU over Metal. | Experimental source project |
| `ProGPU.DirectX` | DirectX-compatible facade and shader-oriented API surface implemented on ProGPU/WebGPU. | [![NuGet](https://img.shields.io/nuget/vpre/ProGPU.DirectX.svg)](https://www.nuget.org/packages/ProGPU.DirectX/) |
| `ProGPU.Transpiler` | Shader/source transformation helpers used by generated GPU pipelines. | [![NuGet](https://img.shields.io/nuget/vpre/ProGPU.Transpiler.svg)](https://www.nuget.org/packages/ProGPU.Transpiler/) |
| `ProGPU.Compute` | Compute pipeline helpers for GPU-side effects, acceleration, and future hit-test indexes. | [![NuGet](https://img.shields.io/nuget/vpre/ProGPU.Compute.svg)](https://www.nuget.org/packages/ProGPU.Compute/) |
Expand All @@ -34,9 +35,21 @@ Local package build:
PROGPU_PACKAGE_VERSION=0.1.0-preview.21 ./eng/progpu-pack.sh
```

## Native iPhone WebGPU sample

`ProGPU.Samples.iOS` is a thin native host for the same `ProGPU.Samples` gallery used by desktop and browser. It renders directly into a physical-pixel `CAMetalLayer` through a Metal-only `wgpu-native` XCFramework and the existing Silk.NET WebGPU API. It has no MAUI, Uno, `WKWebView`, or JavaScript rendering dependency.

```bash
./eng/build-wgpu-native-ios.sh
dotnet build src/ProGPU.Samples.iOS/ProGPU.Samples.iOS.csproj \
-c Debug -r iossimulator-arm64
```

The iOS-specific solution, native build details, ABI pin, architecture research, simulator commands, physical-device guidance, and current limitations are in [`docs/ios.md`](docs/ios.md).

## Browser WebGPU sample

The gallery is split into a shared `ProGPU.Samples` library and thin `ProGPU.Samples.Desktop` and `ProGPU.Samples.Browser` hosts. The browser host publishes with the .NET WebAssembly SDK, negotiates WebGPU capabilities, sends aligned binary command packets directly from WASM memory, and passes embedded WGSL unchanged to `GPUDevice.createShaderModule`.
The gallery is split into a shared `ProGPU.Samples` library and thin `ProGPU.Samples.Desktop`, `ProGPU.Samples.Browser`, and `ProGPU.Samples.iOS` hosts. The browser host publishes with the .NET WebAssembly SDK, negotiates WebGPU capabilities, sends aligned binary command packets directly from WASM memory, and passes embedded WGSL unchanged to `GPUDevice.createShaderModule`.

### Prerequisites

Expand Down
288 changes: 288 additions & 0 deletions docs/ios.md

Large diffs are not rendered by default.

103 changes: 103 additions & 0 deletions eng/build-wgpu-native-ios.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
set -euo pipefail

# Builds the exact wgpu-native ABI consumed by Silk.NET.WebGPU 2.23.0.
# Source remains an external build input under artifacts/ and is never vendored into ProGPU.

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source_dir="${WGPU_NATIVE_SOURCE:-${repo_root}/artifacts/wgpu-native-src}"
output_dir="${WGPU_NATIVE_OUTPUT:-${repo_root}/artifacts/wgpu-native-ios}"
# Silk.NET 2.23 still exposes the WebGPU C ABI used by wgpu-native's
# May 2024 header update (with wgpu-core 0.19.4).
# Do not replace this with Silk's newer wgpu-native submodule pointer: its callback-info
# and surface-source ABI is incompatible with the generated Silk.NET.WebGPU assembly.
expected_commit="33133da4ec5a0174cb21539ef2d3346f75200411"
upstream_url="https://github.com/gfx-rs/wgpu-native.git"

if [[ ! -d "${source_dir}/.git" ]]; then
git clone --filter=blob:none "${upstream_url}" "${source_dir}"
fi

git -C "${source_dir}" fetch --depth 1 origin "${expected_commit}"
git -C "${source_dir}" checkout --detach "${expected_commit}"
git -C "${source_dir}" submodule update --init --depth 1 ffi/webgpu-headers

actual_commit="$(git -C "${source_dir}" rev-parse HEAD)"
if [[ "${actual_commit}" != "${expected_commit}" ]]; then
echo "Expected wgpu-native ${expected_commit}, found ${actual_commit}." >&2
exit 1
fi

rustup target add aarch64-apple-ios aarch64-apple-ios-sim

export IPHONEOS_DEPLOYMENT_TARGET="${IPHONEOS_DEPLOYMENT_TARGET:-15.0}"
export CARGO_PROFILE_RELEASE_LTO="thin"
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS="1"

features="wgsl,metal"
cargo build \
--manifest-path "${source_dir}/Cargo.toml" \
--target aarch64-apple-ios \
--release \
--locked \
--no-default-features \
--features "${features}"
cargo build \
--manifest-path "${source_dir}/Cargo.toml" \
--target aarch64-apple-ios-sim \
--release \
--locked \
--no-default-features \
--features "${features}"

mkdir -p "${output_dir}/include"
cp "${source_dir}/ffi/wgpu.h" "${output_dir}/include/wgpu.h"
cp "${source_dir}/ffi/webgpu-headers/webgpu.h" "${output_dir}/include/webgpu.h"

# Silk resolves entry points lazily. iOS static libraries have no dlopen-able image, so
# retain the C API and expose a single resolver that returns direct function pointers.
resolver_dir="${output_dir}/resolver"
mkdir -p "${resolver_dir}"
resolver_source="${resolver_dir}/progpu_wgpu_resolver.c"
{
echo '#include <stddef.h>'
echo '#include <string.h>'
echo '#include "wgpu.h"'
echo 'void* progpu_wgpu_get_proc_address(const char* proc) {'
echo ' if (proc == NULL) return NULL;'
grep -hEo 'wgpu[A-Z][A-Za-z0-9_]+[[:space:]]*\(' \
"${source_dir}/ffi/webgpu-headers/webgpu.h" \
"${source_dir}/ffi/wgpu.h" | sed -E 's/[[:space:]]*\($//' | sort -u | while IFS= read -r symbol; do
echo " if (strcmp(proc, \"${symbol}\") == 0) return (void*)&${symbol};"
done
echo ' return NULL;'
echo '}'
} > "${resolver_source}"

device_sdk="$(xcrun --sdk iphoneos --show-sdk-path)"
simulator_sdk="$(xcrun --sdk iphonesimulator --show-sdk-path)"
clang -arch arm64 -isysroot "${device_sdk}" -miphoneos-version-min="${IPHONEOS_DEPLOYMENT_TARGET}" \
-I"${output_dir}/include" -c "${resolver_source}" -o "${resolver_dir}/resolver-ios-arm64.o"
clang -arch arm64 -isysroot "${simulator_sdk}" -mios-simulator-version-min="${IPHONEOS_DEPLOYMENT_TARGET}" \
-I"${output_dir}/include" -c "${resolver_source}" -o "${resolver_dir}/resolver-iossimulator-arm64.o"

libtool -static -o "${resolver_dir}/libwgpu_native-ios-arm64.a" \
"${resolver_dir}/resolver-ios-arm64.o" \
"${source_dir}/target/aarch64-apple-ios/release/libwgpu_native.a"
libtool -static -o "${resolver_dir}/libwgpu_native-iossimulator-arm64.a" \
"${resolver_dir}/resolver-iossimulator-arm64.o" \
"${source_dir}/target/aarch64-apple-ios-sim/release/libwgpu_native.a"

xcframework="${output_dir}/wgpu_native.xcframework"
if [[ -e "${xcframework}" ]]; then
rm -rf "${xcframework}"
fi

xcodebuild -create-xcframework \
-library "${resolver_dir}/libwgpu_native-ios-arm64.a" \
-headers "${output_dir}/include" \
-library "${resolver_dir}/libwgpu_native-iossimulator-arm64.a" \
-headers "${output_dir}/include" \
-output "${xcframework}"

echo "Created ${xcframework} from wgpu-native ${expected_commit}."
13 changes: 3 additions & 10 deletions src/ProGPU.Backend/GpuBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,18 +247,10 @@ private void MapReadBuffer(
Span<byte> destination,
bool destroyAfterRead)
{
var mapSignal = new System.Threading.ManualResetEventSlim(false);
var mapStatus = BufferMapAsyncStatus.ValidationError;
var onMapped = PfnBufferMapCallback.From((status, userData) =>
{
mapStatus = status;
mapSignal.Set();
});

_context.Api.BufferMapAsync(buffer, MapMode.Read, offsetBytes, (nuint)sizeBytes, onMapped, null);
var mapTask = _context.Api.BufferMapAsyncTask(buffer, MapMode.Read, offsetBytes, (nuint)sizeBytes);

var stopwatch = System.Diagnostics.Stopwatch.StartNew();
while (!mapSignal.IsSet)
while (!mapTask.IsCompleted)
{
_context.PollDevice(wait: false);
System.Threading.Thread.Sleep(1);
Expand All @@ -269,6 +261,7 @@ private void MapReadBuffer(
}
}

var mapStatus = mapTask.GetAwaiter().GetResult();
if (mapStatus != BufferMapAsyncStatus.Success)
{
CleanupMappedReadBuffer(buffer, destroyAfterRead);
Expand Down
27 changes: 3 additions & 24 deletions src/ProGPU.Backend/GpuTextureReadbackBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,12 @@ public unsafe sealed class GpuTextureReadbackBuffer : IDisposable
public const int DefaultMapTimeoutMilliseconds = 30000;

private readonly WgpuContext _context;
private readonly PfnBufferMapCallback _mapCallback;
private WgpuBuffer* _buffer;
private ManualResetEventSlim? _mapSignal;
private bool _isMapActive;

public GpuTextureReadbackBuffer(WgpuContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_mapCallback = PfnBufferMapCallback.From(OnBufferMapped);
}

public uint Width { get; private set; }
Expand Down Expand Up @@ -227,7 +224,6 @@ public bool TryReadTextureRows(
public void Dispose()
{
QueueBufferDisposal();
_mapSignal = null;
GC.SuppressFinalize(this);
}

Expand Down Expand Up @@ -304,33 +300,22 @@ private bool TryMapAndCopyRows(
LastMapStatus = BufferMapAsyncStatus.ValidationError;
LastMapTimedOut = false;

using var signal = new ManualResetEventSlim(false);
_mapSignal = signal;
_context.Api.BufferMapAsync(_buffer, MapMode.Read, 0, (nuint)BufferSize, _mapCallback, null);
var mapTask = _context.Api.BufferMapAsyncTask(_buffer, MapMode.Read, 0, (nuint)BufferSize);

var stopwatch = Stopwatch.StartNew();
while (!signal.IsSet)
while (!mapTask.IsCompleted)
{
_context.PollDevice(wait: false);
Thread.Sleep(1);
if (stopwatch.ElapsedMilliseconds > timeoutMilliseconds)
{
LastMapTimedOut = true;
if (ReferenceEquals(_mapSignal, signal))
{
_mapSignal = null;
}

QueueBufferDisposal();
return false;
}
}

if (ReferenceEquals(_mapSignal, signal))
{
_mapSignal = null;
}

LastMapStatus = mapTask.GetAwaiter().GetResult();
if (LastMapStatus != BufferMapAsyncStatus.Success)
{
QueueBufferDisposal();
Expand Down Expand Up @@ -369,12 +354,6 @@ private bool TryMapAndCopyRows(
}
}

private void OnBufferMapped(BufferMapAsyncStatus status, void* userData)
{
LastMapStatus = status;
_mapSignal?.Set();
}

private void QueueBufferDisposal()
{
if (_buffer == null)
Expand Down
9 changes: 9 additions & 0 deletions src/ProGPU.Backend/Native/progpu-static-resolver-stub.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#include <stddef.h>

// Link-only counterpart for WgpuContext's iOS static-library resolver import.
// Browser rendering obtains WebGPU from JavaScript and never calls this function.
void *progpu_wgpu_get_proc_address(const char *symbol)
{
(void)symbol;
return NULL;
}
8 changes: 8 additions & 0 deletions src/ProGPU.Backend/ProGPU.Backend.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,12 @@
<PackageReference Include="Silk.NET.Windowing" />
<PackageReference Include="Silk.NET.GLFW" />
</ItemGroup>
<ItemGroup>
<None Update="buildTransitive/ProGPU.Backend.targets"
Pack="true"
PackagePath="buildTransitive/ProGPU.Backend.targets" />
<None Update="Native/progpu-static-resolver-stub.c"
Pack="true"
PackagePath="buildTransitive/native/progpu-static-resolver-stub.c" />
</ItemGroup>
</Project>
Loading
Loading