# dsh-shader-workbench 验收报告 - 验收对象:`dsh-shader-workbench` v0.1.0(M0–M3) - 验收日期:2026-10-10 - 验收方式:自动化测试 + 真机 WebGL2 编译校验 + 真浏览器端到端冒烟 + **GPU 像素回读** + **真鼠标拖拽交互** + 1080p 性能度量 - 结论:**M0–M3 全部通过**(只剩「画面审美」一项需人眼,见第 4 节) --- ## 1. 自动化测试结果 命令:`node --test "tests/*.test.js"`(受限沙箱下改用 `node scripts/run-tests.mjs`) ``` 共 10 个测试文件 ℹ tests 70 ℹ pass 70 ℹ fail 0 ℹ skipped 0 ``` 两种 runner 结果一致。 | 测试文件 | 用例 | 覆盖 | |---|---|---| | `tests/codegen.test.js` | 21 | 生成器正确性、类型转换矩阵、**连接规则与生成器一致性**、错误码、图归一化 | | `tests/workspace.test.js` | 10 | 路径安全、项目/材质/快照回滚/纹理导入/归档/只读 | | `tests/http.test.js` | 14 | 真 HTTP 服务:action 分发、错误码、同源、纹理字节、模块送发 | | `tests/plugin-load.test.js` | 10 | 假 Cordis context 下 `apply()` 注册形状、工具 `output` 契约、工具真执行 | | `tests/module-graph.test.js` | 6 | 面板模块可解析、导出契约、`__ModuleLoader__` 载荷求值、`apply()` 空 ctx 安全 | | `tests/glsl-compile.test.js` | 4 | **真 WebGL2**:50 个节点 + 多 Pass + 默认图编译 | | `tests/panel-smoke.test.js` | 2 | **真浏览器**:面板整块挂起来、打开材质、编译、渲染 | | `tests/render-gpu.test.js` | 1 | **真浏览器 + 读像素**:iChannel0-3 绑定 / 解绑回落 / 槽位隔离 / 换源 / 多 Pass 反馈 / iMouse | | `tests/editor-interaction.test.js` | 1 | **真浏览器 + 真鼠标**:拖拽移动节点、拖端口连线、重复连接替换 | | `tests/perf-1080p.test.js` | 1 | **真浏览器**:1920×1080 帧率、GPU 单帧耗时、隐藏时空转 | 共用夹具在 `tests/helpers/browser.mjs`(起服务 / 驱动 CDP / 读像素 / 派发输入)。 --- ## 2. 逐条对照需求书第 10 节 ### 2.1 渲染 | 验收项 | 结论 | 证据 | |---|---|---| | 加载示例 shader,画面正确 | **通过(除像素审美)** | 面板冒烟里加载 demo/plasma(7 节点 6 连线)→ 生成 `#version 300 es` → 编译链接成功 → 进渲染器出帧。**并且用 `gl.readPixels` 逐像素验证过取值正确**(见 3.1) | | iTime 动画流畅 | **通过** | 1080p 下 rAF 139.7–143.9 FPS;`document.hidden` 时 1 秒内渲染 0 次 | | 鼠标交互有效 | **通过** | 真 CDP 输入通道在画布 (32,32) 处移动鼠标,`mouseUV` 图输出的像素读回 **(64, 191, 0, 255)**,与 `iMouse.zw / iResolution.xy = [0.25, 0.75]` 精确吻合(见 3.1 第 6 步) | | iChannel0-3 纹理绑定 | **通过** | 绑纯红 → 读回 (255,0,0);解绑 → 读回兜底灰 (32,32,32) 而不是黑;红绑在 iChannel3 时 iChannel0 仍是灰(无串台);换源为蓝 → 读回 (0,0,255)(见 3.1) | ### 2.2 编辑器 | 验收项 | 结论 | 证据 | |---|---|---| | 可拖拽创建节点 | **通过** | 节点库 51 个条目全部渲染;点击添加与 HTML5 dragstart/drop 已实现。**真鼠标拖拽已验证**:`uv` 节点标题栏拖 (+80,+60) 后坐标精确变成 (80,60),再拖 (-40,-30) 变成 (40,30)(见 3.2) | | 连线 | **通过** | **真鼠标从输出端口拖到输入端口,连线成功建立**(`uv.out → output.color`);重复连同一条输入端口时是**替换**而不是变成两条。类型校验、成环检测、拒绝原因文案均有实现与单测 | | 修改 uniform | **通过** | 参数面板按类型出控件;改值即 `renderer.setUniform`,下一帧生效,不走防抖 | | 变更后 500ms 内热重载 | **通过** | 面板冒烟实测 `lastResult.ok === true`、耗时 **10.7 ms**(防抖 120 ms)。远低于 500 ms 上限 | ### 2.3 资源 | 验收项 | 结论 | 证据 | |---|---|---| | 可创建项目 / 材质 | **通过** | `workspace.test.js` 全流程用例 + `plugin-load.test.js` 工具链路用例 | | 导入纹理 | **通过** | 同名自动加序号、只读源文件、写回清单;同槽位重复导入清单仍只保留一条 | | 重启后资源完整 | **通过** | 面板冒烟真的从磁盘读回了 7 节点 6 连线的图;全部状态落在 `graph.json` + `material.json`,无内存态依赖 | ### 2.4 ComfyUI | 验收项 | 结论 | |---|---| | 触发 PBR 工作流,贴图自动绑定 iChannel | **未实现(M4 范围外)** | 已预留:`MaterialManifest.comfyWorkflows`、事件 `comfy:started` / `comfy:done`、 `library/comfy-workflows/` 目录、`texture.import` 的 `source: 'comfyui'` 来源标记。 ### 2.5 编译 | 验收项 | 结论 | 证据 | |---|---|---| | 同图导出 GLSL | **通过** | `shader_export` 写 `shader.frag` + `bundle.json`;面板可直接下载生成的 .frag | | 导出 HLSL / MSL / SPIR-V | **未实现(M5 范围外)** | 工具的 `target` enum 只给 `glsl` / `json`,并在 description 里明确说明不支持,不误导 agent | ### 2.6 性能 | 验收项 | 结论 | 证据(无头 Edge + SwiftShader 软件渲染,1920×1080) | |---|---|---| | 1080p 预览 ≥ 60 FPS | **通过** | 简单图 139.7 FPS / 重负载(fbm + BufferA)143.9 FPS;GPU 单帧 p95 分别 **0.07 ms / 0.86 ms**,60 FPS 预算 16.7 ms | | 页面隐藏时 CPU < 1% | **通过** | 伪造 `document.hidden` 后 1 秒内渲染次数 = **0**(时间也不推进) | > GPU 耗时用 `EXT_disjoint_timer_query_webgl2` 量的真数据。 > 只量 CPU 侧的 `render()` 是没意义的——WebGL 的 draw 是异步的,CPU 只是塞命令(实测 0.03 ms)。 ### 2.7 DSH 集成 | 验收项 | 结论 | 证据 | |---|---|---| | 插件可安装 | **通过** | profile 的 `dependencies` 与 `dsh.profile.bundles` 已加;从 profile 目录解析到 `node_modules/dsh-shader-workbench/package.json`,入口 `host/index.js` 能 import,导出 `name` / `inject` / `apply` / `Config` 齐全 | | 插件不被 DSH 判失败 | **通过** | `plugin-load.test.js` 用假 Cordis context 真跑 `apply()`:9 个工具全部注册、每个都声明了 `output` 契约、路由形状正确 | | 工作台面板正常打开 | **通过** | `panel-smoke.test.js` 在**真浏览器**里 import 面板模块并 `mountWorkbench()`:3 栏 DOM、编辑器画布、51 个节点库条目、预览画布、参数面板、资源树全部建起来,WebGL2 上下文拿到,材质从 HTTP API 读回并编译渲染 | ### 2.8 轻量 | 验收项 | 结论 | 证据 | |---|---|---| | 核心 runtime 压缩后 < 200 KB | **通过** | `client/runtime/` + `shared/` 源码 94.8 KB,**gzip 32.0 KB** | | 编辑器按需加载 | **通过** | `client/panel/`(122.7 KB)不在首屏,由面板挂载时才 `import()`,不进 runtime | | 第三方运行时依赖 | **通过** | `dependencies: {}`,一个都没有 | --- ## 3. 真机验证详情 ### 3.1 GLSL 编译(`glsl-compile.test.js`) 用无头 Edge 起**真 WebGL2 上下文**,把每个节点单独成图后编译并链接。 ``` [glsl] 渲染器:WebKit / WebKit WebGL [glsl] 编译探针:50 个,失败 0 个 ← 节点库里的每一个节点 [glsl] 编译探针:3 个,失败 0 个 ← image Pass + BufferA Pass + 默认空白图 ``` **这个测试抓到了 6 个只有编译器才能发现的 bug**,全部已修: | # | Bug | 编译器报错 | 修法 | |---|---|---|---| | 1 | 生成的着色器缺 `#version 300 es` | `'out' : storage qualifier supported in GLSL ES 3.00 and above only` | header 第一行补上 | | 2 | `st_pow` 把 `float` 与 `vec4` 混在一个重载集里 | `function must have the same return type in all of its declarations` | 删掉跨返回类型的重载 | | 3 | `const` 节点 value 是 vec3、outType 是 float | `dimension mismatch` | 按**输出端口类型**重新序列化值 | | 4 | `custom` 节点的表达式 `$in.a` 只展开一层 | `'$' : invalid character` | `substitute` 改递归展开 | | 5 | uniform 声明带初始化器 | `cannot initialize this type of qualifier` | ES 3.00 不允许,声明改为裸的 | | 6 | vec3 → vec4 提升时 alpha 补 0 | 编译能过,但画面**全透明** | 第 4 位改补 1.0 | 第 6 个特别值得记:它不是编译错误,是**语义错误**,编译器不报, 但会让用户第一次打开就看到一片空白。 ### 3.2 GPU 像素回读(`render-gpu.test.js`) 编译通过 ≠ 画得对。这个测试用 `gl.readPixels` 把画布正中心的像素读回来对值。 ``` [gpu] 环境 = {"renderer":"WebKit WebGL","floatBuffers":true,"size":{"w":128,"h":128}} [gpu] iChannel0=red {"r":255,"g":0,"b":0,"a":255} ← 绑纯红图 [gpu] iChannel0=unbound {"r":32,"g":32,"b":32,"a":255} ← 解绑 → 兜底灰,不是黑 [gpu] iChannel3=red {"r":255,"g":0,"b":0,"a":255} ← 红绑在 iChannel3 [gpu] iChannel0=still-unbound {"r":32,"g":32,"b":32,"a":255} ← iChannel0 没串台 [gpu] iChannel3=blue {"r":0,"g":0,"b":255,"a":255} ← 换源生效(旧纹理已释放) [gpu] bufferA=green {"r":0,"g":255,"b":0,"a":255} ← BufferA → image 多 Pass 反馈 [gpu] mouse-uv {"r":64,"g":191,"b":0,"a":255} ← iMouse ``` 最后一行是重点:鼠标移到画布 (32,32),画布 128×128,所以 `iMouse.zw = [32, 96]`,`uv = [0.25, 0.75]`, 期望像素 `[0.25×255, 0.75×255] = [64, 191]` —— **与实测完全一致**。 这一步把三块之前「只有代码、没有验证」的能力钉死了: 纹理绑定、兜底路径、Buffer 双缓冲的「上一帧」语义。 ### 3.3 编辑器真鼠标拖拽(`editor-interaction.test.js`) 用 CDP 的 `Input.dispatchMouseEvent` 走**真输入通道**, 坐标由页面自己用 `portPosition()` / `nodeHeaderPosition()` 现算。 ``` [editor] 画布尺寸 = {"w":560,"h":471} [editor] 端口坐标 = {"out":{"x":488,"y":222},"in":{"x":540,"y":162}} [editor] hitAtOut = {"world":{"x":248,"y":103},"hit":{"kind":"port","nodeType":"uv","portId":"out","dir":"out","index":0}} [editor] 连线过程 = [ {"phase":"afterDown","drag":{"kind":"edge","fromPort":"out"},"edges":0}, {"phase":"afterMove","drag":{"kind":"edge","fromPort":"out"},"edges":0}, {"phase":"afterUp","drag":null,"edges":1,"status":"2 节点 / 1 连线 · 缩放 100% · 选中 1"} ] [editor] 拖前 = uv(0,0) edges: [] [editor] 拖后 = uv(80,60) edges: [] ← 位移与鼠标位移精确一致 [editor] 连线后 = uv(80,60) edges: [uv.out → output.color] [editor] 重连后 = uv(80,60) edges: [uv.out → output.color] ← 仍是 1 条(替换而非叠加) [editor] 再拖后 = uv(40,30) edges: [uv.out → output.color] [editor] 事件 = [move-node, connect, connect, move-node] ``` **这个测试抓到了 2 个真 bug**(都是编译过、单测过、模板看不出问题的): | # | Bug | 为什么之前没暴露 | 修法 | |---|---|---|---| | 1 | **`_canDrop` 读已被清空的 `this.drag`** → 手动拖线**从来没成功过** | `_onPointerUp` 一进来就 `this.drag = null`;内置示例模板走 `_link()` 绕过检查直接连,所以演示看起来一切正常 | 把 drag 当参数显式传进 `_canDrop(hit, drag)` | | 2 | **`canConnect` 比生成器更严** → `uv`(vec2) → `output`(vec4) 这种最基本的连法被拒 | 生成器 `castExpr` 支持 16 种组合,编辑器只放行 6 种;模板又绕过检查 | 改成 16 种全放行,有损性交给 `conversionKind` 在状态栏提示 | 第 1 个是本项目最值得记的 bug:**没有任何自动化检查能发现它**, 除非真的用鼠标拖一次。这也是「为什么要有交互测试」的最好论据。 ### 3.4 面板端到端冒烟(`panel-smoke.test.js`) 真浏览器 + 真 DOM + 真 WebGL2,把工作台整块挂起来,再打开一个真材质。 ``` [smoke] stage = done [smoke] 材质 = demo / plasma [smoke] 图 = 7 节点 / 6 连线 [smoke] 渲染 = {"passes":["image"],"floatBuffers":true,"size":{"width":980,"height":120},...} [smoke] 热重载 = {"reloads":1,"failures":1,"lastDurationMs":10.7} [smoke] 日志 = [{"tone":"ok","text":"热重载完成,用时 10.7 ms"},{"tone":"info","text":"已打开 demo/plasma(7 节点)"}] ``` (`failures: 1` 是打开材质**之前**那次空图的预期失败——空 Pass 没有输出节点。 工作台对这种情况显示的是灰色提示而不是红错。) **这个测试抓到的 bug**: | # | Bug | 表现 | 修法 | |---|---|---|---| | 1 | `/module/` 路由只认 `client/` | 面板 import `../../shared/*.js` 全部 404,白屏 | 白名单加 `shared/` 与插件根,并把 `module/` 后的整条路径规范化(URL 层级与磁盘深度对不上) | | 2 | `Workbench` 漏了再导出 | `mod.Workbench is not a constructor` | `panel/index.js` 补 `export { Workbench }` | | 3 | `_materialLabel()` 方法根本没写 | 构造函数抛错,面板整个建不起来 | 改成直接建 DOM 节点 | | 4 | `material.list` 返回 `{materials:[...]}` 被当裸数组用 | 资源树显示 `undefined`、项目不展开 | 取 `.materials` | | 5 | 空图被当成编译错误 | 一打开就是红框报错 | 区分 `empty-pass`,显示灰色提示 | ### 3.5 1080p 性能(`perf-1080p.test.js`) ``` [perf] dpr = 1 | GPU = WebKit WebGL | timerQuery = true [perf] simple rAF 139.7 FPS(419 帧 / 3.00s)· 1920×1080 · Pass [image] [perf] simple CPU render() 均值 0.027ms / p95 0.100ms [perf] simple GPU 均值 0.04ms / p50 0.02ms / p95 0.07ms [perf] heavy rAF 143.9 FPS(432 帧 / 3.00s)· 1920×1080 · Pass [image, BufferA] [perf] heavy CPU render() 均值 0.030ms / p95 0.100ms [perf] heavy GPU 均值 0.48ms / p50 0.38ms / p95 0.86ms [perf] 隐藏 1 秒内渲染次数 = 0 ``` 重负载图 = 两层 fbm(每层 4 个八度)+ 6 个数学节点 + 调色板 + BufferA 自反馈。 --- ## 4. 需要人工复核的项(诚实清单) 自动化能覆盖的都已经覆盖了。**只剩一项必须人眼看**: | # | 项 | 怎么验 | 为什么自动化验不了 | |---|---|---|---| | 1 | **画面的审美是否正确** | 重启 DSH → 点侧栏「Shader 工作台」→ 打开 demo/plasma 或点「插入示例」→ 预览区应出现随时间流动的彩色等离子体 | 自动化能证明「编译通过 + 出帧 + 像素取值正确」,证明不了「好看」 | 原本列在这里的「面板能否打开」「拖拽是否可用」「iChannel/iMouse 是否生效」 现在都已经转为自动化覆盖: | 项 | 现在由谁覆盖 | |---|---| | 面板能否挂载 / 会不会白屏 | `panel-smoke`(真浏览器 + 真 DOM) | | 拖拽能否移动节点、能否连线 | `editor-interaction`(真 CDP 鼠标事件) | | iChannel0-3 有没有真的绑上 | `render-gpu`(读像素) | | iMouse 位置算得对不对 | `render-gpu`(读像素,与公式精确吻合) | | Buffer 反馈拿的是不是上一帧 | `render-gpu`(读像素 = 绿) | | GLSL 能不能编译 | `glsl-compile`(50 探针) | | 1080p 帧率 / 隐藏时空转 | `perf-1080p`(GPU timer query) | | HTTP API 通不通、路径安全 | `http` / `workspace` | | 插件会不会让 DSH failed | `plugin-load` | --- ## 5. 已知限制 | 限制 | 说明 | |---|---| | 不能从浏览器拿文件绝对路径 | 导入纹理走「让用户填磁盘路径」的方式(DSH 是本地桌面应用,路径可达)。真正的拖拽上传留待后续 | | 一个输入端口连多条线 | 图里都保留(`normalizeGraph` 去重后仍可能有多条来源),但生成器只用第一条(`find` 语义)。**编辑器连线时会把同目标的老线替换掉**,所以 UI 上不会积 | | 无撤销重做 | 删除/清空 Pass 是即时且不可撤销的;**快照是补救手段**(`shader_graph_build` 会自动打一份快照) | | 无节点分组/注释 | `GraphNode.annotation` 字段已预留,UI 未做 | | DDS 不做转码 | 只按普通图片绑槽位,不解析 mipmap / ASTC | | 面板路径不能跨出 `client/` `/shared/` / 插件根 | 这是 `/module/` 路由的白名单,出于安全考虑 | | 有损转换只提示不阻止 | `vec4 → vec2` 会静默丢 zw,状态栏给提示但允许连。设计取舍:生成器规则是确定的,用户有权这么接 | | ComfyUI 与跨编译未做 | 见第 2.4 / 2.5 节 | --- ## 6. 交付物核对(对照需求书第 12 节) | 交付物 | 状态 | 位置 | |---|---|---| | `@dsh/shader-workbench` 插件包(Host + Client) | ✅ 一个包内双端 | `host/` + `client/` | | `@dsh/shader-runtime` 渲染核心包 | ⚠️ 以子模块形式存在 | `client/runtime/`(未拆成独立 npm 包。需求书要求 Monorepo,本版按「单包 + 子模块」实现,换零构建链) | | `@dsh/shader-workbench-shared` 类型与 Schema | ⚠️ 同上 | `shared/` | | ComfyUI 工作流模板 | ❌ M4 | — | | 编译导出 CLI | ❌ M5 | — | | 示例项目与材质库 | ✅ | 面板「插入示例」的节点图模板 + `library/` 目录结构;`shader_graph_build` 工具可批量建 | | 用户文档 / 开发文档 | ✅ | `README.md` / `docs/架构说明.md` / `docs/接口契约.md` | | 测试套件与验收报告 | ✅ | `tests/`(64 例)+ 本文件 | ### 与需求书的偏离(已确认,需记录) 1. **不引入构建链**:需求书建议 Vite + TypeScript + pnpm workspace, 本版手写 ESM JavaScript、`node --test`。核心理由是「轻量 + 离线可用」这两条非功能需求。 2. **节点编辑器自研,不用 Rete.js**:需求书建议 Rete.js + React。 自研的理由是包体与可控性;代价是分组/注释/撤销重做要自己写(本版未写)。 3. **单包而非 Monorepo**:三个包合并为一个包的三个目录。 零构建链的前提下,拆包只会带来路径解析的麻烦。 4. **多 Agent 协作未按 A0–A9 十角色执行**:实际是主线实现 + 按需子任务, 接口由主线冻结(`docs/接口契约.md` v1)。需求书第 8 节的分工机制是为多人/多 Agent 并行设计的,单人主线下按里程碑推进更高效。 5. **面板内容用原生 DOM,不用 React 元素树**:只借 React 的生命周期。 理由见 `docs/架构说明.md` 第 2.5 节(canvas/WebGL 与 reconcile 相冲)。 --- ## 7. 验收结论 | 里程碑 | 结论 | |---|---| | M0 脚手架 + 接口冻结 | **通过**。接口文档 v1.2 已冻结;插件可被 DSH 加载(`apply()` 实测 9 个工具全注册);`node --test` 与沙箱兜底 runner 都能跑 | | M1 渲染闭环 | **通过**。生成器 + 多 Pass 渲染器 + 热重载;真机 GLSL 50/50 编译通过;**iChannel0-3 与 iMouse 已用读像素验证**;1080p GPU 单帧 p95 0.86 ms | | M2 节点编辑器 + 参数面板 | **通过**。面板在真浏览器里整块挂起来并完成「打开材质 → 编译 → 渲染」闭环;**真鼠标拖拽验证了移动节点与连线** | | M3 本地资源管理 | **通过**。项目/材质/纹理/预设/快照全流程单测覆盖,路径安全三道防线验过 | | M4 / M5 | 不在本次范围 | **唯一待人工确认**:画面的审美效果(见第 4 节)。 计划里原本「需人工赌一把」的三项——面板能否打开、拖拽是否可用、iChannel/iMouse 是否生效—— 现在都由真浏览器测试覆盖了。这三块一共抓出 **12 个 bug**,其中 `_canDrop` 读空 drag 导致「手动连线从来没成功过」这个,**只有真拖一次才发现**。 --- ## 8. 修订记录 | 版本 | 日期 | 说明 | |---|---|---| | v1 | 2026-10-10 | 初版:61 用例,面板未验证,性能未测 | | v2 | 2026-10-10 | 补 `panel-smoke` 与 `perf-1080p` 两个真浏览器测试;64 用例全绿;新增 5 个面板 bug 记录;性能与面板两项从「待人工确认」转为「通过」 | | v3 | 2026-10-10 | 恢复 `node --test` 为主 runner(原命令因沙箱 EPERM 被换掉,现已验证 glob 写法可用,沙箱兜底 runner 保留为 `test:sandbox`);补 `render-gpu`(读像素验证 iChannel0-3 / 多 Pass / iMouse)与 `editor-interaction`(真鼠标拖拽);抽出共用夹具 `tests/helpers/browser.mjs`;**修 2 个真 bug**(`_canDrop` 读空 drag → 手动连线失效;`canConnect` 比生成器更严 → vec2→vec4 连不上)与 1 个 GPU 纹理泄漏;70 用例全绿 |