Repository navigation
Batch Bunnymark sprites with public raylib instancing #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| # Bunnymark public-Raylib instancing experiment | ||
|
|
||
| Measured on 2026-07-24 to test whether GocciaScript-Raylib can remove the | ||
| per-sprite FFI bottleneck without a companion native library. | ||
|
|
||
| ## Setup | ||
|
|
||
| - Apple M1 Max, macOS 26.5.2 | ||
| - GocciaScript import-binding fast path at commit `3b19fdbe` | ||
| - production loader SHA-256 | ||
| `4d86950f941e4028e098090a7cb3e9a7bda1d713b78f59334fa92586f99da7d0` | ||
| - GocciaScript-Raylib baseline at commit `ff978c1` | ||
| - dynamically loaded raylib 6.0 | ||
| - 10,000 fixed-seed sprites (`1234`) | ||
| - uncapped target, 25 frames per run | ||
| - one unrecorded warm-up per variant | ||
| - five measured pairs with alternating run order | ||
| - overlays and normal Bunnymark update/collision work enabled | ||
|
|
||
| The baseline is the import-fast-path `DrawTextureV` implementation on | ||
| `origin/main`. The candidate uses only existing public bindings: | ||
| `BeginShaderMode`, `DrawMeshInstanced`, and `EndShaderMode`. | ||
|
|
||
| ## Results | ||
|
|
||
| | Pair | First path | Baseline frame time | Baseline FPS | Instanced frame time | Instanced FPS | | ||
| |---:|---|---:|---:|---:|---:| | ||
| | 1 | baseline | 1963 ms | 12.74 | 811 ms | 30.83 | | ||
| | 2 | instanced | 1906 ms | 13.12 | 828 ms | 30.19 | | ||
| | 3 | baseline | 1908 ms | 13.10 | 822 ms | 30.41 | | ||
| | 4 | instanced | 1908 ms | 13.10 | 810 ms | 30.86 | | ||
| | 5 | baseline | 1941 ms | 12.88 | 849 ms | 29.45 | | ||
| | **Mean** | | **1925.2 ms** | **12.99** | **824.0 ms** | **30.35** | | ||
|
|
||
| The instanced path reduces the measured 25-frame portion by 57.2% and raises | ||
| observed FPS by 2.34x. Mean end-to-end loader execution falls from 2551.57 ms | ||
| to 1346.91 ms. Mean startup falls from 456.6 ms to 343.8 ms. | ||
|
|
||
| ## Rendering equivalence | ||
|
|
||
| The graphical integration test renders frame 8 of the same fixed-seed, | ||
| 1,000-sprite scene through both `DrawTextureV` and `DrawMeshInstanced`, with a | ||
| fixed update delta and the timing overlay disabled. The two 1280x720 PNG files | ||
| are byte-identical on both tested desktop paths: | ||
|
|
||
| - macOS/OpenGL: | ||
| `982eb299ae1a855b3e4cbda832e4e34b49876999e1a3b710c392288f60bc1c3d` | ||
| - Linux/Xvfb/Mesa llvmpipe: | ||
| `e22537b001590b497bad6c8da318d38339bef0fb4cdb95a8c8f449584e47d1a0` | ||
|
|
||
| The macOS image also byte-matches a test-instrumented capture from the | ||
| `origin/main` `DrawTextureV` implementation, not only the retained direct | ||
| fallback in the candidate. | ||
|
|
||
| The custom fragment shader retains raylib's default | ||
| `texture0 * colDiffuse * fragColor` formula. Instance order is insertion order, | ||
| and `BeginShaderMode` flushes prior queued 2D work before the instanced draw. | ||
|
|
||
| ## Scope and tradeoffs | ||
|
|
||
| - This is a Bunnymark-specific `DrawTextureV` fast path for one 32x32 texture, | ||
| not a new general Raylib API. | ||
| - Motion remains double precision. Exact byte colors and float instance | ||
| transforms live in compact typed arrays; a small numeric index array avoids | ||
| a costly custom iterator in the per-frame hot loop. | ||
| - Raylib still allocates, converts, uploads, and frees a 64-byte transform per | ||
| sprite for every `DrawMeshInstanced` call (640 KiB per 10,000 sprites). | ||
| A persistent rlgl VBO could have a higher ceiling but is outside this | ||
| public-Raylib-only experiment. | ||
| - The shader is GLSL 330, matching the repository's pinned macOS and Linux | ||
| desktop raylib builds. It is not a web/mobile shader path. | ||
| - The material owns the custom shader and attached bunny texture. If shader | ||
| creation fails, the example falls back to its direct `DrawTextureV` path. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| import { | ||
| BeginShaderMode, | ||
| DrawMeshInstanced, | ||
| EndShaderMode, | ||
| GenImageColor, | ||
| GenMeshHeightmap, | ||
| IsShaderValid, | ||
| LoadMaterialDefault, | ||
| LoadShaderFromMemory, | ||
| MATERIAL_MAP_DIFFUSE, | ||
| SetMaterialTexture, | ||
| UnloadImage, | ||
| UnloadMaterial, | ||
| UnloadMesh, | ||
| UnloadShader, | ||
| UpdateMeshBuffer, | ||
| Vector3, | ||
| WHITE, | ||
| } from "../../bindings/raylib.ts"; | ||
|
|
||
| const vertexShaderSource = `#version 330 | ||
| in vec3 vertexPosition; | ||
| in vec2 vertexTexCoord; | ||
| in mat4 instanceTransform; | ||
| out vec2 fragTexCoord; | ||
| out vec4 fragColor; | ||
| uniform mat4 mvp; | ||
| void main() { | ||
| fragTexCoord = vertexTexCoord; | ||
| fragColor = instanceTransform[0]; | ||
| vec2 position = vertexPosition.xy + instanceTransform[3].xy; | ||
| gl_Position = mvp*vec4(position, 0.0, 1.0); | ||
| }`; | ||
|
|
||
| const fragmentShaderSource = `#version 330 | ||
| in vec2 fragTexCoord; | ||
| in vec4 fragColor; | ||
| out vec4 finalColor; | ||
| uniform sampler2D texture0; | ||
| uniform vec4 colDiffuse; | ||
| void main() { | ||
| finalColor = texture(texture0, fragTexCoord)*colDiffuse*fragColor; | ||
| }`; | ||
|
|
||
| const quadVertices = new Float32Array([ | ||
| 0, 0, 0, | ||
| 0, 32, 0, | ||
| 32, 32, 0, | ||
| 0, 0, 0, | ||
| 32, 32, 0, | ||
| 32, 0, 0, | ||
| ]); | ||
|
|
||
| const quadTexcoords = new Float32Array([ | ||
| 0, 0, | ||
| 0, 1, | ||
| 1, 1, | ||
| 0, 0, | ||
| 1, 1, | ||
| 1, 0, | ||
| ]); | ||
|
|
||
| export const createBunnyInstancing = (texture) => { | ||
| const shader = LoadShaderFromMemory( | ||
| vertexShaderSource, | ||
| fragmentShaderSource, | ||
| ); | ||
| if (!IsShaderValid(shader)) { | ||
| UnloadShader(shader); | ||
| return null; | ||
| } | ||
|
|
||
| const meshImage = GenImageColor(2, 2, WHITE); | ||
| // GenMeshHeightmap supplies an uploaded 6-vertex mesh; both useful buffers | ||
| // are completely replaced below to form the sprite quad. | ||
| const mesh = GenMeshHeightmap( | ||
| meshImage, | ||
| Vector3.create({ x: texture.width, y: 0, z: texture.height }), | ||
| ); | ||
| UnloadImage(meshImage); | ||
| UpdateMeshBuffer(mesh, 0, quadVertices, quadVertices.byteLength, 0); | ||
| UpdateMeshBuffer(mesh, 1, quadTexcoords, quadTexcoords.byteLength, 0); | ||
|
|
||
| const material = LoadMaterialDefault(); | ||
| material.shader = shader; | ||
| SetMaterialTexture(material, MATERIAL_MAP_DIFFUSE, texture); | ||
| return { material, mesh }; | ||
| }; | ||
|
|
||
| export const drawBunnyInstances = ( | ||
| instancing, | ||
| state, | ||
| ): void => { | ||
| if (state.length === 0) return; | ||
| BeginShaderMode(instancing.material.shader); | ||
| DrawMeshInstanced( | ||
| instancing.mesh, | ||
| instancing.material, | ||
| state.transforms, | ||
| state.length, | ||
| ); | ||
| EndShaderMode(); | ||
| }; | ||
|
|
||
| export const unloadBunnyInstancing = (instancing): void => { | ||
| UnloadMesh(instancing.mesh); | ||
| // The material owns its custom shader and attached bunny texture in raylib. | ||
| UnloadMaterial(instancing.material); | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import { frameNumbers } from "./frames.ts"; | ||
|
|
||
| export const bunnyInstanceStride = 16; | ||
| export const bunnyRedOffset = 0; | ||
| export const bunnyGreenOffset = 4; | ||
| export const bunnyBlueOffset = 8; | ||
| export const bunnyAlphaOffset = 12; | ||
| export const bunnyXOffset = 3; | ||
| export const bunnyYOffset = 7; | ||
|
|
||
| export type BunnyState = { | ||
| capacity: number; | ||
| length: number; | ||
| x: Float64Array; | ||
| y: Float64Array; | ||
| velocityX: Float64Array; | ||
| velocityY: Float64Array; | ||
| colors: Uint8Array; | ||
| transforms: Float32Array; | ||
| indices: number[]; | ||
| }; | ||
|
|
||
| export const createBunnyState = (capacity: number): BunnyState => { | ||
| return { | ||
| capacity, | ||
| length: 0, | ||
| x: new Float64Array(capacity), | ||
| y: new Float64Array(capacity), | ||
| velocityX: new Float64Array(capacity), | ||
| velocityY: new Float64Array(capacity), | ||
| colors: new Uint8Array(capacity * 4), | ||
| transforms: new Float32Array(capacity * bunnyInstanceStride), | ||
| indices: [], | ||
| }; | ||
| }; | ||
|
|
||
| export const addBunnies = ( | ||
| state: BunnyState, | ||
| count: number, | ||
| startX: number, | ||
| startY: number, | ||
| getRandomValue: (minimum: number, maximum: number) => number, | ||
| ): void => { | ||
| const available = Math.min(count, state.capacity - state.length); | ||
| let length = state.length; | ||
|
|
||
| for (const _index of frameNumbers(available)) { | ||
| const colorOffset = length * 4; | ||
| const transformOffset = length * bunnyInstanceStride; | ||
| const velocityX = getRandomValue(-250, 250) / 60; | ||
| const velocityY = getRandomValue(-250, 250) / 60; | ||
| const red = getRandomValue(50, 240); | ||
| const green = getRandomValue(50, 240); | ||
| const blue = getRandomValue(50, 240); | ||
|
|
||
| state.x[length] = startX; | ||
| state.y[length] = startY; | ||
| state.velocityX[length] = velocityX; | ||
| state.velocityY[length] = velocityY; | ||
| state.colors[colorOffset] = red; | ||
| state.colors[colorOffset + 1] = green; | ||
| state.colors[colorOffset + 2] = blue; | ||
| state.colors[colorOffset + 3] = 255; | ||
| state.transforms[transformOffset + bunnyRedOffset] = red / 255; | ||
| state.transforms[transformOffset + bunnyGreenOffset] = green / 255; | ||
| state.transforms[transformOffset + bunnyBlueOffset] = blue / 255; | ||
| state.transforms[transformOffset + bunnyAlphaOffset] = 1; | ||
| state.transforms[transformOffset + bunnyXOffset] = startX; | ||
| state.transforms[transformOffset + bunnyYOffset] = startY; | ||
| state.indices.push(length); | ||
| length += 1; | ||
| } | ||
|
|
||
| state.length = length; | ||
| }; | ||
|
|
||
| export const updateBunnies = ( | ||
| state: BunnyState, | ||
| deltaScale: number, | ||
| textureWidth: number, | ||
| textureHeight: number, | ||
| screenWidth: number, | ||
| screenHeight: number, | ||
| ): void => { | ||
| const halfWidth = textureWidth / 2; | ||
| const halfHeight = textureHeight / 2; | ||
| const xValues = state.x; | ||
| const yValues = state.y; | ||
| const velocityXValues = state.velocityX; | ||
| const velocityYValues = state.velocityY; | ||
| const transforms = state.transforms; | ||
|
|
||
| for (const index of state.indices) { | ||
| const x = xValues[index] + velocityXValues[index] * deltaScale; | ||
| const y = yValues[index] + velocityYValues[index] * deltaScale; | ||
|
|
||
| xValues[index] = x; | ||
| yValues[index] = y; | ||
| if (x + halfWidth > screenWidth || x + halfWidth < 0) { | ||
| velocityXValues[index] *= -1; | ||
| } | ||
| if ( | ||
| y + halfHeight > screenHeight || | ||
| y + halfHeight - 40 < 0 | ||
| ) { | ||
| velocityYValues[index] *= -1; | ||
| } | ||
|
|
||
| const transformOffset = index * bunnyInstanceStride; | ||
| transforms[transformOffset + bunnyXOffset] = x; | ||
| transforms[transformOffset + bunnyYOffset] = y; | ||
| } | ||
| }; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.