Skip to content

Commit

Permalink
WebGPU tests on JS/HTML (#2255)
Browse files Browse the repository at this point in the history
* WebGPU tests on JS/HTML

* Do not fail if we cannot find a valid webgpu adapter

* Enable Vulkan as suggested here: https://stackoverflow.com/questions/72294876/i-enable-webgpu-in-chrome-dev-and-it-still-doesnt-work

* Do not explicitly disable gpu
  • Loading branch information
soywiz authored Jul 2, 2024
1 parent da21b3a commit 68c7bfe
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 4 deletions.
15 changes: 15 additions & 0 deletions karma.config.d/extra.karma.conf.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
const basePath = config.basePath.replace(/\/node_modules$/, "")
config.set({
browsers: ['ChromeHeadlessWebGPU'],
customLaunchers: {
ChromeHeadlessWebGPU: {
base: 'ChromeHeadless',
flags: [
'--enable-unsafe-webgpu', // Enable WebGPU
'--enable-features=Vulkan', // Enable Vulkan
'--no-sandbox', // Optional: Helps to run Chrome in certain CI environments
'--disable-dev-shm-usage', // Optional: Helps to avoid issues with /dev/shm partition in certain CI environments
'--headless', // Ensures that Chrome runs in headless mode
//'--disable-gpu', // Optional: Disable hardware GPU acceleration (useful in some CI environments)
'--remote-debugging-port=9222' // Optional: Allows remote debugging
]
}
},
"basePath": basePath,
"files": [
...config.files,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1373,7 +1373,8 @@ typealias GPUColorWriteFlags = Number

typealias GPUDepthBias = Int

typealias GPUFlagsConstant = Number
//typealias GPUFlagsConstant = Number
typealias GPUFlagsConstant = Int

typealias GPUIndex32 = Int

Expand Down
220 changes: 217 additions & 3 deletions korge-core/test@js/korlibs/webgpu/WebGPUTest.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package korlibs.webgpu

import io.ygdrasil.wgpu.internal.js.*
import korlibs.image.bitmap.*
import korlibs.image.format.*
import korlibs.io.async.*
import korlibs.js.*
import korlibs.render.*
import korlibs.math.geom.*
import kotlinx.browser.*
import kotlinx.coroutines.*
import org.khronos.webgl.*
import org.w3c.dom.*
import kotlin.test.*

Expand Down Expand Up @@ -83,8 +86,8 @@ class WebGPUTest {
);

fun frame() {
val commandEncoder = device.createCommandEncoder();
val textureView = context.getCurrentTexture().createView();
val commandEncoder = device.createCommandEncoder()
val textureView = context.getCurrentTexture().createView()

val renderPassDescriptor: GPURenderPassDescriptor = jsObjectOf(
"colorAttachments" to arrayOf(
Expand All @@ -108,8 +111,219 @@ class WebGPUTest {

frame()
}

// https://github.com/denoland/webgpu-examples/blob/e8498aa0e001168b77762dde8a1f5fca30c551a7/hello-triangle/mod.ts
@Test
fun testOffscreen() = suspendTest {
val dimensions = SizeInt(200, 200)
val adapter: GPUAdapter = navigator.gpu.requestAdapter().await()
?: (return@suspendTest Unit.also {
//asserter.assertEquals()
println("No adapter found. Cannot run test")
})
val device = adapter.requestDevice().await()

val shaderCode = """
@vertex
fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4<f32> {
let x = f32(i32(in_vertex_index) - 1);
let y = f32(i32(in_vertex_index & 1u) * 2 - 1);
return vec4<f32>(x, y, 0.0, 1.0);
}
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}
"""

val shaderModule = device.createShaderModule(
jsObjectOf("code" to shaderCode)
);

val pipelineLayout = device.createPipelineLayout(
jsObjectOf(
"bindGroupLayouts" to jsEmptyArray(),
)
);

val renderPipeline = device.createRenderPipeline(
jsObjectOf(
"layout" to pipelineLayout,
"vertex" to jsObjectOf(
"module" to shaderModule,
"entryPoint" to "vs_main",
),
"fragment" to jsObjectOf(
"module" to shaderModule,
"entryPoint" to "fs_main",
"targets" to arrayOf(
jsObjectOf(
"format" to "rgba8unorm-srgb",
),
),
),
)
);

val (texture, outputBuffer) = createCapture(
device,
dimensions.width,
dimensions.height,
);

val encoder = device.createCommandEncoder();
val renderPass = encoder.beginRenderPass(
jsObjectOf(
"colorAttachments" to arrayOf(
jsObjectOf(
"view" to texture.createView(),
"storeOp" to "store",
"loadOp" to "clear",
"clearValue" to arrayOf(0, 1, 0, 1),
),
),
)
);
renderPass.setPipeline(renderPipeline);
renderPass.draw(3, 1);
renderPass.end();

copyToBuffer(encoder, texture, outputBuffer, dimensions);

device.queue.submit(arrayOf(encoder.finish()));

createPng(outputBuffer, dimensions)
}

data class CreateCapture(val texture: GPUTexture, val outputBuffer: GPUBuffer)

private fun createCapture(
device: GPUDevice,
width: Int,
height: Int,
): CreateCapture {
val padded = getRowPadding(width).padded;
val outputBuffer = device.createBuffer(jsObjectOf(
"label" to "Capture",
"size" to padded * height,
"usage" to (GPUBufferUsage.MAP_READ or GPUBufferUsage.COPY_DST),
));
val texture = device.createTexture(jsObjectOf(
"label" to "Capture",
"size" to jsObjectOf(
"width" to width,
"height" to height,
),
"format" to "rgba8unorm-srgb",
"usage" to (GPUTextureUsage.RENDER_ATTACHMENT or GPUTextureUsage.COPY_SRC),
));

return CreateCapture(texture, outputBuffer)
}


/** Return value for {@linkcode getRowPadding}. */
data class Padding(
/** The number of bytes per row without padding calculated. */
val unpadded: Int,
/** The number of bytes per row with padding calculated. */
val padded: Int,
)
/** Buffer-Texture copies must have [`bytes_per_row`] aligned to this number. */
val COPY_BYTES_PER_ROW_ALIGNMENT = 256;
/** Number of bytes per pixel. */
val BYTES_PER_PIXEL = 4;


private fun getRowPadding(width: Int): Padding {
// It is a WebGPU requirement that
// GPUImageCopyBuffer.layout.bytesPerRow % COPY_BYTES_PER_ROW_ALIGNMENT == 0
// So we calculate paddedBytesPerRow by rounding unpaddedBytesPerRow
// up to the next multiple of COPY_BYTES_PER_ROW_ALIGNMENT.

val unpaddedBytesPerRow = width * BYTES_PER_PIXEL;
val paddedBytesPerRowPadding = (COPY_BYTES_PER_ROW_ALIGNMENT -
(unpaddedBytesPerRow % COPY_BYTES_PER_ROW_ALIGNMENT)) %
COPY_BYTES_PER_ROW_ALIGNMENT;
val paddedBytesPerRow = unpaddedBytesPerRow + paddedBytesPerRowPadding;

return Padding(
unpadded = unpaddedBytesPerRow,
padded = paddedBytesPerRow,
)
}

private fun copyToBuffer(
encoder: GPUCommandEncoder,
texture: GPUTexture,
outputBuffer: GPUBuffer,
dimensions: SizeInt,
) {
val padded = getRowPadding(dimensions.width).padded;

encoder.copyTextureToBuffer(
jsObjectOf(
"texture" to texture,
).unsafeCast<GPUImageCopyTexture>(),
jsObjectOf(
"buffer" to outputBuffer,
"bytesPerRow" to padded,
).unsafeCast<GPUImageCopyBuffer>(),
jsObjectOf("width" to dimensions.width, "height" to dimensions.height).unsafeCast<GPUExtent3DDictStrict>(),
);
}

private suspend fun createPng(
buffer: GPUBuffer,
dimensions: SizeInt,
) {
buffer.mapAsync(1).await()
val inputBuffer = Uint8Array(buffer.getMappedRange())
val padding = getRowPadding(dimensions.width)
val padded = padding.padded
val unpadded = padding.unpadded
val outputBuffer = Uint8Array(unpadded * dimensions.height);

for (i in 0 until dimensions.height) {
val slice: Uint8Array = inputBuffer.asDynamic()
.slice(i * padded, (i + 1) * padded)
.slice(0, unpadded)
.unsafeCast<Uint8Array>()

outputBuffer.set(slice, (i * unpadded));
}

val bitmap = Bitmap32(dimensions.width, dimensions.height, Int32Array(outputBuffer.buffer).unsafeCast<IntArray>())

val nativeImage = nativeImageFormatProvider.create(dimensions.width, dimensions.height).unsafeCast<HtmlNativeImage>()
//HtmlNativeImage()
//println(PNG.encode(bitmap).base64)
nativeImage.context2d {
drawImage(bitmap, Point(0, 0))
}

println(nativeImage.element.unsafeCast<HTMLCanvasElement>().toDataURL("image/png"))

//PNG.encode()
//val image = png.encode(
// outputBuffer,
//dimensions.width,
//dimensions.height,
//{
// stripAlpha: true,
// color: 2,
//},
//);
//Deno.writeFileSync("./output.png", image);

buffer.unmap();
}

}

private external val navigator: NavigatorGPU

/*
import triangleVertWGSL from '../../shaders/triangle.vert.wgsl';
import redFragWGSL from '../../shaders/red.frag.wgsl';
Expand Down

0 comments on commit 68c7bfe

Please sign in to comment.