From f456ff33ea3a1137190c024c6d0149352c787f27 Mon Sep 17 00:00:00 2001 From: poxular Date: Mon, 1 Jun 2026 17:20:00 +0200 Subject: [PATCH] Fix preview: stable WebGL backing store, re-upload LUT on slider, fill node height, 2048 cache. Co-authored-by: Cursor --- js/magicmatch_live.js | 166 ++++++++++++++++++++++------------ magicmatch_core/live_cache.py | 2 +- 2 files changed, 110 insertions(+), 58 deletions(-) diff --git a/js/magicmatch_live.js b/js/magicmatch_live.js index 78c8b61..a2cb906 100644 --- a/js/magicmatch_live.js +++ b/js/magicmatch_live.js @@ -1,12 +1,14 @@ /** * In-node live preview — WebGL merged-LUT (same reshape as CPU path). + * Backing store stays at cache resolution; CSS scales with the node. */ import { app } from "../../../scripts/app.js"; import { api } from "../../../scripts/api.js"; const PREVIEW_CLASS = "MagicMatchPreview"; const LUT_SIZE = 25; -const MIN_PREVIEW_W = 80; +const MIN_PREVIEW_PX = 64; +const NODE_CHROME_H = 118; function b64ToArrayBuffer(b64) { const bin = atob(b64); @@ -15,7 +17,7 @@ function b64ToArrayBuffer(b64) { return buf.buffer; } -/** RGBc merged 25³ → cBGR flat (same as reshape_merged_lut_for_apply in lut.py) */ +/** RGBc merged 25³ → cBGR flat (reshape_merged_lut_for_apply in lut.py) */ function reshapeMergedLutForApply(mergedRgb) { const size = LUT_SIZE; const merged = mergedRgb; @@ -34,7 +36,7 @@ function reshapeMergedLutForApply(mergedRgb) { return reshaped; } -/** cBGR flat → 3D texel (x=B,y=G,z=R) with RGB = output R,G,B at that voxel */ +/** cBGR flat → 3D texels (OpenGL x=B fastest) */ function reshapedLutTo3DUint8(reshaped) { const size = LUT_SIZE; const n = size * size * size; @@ -63,6 +65,7 @@ class LivePreviewPanel { this.node = node; this.cache = null; this._img = null; + this._reshapedLut = null; this.gl = null; this.prog = null; this.lutTex = null; @@ -73,6 +76,8 @@ class LivePreviewPanel { this.displayW = 200; this.displayH = 200; this.aspect = 1; + this._glBw = 0; + this._glBh = 0; this._srcCanvas = document.createElement("canvas"); this.wrap = document.createElement("div"); @@ -85,10 +90,9 @@ class LivePreviewPanel { this.canvas = document.createElement("canvas"); this.canvas.style.cssText = - "display:block;width:100%;height:auto;background:#1a1a1a;border-radius:4px;"; + "display:block;max-width:100%;background:#1a1a1a;border-radius:4px;image-rendering:auto;"; } - /** Reset GL handles after canvas resize (resizing clears the WebGL context). */ resetGl() { this.gl = null; this.prog = null; @@ -101,7 +105,10 @@ class LivePreviewPanel { initGl() { if (this.gl) return true; - const gl = this.canvas.getContext("webgl2", { premultipliedAlpha: false }); + const gl = this.canvas.getContext("webgl2", { + premultipliedAlpha: false, + preserveDrawingBuffer: true, + }); if (!gl) { this.hint.textContent = "WebGL2 required for live preview"; return false; @@ -176,29 +183,66 @@ class LivePreviewPanel { return true; } - layoutFromNodeWidth(nodeWidth) { - const w = Math.max(MIN_PREVIEW_W, Math.floor(nodeWidth - 24)); - const h = Math.max(MIN_PREVIEW_W, Math.round(w / this.aspect)); + /** CSS display size — uses node width and height so portrait fills tall nodes. */ + layoutPreview(nodeWidth, nodeHeight) { + const pad = 16; + const availW = Math.max(MIN_PREVIEW_PX, Math.floor(nodeWidth - pad)); + const availH = nodeHeight + ? Math.max(MIN_PREVIEW_PX, Math.floor(nodeHeight - NODE_CHROME_H)) + : null; + + let w = availW; + let h = Math.max(MIN_PREVIEW_PX, Math.round(w / this.aspect)); + + if (availH && h < availH) { + h = availH; + w = Math.max(MIN_PREVIEW_PX, Math.round(h * this.aspect)); + if (w > availW) { + w = availW; + h = Math.max(MIN_PREVIEW_PX, Math.round(w / this.aspect)); + } + } + this.displayW = w; this.displayH = h; - return [w, h + 32]; + this.canvas.style.width = `${w}px`; + this.canvas.style.height = `${h}px`; + return [nodeWidth, h + NODE_CHROME_H]; } - /** Apply backing-store size; must run before initGl or call resetGl() afterward. */ - applyCanvasSize() { + /** Backing store = cache pixels × DPR (only changes when cache size changes). */ + syncGlBacking() { + if (!this.cache) return false; const dpr = window.devicePixelRatio || 1; - const bw = Math.max(1, Math.round(this.displayW * dpr)); - const bh = Math.max(1, Math.round(this.displayH * dpr)); - if (this.canvas.width === bw && this.canvas.height === bh) return false; - this.canvas.width = bw; - this.canvas.height = bh; + const bw = Math.max(1, Math.round(this.cache.w * dpr)); + const bh = Math.max(1, Math.round(this.cache.h * dpr)); + if (bw === this._glBw && bh === this._glBh && this.gl) return false; + this._glBw = bw; + this._glBh = bh; + if (this.canvas.width !== bw || this.canvas.height !== bh) { + this.canvas.width = bw; + this.canvas.height = bh; + this.resetGl(); + } return true; } - uploadLut(gl, cache) { + ensureGlReady() { + if (!this.cache) return false; + this.syncGlBacking(); + return this.initGl(); + } + + prepareLutData(cache) { const floats = new Float32Array(b64ToArrayBuffer(cache.lut)); - const reshaped = reshapeMergedLutForApply(floats); - const texData = reshapedLutTo3DUint8(reshaped); + this._reshapedLut = reshapeMergedLutForApply(floats); + return reshapedLutTo3DUint8(this._reshapedLut); + } + + uploadLut(gl) { + if (!this.cache) return; + const texData = this._lutTexData || this.prepareLutData(this.cache); + this._lutTexData = texData; gl.bindTexture(gl.TEXTURE_3D, this.lutTex); gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); @@ -247,36 +291,25 @@ class LivePreviewPanel { ); } - ensureGlReady() { - if (!this.cache) return false; - const sizeChanged = this.applyCanvasSize(); - if (sizeChanged) this.resetGl(); - if (!this.initGl()) return false; - return true; - } - refreshLayout() { - if (!this.cache || !this.node.size) return; - this.layoutFromNodeWidth(this.node.size[0]); - if (!this.ensureGlReady()) return; - const gl = this.gl; - this.uploadLut(gl, this.cache); - if (this._img) { - this.uploadSource(gl); - this.drawPreview(this.getStrength()); + if (!this.cache) return; + const nw = this.node.size?.[0] ?? 320; + const nh = this.node.size?.[1]; + this.layoutPreview(nw, nh); + this.drawPreview(this.getStrength()); + if (this.node.setSize) { + this.node.setSize(this.node.computeSize()); } } setCache(cache) { this.cache = cache; this.aspect = cache.w / cache.h; - if (this.node.size) { - this.layoutFromNodeWidth(this.node.size[0]); - } - if (!this.ensureGlReady()) return; - - const gl = this.gl; - this.uploadLut(gl, cache); + this._lutTexData = null; + this._reshapedLut = null; + this._glBw = 0; + this._glBh = 0; + this.resetGl(); const img = new Image(); img.onload = () => { @@ -293,20 +326,28 @@ class LivePreviewPanel { getStrength() { const w = this.node.widgets?.find((x) => x.name === "strength"); - return w ? Number(w.value) : 1.0; + const v = w != null ? w.value : 1.0; + const n = Number(v); + return Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : 1.0; } drawPreview(strength) { if (!this.ensureGlReady()) return; const gl = this.gl; + this.uploadLut(gl); if (this._img) this.uploadSource(gl); this.render(strength); } render(strength) { - if (!this.cache || !this.gl || !this.prog || !this._img) return; + if (!this.cache || !this.gl || !this.prog) return; const gl = this.gl; + const s = Math.max(0, Math.min(1, Number(strength))); + if (!Number.isFinite(s)) return; + gl.viewport(0, 0, this.canvas.width, this.canvas.height); + gl.clearColor(0.1, 0.1, 0.1, 1.0); + gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(this.prog); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.srcTex); @@ -314,7 +355,7 @@ class LivePreviewPanel { gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_3D, this.lutTex); gl.uniform1i(this.uLut, 1); - gl.uniform1f(this.uStrength, Math.max(0, Math.min(1, strength))); + gl.uniform1f(this.uStrength, s); gl.drawArrays(gl.TRIANGLES, 0, 6); } } @@ -330,6 +371,18 @@ function getPanel(node) { return p; } +function bindStrengthSlider(node, panel) { + const strengthWidget = node.widgets?.find((w) => w.name === "strength"); + if (!strengthWidget || strengthWidget._magicmatchBound) return; + strengthWidget._magicmatchBound = true; + + const prev = strengthWidget.callback; + strengthWidget.callback = function (...args) { + if (prev) prev.apply(this, args); + panel.drawPreview(panel.getStrength()); + }; +} + app.registerExtension({ name: "MAGICMATCH.live_in_node", @@ -350,7 +403,7 @@ app.registerExtension({ domWidget.computeSize = function (width) { if (panel.cache) { - return panel.layoutFromNodeWidth(width); + return panel.layoutPreview(width, panel.node.size?.[1]); } return [width, 200]; }; @@ -358,18 +411,14 @@ app.registerExtension({ const origOnResize = node.onResize; node.onResize = function (size) { const res = origOnResize?.apply(this, arguments); - panel.refreshLayout(); + if (panel.cache) { + panel.layoutPreview(size[0], size[1]); + panel.drawPreview(panel.getStrength()); + } return res; }; - const strengthWidget = node.widgets?.find((w) => w.name === "strength"); - if (strengthWidget) { - const prev = strengthWidget.callback; - strengthWidget.callback = function (v, ...rest) { - if (prev) prev.call(this, v, ...rest); - panel.drawPreview(Number(v)); - }; - } + bindStrengthSlider(node, panel); }, async setup() { @@ -383,7 +432,10 @@ app.registerExtension({ app.graph._nodes_by_id?.[nodeId]; if (!graphNode || graphNode.comfyClass !== PREVIEW_CLASS) return; - getPanel(graphNode).setCache(out.magicmatch_live[0]); + const panel = getPanel(graphNode); + bindStrengthSlider(graphNode, panel); + panel.setCache(out.magicmatch_live[0]); + panel.drawPreview(panel.getStrength()); }); }, }); diff --git a/magicmatch_core/live_cache.py b/magicmatch_core/live_cache.py index f1a7234..bd29c26 100644 --- a/magicmatch_core/live_cache.py +++ b/magicmatch_core/live_cache.py @@ -7,7 +7,7 @@ import io import numpy as np -LIVE_PREVIEW_MAX_EDGE = 1024 +LIVE_PREVIEW_MAX_EDGE = 2048 def _downscale_hwc(hwc: np.ndarray, max_edge: int) -> np.ndarray: