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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ 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 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.
Max. The packed-palette fallback reaches 4.97 FPS, and presenting the indexed
framebuffer through a palette-texture shader reaches 6.32 FPS in controlled
steady-state measurements. It is genuine but not yet real-time.

## Bunnymark performance

Expand Down
21 changes: 17 additions & 4 deletions benchmarks/application-performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,20 @@ 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.
The indexed-texture shader was measured in five paired runs against that packed
RGBA fallback, alternating path order. Each run discarded 12 warm-up frames and
timed the next 24 frames with raylib's monotonic clock, for 120 measured frames
per path. The normal FPS overlay and periodic garbage collections remained
enabled.

| Display path | Mean frame time | Mean FPS | FPS range | Sample SD |
|---|---:|---:|---:|---:|
| packed RGBA fallback | 201.22 ms | 4.97 | 4.96-5.00 | 0.016 |
| indexed-texture shader | 158.33 ms | 6.32 | 6.28-6.35 | 0.028 |

The shader reduces whole-frame time by 21.31% and raises steady-state
performance by 1.27x for this stationary early-E1M1 scene. The deterministic
frame-8 shader and fallback screenshots are byte-identical when only the live
FPS overlay is disabled. The focused palette test checks all 256 entries in all
14 palette slots with 28,672 assertions in each of interpreter and bytecode
modes.
2 changes: 1 addition & 1 deletion examples/doom-gpl/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ verify-source: fetch
@test ! -e "$(UPSTREAM_DIR)/DOOM-SHAREWARE.WAD"
@echo "Pure-JavaScript LinuxDoom source pin and GPL boundary verified."

$(GENERATED_STAMP): verify-source prepare-port.mjs src/i_input.ts src/i_sound.ts src/i_video.ts
$(GENERATED_STAMP): verify-source prepare-port.mjs src/i_input.ts src/i_sound.ts src/i_video.ts src/palette.ts
node prepare-port.mjs "$(UPSTREAM_DIR)" "$(GENERATED_DIR)"
touch "$@"

Expand Down
10 changes: 6 additions & 4 deletions examples/doom-gpl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,12 @@ 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 test-palette` checks all 256 palette indices across all 14 DOOM
`PLAYPAL` slots against the packed RGBA fallback in both interpreter and
bytecode modes. The palette and fallback framebuffer use paired byte and
32-bit views, so the copied bytes remain RGBA on either little- or big-endian
hosts. `make smoke` uses the indexed-texture shader when available and falls
back to the packed RGBA display path if shader setup fails.

`make smoke` runs deterministic engine frames against cached Freedoom data and
writes `.cache/doom-goccia-smoke.png`. The current upstream port claims vanilla
Expand Down
159 changes: 145 additions & 14 deletions examples/doom-gpl/src/i_video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,37 @@ import {
BLACK,
BeginDrawing,
BeginMode2D,
BeginShaderMode,
Camera2D,
ClearBackground,
CloseWindow,
DrawFPS,
DrawTexture,
EndDrawing,
EndMode2D,
EndShaderMode,
GenImageColor,
GetShaderLocation,
ImageFormat,
InitWindow,
IsShaderValid,
IsTextureValid,
LoadShaderFromMemory,
LoadTextureFromImage,
RAYWHITE,
PIXELFORMAT_UNCOMPRESSED_GRAYSCALE,
PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
SetShaderValueTexture,
SetExitKey,
SetTargetFPS,
SetTextureFilter,
SetWindowTitle,
TEXTURE_FILTER_POINT,
UnloadImage,
UnloadShader,
UnloadTexture,
UpdateTexture,
Vector2,
WHITE,
closeRaylib,
} from "../../../bindings/raylib.ts";
import {
Expand All @@ -38,6 +49,33 @@ 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 forceRgbaFallback =
globalThis.DOOM_EXAMPLE_DISPLAY_PATH === "rgba";
const showFps = globalThis.DOOM_EXAMPLE_SHOW_FPS !== false;
const vertexShaderSource = `#version 330
in vec3 vertexPosition;
in vec2 vertexTexCoord;
in vec4 vertexColor;
out vec2 fragTexCoord;
out vec4 fragColor;
uniform mat4 mvp;
void main() {
fragTexCoord = vertexTexCoord;
fragColor = vertexColor;
gl_Position = mvp*vec4(vertexPosition, 1.0);
}`;
const fragmentShaderSource = `#version 330
in vec2 fragTexCoord;
in vec4 fragColor;
out vec4 finalColor;
uniform sampler2D texture0;
uniform sampler2D paletteTexture;
uniform vec4 colDiffuse;
void main() {
float paletteIndex = floor(texture(texture0, fragTexCoord).r*255.0 + 0.5);
vec2 paletteCoord = vec2((paletteIndex + 0.5)/256.0, 0.5);
finalColor = texture(paletteTexture, paletteCoord)*colDiffuse*fragColor;
}`;
const camera = Camera2D.create({
offset: Vector2.create({ x: 0, y: 0 }),
target: Vector2.create({ x: 0, y: 0 }),
Expand All @@ -49,8 +87,13 @@ let video = null;
let playpal = null;
let paletteIndex = 0;
let packedPaletteIndex = -1;
let image = null;
let texture = null;
let uploadedPaletteIndex = -1;
let indexedTexture = null;
let paletteTexture = null;
let rgbaTexture = null;
let shader = null;
let paletteLocation = -1;
let indexedShaderActive = false;
let initialized = false;

const refreshPackedPalette = (): void => {
Expand All @@ -59,6 +102,70 @@ const refreshPackedPalette = (): void => {
packedPaletteIndex = paletteIndex;
};

const createTexture = (
width: number,
height: number,
format: number,
) => {
const image = GenImageColor(width, height, BLACK);
if (format !== PIXELFORMAT_UNCOMPRESSED_R8G8B8A8) {
ImageFormat(image, format);
}
const created = LoadTextureFromImage(image);
UnloadImage(image);
SetTextureFilter(created, TEXTURE_FILTER_POINT);
return created;
};

const unloadTexture = (texture): void => {
if (texture !== null && IsTextureValid(texture)) UnloadTexture(texture);
};

const initializeRgbaFallback = (): void => {
rgbaTexture = createTexture(
sourceWidth,
sourceHeight,
PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
);
indexedShaderActive = false;
};

const initializeIndexedShader = (): boolean => {
if (forceRgbaFallback) return false;

shader = LoadShaderFromMemory(vertexShaderSource, fragmentShaderSource);
if (!IsShaderValid(shader)) return false;

paletteLocation = GetShaderLocation(shader, "paletteTexture");
if (paletteLocation < 0) return false;

indexedTexture = createTexture(
sourceWidth,
sourceHeight,
PIXELFORMAT_UNCOMPRESSED_GRAYSCALE,
);
paletteTexture = createTexture(
256,
1,
PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
);

if (
!IsTextureValid(indexedTexture) ||
!IsTextureValid(paletteTexture)
) {
unloadTexture(indexedTexture);
unloadTexture(paletteTexture);
indexedTexture = null;
paletteTexture = null;
return false;
}

uploadedPaletteIndex = -1;
indexedShaderActive = true;
return true;
};
Comment thread
frostney marked this conversation as resolved.

const I_InitGraphics = (): void => {
if (initialized) return;
InitWindow(
Expand All @@ -68,17 +175,19 @@ const I_InitGraphics = (): void => {
);
SetExitKey(0);
SetTargetFPS(35);
image = GenImageColor(sourceWidth, sourceHeight, BLACK);
texture = LoadTextureFromImage(image);
UnloadImage(image);
image = null;
SetTextureFilter(texture, TEXTURE_FILTER_POINT);
if (!initializeIndexedShader()) initializeRgbaFallback();
console.log(
`DOOM display path: ${
indexedShaderActive ? "indexed texture shader" : "RGBA fallback"
}`,
);
initialized = true;
};

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

Expand All @@ -89,22 +198,44 @@ const I_SetPalette = (nextPalette: number): void => {

const I_FinishUpdate = (): void => {
const indexed = video.screens[0];
expandIndexedFrame(indexed, packedPalette, rgba32);

UpdateTexture(texture, rgba);
if (indexedShaderActive) {
UpdateTexture(indexedTexture, indexed);
if (uploadedPaletteIndex !== packedPaletteIndex) {
UpdateTexture(paletteTexture, packedPaletteBytes);
uploadedPaletteIndex = packedPaletteIndex;
}
} else {
expandIndexedFrame(indexed, packedPalette, rgba32);
UpdateTexture(rgbaTexture, rgba);
}

BeginDrawing();
ClearBackground(BLACK);
BeginMode2D(camera);
DrawTexture(texture, 0, 0, RAYWHITE);
if (indexedShaderActive) {
BeginShaderMode(shader);
SetShaderValueTexture(shader, paletteLocation, paletteTexture);
DrawTexture(indexedTexture, 0, 0, WHITE);
EndShaderMode();
} else {
DrawTexture(rgbaTexture, 0, 0, WHITE);
}
Comment thread
frostney marked this conversation as resolved.
EndMode2D();
DrawFPS(8, 8);
if (showFps) DrawFPS(8, 8);
EndDrawing();
};

const I_ShutdownGraphics = (): void => {
if (!initialized) return;
UnloadTexture(texture);
texture = null;
unloadTexture(indexedTexture);
unloadTexture(paletteTexture);
unloadTexture(rgbaTexture);
if (shader !== null && IsShaderValid(shader)) UnloadShader(shader);
indexedTexture = null;
paletteTexture = null;
rgbaTexture = null;
shader = null;
CloseWindow();
closeRaylib();
initialized = false;
Expand Down
52 changes: 38 additions & 14 deletions examples/doom-gpl/tests/palette.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,59 @@ import {
packPaletteBytes,
} from "../src/palette.ts";

const paletteCount = 14;
const colourCount = 256;

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

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

const packedPaletteBytes = new Uint8Array(256 * 4);
const packedPaletteBytes = new Uint8Array(colourCount * 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);
for (
let paletteIndex = 0;
paletteIndex < paletteCount;
paletteIndex += 1
) {
packPaletteBytes(playpal, paletteIndex, 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);
const paletteOffset = paletteIndex * colourCount * 3;
for (let index = 0; index < colourCount; index += 1) {
const colourOffset = paletteOffset + indexed[index] * 3;
const pixelOffset = index * 4;
expect(packedPaletteBytes[pixelOffset]).toBe(
playpal[colourOffset],
);
expect(packedPaletteBytes[pixelOffset + 1]).toBe(
playpal[colourOffset + 1],
);
expect(packedPaletteBytes[pixelOffset + 2]).toBe(
playpal[colourOffset + 2],
);
expect(packedPaletteBytes[pixelOffset + 3]).toBe(255);
expect(rgba[pixelOffset]).toBe(packedPaletteBytes[pixelOffset]);
expect(rgba[pixelOffset + 1]).toBe(
packedPaletteBytes[pixelOffset + 1],
);
expect(rgba[pixelOffset + 2]).toBe(
packedPaletteBytes[pixelOffset + 2],
);
expect(rgba[pixelOffset + 3]).toBe(
packedPaletteBytes[pixelOffset + 3],
);
}
}
});
});
Loading