KNI spike: engine core + Friflo render via WebGL in the browser

spikes/KniWeb (outside LittleSim.sln): a kni-blazor-gl template project
(KNI 4.2.9001, net8.0) referencing MrGameEng.Core directly. A mini-host
in the GameHost mold drives EngineContext/GameClock/Scene phases over
KNI's Game; the scene moves 300 Friflo entities in the update phase and
draws them with SpriteBatch (WebGL). Verified in a real browser: sprites
render and animate, browser console is clean.

Decision (docs/web-client.md): path A — KNI — is the primary route for
the web client; the core runs in Blazor WASM unchanged thanks to the
Core/Host split. Known follow-ups: per-platform compilation of the
graphics libraries against nkast.* packages, shader compatibility for
Renderer2D, HTTP-served content instead of the filesystem.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-12 23:27:40 +03:00
co-authored by Claude Fable 5
parent 8a7e2cce52
commit ce8f8ba2ed
26 changed files with 3231 additions and 0 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+77
View File
@@ -0,0 +1,77 @@
// micProcessor.js
class MicProcessor extends AudioWorkletProcessor
{
constructor()
{
super();
// global variables for testing
var sampleRate = globalThis.sampleRate;
var currentFrame = globalThis.currentFrame;
var currentTime = globalThis.currentTime;
var currentRenderQuantum = globalThis.currentRenderQuantum;
this.SampleRate = sampleRate;
this.TargetSamples = Math.floor(this.SampleRate * 0.1); // 100ms
this.Buffer = new Float32Array(this.TargetSamples);
this.BufferIndex = 0;
this.port.onmessage = (event) =>
{
var data = event.data;
if (typeof data === 'number')
{
//this.port.postMessage(data); // echo back test
}
if (data instanceof Uint8Array)
{
}
};
}
process(inputs, outputs, parameters)
{
var inChannel0 = inputs[0][0];
if (!inChannel0) return true;
let srcIndex = 0;
var srcLen = inChannel0.length;
while (srcIndex < srcLen)
{
var remaining = this.TargetSamples - this.BufferIndex;
var copyCount = Math.min(remaining, srcLen - srcIndex);
this.Buffer.set(
inChannel0.subarray(srcIndex, srcIndex + copyCount),
this.BufferIndex);
this.BufferIndex += copyCount;
srcIndex += copyCount;
if (this.BufferIndex >= this.TargetSamples)
{
this.SendBuffer();
this.BufferIndex = 0;
}
}
return true;
}
SendBuffer()
{
// convert to 16-6bit PCM
var int16 = new Int16Array(this.TargetSamples);
for (var i = 0; i < this.TargetSamples; i++)
{
int16[i] = this.Buffer[i] * 32767;
}
var byteArray = new Uint8Array(int16.buffer);
this.port.postMessage(byteArray, [byteArray.buffer]);
}
}
registerProcessor('mic-processor', MicProcessor);
@@ -0,0 +1,87 @@
// streamProcessor.js
class StreamProcessor extends AudioWorkletProcessor
{
constructor()
{
super();
this.queue = [];
this.port.onmessage = (event) =>
{
var data = event.data;
if (typeof data === 'number')
{
if (data === 2)
{
this.queue = [];
}
}
if (data instanceof Uint8Array)
{
const buffer = new Int16Array(data.buffer, data.byteOffset, data.length / 2);
buffer.offset = 0;
this.queue.push(buffer);
}
};
}
process(inputs, outputs, parameters)
{
const output = outputs[0];
const channelCount = output.length;
const sampleCount = output[0].length;
let written = 0;
while (written < sampleCount && this.queue.length > 0)
{
const buffer = this.queue[0];
const offset = buffer.offset;
const available = buffer.length - offset;
const needed = sampleCount - written;
const copyCount = Math.min(available, needed);
for (let i = 0; i < copyCount; i++)
{
for (let c = 0; c < channelCount; c++)
{
const channel = output[c];
let value = (buffer[offset+i] / 32767);
channel[written+i] = value;
}
}
written += copyCount;
buffer.offset += copyCount;
if (buffer.offset >= buffer.length)
{
this.queue.shift();
this.port.postMessage(1);
}
}
// Fill remaining samples with silence
if (written < sampleCount)
{
for (let c = 0; c < channelCount; c++)
{
const channel = output[c];
for (let i = written; i < sampleCount; i++)
{
let value = 0;
channel[i] = value;
}
}
}
return true;
}
}
registerProcessor("stream-processor", StreamProcessor);