Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,10 @@ directory is excluded from the MIT npm tarball.
The full engine legitimately uses classic `for`, `while`, and `do...while`
loops. Their compatibility flags are scoped to `examples/doom-gpl/goccia.json`;
the bindings and smaller MIT examples do not enable them. The initial
GocciaScript 0.10.0 implementation renders at roughly 3 FPS on an Apple M1 Max,
so it is genuine but not yet real-time.
GocciaScript 0.10.0 implementation rendered at roughly 3 FPS on an Apple M1
Max. Caching the framebuffer length and using the packed-palette display path
raised the profiled steady-state estimate to 4.71 FPS, so it is genuine but not
yet real-time.

## Bunnymark performance

Expand All @@ -114,9 +116,11 @@ performance case for compatibility loops:
| indexed classic `for` | 3.96 | 6.4842 s |
| indexed `while` | 4.02 | 6.3692 s |

The result is dominated by rendering and the per-sprite `DrawTextureV` FFI
crossing. The current `for...of` loop stays, and both legacy loop flags remain
disabled. [Full methodology and raw results](benchmarks/bunnymark-loop-syntax.md)
The current `for...of` loop stays, and both legacy loop flags remain disabled.
That loop experiment predates the application-side import aliases and scalar
bunny state: the optimized 10,000-sprite workload now measures 12.13 FPS on the
same machine and runtime. [Loop methodology](benchmarks/bunnymark-loop-syntax.md)
and [current application-side validation](benchmarks/application-performance.md)
are recorded in the repository.

## Project scripts and tests
Expand Down
51 changes: 51 additions & 0 deletions benchmarks/application-performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Application-side performance validation

Measured on 2026-07-24 after applying the validated Bunnymark and DOOM
application-side improvements.

## Setup

- Apple M1 Max
- GocciaScript 0.10.0 in bytecode mode
- dynamically loaded raylib 6.0
- the repository's pinned Bunnymark asset and Freedoom 0.13.0 data

## Bunnymark

The workload used 10,000 sprites, an uncapped target, 25 frames per run, and
five measured trials. Each trial included application startup; frame time below
subtracts the reported startup time from GocciaScript's execution time.

| Trial | Startup | Execution | 25-frame time after startup |
|---:|---:|---:|---:|
| 1 | 451 ms | 2509.53 ms | 2058.53 ms |
| 2 | 451 ms | 2513.74 ms | 2062.74 ms |
| 3 | 444 ms | 2522.43 ms | 2078.43 ms |
| 4 | 452 ms | 2504.94 ms | 2052.94 ms |
| 5 | 444 ms | 2496.96 ms | 2052.96 ms |
| Mean | 448.4 ms | 2509.52 ms | 2061.12 ms |

The mean measured frame portion is 12.13 FPS. The preceding profiling baseline
for the same workload was 4.00 FPS. The example's normal 1,000-sprite,
60-FPS-capped startup smoke also passes.

## DOOM

Three 30-frame runs measured the cached-length and packed-palette candidate:

| Trial | Execution |
|---:|---:|
| 1 | 9000.02 ms |
| 2 | 9169.85 ms |
| 3 | 9016.46 ms |
| Mean | 9062.11 ms |

The retained profiling baseline took 13.30-13.41 seconds for the same 30-frame
workload, so the reproduced candidate reduces end-to-end execution time by
about 32%. This includes startup and is separate from the profiler's 4.71 FPS
steady-state estimate.

The deterministic frame-8 smoke screenshot has zero changed pixels below the
FPS overlay compared with the retained byte-wise conversion baseline. The
focused palette test also checks all 256 palette entries, with 1,024 byte-level
assertions in each of interpreter and bytecode modes.
7 changes: 6 additions & 1 deletion examples/doom-gpl/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ FREEDOOM_WAD := $(CACHE_DIR)/freedoom-$(FREEDOOM_VERSION)/freedoom1.wad
CACHED_IWAD := $(CACHE_DIR)/iwad.wad

GOCCIA_LOADER ?= GocciaScriptLoader
GOCCIA_TEST_RUNNER ?= GocciaTestRunner
GOCCIA_CONFIG := examples/doom-gpl/goccia.json

.PHONY: all clean fetch freedoom prepare run run-freedoom smoke verify-source
.PHONY: all clean fetch freedoom prepare run run-freedoom smoke test-palette verify-source

all: prepare

Expand Down Expand Up @@ -85,6 +86,10 @@ smoke: prepare freedoom
cd "$(PROJECT_ROOT)" && "$(GOCCIA_LOADER)" examples/doom-gpl/smoke.ts --config="$(GOCCIA_CONFIG)"
@test -s "$(CACHE_DIR)/doom-goccia-smoke.png"

test-palette:
cd "$(PROJECT_ROOT)" && "$(GOCCIA_TEST_RUNNER)" examples/doom-gpl/tests --config="$(GOCCIA_CONFIG)" --mode=interpreted
cd "$(PROJECT_ROOT)" && "$(GOCCIA_TEST_RUNNER)" examples/doom-gpl/tests --config="$(GOCCIA_CONFIG)" --mode=bytecode

clean:
@if [ "$(CACHE_DIR)" = "$(CURDIR)/.cache" ]; then \
rm -rf "$(CACHE_DIR)"; \
Expand Down
6 changes: 6 additions & 0 deletions examples/doom-gpl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,15 @@ Doom 1.
```sh
make verify-source
make prepare
make test-palette
make smoke
```

`make test-palette` checks the packed palette conversion against the original
byte-wise RGBA result in both interpreter and bytecode modes. The palette and
framebuffer use paired byte and 32-bit views, so the copied bytes remain RGBA
on either little- or big-endian hosts.

`make smoke` runs deterministic engine frames against cached Freedoom data and
writes `.cache/doom-goccia-smoke.png`. The current upstream port claims vanilla
demo synchronization, but its published repository does not contain the
Expand Down
30 changes: 19 additions & 11 deletions examples/doom-gpl/src/i_video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,18 @@ import {
Vector2,
closeRaylib,
} from "../../../bindings/raylib.ts";
import {
expandIndexedFrame,
packPaletteBytes,
} from "./palette.ts";

const sourceWidth = 320;
const sourceHeight = 200;
const scale = 2;
const rgba = new Uint8Array(sourceWidth * sourceHeight * 4);
const rgba32 = new Uint32Array(rgba.buffer);
const packedPaletteBytes = new Uint8Array(256 * 4);
const packedPalette = new Uint32Array(packedPaletteBytes.buffer);
const camera = Camera2D.create({
offset: Vector2.create({ x: 0, y: 0 }),
target: Vector2.create({ x: 0, y: 0 }),
Expand All @@ -41,10 +48,17 @@ const camera = Camera2D.create({
let video = null;
let playpal = null;
let paletteIndex = 0;
let packedPaletteIndex = -1;
let image = null;
let texture = null;
let initialized = false;

const refreshPackedPalette = (): void => {
if (playpal === null || packedPaletteIndex === paletteIndex) return;
packPaletteBytes(playpal, paletteIndex, packedPaletteBytes);
packedPaletteIndex = paletteIndex;
};

const I_InitGraphics = (): void => {
if (initialized) return;
InitWindow(
Expand All @@ -64,24 +78,18 @@ const I_InitGraphics = (): void => {

const I_RegisterPlaypal = (paletteBytes: Uint8Array): void => {
playpal = paletteBytes;
packedPaletteIndex = -1;
refreshPackedPalette();
};

const I_SetPalette = (nextPalette: number): void => {
paletteIndex = nextPalette;
refreshPackedPalette();
};

const I_FinishUpdate = (): void => {
const indexed = video.screens[0];
const paletteOffset = paletteIndex * 768;

for (let index = 0; index < indexed.length; index += 1) {
const colourOffset = paletteOffset + indexed[index] * 3;
const pixelOffset = index * 4;
rgba[pixelOffset] = playpal[colourOffset];
rgba[pixelOffset + 1] = playpal[colourOffset + 1];
rgba[pixelOffset + 2] = playpal[colourOffset + 2];
rgba[pixelOffset + 3] = 255;
}
expandIndexedFrame(indexed, packedPalette, rgba32);

UpdateTexture(texture, rgba);
BeginDrawing();
Expand All @@ -103,7 +111,7 @@ const I_ShutdownGraphics = (): void => {
};

const I_RestoreDefaultPalette = (): void => {
paletteIndex = 0;
I_SetPalette(0);
};

export default {
Expand Down
28 changes: 28 additions & 0 deletions examples/doom-gpl/src/palette.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: GPL-2.0-only

export const packPaletteBytes = (
playpal: Uint8Array,
paletteIndex: number,
packedPaletteBytes: Uint8Array,
): void => {
const paletteOffset = paletteIndex * 768;
for (let colourIndex = 0; colourIndex < 256; colourIndex += 1) {
const sourceOffset = paletteOffset + colourIndex * 3;
const targetOffset = colourIndex * 4;
packedPaletteBytes[targetOffset] = playpal[sourceOffset];
packedPaletteBytes[targetOffset + 1] = playpal[sourceOffset + 1];
packedPaletteBytes[targetOffset + 2] = playpal[sourceOffset + 2];
packedPaletteBytes[targetOffset + 3] = 255;
}
};

export const expandIndexedFrame = (
indexed: Uint8Array,
packedPalette: Uint32Array,
rgba32: Uint32Array,
): void => {
const pixelCount = indexed.length;
for (let index = 0; index < pixelCount; index += 1) {
rgba32[index] = packedPalette[indexed[index]];
}
};
39 changes: 39 additions & 0 deletions examples/doom-gpl/tests/palette.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-2.0-only

import {
expandIndexedFrame,
packPaletteBytes,
} from "../src/palette.ts";

describe("DOOM palette conversion", () => {
test("matches the byte-wise RGBA reference for every palette entry", () => {
const playpal = new Uint8Array(2 * 768);
for (let index = 0; index < playpal.length; index += 1) {
playpal[index] = (index * 37 + 11) % 256;
}

const indexed = new Uint8Array(256);
for (let index = 0; index < indexed.length; index += 1) {
indexed[index] = index;
}

const packedPaletteBytes = new Uint8Array(256 * 4);
const packedPalette = new Uint32Array(packedPaletteBytes.buffer);
const rgba = new Uint8Array(indexed.length * 4);
const rgba32 = new Uint32Array(rgba.buffer);

packPaletteBytes(playpal, 1, packedPaletteBytes);
expandIndexedFrame(indexed, packedPalette, rgba32);

const paletteOffset = 768;
const pixelCount = indexed.length;
for (let index = 0; index < pixelCount; index += 1) {
const colourOffset = paletteOffset + indexed[index] * 3;
const pixelOffset = index * 4;
expect(rgba[pixelOffset]).toBe(playpal[colourOffset]);
expect(rgba[pixelOffset + 1]).toBe(playpal[colourOffset + 1]);
expect(rgba[pixelOffset + 2]).toBe(playpal[colourOffset + 2]);
expect(rgba[pixelOffset + 3]).toBe(255);
}
});
});
Loading
Loading