[fix](trx-frontend-http): optimize WebGL rendering and pin bookmarks to top

Improve WebGL runtime performance by caching/downsampling overview waterfall texture updates and batching marker/dashed-line draws; keep bookmark chips anchored at the top of the waterfall area.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-05 21:41:49 +01:00
parent 27b90a62c5
commit c4f0844137
3 changed files with 90 additions and 21 deletions
@@ -764,6 +764,12 @@ let overviewSignalTimer = null;
let overviewWaterfallRows = [];
let overviewWaterfallPushCount = 0; // monotonically increments on every push
const HEADER_SIG_WINDOW_MS = 10_000;
const OVERVIEW_WF_TEX_MAX_W = 512;
let overviewWfTexData = null;
let overviewWfTexWidth = 0;
let overviewWfTexHeight = 0;
let overviewWfTexPushCount = 0;
let overviewWfTexPalKey = "";
function cssColorToRgba(color, alphaMul = 1) {
const parser = typeof window.trxParseCssColor === "function" ? window.trxParseCssColor : null;
@@ -780,6 +786,18 @@ function rgbaWithAlpha(color, alphaMul = 1) {
return cssColorToRgba(color, alphaMul);
}
function overviewWfResetTextureCache() {
overviewWfTexData = null;
overviewWfTexWidth = 0;
overviewWfTexHeight = 0;
overviewWfTexPushCount = 0;
overviewWfTexPalKey = "";
}
function overviewWfPaletteKey(pal, viewKey = "") {
return `${pal.waterfallHue}|${pal.waterfallSat}|${pal.waterfallLight}|${pal.waterfallAlpha}|${spectrumFloor}|${spectrumRange}|${viewKey}`;
}
function resizeHeaderSignalCanvas() {
if (!ensureOverviewCanvasBackingStore()) return;
positionRdsPsOverlay();
@@ -794,6 +812,7 @@ function ensureOverviewCanvasBackingStore() {
const dpr = window.devicePixelRatio || 1;
const resized = overviewGl.ensureSize(cssW, cssH, dpr);
if (resized) {
overviewWfResetTextureCache();
trimOverviewWaterfallRows();
}
return true;
@@ -983,32 +1002,67 @@ function drawOverviewWaterfall(W, H, pal) {
const rows = overviewWaterfallRows.slice(-maxVisible);
if (rows.length === 0) return;
const iW = Math.max(1, Math.ceil(W));
const iH = Math.max(1, Math.ceil(H));
const rgba = new Uint8Array(iW * iH * 4);
const iW = Math.max(96, Math.min(OVERVIEW_WF_TEX_MAX_W, Math.ceil(W / 2)));
const iH = Math.max(1, rows.length);
const minDb = Number.isFinite(spectrumFloor) ? spectrumFloor : -115;
const maxDb = minDb + Math.max(20, Number.isFinite(spectrumRange) ? spectrumRange : 90);
const view = lastSpectrumData ? spectrumVisibleRange(lastSpectrumData) : null;
const viewKey = view ? `${Math.round(view.visLoHz)}:${Math.round(view.visHiHz)}` : "na";
const palKey = overviewWfPaletteKey(pal, viewKey);
const rowStride = iW * 4;
const expectedSize = iW * iH * 4;
const steadyState = rows.length >= maxVisible;
const newPushes = overviewWaterfallPushCount - overviewWfTexPushCount;
const sizeChanged = overviewWfTexWidth !== iW || overviewWfTexHeight !== iH;
const palChanged = overviewWfTexPalKey !== palKey;
const needsFull = !overviewWfTexData || sizeChanged || palChanged || overviewWfTexPushCount === 0;
for (let y = 0; y < iH; y++) {
const rowFrac = y / Math.max(1, iH - 1);
const rowIdx = Math.max(0, Math.min(rows.length - 1, Math.floor(rowFrac * rows.length)));
const bins = rows[rowIdx];
if (!Array.isArray(bins) || bins.length === 0) continue;
const { startIdx, endIdx } = overviewVisibleBinWindow(lastSpectrumData, bins.length);
if (!overviewWfTexData || overviewWfTexData.length !== expectedSize) {
overviewWfTexData = new Uint8Array(expectedSize);
}
overviewWfTexWidth = iW;
overviewWfTexHeight = iH;
function renderRow(dstY, srcBins) {
if (!Array.isArray(srcBins) || srcBins.length === 0) return;
const { startIdx, endIdx } = overviewVisibleBinWindow(lastSpectrumData, srcBins.length);
const spanBins = Math.max(1, endIdx - startIdx);
const rowBase = dstY * rowStride;
for (let x = 0; x < iW; x++) {
const frac = x / Math.max(1, iW - 1);
const binIdx = Math.min(endIdx, startIdx + Math.floor(frac * spanBins));
const c = waterfallColorRgba(bins[binIdx], pal, minDb, maxDb);
const p = (y * iW + x) * 4;
rgba[p + 0] = Math.round(c[0] * 255);
rgba[p + 1] = Math.round(c[1] * 255);
rgba[p + 2] = Math.round(c[2] * 255);
rgba[p + 3] = Math.round(c[3] * 255);
const c = waterfallColorRgba(srcBins[binIdx], pal, minDb, maxDb);
const p = rowBase + x * 4;
overviewWfTexData[p + 0] = Math.round(c[0] * 255);
overviewWfTexData[p + 1] = Math.round(c[1] * 255);
overviewWfTexData[p + 2] = Math.round(c[2] * 255);
overviewWfTexData[p + 3] = Math.round(c[3] * 255);
}
}
overviewGl.uploadRgbaTexture("overview-waterfall", iW, iH, rgba, "linear");
if (needsFull) {
for (let y = 0; y < iH; y++) {
renderRow(y, rows[y]);
}
overviewWfTexPushCount = overviewWaterfallPushCount;
overviewWfTexPalKey = palKey;
} else if (steadyState && newPushes > 0) {
const newCount = Math.min(newPushes, iH);
if (newCount >= iH) {
for (let y = 0; y < iH; y++) renderRow(y, rows[y]);
} else {
const shiftBytes = newCount * rowStride;
overviewWfTexData.copyWithin(0, shiftBytes);
const startRow = iH - newCount;
for (let y = startRow; y < iH; y++) {
renderRow(y, rows[y]);
}
}
overviewWfTexPushCount = overviewWaterfallPushCount;
overviewWfTexPalKey = palKey;
}
overviewGl.uploadRgbaTexture("overview-waterfall", iW, iH, overviewWfTexData, "linear");
overviewGl.drawTexture("overview-waterfall", 0, 0, W, H, 1, true);
}
@@ -6144,6 +6198,7 @@ function startSpectrumStreaming() {
clearSpectrumPeakHoldFrames();
overviewWaterfallRows = [];
overviewWaterfallPushCount = 0;
overviewWfResetTextureCache();
scheduleOverviewDraw();
clearSpectrumCanvas();
updateRdsPsOverlay(null);
@@ -6192,6 +6247,7 @@ function stopSpectrumStreaming() {
clearSpectrumPeakHoldFrames();
overviewWaterfallRows = [];
overviewWaterfallPushCount = 0;
overviewWfResetTextureCache();
scheduleOverviewDraw();
updateRdsPsOverlay(null);
clearSpectrumCanvas();
@@ -2299,7 +2299,7 @@ button:focus-visible, input:focus-visible, select:focus-visible {
top: 0;
left: 0;
right: 0;
z-index: 6;
z-index: 8;
height: 0;
overflow: hidden;
font-size: 0.68rem;
@@ -2311,8 +2311,8 @@ button:focus-visible, input:focus-visible, select:focus-visible {
}
.spectrum-bookmark-chip {
position: absolute;
transform: translate(-50%, -50%);
top: 50%;
transform: translateX(-50%);
top: 2px;
white-space: nowrap;
cursor: pointer;
font-weight: 600;
@@ -373,9 +373,20 @@
if (!Array.isArray(points) || points.length < 2) return;
const radius = Math.max(1, Number(size) || 1);
const rgba = normalizeColor(color);
const verts = [];
for (let i = 0; i < points.length; i += 2) {
this.fillRect(points[i] - radius, points[i + 1] - radius, radius * 2, radius * 2, rgba);
const x = points[i] - radius;
const y = points[i + 1] - radius;
const w = radius * 2;
const h = radius * 2;
pushColoredVertex(verts, x, y, rgba);
pushColoredVertex(verts, x + w, y, rgba);
pushColoredVertex(verts, x + w, y + h, rgba);
pushColoredVertex(verts, x, y, rgba);
pushColoredVertex(verts, x + w, y + h, rgba);
pushColoredVertex(verts, x, y + h, rgba);
}
this._drawColorGeometry(verts, this.gl.TRIANGLES);
}
drawDashedVerticalLine(x, y0, y1, dashLen, gapLen, color, width = 1) {
@@ -383,10 +394,12 @@
const gap = Math.max(1, Number(gapLen) || 1);
const top = Math.min(y0, y1);
const bottom = Math.max(y0, y1);
const segments = [];
for (let y = top; y < bottom; y += dash + gap) {
const segEnd = Math.min(bottom, y + dash);
this.drawSegments([x, y, x, segEnd], color, width);
segments.push(x, y, x, segEnd);
}
this.drawSegments(segments, color, width);
}
uploadRgbaTexture(name, width, height, data, filter = "linear") {