diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index f24d1b8..7d3a3af 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -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(); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index 07d0402..a858930 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -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; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/webgl-renderer.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/webgl-renderer.js index 76a7fca..a640d91 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/webgl-renderer.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/webgl-renderer.js @@ -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") {