Skip to content

Instantly share code, notes, and snippets.

@iggyvolz
Created February 6, 2025 04:54
Show Gist options
  • Save iggyvolz/9a3b051591a1728972cccd5b73e2af79 to your computer and use it in GitHub Desktop.
Save iggyvolz/9a3b051591a1728972cccd5b73e2af79 to your computer and use it in GitHub Desktop.
Dawn triangle in PHP
<?php
use Amp\DeferredFuture;
use Amp\Future;
use FFI\CData;
use Revolt\EventLoop;
require_once __DIR__ . '/vendor/autoload.php';
$dawn = FFI::cdef(file_get_contents(__DIR__ . '/webgpu.h'), '/home/iggyvolz/dawn/build/src/dawn/native/libwebgpu_dawn.so');
$glfw = FFI::cdef(file_get_contents(__DIR__ . '/glfw3.h'), '/home/iggyvolz/dawn/build/third_party/glfw/src/libglfw.so.3.5');
if($glfw->glfwInit() === 0) throw new Exception("GLFW init failed");
$glfw->glfwWindowHint(/* GLFW_CLIENT_API */ 0x00022001, /* GLFW_NO_API */0);
$window = $glfw->glfwCreateWindow(512, 512, "Dawn", NULL, NULL);
$glfw->glfwShowWindow($window);
$instanceDescriptor = $dawn->new("WGPUInstanceDescriptor");
$instance = $dawn->wgpuCreateInstance(FFI::addr($instanceDescriptor));
$requestAdapterOptions = $dawn->new("WGPURequestAdapterOptions");
[$status, $adapter, $message] = wgpuFuture(fn(Closure $cb): null => $dawn->wgpuInstanceRequestAdapter($instance, FFI::addr($requestAdapterOptions), $cb, null))->await();
if($status !== 1) throw new Exception(stringView($message));
$deviceDescriptor = $dawn->new("WGPUDeviceDescriptor");
//typedef void (*WGPUUncapturedErrorCallback)(WGPUDevice const * device, WGPUErrorType type, struct WGPUStringView message, void* userdata1, void* userdata2) ;
$deviceDescriptor->uncapturedErrorCallbackInfo2->callback = function(CData $device, int $type, CData $message): void {
echo ("Uncaptured error: " . stringView($message) . "\n");
throw new RuntimeException("Uncaptured error: " . stringView($message) . "\n");
};
// WGPURequestDeviceStatus status, WGPUDevice device, struct WGPUStringView message, void * userdata
[$status, $device, $message] = wgpuFuture(fn(Closure $cb): null => $dawn->wgpuAdapterRequestDevice($adapter, FFI::addr($deviceDescriptor), $cb, null))->await();
if($status !== 1) throw new Exception(stringView($message));
$sourceSurface = $dawn->new("WGPUSurfaceSourceWaylandSurface");
$sourceSurface->chain->sType = 0x00000007;
$sourceSurface->display = $glfw->glfwGetWaylandDisplay();
$sourceSurface->surface = $glfw->glfwGetWaylandWindow($window);
$descriptor = $dawn->new("WGPUSurfaceDescriptor");
$descriptor->nextInChain = $dawn->cast("struct WGPUChainedStruct*", FFI::addr($sourceSurface));
$surface = $dawn->wgpuInstanceCreateSurface($instance, FFI::addr($descriptor));
$surfaceCapabilities = $dawn->new("WGPUSurfaceCapabilities");
$status = $dawn->wgpuSurfaceGetCapabilities($surface, $adapter, FFI::addr($surfaceCapabilities));
if($status !== 1) throw new Exception("Failed to get surface capabilities");
for($i = 0; $i < $surfaceCapabilities->formatCount; $i++) {
var_dump($surfaceCapabilities->formats[$i]);
}
$surfaceConfiguration = $dawn->new("WGPUSurfaceConfiguration");
$surfaceConfiguration->device = $device;
$surfaceConfiguration->format = $format = $surfaceCapabilities->formats[1];
$surfaceConfiguration->usage = $surfaceCapabilities->usages & ~0x0000000000000008;
$surfaceConfiguration->width = 512;
$surfaceConfiguration->height = 512;
$surfaceConfiguration->presentMode = $surfaceCapabilities->presentModes[0];
$dawn->wgpuSurfaceConfigure($surface, FFI::addr($surfaceConfiguration));
$shaderCode = <<<EOF
@vertex fn vertexMain(@builtin(vertex_index) i : u32) ->
@builtin(position) vec4f {
const pos = array(vec2f(0, 1), vec2f(-1, -1), vec2f(1, -1));
return vec4f(pos[i], 0, 1);
}
@fragment fn fragmentMain() -> @location(0) vec4f {
return vec4f(1, 0, 0, 1);
}
EOF;
$shaderModule = withStringView($dawn, $shaderCode, function(CData $shaderString) use($device, $dawn): CData {
$wgslDesc = $dawn->new("WGPUShaderSourceWGSL");
$wgslDesc->code = $shaderString;
$wgslDesc->chain->sType = 0x00000002;
$shaderModuleDescriptor = $dawn->new("WGPUShaderModuleDescriptor");
$shaderModuleDescriptor->nextInChain = $dawn->cast("struct WGPUChainedStruct*", FFI::addr($wgslDesc));
return $dawn->wgpuDeviceCreateShaderModule($device, FFI::addr($shaderModuleDescriptor));
});
$colorTargetState = $dawn->new("WGPUColorTargetState");
$colorTargetState->format = $format;
$colorTargetState->writeMask = /* WGPUColorWriteMask_All*/ 0x000000000000000F;
$pipeline = withStringView($dawn, "fragmentMain", function(CData $fragmentEntryPoint) use($device, $dawn, $shaderModule, $colorTargetState): CData {
$fragmentState = $dawn->new("WGPUFragmentState");
$fragmentState->module = $shaderModule;
$fragmentState->targetCount = 1;
$fragmentState->entryPoint = $fragmentEntryPoint;
$fragmentState->targets = FFI::addr($colorTargetState);
return withStringView($dawn, "vertexMain", function(CData $vertexEntryPoint) use($device, $dawn, $shaderModule, $fragmentState): CData {
$renderPipelineDescriptor = $dawn->new("WGPURenderPipelineDescriptor");
$renderPipelineDescriptor->vertex->module = $shaderModule;
$renderPipelineDescriptor->vertex->entryPoint = $vertexEntryPoint;
$renderPipelineDescriptor->multisample->count = 1;
$renderPipelineDescriptor->multisample->mask = 0xFFFFFFFF;
$renderPipelineDescriptor->fragment = FFI::addr($fragmentState);
return $dawn->wgpuDeviceCreateRenderPipeline($device, FFI::addr($renderPipelineDescriptor));
});
});
$surfaceTexture = $dawn->new("WGPUSurfaceTexture");
$attachment = $dawn->new("WGPURenderPassColorAttachment");
$attachment->loadOp = /* WGPULoadOp_Clear */0x00000002;
$attachment->storeOp = /* WGPUStoreOp_Store */0x00000001;
$attachment->depthSlice = -1;
$attachment->clearValue->r = 0;
$textureViewDescriptor = $dawn->new("WGPUTextureViewDescriptor");
$textureViewDescriptor->arrayLayerCount = -1;
$textureViewDescriptor->mipLevelCount = -1;
$renderPassDescriptor = $dawn->new("WGPURenderPassDescriptor");
$renderPassDescriptor->colorAttachmentCount = 1;
$renderPassDescriptor->colorAttachments = FFI::addr($attachment);
$encoderDescriptor = $dawn->new("WGPUCommandEncoderDescriptor");
$bufferDescriptor = $dawn->new("WGPUCommandBufferDescriptor");
$last = microtime(true);
while(!$glfw->glfwWindowShouldClose($window)) {
$now = microtime(true);
echo 1/($now - $last) . " fps, " . 1000*($now - $last) . "ms\n";
$last = $now;
$glfw->glfwPollEvents();
$dawn->wgpuSurfaceGetCurrentTexture($surface, FFI::addr($surfaceTexture));
$attachment->view = $dawn->wgpuTextureCreateView($surfaceTexture->texture, FFI::addr($textureViewDescriptor));
$encoder = $dawn->wgpuDeviceCreateCommandEncoder($device, FFI::addr($encoderDescriptor));
$pass = $dawn->wgpuCommandEncoderBeginRenderPass($encoder, FFI::addr($renderPassDescriptor));
$dawn->wgpuRenderPassEncoderSetPipeline($pass, $pipeline);
$dawn->wgpuRenderPassEncoderDraw($pass, 3, 1, 0, 0);
$dawn->wgpuRenderPassEncoderEnd($pass);
$commands = $dawn->wgpuCommandEncoderFinish($encoder, FFI::addr($bufferDescriptor));
$queue = $dawn->wgpuDeviceGetQueue($device);
$dawn->wgpuQueueSubmit($queue, 1, FFI::addr($commands));
$dawn->wgpuSurfacePresent($surface);
$dawn->wgpuInstanceProcessEvents($instance);
}
function wgpuFuture(callable $fn): Future
{
$future = new DeferredFuture();
$fn(fn(mixed ...$args) => $future->complete($args));
return $future->getFuture();
}
function stringView(CData $cData): string
{
return FFI::string($cData->data, $cData->length);
}
/**
* @template T
* @param Closure(CData):T $callback Callback taking a string view as the first parameter, which is immediately executed
* @return T
*/
function withStringView(FFI $dawn, string $str, Closure $callback): mixed
{
$buf = $dawn->new("char[" . strlen($str) . "]", owned: false);
try {
FFI::memcpy($buf, $str, strlen($str));
$stringView = $dawn->new("struct WGPUStringView");
$stringView->data = $buf;
$stringView->length = strlen($str);
return $callback($stringView);
} finally {
FFI::free($buf);
}
}
EventLoop::run();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment