mirror of
https://github.com/YusufB5/ASCILINE.git
synced 2026-06-17 22:35:13 +02:00
feat: adaptive raw/zlib/delta frame codec (opt-in, backward compatible)
The binary protocol re-sent the full grid every frame. This adds an opt-in
per-frame codec that picks the smallest of three encodings and tags it in a
1-byte header, without changing the rendered output:
0 RAW framebuffer as-is (legacy)
1 ZLIB zlib(framebuffer)
2 DELTA only the cells changed since the previous frame, patched on top
Clients opt in via /ws?codec=adaptive; omitting it yields the original protocol
byte-for-byte, so existing clients are unaffected. A keyframe is forced
periodically for resync. codec.js is shared by the browser and the Node test,
so the shipped decode path is the tested one.
Optional --quality {lossless,high,balanced,low} enables lossy temporal delta
(conditional replenishment): a colour cell is only re-sent once it drifts past a
tolerance from what the viewer already sees; the character plane stays exact.
Default lossless = bit-exact.
Measured wire savings (mode 5, 200x80): static screen 0.3% of legacy (~375x),
pixel mode 11.6%, high-motion 63% (never worse). Encoder tuned (zlib level 3,
smart candidate selection) to stay well under the frame budget.
Verified bit-exact two independent ways: Python->Node vectors and a live
adaptive-vs-legacy WebSocket diff. (A fuller mutation + Autobahn conformance
harness exists on request.)
This commit is contained in:
parent
8c60ef12a0
commit
e3f282910d
10 changed files with 478 additions and 10 deletions
74
codec.js
Normal file
74
codec.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* codec.js — Adaptive frame decoder for ASCILINE.
|
||||
*
|
||||
* Mirrors codec.py. Runs in the browser (attaches window.AscilineCodec) and in
|
||||
* Node (module.exports) so the end-to-end test exercises the exact shipped path.
|
||||
*
|
||||
* Wire format per binary frame:
|
||||
* [4B frame_index big-endian][1B tag][payload]
|
||||
* tag 0 RAW : payload is the framebuffer bytes
|
||||
* tag 1 ZLIB : payload is zlib(framebuffer bytes) -> 'deflate'
|
||||
* tag 2 DELTA : payload is zlib(indices[uint32 LE] ++ changed values)
|
||||
*
|
||||
* Decoding MUST stay in arrival order (deltas patch the previous frame), so
|
||||
* callers feed messages through a sequential queue (see makeDecoder).
|
||||
*/
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
||||
else root.AscilineCodec = api;
|
||||
})(typeof self !== 'undefined' ? self : this, function () {
|
||||
const TAG_RAW = 0, TAG_ZLIB = 1, TAG_DELTA = 2;
|
||||
|
||||
async function inflate(bytes) {
|
||||
// Python zlib.compress -> RFC1950 zlib wrapper -> 'deflate' here.
|
||||
const ds = new DecompressionStream('deflate');
|
||||
const stream = new Blob([bytes]).stream().pipeThrough(ds);
|
||||
const buf = await new Response(stream).arrayBuffer();
|
||||
return new Uint8Array(buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a stateful decoder. `cellBytes` = channels per cell (4 ASCII color,
|
||||
* 3 pixel). Returns { decode(message) -> {frameIndex, frame}, reset() }.
|
||||
* `frame` is a Uint8Array of the full framebuffer for that frame.
|
||||
*/
|
||||
function makeDecoder(cellBytes) {
|
||||
let prev = null; // Uint8Array of last full frame
|
||||
|
||||
async function decode(message) {
|
||||
const bytes = new Uint8Array(message);
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const frameIndex = view.getUint32(0, false); // big-endian
|
||||
const tag = bytes[4];
|
||||
const payload = bytes.subarray(5);
|
||||
|
||||
let frame;
|
||||
if (tag === TAG_RAW) {
|
||||
frame = payload.slice(); // own copy; becomes next prev
|
||||
} else if (tag === TAG_ZLIB) {
|
||||
frame = await inflate(payload);
|
||||
} else if (tag === TAG_DELTA) {
|
||||
const body = await inflate(payload);
|
||||
const k = body.length / (4 + cellBytes);
|
||||
const idx = new DataView(body.buffer, body.byteOffset, body.byteLength);
|
||||
frame = prev.slice(); // patch onto a copy of previous frame
|
||||
const valuesOffset = k * 4;
|
||||
for (let j = 0; j < k; j++) {
|
||||
const cell = idx.getUint32(j * 4, true); // little-endian indices
|
||||
const dst = cell * cellBytes;
|
||||
const src = valuesOffset + j * cellBytes;
|
||||
for (let c = 0; c < cellBytes; c++) frame[dst + c] = body[src + c];
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unknown ASCILINE codec tag: ' + tag);
|
||||
}
|
||||
prev = frame;
|
||||
return { frameIndex, frame };
|
||||
}
|
||||
|
||||
return { decode, reset() { prev = null; } };
|
||||
}
|
||||
|
||||
return { makeDecoder, inflate, TAG_RAW, TAG_ZLIB, TAG_DELTA };
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue