mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-08-07 13:48:27 +02:00
Add WebGPU renderer (#648)
This commit is contained in:
186
src/modules/player/webgpu/webgpu-player.ts
Normal file
186
src/modules/player/webgpu/webgpu-player.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import wgslClarityBoost from "./shaders/clarity-boost.wgsl" with { type: "text" };
|
||||
import { BaseCanvasPlayer } from "../base-canvas-player";
|
||||
import { StreamPlayerType } from "@/enums/pref-values";
|
||||
import { BxEventBus } from "@/utils/bx-event-bus";
|
||||
|
||||
export class WebGPUPlayer extends BaseCanvasPlayer {
|
||||
static device: GPUDevice;
|
||||
|
||||
context!: GPUCanvasContext | null;
|
||||
pipeline!: GPURenderPipeline | null;
|
||||
sampler!: GPUSampler | null;
|
||||
bindGroup!: GPUBindGroup | null;
|
||||
optionsUpdated: boolean = false;
|
||||
paramsBuffer!: GPUBuffer | null;
|
||||
vertexBuffer!: GPUBuffer | null;
|
||||
|
||||
static async prepare(): Promise<void> {
|
||||
if (!navigator.gpu) {
|
||||
BxEventBus.Script.emit('webgpu.ready', {});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const adapter = await navigator.gpu.requestAdapter();
|
||||
|
||||
if (adapter) {
|
||||
WebGPUPlayer.device = await adapter.requestDevice();
|
||||
WebGPUPlayer.device?.addEventListener('uncapturederror', e => {
|
||||
console.error((e as GPUUncapturedErrorEvent).error.message);
|
||||
});
|
||||
}
|
||||
} catch (ex) {
|
||||
alert(ex);
|
||||
}
|
||||
|
||||
BxEventBus.Script.emit('webgpu.ready', {});
|
||||
}
|
||||
|
||||
constructor($video: HTMLVideoElement) {
|
||||
super(StreamPlayerType.WEBGPU, $video, 'WebGPUPlayer');
|
||||
}
|
||||
|
||||
protected setupShaders(): void {
|
||||
this.context = this.$canvas.getContext('webgpu')!;
|
||||
if (!this.context) {
|
||||
alert('Can\'t initiate context');
|
||||
return;
|
||||
}
|
||||
|
||||
const format = navigator.gpu.getPreferredCanvasFormat();
|
||||
this.context.configure({
|
||||
device: WebGPUPlayer.device,
|
||||
format,
|
||||
alphaMode: 'opaque',
|
||||
});
|
||||
|
||||
this.vertexBuffer = WebGPUPlayer.device.createBuffer({
|
||||
label: 'vertex buffer',
|
||||
size: 6 * 4, // 6 floats (2 per vertex)
|
||||
usage: GPUBufferUsage.VERTEX,
|
||||
mappedAtCreation: true,
|
||||
});
|
||||
|
||||
const mappedRange = this.vertexBuffer.getMappedRange();
|
||||
new Float32Array(mappedRange).set([
|
||||
-1, 3, // Vertex 1
|
||||
-1, -1, // Vertex 2
|
||||
3, -1, // Vertex 3
|
||||
]);
|
||||
this.vertexBuffer.unmap();
|
||||
|
||||
const shaderModule = WebGPUPlayer.device.createShaderModule({ code: wgslClarityBoost });
|
||||
this.pipeline = WebGPUPlayer.device.createRenderPipeline({
|
||||
layout: 'auto',
|
||||
vertex: {
|
||||
module: shaderModule,
|
||||
entryPoint: 'vsMain',
|
||||
buffers: [{
|
||||
arrayStride: 8,
|
||||
attributes: [{
|
||||
format: 'float32x2',
|
||||
offset: 0,
|
||||
shaderLocation: 0,
|
||||
}],
|
||||
}],
|
||||
},
|
||||
fragment: {
|
||||
module: shaderModule,
|
||||
entryPoint: 'fsMain',
|
||||
targets: [{ format }],
|
||||
},
|
||||
primitive: { topology: 'triangle-list' },
|
||||
});
|
||||
|
||||
this.sampler = WebGPUPlayer.device.createSampler({ magFilter: 'linear', minFilter: 'linear' });
|
||||
this.updateCanvas();
|
||||
}
|
||||
|
||||
private prepareUniformBuffer(value: any, classType: any) {
|
||||
const uniform = new classType(value);
|
||||
const uniformBuffer = WebGPUPlayer.device.createBuffer({
|
||||
size: uniform.byteLength,
|
||||
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
||||
});
|
||||
|
||||
WebGPUPlayer.device.queue.writeBuffer(uniformBuffer, 0, uniform);
|
||||
return uniformBuffer;
|
||||
}
|
||||
|
||||
private updateCanvas() {
|
||||
const externalTexture = WebGPUPlayer.device.importExternalTexture({ source: this.$video });
|
||||
|
||||
if (!this.optionsUpdated) {
|
||||
this.paramsBuffer = this.prepareUniformBuffer([
|
||||
this.toFilterId(this.options.processing),
|
||||
this.options.sharpness,
|
||||
this.options.brightness / 100,
|
||||
this.options.contrast / 100,
|
||||
this.options.saturation / 100,
|
||||
], Float32Array);
|
||||
|
||||
this.optionsUpdated = true;
|
||||
}
|
||||
|
||||
this.bindGroup = WebGPUPlayer.device.createBindGroup({
|
||||
layout: this.pipeline!.getBindGroupLayout(0),
|
||||
entries: [
|
||||
{ binding: 0, resource: this.sampler },
|
||||
{ binding: 1, resource: externalTexture as any },
|
||||
{ binding: 2, resource: { buffer: this.paramsBuffer } },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
updateFrame(): void {
|
||||
this.updateCanvas();
|
||||
|
||||
const commandEncoder = WebGPUPlayer.device.createCommandEncoder();
|
||||
const passEncoder = commandEncoder.beginRenderPass({
|
||||
colorAttachments: [{
|
||||
view: this.context!.getCurrentTexture().createView(),
|
||||
loadOp: 'clear',
|
||||
storeOp: 'store',
|
||||
clearValue: [0.0, 0.0, 0.0, 1.0],
|
||||
}]
|
||||
});
|
||||
|
||||
passEncoder.setPipeline(this.pipeline!);
|
||||
passEncoder.setBindGroup(0, this.bindGroup);
|
||||
passEncoder.setVertexBuffer(0, this.vertexBuffer);
|
||||
passEncoder.draw(3);
|
||||
passEncoder.end();
|
||||
|
||||
WebGPUPlayer.device.queue.submit([commandEncoder.finish()]);
|
||||
}
|
||||
|
||||
refreshPlayer(): void {
|
||||
this.optionsUpdated = false;
|
||||
this.updateCanvas();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
super.destroy();
|
||||
|
||||
this.isStopped = true;
|
||||
|
||||
// Unset GPU resources
|
||||
this.pipeline = null;
|
||||
this.bindGroup = null;
|
||||
this.sampler = null;
|
||||
|
||||
this.paramsBuffer?.destroy();
|
||||
this.paramsBuffer = null;
|
||||
|
||||
this.vertexBuffer?.destroy();
|
||||
this.vertexBuffer = null;
|
||||
|
||||
// Reset the WebGPU context (force garbage collection)
|
||||
if (this.context) {
|
||||
this.context.unconfigure();
|
||||
this.context = null;
|
||||
}
|
||||
|
||||
console.log('WebGPU context successfully freed.');
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user