more progress, but some of them are wip

This commit is contained in:
2026-07-07 16:48:44 +08:00
parent 431ca6e7b1
commit 0f484b78bb
10 changed files with 986 additions and 93 deletions

132
PLAN.md
View File

@@ -333,32 +333,89 @@ Map GammaRay client capabilities to MCP tool calls.
All introspection tools below call `session->ensureConnected(timeoutMs)` first; if a URL is known and the session is `Disconnected`/`Failed`, they transparently reconnect. If no URL is known, they return a JSON error pointing the client at `connectProbe()`.
### Navigation tools
- `list_quick_windows` → read `com.kdab.GammaRay.QuickWindowModel`, return window addresses
- `select_quick_window(index)``QuickInspectorInterface::selectWindow(int)`
- `list_quick_items` → traverse `com.kdab.GammaRay.QuickItemModel`, return item tree (id, type, geometry)
- `list_scenegraph_nodes(parentObjectId?)` → traverse `com.kdab.GammaRay.QuickSceneGraphModel`, return node tree (id, type, address)
- `get_scenegraph_node_detail(objectId)` → select node via selection model, read property models
- `listQuickWindows()` → read `com.kdab.GammaRay.QuickWindowModel`, return window addresses
- `selectQuickWindow(index)``QuickInspectorInterface::selectWindow(int)`
- `listQuickItems()` → traverse `com.kdab.GammaRay.QuickItemModel`, return item tree (id, type, geometry)*not yet implemented*
- `listScenegraphNodes()` → traverse `com.kdab.GammaRay.QuickSceneGraphModel`, return node tree (id, type, address). Uses `primeAndWait()` for async fill.
- `selectScenegraphNode(address)` → select node via selection model (currently blocked — see Known issues)
### Geometry tools
- `get_node_vertices(objectId)` → select SG node, read `sgGeometryVertexModel` (vertices: x, y, z, w + render role)
- `get_node_adjacency(objectId)` → read `sgGeometryAdjacencyModel` (drawing mode, vertex indices)
- `getNodeVertices(address)` → select SG node, read `sgGeometryVertexModel` (vertices: x, y, z, w + render role)*blocked*
- `getNodeAdjacency(address)` → read `sgGeometryAdjacencyModel` (drawing mode, vertex indices)*blocked*
### Material/Shader tools
- `get_material_shaders(objectId)` → read `shaderModel` for selected node
- `get_shader_source(row)``MaterialExtensionInterface::getShader(int)` (async: listen for `gotShader` signal)
- `get_material_properties(objectId)` → read `materialPropertyModel`
- `getMaterialShaders(address)` → read `shaderModel` for selected node*blocked*
- `getShaderSource(row)``MaterialExtensionInterface::getShader(int)` (async: listen for `gotShader` signal)*implemented, untested end-to-end (blocked upstream)*
- `getMaterialProperties(address)` → read `materialPropertyModel`*blocked*
### Rendering visualization tools
- `set_render_mode(mode)``QuickInspectorInterface::setCustomRenderMode(mode)` where mode ∈ {Normal, VisualizeClipping, VisualizeOverdraw, VisualizeBatches, VisualizeChanges, VisualizeTraces} (quickinspectorinterface.h:52-66)
- `set_slow_mode(bool)``QuickInspectorInterface::setSlowMode(bool)`
- `setRenderMode(mode)``QuickInspectorInterface::setCustomRenderMode(mode)` where mode ∈ {NormalRendering, VisualizeClipping, VisualizeOverdraw, VisualizeBatches, VisualizeChanges, VisualizeTraces} (quickinspectorinterface.h:52-66). **NOTE**: takes enum NAME string, not int. Values map 1:1 to GammaRay's `RenderMode` enum.
- `setSlowMode(enabled)``QuickInspectorInterface::setSlowMode(bool)`
### Texture tools
- `grab_texture(objectId)``TextureExtension` (async image grab; image arrives via RemoteViewInterface as TransferImage)
- `grabTexture(objectId)``TextureExtension` (async image grab; image arrives via `RemoteViewInterface` as `TransferImage`) — *not yet implemented*
### Implementation status
-`list_quick_windows` (as `listQuickWindows`), `select_quick_window` (as `selectQuickWindow`), `list_scenegraph_nodes` (as `listScenegraphNodes`) — working end-to-end against a live probe.
- ✅ Connection management (`connectProbe`/`connectProbeDefault`/`disconnectProbe`/`probeStatus`) — implemented; bridge no longer hard-requires `--connect`.
- ⏳ Geometry/Material/Shader/Render/Texture tools — pending. They require (a) selecting an SG node via the `QuickSceneGraphModel` selection model so the per-node `sgGeometryVertexModel`/`sgGeometryAdjacencyModel`/`shaderModel`/`materialPropertyModel` populate, and (b) for shader/texture the bridge needs to receive async signals from `MaterialExtensionInterface`/`TextureExtension` (which are NOT in installed headers — will need either a thin custom client stub connected via `Endpoint::registerObject` on the client side, or a signal-forwarding shim built on `Endpoint::invokeObject` + a probe-side callback. To be designed.)
### Implementation status (updated — see "Known issues / Blocked" section for details on the sub-model population blocker)
**Working end-to-end (verified via stdio MCP smoke test against a live probe):**
- `connectProbe(host?, port?)` / `connectProbeDefault()` / `disconnectProbe()` / `probeStatus()` — lazy connect + auto-reconnect state machine (`Disconnected → Connecting → Ready`, with `→ Failed` on persistent error). `disconnectProbe()` clears remembered URL and stops auto-reconnect.
-`listQuickWindows()` — reads `QuickWindowModel`, returns `[{address, type}]`.
-`selectQuickWindow(index)` — forwards via `Endpoint::invokeObject("com.kdab.GammaRay.QuickInspectorInterface/1.0", "selectWindow", {index})`.
-`listScenegraphNodes()` — returns real QSGNode tree (verified: returns 11 GeometryNodes on the test QML). Uses signal-driven `primeAndWait()` (see Implementation findings) — first call may show "Loading..." for deep nodes, second call returns the full tree.
-`setRenderMode(mode)` — accepts render-mode enum **name** string (NOT int): `NormalRendering` / `VisualizeClipping` / `VisualizeOverdraw` / `VisualizeBatches` / `VisualizeChanges` / `VisualizeTraces`. Calls `invokeObject(..., "setCustomRenderMode", {QVariant::fromValue<RenderMode>(mode)})`. Cross-process QVariant serialization works thanks to `quickinspector_types.h` `RenderMode` enum + `Q_DECLARE_METATYPE` with matching fully-qualified name `GammaRay::QuickInspectorInterface::RenderMode`.
-`setSlowMode(enabled)` — calls `invokeObject(..., "setSlowMode", {bool})`. Verified by reading back state.
**Implemented but BLOCKED by selection sync issue — DISABLED (wrapped in `#if 0` with TODO markers; registration removed from `main.cpp`). Re-enable when the bug is fixed:**
-`selectScenegraphNode(address)` (disabled) — finds node in SG tree, selects via `ObjectBroker::selectionModel(sgModel)` + direct `Message` send via `Protocol::fromQModelIndex`. Caches `m_selectedAddress` to avoid redundant reselection. Selection IS sent and IS received by the probe (`sgSelectionChanged()` fires), but the probe-side selection maps to a `QSGTransformNode` (row 0) instead of the target `QSGGeometryNode`. Sub-models stay empty.
-`getNodeVertices(address)` (disabled) — reads `sgGeometryVertexModel` (display + headers + `kIsCoordinateRole=257`). Returns `rowCount=0` because the model is never populated (selection wrong on probe side).
-`getNodeAdjacency(address)` (disabled) — reads `sgGeometryAdjacencyModel` (drawing mode + indices, `kDrawingModeRole=257`, `kRenderRole=258`). Same blocker.
-`getMaterialShaders(address)` (disabled) — reads `shaderModel` (shader stage names). Same blocker.
-`getShaderSource(row)` (disabled) — async path implemented: `MaterialExtensionInterface` proxy (`material_interface.{h,cpp}`) with matching IID `com.kdab.GammaRay.MaterialExtensionInterface`, self-registers via `ObjectBroker::registerObject`, factory registered in `initOnce()`. Calls `invokeObject(..., "getShader", {row})` and waits for `gotShader(QString)` signal. **Untested end-to-end** because the upstream `shaderModel` is never populated.
-`getMaterialProperties(address)` (disabled) — reads `materialPropertyModel`. Same blocker.
**Not yet implemented:**
-`grabTexture(objectId)``TextureExtension` async image grab via `RemoteViewInterface` `TransferImage`. Not started (depends on selection working first to populate the texture list).
### Known issues / Blocked — sub-models not populating after SG node selection
This is the single biggest open issue. Extensive debugging has narrowed it down but not yet fixed it.
**Symptom**: After `selectScenegraphNode(address)` selects a QSGGeometryNode, all four sub-models (`sgGeometryVertexModel` / `sgGeometryAdjacencyModel` / `shaderModel` / `materialPropertyModel`) stay at `rowCount=0` / `columnCount=0`. Tested against all 11 GeometryNodes in the test QML — none populate.
**What's been verified working**:
- All four sub-model addresses are valid on the probe (vertex=161, adjacency=162, shader=154, materialProp=153, selectionModel=142).
- The client-side selection model name is correct: `"com.kdab.GammaRay.QuickSceneGraphModel.selection"` (NOT `".selection"``_diag_dotSelAddr=0` confirms the dotted form is wrong).
- `sourceModelForProxy(sgFilterProxy)` returns `sgFilterProxy` itself (it's registered), so `SelectionModelServer` is created directly for it with the name matching the client's `SelectionModelClient`.
- Client-side selection IS correct: `hasSelection()=true`, `selectedRows()` returns the expected index with the full path matching the target address.
- `Endpoint::isConnected()=true` — selection messages ARE being sent.
- Probe-side `QuickInspector::sgSelectionChanged()` IS being called (confirmed via `qWarning()` debug logging in the journal — added to `/home/blumia/Sources/GammaRay/plugins/quickinspector/quickinspector.cpp:737`).
**Root cause identified (still being narrowed down)**:
The server's `sendSelection()` (triggered by `SelectionModelStateRequest` from `SelectionModelClient`'s 125ms timer, OR by a model-change timer) selects the DEFAULT item (row 0 at root = `QSGTransformNode`) and sends `SelectionModelSelect` back to the client, overriding our selection. Even with: a 2000ms settle before selecting, re-sending the selection twice, manually constructing `SelectionModelSelect` messages via `Protocol::fromQModelIndex`, and turning off slow mode — the sub-models still return `rowCount=0`.
The second `sgSelectionChanged` call shows `parentValid=true parentRow=0` — selection IS reaching a child node, but it's a `QSGTransformNode`, NOT our `QSGGeometryNode`. The path (0,0)→(0,0)→...→(0,0) is either:
(a) being truncated on the wire,
(b) mapping to the wrong node on the server side via `translateSelection()` / `toQModelIndex()`, OR
(c) the server's default-selection response is arriving AFTER our selection and overriding it.
**Things tried that did NOT fix it**:
- Waiting longer (2s settle before + after selection, re-sending selection twice).
- Disabling slow mode before selection.
- Manually constructing `SelectionModelSelect` messages via `Protocol::fromQModelIndex` (bypassing `SelectionModelClient`).
- Turning off slow mode.
**Things NOT yet tried** (see Next steps):
- Add diagnostic logging to the probe-side `NetworkSelectionModel::translateSelection()` and `toQModelIndex()` to trace exactly what path arrives and how it's walked.
- Check whether the server's `sendSelection()` response is arriving AFTER our selection and overriding it (could disable the `SelectionModelClient`'s `requestSelection` 125ms one-shot timer, or call `sendSelection()` on the server after our selection to force-sync).
- Patch `QuickInspector::selectSGNode()` (line 474, currently NOT Q_INVOKABLE) to make it Q_INVOKABLE so the bridge can call it directly via `invokeObject`, bypassing the whole selection-sync mechanism.
- Run the GammaRay GUI client (`xvfb-run` or `QT_QPA_PLATFORM=offscreen`) against the same probe to verify the selection mechanism works at all with this probe setup — if the GUI also fails to populate sub-models, the bug is in the probe plugin, not the bridge.
### Half-finished artifacts to clean up once the blocker is resolved
- **Debug `qWarning()` logging in probe plugin**: `/home/blumia/Sources/GammaRay/plugins/quickinspector/quickinspector.cpp:737` (`SGDBG:` lines). Must rebuild+install the probe plugin after any change: `cmake --build /home/blumia/Sources/GammaRay/build -j --target gammaray_quickinspector && cmake --install /home/blumia/Sources/GammaRay/build --prefix $(pwd)/install-prefix`.
- **`m_lastSelectPath` diagnostic field in `SceneGraphTools`**: added to `scenegraph_tools.{h,cpp}` to log the serialized selection path. Pure diagnostic — should be removed (or kept behind a `#ifndef QT_NO_DEBUG`) once the issue is fixed.
- **Disabled tool registrations in `main.cpp` + `scenegraph_tools.{h,cpp}`**: 6 broken tools (`selectScenegraphNode` / `getNodeVertices` / `getNodeAdjacency` / `getMaterialShaders` / `getShaderSource` / `getMaterialProperties`) wrapped in `#if 0` or commented out with `TODO` markers. Remove the `#if 0` / uncomment when the blocker is fixed.
- `listScenegraphNodes` first-call fill stability — first call still shows "Loading..." for deep nodes; second call returns the full tree. The `primeAndWait()` loop already handles this, but a single-call fix would be nicer.
## Step 7: Launch and run
@@ -450,24 +507,34 @@ When in doubt, these files have the ground truth:
-**Step 1 done**: GammaRay 3.4.0 built + installed to `install-prefix/`. Protocol version 38.
-**Minimal client built & run**: `minimal-client/` links `gammaray_client` + `gammaray_common`, connects to a live probe, receives `ready()`, fetches `QuickSceneGraphModel` / `QuickWindowModel` via `ObjectBroker` — clean exit. Proves the client ABI + protocol path is viable for the bridge.
-**qtmcp FetchContent viable**: `Qt6::McpServer`/`Qt6::McpCommon` build & link from a `FetchContent_MakeAvailable()` call. No separate clone/install.
-**Bridge scaffold built & working end-to-end** (`bridge/`): `qml-sg-mcp-bridge` links GammaRay client + qtmcp, runs as a stdio MCP server, and against a live probe returns REAL data:
-**Bridge scaffold built & working end-to-end** (`bridge/`): `qml-sg-mcp-bridge` links GammaRay client + qtmcp, runs as a stdio MCP server. Smoke test (`/tmp/opencode/smoke_test.py`) against a live probe confirms:
- `initialize` → serverInfo + capabilities + protocolVersion `2024-11-05`
- `tools/list``connectProbe`, `connectProbeDefault`, `disconnectProbe`, `probeStatus`, `listQuickWindows`, `selectQuickWindow`, `listScenegraphNodes`
- `connectProbe(host, port)` → connects + waits for handshake, returns `{connected, state, url, error?}`
- `probeStatus()``{state, ready, url, error?}` (verified: returns `disconnected` with empty url when no probe configured; returns `ready` after `connectProbe`)
- `listQuickWindows``[{"address":"0x... (QQuickWindowQmlImpl)"}]`
- `selectQuickWindow(index)` → forwards via `Endpoint::invokeObject("com.kdab.GammaRay.QuickInspectorInterface/1.0", "selectWindow", {index})`
- `listScenegraphNodes` → nested QSGNode tree (Root Node → Transform Node → ...)
-**Connection model revised**: bridge no longer hard-requires `--connect` at startup. Lazy connect + `connectProbe` MCP tool + auto-reconnect on dropped session. See "Connection model (decided)" in Step 7. State machine (`Disconnected → Connecting → Ready`, with `→ Failed` on persistent error) exposed via `probeStatus()`. Verified via standalone stdio test: with no `--connect`, `probeStatus` returns `disconnected`/empty URL; `listQuickWindows` returns a JSON error hinting at `connectProbe`; after `connectProbe(127.0.0.1, 11732)` against a live probe, tools return real data.
- `tools/list` 15 tools registered: `connectProbe`, `connectProbeDefault`, `disconnectProbe`, `getMaterialProperties`, `getMaterialShaders`, `getNodeAdjacency`, `getNodeVertices`, `getShaderSource`, `listQuickWindows`, `listScenegraphNodes`, `probeStatus`, `selectQuickWindow`, `selectScenegraphNode`, `setRenderMode`, `setSlowMode`
- `probeStatus` (before connect) → `{"ready":false,"state":"disconnected","url":""}`
- `connectProbeDefault()``{"connected":true,"state":"ready","url":"tcp://127.0.0.1:11732"}`
- `probeStatus` (after connect)`{"ready":true,"state":"ready","url":"tcp://127.0.0.1:11732"}`
- `listQuickWindows``[{"address":"0x... (ApplicationWindow_QMLTYPE_0)","type":""}]`
- `selectQuickWindow(0)``{"index":0,"status":"selected"}`
- `setSlowMode(true)``{"slowMode":true}`
- `setRenderMode("VisualizeOverdraw")``{"renderMode":"VisualizeOverdraw"}` ✅ (NOTE: takes enum name string, NOT int — see "Known issues")
- `setRenderMode("NormalRendering")``{"renderMode":"NormalRendering"}`
- `disconnectProbe()``{"ready":false,"state":"disconnected","url":""}`
- `listScenegraphNodes` (round 2, after 1s sleep) → full QSGNode tree with 11 GeometryNodes ✅
-**Connection model revised**: bridge no longer hard-requires `--connect` at startup. Lazy connect + `connectProbe` MCP tool + auto-reconnect on dropped session. See "Connection model (decided)" in Step 7. State machine (`Disconnected → Connecting → Ready`, with `→ Failed` on persistent error) exposed via `probeStatus()`.
### Key implementation findings (new, verified during scaffold work)
- **`Endpoint::invokeObject()` needs the object name == interface IID, NOT the bare name**. `QuickInspectorInterface` is registered via `ObjectBroker::registerObject<QuickInspectorInterface*>(this)` (quickinspectorinterface.cpp:59), which derives the name from `qobject_interface_iid` = `"com.kdab.GammaRay.QuickInspectorInterface/1.0"`. The bare `"com.kdab.GammaRay.QuickInspectorInterface"` is NOT in the client's name map → `Q_ASSERT(obj)` crash in debug builds. Also the method arg is the bare slot name `"selectWindow"` (no `(int)` signature), matching `QuickInspectorClient::selectWindow`.
- **No client-side stub object needed for `invokeObject`**: it only uses the name→address mapping (synced at handshake `ready()`), not a local object. So `selectQuickWindow` works without registering any factory/stub — `Endpoint::invokeObject` is public in the installed `endpoint.h`. (This resolves open question #1's "approach (b)" more cleanly than expected.)
- **RemoteModel is lazily fetched** (client/remotemodel.cpp): `data()`, `rowCount()`, `hasChildren()` return placeholders ("Loading..." / 0 / false) and queue server fetches whose results arrive async via `dataChanged`/`rowsInserted`. So a tool that reads a model right after `ready()` gets stale/empty data. The scaffold handles this with: `waitForReady()` (block until handshake), `waitForData()` (poll column 0 until non-"Loading..."), and for the SG tree a `primeTree()` + `settle()` round-robin (walk touching every cell + rowCount to queue fetches, run the event loop ~600ms, repeat 4x) before snapshotting with `walkChildren()`.
- **RemoteModel is lazily fetched** (client/remotemodel.cpp): `data()`, `rowCount()`, `hasChildren()` return placeholders ("Loading..." / 0 / false) and queue server fetches whose results arrive async via `dataChanged`/`rowsInserted`. So a tool that reads a model right after `ready()` gets stale/empty data. The scaffold handles this with: `waitForReady()` (block until handshake), `waitForData()` (poll column 0 until non-"Loading..."), and for the SG tree a `primeAndWait()` (see below) before snapshotting with `walkChildren()`.
- **`primeAndWait()` — signal-driven tree loading** (new): replaces the old fixed-time `primeTree()`+`settle()` loop. Tracks `dataChanged`/`rowsInserted` signals on the model, settles until quiet, then a final long settle. Signature `primeAndWait(m, rounds, settleMs, quietMs, finalSettleMs)` — 6 rounds of prime+settle (300ms settle, 500ms quiet), then final 2s settle + re-prime. Fixes "Loading..." for deep nodes: second call to `listScenegraphNodes` returns the full tree (verified: 11 GeometryNodes on test QML). First call may still show "Loading..." for deep nodes because RemoteModel's `requestDataAndFlags()` sets a cell to "Loading" state and won't re-request until the response arrives — the second call is the workaround.
- **`QApplication` required** (already noted): the headless bridge runs `QApplication` + `QT_QPA_PLATFORM=offscreen`.
- **FetchContent + qtmcp CMake scope gotcha**: a top-level `find_package(Qt6)`/`find_package(GammaRay)` BEFORE `FetchContent_MakeAvailable(qtmcp)` creates `Threads::Threads` at top scope; qtmcp's subdirectory then fails to promote it to global ("not built in this directory"). Fix: run `FetchContent_MakeAvailable(qtmcp)` FIRST (so qtmcp's own `find_package(Qt6)` creates+promotes 3rd-party targets inside its subdirectory), THEN `find_package(GammaRay)`.
- **qtmcp tool return types**: only `void`/`bool`/`QString`/`QStringList`/`QImage` (sync) + `QFuture<QList<QMcpCallToolResultContent>>` (async). `QJsonObject` hits `qFatal`. Structured results → JSON-serialized `QString` (see Step 2 tool section).
- **Cross-process QVariant enum serialization** (new, verified): `setRenderMode` needs to send a `RenderMode` enum value through `Endpoint::invokeObject()``QDataStream` → probe. The bridge defines a minimal `RenderMode` enum in `quickinspector_types.h` with `Q_DECLARE_METATYPE(GammaRay::QuickInspectorInterface::RenderMode)` so the enum's fully-qualified name matches GammaRay's. Values match exactly: `NormalRendering=0`, `Overdraw=1`, `Batches=2`, `Changes=3` (and `VisualizeClipping`/`VisualizeTraces` from the interface). Without the matching `Q_DECLARE_METATYPE`, the enum fails to serialize and the probe silently ignores the call.
- **`MaterialExtensionInterface` proxy pattern** (new, implemented): `MaterialExtensionInterface` and `MaterialExtensionClient` are NOT in installed headers — they're plugin-private. To receive the `gotShader(QString)` signal from the probe, the bridge defines a minimal `MaterialExtensionInterface` proxy class with the SAME IID (`com.kdab.GammaRay.MaterialExtensionInterface`), a `gotShader(QString)` signal, and a `getShader(int row)` slot that forwards via `Endpoint::invokeObject("com.kdab.GammaRay.QuickSceneGraph.material", "getShader", {row})`. The concrete `MaterialExtensionProxy` self-registers via `ObjectBroker::registerObject(name, this)` and the factory is registered via `registerClientObjectFactoryCallback<MaterialExtensionInterface*>()` in `initOnce()`. The `material_interface.moc` include is required (it's a header-only Q_OBJECT class — `#include "material_interface.moc"` at the bottom of the .cpp).
- **Direct `Message` construction for selection** (new, for the blocked selection work): `bridge/CMakeLists.txt` adds `/home/blumia/Sources/GammaRay/common` to `target_include_directories` so the bridge can include `<message.h>` and `<protocol.h>` directly from the GammaRay source tree. This lets the bridge manually construct a `SelectionModelSelect` message via `Protocol::fromQModelIndex(idx)` and send it with `Endpoint::send(msg)`, bypassing the `SelectionModelClient`'s state machine. `Protocol::fromQModelIndex()` serializes the QModelIndex as a path of (row, column) pairs — NOT internal pointers — so this works across processes. (See "Known issues / Blocked" — this bypass did NOT fix the sub-model population issue, but is kept for future debugging.)
- **Selection mechanism chain** (verified by reading source): `ObjectBroker::selectionModel(sgModel)` creates a `SelectionModelClient` (name = `model->objectName() + ".selection"`), which syncs to the probe's `SelectionModelServer`. The probe's `QuickInspector::sgSelectionChanged()` reads `ObjectModel::ObjectRole``QSGNode*`, then calls `m_sgPropertyController->setObject(node, className)`, which calls `setObject()` on all extensions (SGGeometryExtension, MaterialExtension, etc.) which populate the sub-models. `sourceModelForProxy()` stops at the first REGISTERED model (in `ObjectBroker`'s model map), not at the root source model — for `sgFilterProxy` (which IS registered), it returns `sgFilterProxy` itself, so `SelectionModelServer` is created directly for it.
### Running the bridge
@@ -485,4 +552,15 @@ cmake --build build
```
`run.sh` sets `LD_LIBRARY_PATH` (GammaRay + qtmcp libs) and `QT_PLUGIN_PATH` (qtmcp `mcpserverbackend/libqmcpserverstdio.so`) and `QT_QPA_PLATFORM=offscreen`.
-**Next**: implement the remaining Step 6 tools (`get_node_vertices`/`get_node_adjacency``sgGeometryVertexModel`/`sgGeometryAdjacencyModel`, `get_material_shaders`/`get_shader_source``shaderModel` + `MaterialExtensionInterface::getShader` async, `get_material_properties`, `set_render_mode`/`set_slow_mode` via `QuickInspectorInterface` invokeObject, `grab_texture`). The geometry/material models require selecting a SG node first (via a selection model on `QuickSceneGraphModel`), which is the next pattern to wire up.
## Next steps
1. **Unblock sub-model population** — the single highest-priority issue. Try (in order of likelihood):
1. **Run the GammaRay GUI client** (`xvfb-run install-prefix/bin/gammaray-client` or `QT_QPA_PLATFORM=offscreen`) against the same probe and click a GeometryNode. If the GUI ALSO fails to populate the vertex/adjacency/shader/material-property sub-models, the bug is in the probe plugin (or the offscreen platform), not the bridge — re-investigate `SGGeometryExtension::setObject()` and the `typeName == "QSGGeometryNode"` check in `sggeometryextension.cpp`. If the GUI WORKS, the bug is in our selection-sync path.
2. **Add diagnostic logging to probe-side `NetworkSelectionModel::translateSelection()` and `toQModelIndex()`** (in `/home/blumia/Sources/GammaRay/common/networkselectionmodel.cpp` and `common/protocol.cpp`) to trace exactly what path arrives on the wire and how it's walked back into a QModelIndex. The current `SGDBG:` logging only shows the FINAL node, not the path translation.
3. **Check if `sendSelection()` response arrives AFTER our selection and overrides it** — disable the `SelectionModelClient`'s `requestSelection` 125ms one-shot timer (`client/selectionmodelclient.cpp:21`), OR call `sendSelection()` on the server AFTER our selection to force-sync the client's state to OUR selection instead of the server's default.
4. **Patch `QuickInspector::selectSGNode()` to be Q_INVOKABLE** (`/home/blumia/Sources/GammaRay/plugins/quickinspector/quickinspector.cpp:474`). This bypasses the entire selection-sync mechanism — the bridge calls `invokeObject("com.kdab.GammaRay.QuickInspectorInterface/1.0", "selectSGNode", {QVariant::fromValue<void*>(nodePtr)})` directly. Requires the bridge to send the raw QSGNode pointer (which it has from the `QuickSceneGraphModel`'s `ObjectRole`). This is the most invasive option (modifies GammaRay source) but the most likely to work.
2. **Clean up debug artifacts** once the blocker is resolved — remove the `SGDBG:` `qWarning()` logging in `quickinspector.cpp:737`, remove the `m_lastSelectPath` diagnostic field in `scenegraph_tools.{h,cpp}`.
3. **Test `getShaderSource` end-to-end** once `shaderModel` populates — verify the `MaterialExtensionInterface` proxy + `invokeObject("getShader", {row})` + `gotShader(QString)` signal flow works.
4. **Implement `grabTexture(objectId)`**`TextureExtension` async image grab via `RemoteViewInterface` `TransferImage`. Follows the same `MaterialExtensionInterface` proxy pattern: a thin `TextureExtensionInterface` proxy with matching IID. Depends on selection working first to populate the texture list.
5. **Fix `listScenegraphNodes` first-call fill stability** — currently the first call shows "Loading..." for deep nodes and the second call returns the full tree. Either: (a) make `primeAndWait()` smarter (detect when a "Loading..." cell has been requested but not yet answered and wait for the `dataChanged`), or (b) just call `primeAndWait()` twice internally within a single `listScenegraphNodes` call.
6. **Pin qtmcp `GIT_TAG`** to a specific commit/tag in `bridge/CMakeLists.txt` for reproducibility (currently tracks `main`).