From 24075dc44284ff78a499c574ade158260c9d1d8f Mon Sep 17 00:00:00 2001 From: Jorge Ferreira Date: Thu, 7 May 2026 00:18:39 +0000 Subject: [PATCH 1/7] GUI: Handle display controls for multiple loaded technologies (Web GUI) Signed-off-by: Jorge Ferreira --- src/web/src/checkbox-tree-model.js | 5 + src/web/src/display-controls.js | 295 ++++++++++++++++++----------- src/web/src/inspector.js | 14 +- src/web/src/tile_generator.cpp | 64 ++++++- src/web/src/tile_generator.h | 3 +- 5 files changed, 267 insertions(+), 114 deletions(-) diff --git a/src/web/src/checkbox-tree-model.js b/src/web/src/checkbox-tree-model.js index f28f181f607..8c98d1e231a 100644 --- a/src/web/src/checkbox-tree-model.js +++ b/src/web/src/checkbox-tree-model.js @@ -71,6 +71,11 @@ export class CheckboxTreeModel { return this._nodeMap.get(id); } + isVisible(id) { + const node = this.get(id); + return node ? node.checked : false; + } + // Handle a user check/uncheck. Propagates down then up. check(id, checked) { const node = this._nodeMap.get(id); diff --git a/src/web/src/display-controls.js b/src/web/src/display-controls.js index 4f6b2b59c77..3379b8272b7 100644 --- a/src/web/src/display-controls.js +++ b/src/web/src/display-controls.js @@ -73,26 +73,86 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer, if (raw) savedHiddenLayers = new Set(JSON.parse(decodeURIComponent(raw))); } catch (_) { /* ignore */ } - const layerSpec = { - id: 'layers_parent', - children: techData.layers.map((name, index) => { - const layer = new WebSocketTileLayer(app.websocketManager, name, { - opacity: 0.7, - zIndex: index + 3, + // Global counter so each layer (across the whole hierarchy) gets a unique + // z-index and palette slot regardless of which chiplet it belongs to. + let nextLayerSlot = 0; + + function buildLayerSpec(hierarchyNode, parentId = 'layers') { + const children = []; + + if (hierarchyNode.instances && hierarchyNode.instances.length > 0) { + hierarchyNode.instances.forEach((inst, idx) => { + const instId = parentId + "/" + (inst.name || idx); + children.push(buildLayerSpec(inst, instId)); }); - const visible = !savedHiddenLayers.has(name); - if (visible) { - layer.addTo(app.map); - app.visibleLayers.add(name); - } - app.allLayers.push(layer); - leafletLayers.push(layer); + } - const id = `layer_${index}`; - layerIds.push(id); - return { id, data: { name, layer, colorIndex: index }, checked: visible }; - }), - }; + if (hierarchyNode.layers && hierarchyNode.layers.length > 0) { + hierarchyNode.layers.forEach((layerObj) => { + const name = layerObj.name || layerObj; + const slot = nextLayerSlot++; + const zIndex = slot + 3; + const layer = new WebSocketTileLayer(app.websocketManager, name, { + opacity: 0.7, + zIndex: zIndex, + }); + + const id = `${parentId}/${name}`; + layer._nodeId = id; + + const visible = !savedHiddenLayers.has(id); + if (visible) { + layer.addTo(app.map); + app.visibleLayers.add(id); + } + app.allLayers.push(layer); + leafletLayers.push(layer); + + layerIds.push(id); + children.push({ + id, + data: { name, layer, color: layerObj.color, colorIndex: slot, nodeId: id }, + checked: visible, + }); + }); + } + + return { + id: parentId, + data: { name: hierarchyNode.name, isInstance: true }, + children: children, + }; + } + + let layerSpec; + if (techData.layer_hierarchy) { + layerSpec = buildLayerSpec(techData.layer_hierarchy, 'layers_parent'); + } else { + // Fallback for old backends + layerSpec = { + id: 'layers_parent', + children: techData.layers.map((name, index) => { + const layer = new WebSocketTileLayer(app.websocketManager, name, { + opacity: 0.7, + zIndex: index + 3, + }); + + const id = name; + layer._nodeId = id; + + const visible = !savedHiddenLayers.has(id); + if (visible) { + layer.addTo(app.map); + app.visibleLayers.add(id); + } + app.allLayers.push(layer); + leafletLayers.push(layer); + + layerIds.push(id); + return { id, data: { name, layer, colorIndex: index, nodeId: id }, checked: visible }; + }), + }; + } const layerModel = new CheckboxTreeModel(() => { // Sync DOM and Leaflet layer visibility from model. @@ -102,12 +162,13 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer, node.cb.indeterminate = node.indeterminate; } if (node.data && node.data.layer) { + const id = node.data.nodeId || node.data.name; if (node.checked) { node.data.layer.addTo(app.map); - app.visibleLayers.add(node.data.name); + app.visibleLayers.add(id); } else { app.map.removeLayer(node.data.layer); - app.visibleLayers.delete(node.data.name); + app.visibleLayers.delete(id); } } }); @@ -115,66 +176,15 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer, if (app.pinsLayer && app.map.hasLayer(app.pinsLayer)) { app.pinsLayer.refreshTiles(); } - // Persist hidden layers to cookie. - const hidden = techData.layers.filter(n => !app.visibleLayers.has(n)); - setCookie('or_hidden_layers', encodeURIComponent(JSON.stringify(hidden))); + const allNodes = []; + layerModel.forEach(n => { if (n.data && n.data.layer) allNodes.push(n.data.nodeId || n.data.name); }); + const hiddenNodes = allNodes.filter(n => !app.visibleLayers.has(n)); + setCookie('or_hidden_layers', encodeURIComponent(JSON.stringify(hiddenNodes))); }); + + app.layerModel = layerModel; // expose it so other rendering mechanism can use it layerModel.addFromSpec(layerSpec); - // Build layer DOM. - const layerGroup = document.createElement('div'); - layerGroup.className = 'vis-group'; - - const layerHeader = document.createElement('label'); - layerHeader.className = 'vis-group-header'; - const layerArrow = document.createElement('span'); - layerArrow.className = 'vis-arrow'; - layerArrow.textContent = '▼'; - layerHeader.appendChild(layerArrow); - - const parentNode = layerModel.get('layers_parent'); - const parentCb = document.createElement('input'); - parentCb.type = 'checkbox'; - parentCb.checked = parentNode.checked; - parentCb.indeterminate = parentNode.indeterminate; - parentNode.cb = parentCb; - parentCb.addEventListener('change', () => { - layerModel.check('layers_parent', parentCb.checked); - }); - layerHeader.appendChild(parentCb); - layerHeader.appendChild(document.createTextNode('Layers')); - layerGroup.appendChild(layerHeader); - - const layerChildren = document.createElement('div'); - layerChildren.className = 'vis-group-children'; - - techData.layers.forEach((name, index) => { - const id = layerIds[index]; - const modelNode = layerModel.get(id); - - const label = document.createElement('label'); - - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.checked = modelNode.checked; - modelNode.cb = checkbox; - checkbox.addEventListener('change', () => { - layerModel.check(id, checkbox.checked); - }); - label.appendChild(checkbox); - - const colorSwatch = document.createElement('span'); - colorSwatch.className = 'layer-color'; - const c = (techData.layer_colors && techData.layer_colors[index]) - || fallbackLayerPalette[index % fallbackLayerPalette.length]; - colorSwatch.style.backgroundColor = `rgb(${c[0]},${c[1]},${c[2]})`; - label.appendChild(colorSwatch); - - label.appendChild(document.createTextNode(name)); - layerChildren.appendChild(label); - }); - layerGroup.appendChild(layerChildren); - // --- Layer context menu (right-click) --- const contextMenu = document.createElement('div'); contextMenu.className = 'context-menu'; @@ -184,7 +194,7 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer, function showOnlyLayers(indices) { const updates = {}; layerIds.forEach((id, i) => { - updates[id] = indices.has(i); + if (id) updates[id] = indices.has(i); }); layerModel.checkSet(updates); } @@ -193,7 +203,7 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer, contextMenu.style.display = 'none'; } - const n = techData.layers.length; + const n = leafletLayers.length; const menuItems = [ { label: 'Show only this layer', fn: (i) => layerRangeSet(i, 0, 0, n) }, { label: 'Show layer range \u2195', fn: (i) => layerRangeSet(i, 1, 1, n) }, @@ -202,27 +212,6 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer, { label: 'Show layer range \u2191', fn: (i) => layerRangeSet(i, 0, 1, n) }, ]; - layerChildren.querySelectorAll('label').forEach((label, index) => { - label.addEventListener('contextmenu', (e) => { - e.preventDefault(); - e.stopPropagation(); - contextMenu.innerHTML = ''; - for (const item of menuItems) { - const div = document.createElement('div'); - div.className = 'context-menu-item'; - div.textContent = item.label; - div.addEventListener('click', () => { - showOnlyLayers(item.fn(index)); - hideContextMenu(); - }); - contextMenu.appendChild(div); - } - contextMenu.style.left = e.clientX + 'px'; - contextMenu.style.top = e.clientY + 'px'; - contextMenu.style.display = 'block'; - }); - }); - document.addEventListener('click', (e) => { if (!contextMenu.contains(e.target)) hideContextMenu(); }); @@ -230,13 +219,103 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer, if (e.key === 'Escape') hideContextMenu(); }); - // Toggle collapse - layerArrow.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - const collapsed = layerChildren.classList.toggle('collapsed'); - layerArrow.textContent = collapsed ? '▶' : '▼'; - }); + function buildLayerDOM(node, isRoot = false) { + if (!node.children || node.children.length === 0) { + // Leaf node (layer) + const label = document.createElement('label'); + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.checked = node.checked; + node.cb = checkbox; + checkbox.addEventListener('change', () => { + layerModel.check(node.id, checkbox.checked); + }); + label.appendChild(checkbox); + + const index = node.data.colorIndex; + const name = node.data.name; + + const colorSwatch = document.createElement('span'); + colorSwatch.className = 'layer-color'; + const c = node.data.color || (techData.layer_colors && techData.layer_colors[index]) || fallbackLayerPalette[index % fallbackLayerPalette.length]; + colorSwatch.style.backgroundColor = `rgb(${c[0]},${c[1]},${c[2]})`; + label.appendChild(colorSwatch); + + label.appendChild(document.createTextNode(name)); + + // Setup context menu for layer + label.addEventListener('contextmenu', (e) => { + e.preventDefault(); + e.stopPropagation(); + contextMenu.innerHTML = ''; + for (const item of menuItems) { + const div = document.createElement('div'); + div.className = 'context-menu-item'; + div.textContent = item.label; + div.addEventListener('click', () => { + showOnlyLayers(item.fn(index)); + hideContextMenu(); + }); + contextMenu.appendChild(div); + } + contextMenu.style.left = e.clientX + 'px'; + contextMenu.style.top = e.clientY + 'px'; + contextMenu.style.display = 'block'; + }); + + return label; + } else { + // Group node (top or sub-instance) + const group = document.createElement('div'); + group.className = 'vis-group'; + + const header = document.createElement('label'); + header.className = 'vis-group-header'; + + const arrow = document.createElement('span'); + arrow.className = 'vis-arrow'; + arrow.textContent = '▼'; + header.appendChild(arrow); + + const cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.checked = node.checked; + cb.indeterminate = node.indeterminate; + node.cb = cb; + cb.addEventListener('change', () => { + layerModel.check(node.id, cb.checked); + }); + header.appendChild(cb); + + const name = isRoot ? 'Layers' : (node.data.name || 'Group'); + header.appendChild(document.createTextNode(name)); + group.appendChild(header); + + const kids = document.createElement('div'); + kids.className = 'vis-group-children'; + + // Build children recursively + for (const child of node.children) { + kids.appendChild(buildLayerDOM(child, false)); + } + group.appendChild(kids); + + // Toggle collapse + arrow.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + const collapsed = kids.classList.toggle('collapsed'); + arrow.textContent = collapsed ? '▶' : '▼'; + }); + + return group; + } + } + + // Build layer DOM. + const parentNode = layerModel.roots[0] || layerModel.get('layers_parent'); + const layerGroup = buildLayerDOM(parentNode, true); app.displayControlsEl.appendChild(layerGroup); @@ -336,7 +415,7 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer, if (!app.heatMapLayer) { app.heatMapLayer = new HeatMapTileLayer(app.websocketManager, app, { - zIndex: techData.layers.length + 10, + zIndex: leafletLayers.length + 10, opacity: 1, }); } diff --git a/src/web/src/inspector.js b/src/web/src/inspector.js index 00c5cd1094f..1e1b3761046 100644 --- a/src/web/src/inspector.js +++ b/src/web/src/inspector.js @@ -168,8 +168,20 @@ export function createInspectorPanel(app, redrawAllLayers) { return; } + const validRects = []; + for (const [x1, y1, x2, y2, layer_id, instance_path] of rects) { + if (app.layerModel && layer_id) { + const nodeId = instance_path ? `layers_parent/${instance_path}/${layer_id}` : layer_id; + // Check layerModel visibility + if (app.layerModel.isVisible && !app.layerModel.isVisible(nodeId)) { + continue; // skip drawing operations + } + } + validRects.push([x1, y1, x2, y2]); + } + app.hoverHighlightLayer = L.layerGroup( - rects.map(([x1, y1, x2, y2]) => L.rectangle(boundsWithMinimumScreenSize(x1, y1, x2, y2), { + validRects.map(([x1, y1, x2, y2]) => L.rectangle(boundsWithMinimumScreenSize(x1, y1, x2, y2), { color: '#ffe85c', weight: 3, fill: true, diff --git a/src/web/src/tile_generator.cpp b/src/web/src/tile_generator.cpp index 29b32a9c9bf..8cb73bb5c35 100644 --- a/src/web/src/tile_generator.cpp +++ b/src/web/src/tile_generator.cpp @@ -585,11 +585,11 @@ static std::map buildLayerColorMap(odb::dbTech* tech) return colors; } -const std::map& TileGenerator::getLayerColorMap() - const +const std::map& TileGenerator::getLayerColorMap( + odb::dbTech* req_tech) const { std::lock_guard lock(layer_colors_mutex_); - odb::dbTech* tech = db_->getTech(); + odb::dbTech* tech = req_tech ? req_tech : db_->getTech(); auto [it, inserted] = layer_colors_by_tech_.try_emplace(tech); if (inserted) { it->second = buildLayerColorMap(tech); @@ -3042,6 +3042,53 @@ void collectTimingPathShapes(odb::dbBlock* block, process_nodes(path.capture_nodes, kCaptureClkColor, kCaptureClkColor); } +namespace { + +boost::json::object buildLayerHierarchy(odb::dbChip* chip, + const TileGenerator& gen, + const std::string& name, + const std::string& type) +{ + boost::json::object node; + node["name"] = name; + node["type"] = type; + + boost::json::array layers_arr; + if (odb::dbTech* tech = chip->getTech()) { + const auto& layer_colors = gen.getLayerColorMap(tech); + for (odb::dbTechLayer* layer : tech->getLayers()) { + if (layer->getRoutingLevel() > 0 + || layer->getType() == odb::dbTechLayerType::CUT) { + boost::json::object layer_obj; + layer_obj["name"] = layer->getName(); + Color c{.r = 200, .g = 200, .b = 200, .a = 180}; + auto it = layer_colors.find(layer); + if (it != layer_colors.end()) { + c = it->second; + } + layer_obj["color"] = boost::json::array{static_cast(c.r), + static_cast(c.g), + static_cast(c.b)}; + layers_arr.emplace_back(std::move(layer_obj)); + } + } + } + node["layers"] = std::move(layers_arr); + + boost::json::array instances_arr; + for (odb::dbChipInst* inst : chip->getChipInsts()) { + if (odb::dbChip* masterChip = inst->getMasterChip()) { + instances_arr.emplace_back( + buildLayerHierarchy(masterChip, gen, inst->getName(), "instance")); + } + } + node["instances"] = std::move(instances_arr); + + return node; +} + +} // namespace + boost::json::object serializeTechResponse(const TileGenerator& gen) { boost::json::object out; @@ -3077,11 +3124,20 @@ boost::json::object serializeTechResponse(const TileGenerator& gen) out["sites"] = std::move(sites); out["has_liberty"] = gen.hasSta(); + // For 3DBlox designs the top dbChip is HIER and has no dbBlock; fall back to + // the top chip itself so layer_hierarchy is still emitted with chiplet + // instances under it. + odb::dbChip* top_chip + = gen.getBlock() ? gen.getBlock()->getChip() : gen.getChip(); if (gen.getBlock()) { out["dbu_per_micron"] = gen.getBlock()->getDbUnitsPerMicron(); out["block_name"] = gen.getBlock()->getName(); } else { - out["block_name"] = ""; + out["block_name"] = top_chip ? top_chip->getName() : ""; + } + if (top_chip) { + out["layer_hierarchy"] + = buildLayerHierarchy(top_chip, gen, top_chip->getName(), "block"); } return out; } diff --git a/src/web/src/tile_generator.h b/src/web/src/tile_generator.h index e6cc95f5d92..be4eb40d2d5 100644 --- a/src/web/src/tile_generator.h +++ b/src/web/src/tile_generator.h @@ -184,7 +184,8 @@ class TileGenerator // Per-layer colors matching gui::DisplayControls layer palette. Computed // lazily and cached; the cache is rebuilt only if the tech changes. - const std::map& getLayerColorMap() const; + const std::map& getLayerColorMap(odb::dbTech* tech + = nullptr) const; std::vector selectAt( int dbu_x, From b34218adc8c89f0f870df6b3624d64d95956bb72 Mon Sep 17 00:00:00 2001 From: Jorge Ferreira Date: Tue, 12 May 2026 21:02:01 +0000 Subject: [PATCH 2/7] WIP: save tile_generator and sta submodule changes Signed-off-by: Jorge Ferreira --- src/web/src/tile_generator.cpp | 57 ++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/src/web/src/tile_generator.cpp b/src/web/src/tile_generator.cpp index 8cb73bb5c35..8ede4f27095 100644 --- a/src/web/src/tile_generator.cpp +++ b/src/web/src/tile_generator.cpp @@ -493,14 +493,17 @@ int TileGenerator::getPinMaxSize() const std::vector TileGenerator::getLayers() const { std::vector layers; - odb::dbTech* tech = db_->getTech(); - if (!tech) { - return layers; - } - for (odb::dbTechLayer* layer : tech->getLayers()) { - if (layer->getRoutingLevel() > 0 - || layer->getType() == odb::dbTechLayerType::CUT) { - layers.push_back(layer->getName()); + std::set seen; + for (odb::dbTech* tech : db_->getTechs()) { + if (!tech) { + continue; + } + for (odb::dbTechLayer* layer : tech->getLayers()) { + if ((layer->getRoutingLevel() > 0 + || layer->getType() == odb::dbTechLayerType::CUT) + && seen.insert(layer->getName()).second) { + layers.push_back(layer->getName()); + } } } return layers; @@ -589,7 +592,17 @@ const std::map& TileGenerator::getLayerColorMap( odb::dbTech* req_tech) const { std::lock_guard lock(layer_colors_mutex_); - odb::dbTech* tech = req_tech ? req_tech : db_->getTech(); + // In multi-tech databases db_->getTech() throws ODB-0432, so resolve the + // single tech via getTechs() instead. When no caller-supplied tech and + // more than one is loaded, return an empty map: per-tech color lookups go + // through the explicit-tech overload (see buildLayerHierarchy). + odb::dbTech* tech = req_tech; + if (!tech) { + auto techs = db_->getTechs(); + if (techs.size() == 1) { + tech = *techs.begin(); + } + } auto [it, inserted] = layer_colors_by_tech_.try_emplace(tech); if (inserted) { it->second = buildLayerColorMap(tech); @@ -690,10 +703,19 @@ std::vector TileGenerator::selectAt( return a.bbox.area() > b.bbox.area(); }); - // Search nets via routing shapes on each layer + // Search nets via routing shapes on each layer. Iterate every loaded + // tech so multi-chiplet (3DBlox) designs with one dbTech per chiplet + // contribute their routing layers too. std::set seen_nets; - odb::dbTech* tech = db_->getTech(); - for (odb::dbTechLayer* layer : tech->getLayers()) { + std::set visited_layers; + for (odb::dbTech* tech : db_->getTechs()) { + if (!tech) { + continue; + } + for (odb::dbTechLayer* layer : tech->getLayers()) { + if (!visited_layers.insert(layer).second) { + continue; + } if (layer->getRoutingLevel() <= 0 && layer->getType() != odb::dbTechLayerType::CUT) { continue; @@ -759,6 +781,7 @@ std::vector TileGenerator::selectAt( } } } + } } debugPrint(logger_, @@ -785,7 +808,15 @@ odb::dbChip* TileGenerator::getChip() const odb::dbTech* TileGenerator::getTech() const { - return db_->getTech(); + // db_->getTech() throws ODB-0432 when more than one tech is loaded + // (multi-chiplet 3DBlox designs). The web GUI emits per-tech data via + // layer_hierarchy, so callers that still want "the" tech only get one + // when the database actually has a single tech; otherwise nullptr. + auto techs = db_->getTechs(); + if (techs.size() == 1) { + return *techs.begin(); + } + return nullptr; } std::vector TileGenerator::generateTile( From c272f7a55ca422ac052341b5f572d4849f9e2807 Mon Sep 17 00:00:00 2001 From: Jorge Ferreira Date: Wed, 6 May 2026 03:44:59 +0000 Subject: [PATCH 3/7] feat(web): add chiplets section to display controls Signed-off-by: Jorge Ferreira --- src/web/src/display-controls.js | 168 ++ src/web/src/main.js | 23 +- src/web/src/search.cpp | 75 +- src/web/src/search.h | 16 +- src/web/src/tile_generator.cpp | 2265 ++++++++++++++++----------- src/web/src/tile_generator.h | 53 +- src/web/src/websocket-tile-layer.js | 67 +- 7 files changed, 1723 insertions(+), 944 deletions(-) diff --git a/src/web/src/display-controls.js b/src/web/src/display-controls.js index 3379b8272b7..22e005260c2 100644 --- a/src/web/src/display-controls.js +++ b/src/web/src/display-controls.js @@ -17,6 +17,31 @@ export function layerRangeSet(center, lower, upper, count) { return indices; } +// Build the CheckboxTreeModel input for the Chiplets group. Each +// `chipletData` entry comes from the backend serializeTechResponse and +// has shape { path, name, parent, master, depth }. `savedHidden` is the +// set of chiplet paths the user has hidden (loaded from the cookie). +// +// Returns a flat array of nodes — id is the entry index, parentId is the +// index of the entry whose `path` matches `parent` (or -1 for the root), +// hasCheckbox is false for the root (no toggle for the whole stack). +export function buildChipletFlatNodes(chipletData, savedHidden) { + const pathToId = new Map(); + chipletData.forEach((c, i) => pathToId.set(c.path, i)); + return chipletData.map((c, i) => { + const visible = !savedHidden.has(c.path); + return { + id: i, + parentId: c.parent != null && pathToId.has(c.parent) + ? pathToId.get(c.parent) : -1, + hasCheckbox: c.parent != null, + checked: visible, + data: { path: c.path, name: c.name, master: c.master, + depth: c.depth }, + }; + }); +} + // Fallback color used when the server didn't supply a layer color. const fallbackLayerPalette = [ [70, 130, 210], // moderate blue @@ -319,6 +344,149 @@ export function populateDisplayControls(app, visibility, WebSocketTileLayer, app.displayControlsEl.appendChild(layerGroup); + // --- Chiplets group (multi-die / 3D-IC visibility) --- + // + // Mirrors the Qt GUI's per-chiplet visibility (see + // gui::DisplayControls::setCurrentChip). Backend sends one entry + // per dbChip / dbChipInst node with a unique `path` ("top", + // "top.soc_inst", "top.soc_inst.sub_ip", …). Toggling a node + // refreshes every Leaflet tile so the server's chiplet filter + // (`visible_chiplets`) takes effect on the next render. + const chipletData = (techData && Array.isArray(techData.chiplets)) + ? techData.chiplets : []; + if (chipletData.length > 1) { + let savedHiddenChiplets = new Set(); + try { + const raw = getCookie('or_hidden_chiplets'); + if (raw) { + savedHiddenChiplets = new Set( + JSON.parse(decodeURIComponent(raw))); + } + } catch (_) { /* ignore */ } + + // Build a flat node list keyed by path; CheckboxTreeModel will + // wire parent/child relationships from `parent` strings. Force + // hasCheckbox=true on the root so its tri-state drives the + // group-header checkbox below. buildChipletFlatNodes itself + // returns hasCheckbox=false for the root (its DOM is rendered + // by the header, not by renderChipletNode). + const flatNodes = buildChipletFlatNodes(chipletData, + savedHiddenChiplets); + const rootIdx = flatNodes.findIndex(n => n.parentId === -1); + const rootId = rootIdx >= 0 ? flatNodes[rootIdx].id : null; + if (rootIdx >= 0) { + flatNodes[rootIdx].hasCheckbox = true; + } + + // Initialize visible set from the saved cookie state. + app.visibleChiplets = new Set( + chipletData + .filter(c => !savedHiddenChiplets.has(c.path)) + .map(c => c.path)); + + const chipletModel = new CheckboxTreeModel(() => { + // Sync DOM checkboxes and recompute the visibility set in + // a single pass (renaming `node.cb` mirrors the layer + // group's pattern). + const newVisible = new Set(); + chipletModel.forEach(node => { + if (node.cb) { + node.cb.checked = node.checked; + node.cb.indeterminate = node.indeterminate; + } + if (node.checked && node.data && node.data.path) { + newVisible.add(node.data.path); + } + }); + app.visibleChiplets = newVisible; + + // Persist hidden paths to a cookie. + const hidden = chipletData + .filter(c => !newVisible.has(c.path)) + .map(c => c.path); + setCookie('or_hidden_chiplets', + encodeURIComponent(JSON.stringify(hidden))); + + // Refresh every Leaflet tile so the server applies the + // updated `visible_chiplets` filter on the next request. + redrawAllLayers(); + }); + chipletModel.buildFromNodes(flatNodes); + + const chipletGroup = document.createElement('div'); + chipletGroup.className = 'vis-group'; + + const chipletHeader = document.createElement('label'); + chipletHeader.className = 'vis-group-header'; + const chipletArrow = document.createElement('span'); + chipletArrow.className = 'vis-arrow'; + chipletArrow.textContent = '▼'; + chipletHeader.appendChild(chipletArrow); + + // Group-level checkbox: toggles every chiplet at once and + // shows tri-state when the children disagree, matching the + // Layers group's UX. + const rootNode = rootId != null ? chipletModel.get(rootId) : null; + if (rootNode) { + const parentCb = document.createElement('input'); + parentCb.type = 'checkbox'; + parentCb.checked = rootNode.checked; + parentCb.indeterminate = rootNode.indeterminate; + rootNode.cb = parentCb; + parentCb.addEventListener('change', (e) => { + e.stopPropagation(); + chipletModel.check(rootId, parentCb.checked); + }); + chipletHeader.appendChild(parentCb); + } + chipletHeader.appendChild(document.createTextNode('Chiplets')); + chipletGroup.appendChild(chipletHeader); + + const chipletChildren = document.createElement('div'); + chipletChildren.className = 'vis-group-children'; + + function renderChipletNode(node) { + const c = node.data; + // The root is rendered by the header above; skip it here. + if (node !== rootNode) { + const label = document.createElement('label'); + label.style.paddingLeft = (8 * (c.depth - 1)) + 'px'; + label.title = c.path + + (c.master ? ` (${c.master})` : ''); + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.checked = node.checked; + checkbox.indeterminate = node.indeterminate; + node.cb = checkbox; + checkbox.addEventListener('change', () => { + chipletModel.check(node.id, checkbox.checked); + }); + label.appendChild(checkbox); + label.appendChild(document.createTextNode(c.name)); + chipletChildren.appendChild(label); + } + if (node.children) { + node.children.forEach(renderChipletNode); + } + } + chipletModel.roots.forEach(renderChipletNode); + chipletGroup.appendChild(chipletChildren); + + chipletArrow.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + const collapsed = chipletChildren.classList.toggle('collapsed'); + chipletArrow.textContent = collapsed ? '▶' : '▼'; + }); + + app.displayControlsEl.appendChild(chipletGroup); + } else { + // Single-chip designs render every chiplet (i.e. only the top). + // Clearing keeps WebSocketTileLayer's serializer from sending an + // empty array and accidentally enabling the filter. + app.visibleChiplets = null; + } + // --- Visibility tree (ordered to match Qt GUI display controls) --- const visTree = new VisTree(visibility, redrawAllLayers); visTree.add({ label: 'Nets', children: [ diff --git a/src/web/src/main.js b/src/web/src/main.js index b91c8952d76..1fe0330c632 100644 --- a/src/web/src/main.js +++ b/src/web/src/main.js @@ -92,6 +92,10 @@ const app = { focusNets: new Set(), routeGuideNets: new Set(), visibleLayers: new Set(), + // Set of chiplet `path`s currently visible. Populated by + // display-controls.js once techData.chiplets arrives; null means + // "render every chiplet" (single-chip designs). + visibleChiplets: null, heatMapData: null, activeHeatMap: '', heatMapLayer: null, @@ -183,7 +187,11 @@ try { // Ignore malformed cookie. } -const WebSocketTileLayer = createWebSocketTileLayer(visibility, app.visibleLayers); +// `app` is forwarded so the tile layer can read app.visibleChiplets +// lazily on every request — the field is populated by display-controls +// once the server's tech metadata arrives. +const WebSocketTileLayer + = createWebSocketTileLayer(visibility, app.visibleLayers, app); const BLANK_TILE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='; @@ -971,7 +979,18 @@ app.websocketManager.readyPromise.then(async () => { for (const [k, v] of Object.entries(visibility)) { vf[k] = !!v; } - app.websocketManager.request({ type: 'select', dbu_x, dbu_y, zoom: Math.round(app.map.getZoom()), visible_layers: [...app.visibleLayers], ...vf }) + const selectRequest = { + type: 'select', + dbu_x, + dbu_y, + zoom: Math.round(app.map.getZoom()), + visible_layers: [...app.visibleLayers], + ...vf, + }; + if (app.visibleChiplets instanceof Set) { + selectRequest.visible_chiplets = [...app.visibleChiplets]; + } + app.websocketManager.request(selectRequest) .then(data => { console.log('Select response:', data, 'at dbu', dbu_x, dbu_y); app.map.closePopup(); diff --git a/src/web/src/search.cpp b/src/web/src/search.cpp index 4c71052dfdf..6310060e6d0 100644 --- a/src/web/src/search.cpp +++ b/src/web/src/search.cpp @@ -204,19 +204,35 @@ void Search::setTopChip(odb::dbChip* chip) } odb::dbBlock* block = chip->getBlock(); if (top_chip_ != chip) { - clear(); - - if (top_chip_ != nullptr) { - removeOwner(); - } - - addOwner(block); // register as a callback object + { + // Hold the unique lock for the entire reset+repopulate cycle so + // that shapesReady()/getData() callers from other threads see + // either the old or the new state, never a partial view. + std::unique_lock lock(child_block_data_mutex_); + clear(); + + if (top_chip_ != nullptr) { + removeOwner(); + } - // Pre-populate children so we don't have to lock access to - // child_block_data_ later - if (block) { - for (auto child : block->getChildren()) { - child_block_data_[child]; + addOwner(block); // register as a callback object + + // Pre-populate children so the common path through getData() is + // a lock-free hit. This covers two distinct hierarchies: + // 1. dbBlock children of the top block (intra-chip hierarchy). + // 2. Every dbBlock reachable through dbChipInst (3D-IC / + // multi-die hierarchy). These belong to dbChips other than + // top_chip_, so getData() routes them into + // child_block_data_ as well. + if (block) { + for (auto child : block->getChildren()) { + child_block_data_[child]; + } + } + for (const ChipletNode& node : collectChiplets(chip)) { + if (node.block && node.block != block) { + child_block_data_[node.block]; + } } } } @@ -226,6 +242,23 @@ void Search::setTopChip(odb::dbChip* chip) // emit newChip(chip); } +bool Search::shapesReady() const +{ + if (top_block_data_.shapes_init.load()) { + return true; + } + // Multi-die designs: check chiplet master blocks too. Hold the + // shared lock for the iteration so a concurrent setTopChip() (which + // holds the unique lock) cannot reshape the map under us. + std::shared_lock lock(child_block_data_mutex_); + for (const auto& [block, data] : child_block_data_) { + if (data.shapes_init.load()) { + return true; + } + } + return false; +} + void Search::announceModified(std::atomic_bool& flag) { const bool prev_flag = flag.exchange(false); @@ -304,8 +337,22 @@ void Search::eagerInit(odb::dbBlock* block) Search::BlockData& Search::getData(odb::dbBlock* block) { - return block->getChip() == top_chip_ ? top_block_data_ - : child_block_data_[block]; + if (block->getChip() == top_chip_) { + return top_block_data_; + } + // Common path: setTopChip() pre-populated this entry. Try a + // lock-free find under shared lock first; only escalate to a unique + // lock when we actually need to insert (rare — happens for blocks + // first touched by a db callback). + { + std::shared_lock lock(child_block_data_mutex_); + auto it = child_block_data_.find(block); + if (it != child_block_data_.end()) { + return it->second; + } + } + std::unique_lock lock(child_block_data_mutex_); + return child_block_data_[block]; } void Search::updateShapes(odb::dbBlock* block) diff --git a/src/web/src/search.h b/src/web/src/search.h index 4ba5830487f..1d7d3d04077 100644 --- a/src/web/src/search.h +++ b/src/web/src/search.h @@ -147,8 +147,14 @@ class Search : public odb::dbBlockCallBackObj // Pre-build all R-tree indices in parallel. void eagerInit(odb::dbBlock* block); - // Returns true once shape R-trees are built. - bool shapesReady() const { return top_block_data_.shapes_init.load(); } + // Returns true once shape R-trees are built for the active design. + // For 3DBX/multi-die designs the top dbChip is a HIER container with + // no block, so top_block_data_ never receives geometry — shapes live + // in chiplet master blocks under child_block_data_. We consider the + // search "ready" as soon as any indexed block has populated its + // shape R-tree, otherwise the frontend would stay frozen on + // "Loading shapes…". + bool shapesReady() const; // Build the structure for the given chip. void setTopChip(odb::dbChip* chip); @@ -349,6 +355,12 @@ class Search : public odb::dbBlockCallBackObj std::atomic_bool obstructions_init{false}; std::atomic_bool rows_init{false}; }; + // child_block_data_ is pre-populated in setTopChip(). After that, + // getData() may still insert entries for blocks reached only via db + // callbacks (rare but legal). child_block_data_mutex_ guards the + // map's structure (insert/clear), not the BlockData inside — those + // each have their own per-category mutexes. + mutable std::shared_mutex child_block_data_mutex_; std::map child_block_data_; BlockData top_block_data_; }; diff --git a/src/web/src/tile_generator.cpp b/src/web/src/tile_generator.cpp index 8ede4f27095..d324e894071 100644 --- a/src/web/src/tile_generator.cpp +++ b/src/web/src/tile_generator.cpp @@ -141,6 +141,15 @@ void TileVisibility::parseFromJson(const boost::json::object& json) } } + visible_chiplets.clear(); + has_visible_chiplets = false; + if (auto it = json.find("visible_chiplets"); it != json.end()) { + has_visible_chiplets = true; + for (const auto& v : it->value().as_array()) { + visible_chiplets.emplace(v.as_string()); + } + } + // Per-site flags are only consulted when rows are visible; skip the // full-object scan otherwise. sites.clear(); @@ -156,6 +165,14 @@ void TileVisibility::parseFromJson(const boost::json::object& json) } } +bool TileVisibility::isChipletVisible(const std::string& path) const +{ + if (!has_visible_chiplets) { + return true; + } + return visible_chiplets.contains(path); +} + bool TileVisibility::isSiteVisible(const std::string& site_name) const { if (!rows) { @@ -328,13 +345,28 @@ TileGenerator::~TileGenerator() = default; void TileGenerator::eagerInit() { + // Invalidate the chiplet cache: setTopChip below may swap to a fresh + // dbChip whose ChipletNode addresses (dbBlock*, dbChip*, etc.) differ + // from the previous design's. + { + std::lock_guard lock(chiplets_mutex_); + chiplets_cache_.clear(); + chiplets_cache_valid_ = false; + } + odb::dbChip* chip = db_->getChip(); if (chip) { search_->setTopChip(chip); } - odb::dbBlock* block = getBlock(); - if (block) { - search_->eagerInit(block); + // Index every block reachable via dbChipInst, not just the top block, + // so the recursive tile renderer can query searchInsts/searchBoxShapes + // for any chiplet's master block. + if (chip) { + for (const ChipletNode& node : chiplets()) { + if (node.block) { + search_->eagerInit(node.block); + } + } } computePinLabelMargin(); @@ -347,6 +379,16 @@ void TileGenerator::eagerInit() } } +const std::vector& TileGenerator::chiplets() const +{ + std::lock_guard lock(chiplets_mutex_); + if (!chiplets_cache_valid_) { + chiplets_cache_ = collectChiplets(db_->getChip()); + chiplets_cache_valid_ = true; + } + return chiplets_cache_; +} + void TileGenerator::computePinLabelMargin() { odb::dbBlock* block = getBlock(); @@ -465,15 +507,51 @@ void TileGenerator::fillPolygon(std::vector& image, odb::Rect TileGenerator::getBounds() const { + // Union of every reachable chiplet's bbox in world coordinates. + // For single-chip designs the loop reduces to the top block's bbox, + // matching the previous behavior. For multi-die / 3D-IC designs it + // expands the viewport to include all dbChipInst placements so + // zoom-to-fit shows the entire stack. + odb::dbChip* root = getChip(); + if (!root) { + return {}; + } odb::Rect bounds; - if (odb::dbBlock* block = getBlock()) { - bounds = block->getBBox()->getBox(); - if (pin_label_margin_dbu_ > 0) { - bounds.set_xlo(bounds.xMin() - pin_label_margin_dbu_); - bounds.set_ylo(bounds.yMin() - pin_label_margin_dbu_); - bounds.set_xhi(bounds.xMax() + pin_label_margin_dbu_); - bounds.set_yhi(bounds.yMax() + pin_label_margin_dbu_); + bounds.mergeInit(); + bool any = false; + const std::vector& nodes = chiplets(); + // In single-chip designs the previous behavior used only the top + // block's bbox; expanding to the full die-area changes `scale` for + // every tile and breaks tests that depend on the marker/label + // pixel-size threshold. Only multi-die designs (more than one + // chiplet) benefit from the die-area expansion needed by the + // chiplet outline overlay. + const bool include_die_area = nodes.size() > 1; + for (const ChipletNode& node : nodes) { + if (!node.block) { + continue; + } + odb::Rect b = node.block->getBBox()->getBox(); + node.world_xfm.apply(b); + bounds.merge(b); + if (include_die_area) { + const odb::Rect die = node.block->getDieArea(); + if (die.area() > 0) { + odb::Rect d = die; + node.world_xfm.apply(d); + bounds.merge(d); + } } + any = true; + } + if (!any) { + return {}; + } + if (pin_label_margin_dbu_ > 0) { + bounds.set_xlo(bounds.xMin() - pin_label_margin_dbu_); + bounds.set_ylo(bounds.yMin() - pin_label_margin_dbu_); + bounds.set_xhi(bounds.xMax() + pin_label_margin_dbu_); + bounds.set_yhi(bounds.yMax() + pin_label_margin_dbu_); } return bounds; } @@ -492,17 +570,40 @@ int TileGenerator::getPinMaxSize() const std::vector TileGenerator::getLayers() const { + // Collect the union of routing/cut layers from every tech reachable + // through dbChipInst, mirroring DisplayControls::setCurrentChip(). + // This is what makes layers exclusive to a chiplet's tech show up in + // the web display-controls panel. Names are deduplicated; insertion + // order follows the layer enumeration order of the first tech that + // contributes the layer. std::vector layers; std::set seen; - for (odb::dbTech* tech : db_->getTechs()) { - if (!tech) { - continue; + std::set visited_techs; + + auto collectFromTech = [&](odb::dbTech* tech) { + if (!tech || !visited_techs.insert(tech).second) { + return; } for (odb::dbTechLayer* layer : tech->getLayers()) { - if ((layer->getRoutingLevel() > 0 - || layer->getType() == odb::dbTechLayerType::CUT) - && seen.insert(layer->getName()).second) { - layers.push_back(layer->getName()); + if (layer->getRoutingLevel() > 0 + || layer->getType() == odb::dbTechLayerType::CUT) { + const std::string name = layer->getName(); + if (seen.insert(name).second) { + layers.push_back(name); + } + } + } + }; + + // Top-tech first so single-chip designs preserve the previous order. + collectFromTech(db_->getTech()); + for (const ChipletNode& node : chiplets()) { + if (node.chip) { + collectFromTech(node.chip->getTech()); + } + if (node.block) { + for (odb::dbBlock* child : node.block->getChildren()) { + collectFromTech(child->getTech()); } } } @@ -664,8 +765,8 @@ std::vector TileGenerator::selectAt( const std::set& visible_layers) { std::vector results; - odb::dbBlock* block = getBlock(); - if (!block) { + odb::dbChip* root = getChip(); + if (!root) { return results; } // Compute a search margin of 2 pixels at the current zoom level. @@ -684,113 +785,154 @@ std::vector TileGenerator::selectAt( zoom, margin); - const int x_lo = dbu_x - margin; - const int y_lo = dbu_y - margin; - const int x_hi = dbu_x + margin; - const int y_hi = dbu_y + margin; - const odb::Point click_pt(dbu_x, dbu_y); - - // Search instances - for (odb::dbInst* inst : - search_->searchInsts(block, x_lo, y_lo, x_hi, y_hi)) { - const odb::Rect bbox = inst->getBBox()->getBox(); - if (bbox.intersects(click_pt) && vis.isInstVisible(inst, sta_)) { - results.push_back({inst, inst->getName(), "Inst", bbox}); - } - } - // Sort instances by area descending so larger instances (macros) come first - std::ranges::sort(results, [](const auto& a, const auto& b) { - return a.bbox.area() > b.bbox.area(); - }); - - // Search nets via routing shapes on each layer. Iterate every loaded - // tech so multi-chiplet (3DBlox) designs with one dbTech per chiplet - // contribute their routing layers too. std::set seen_nets; - std::set visited_layers; - for (odb::dbTech* tech : db_->getTechs()) { - if (!tech) { - continue; - } - for (odb::dbTechLayer* layer : tech->getLayers()) { - if (!visited_layers.insert(layer).second) { + + // Iterate every chiplet so clicks inside a translated/rotated + // dbChipInst land on the right object. We map the world click into + // the chiplet's local frame using the inverse of world_xfm so that + // R90/R180/R270/mirrored chiplets work the same as R0 ones. + for (const ChipletNode& node : chiplets()) { + if (!vis.isChipletVisible(node.path)) { continue; } - if (layer->getRoutingLevel() <= 0 - && layer->getType() != odb::dbTechLayerType::CUT) { + odb::dbBlock* block = node.block; + if (!block || !node.chip) { continue; } - if (!visible_layers.empty() && !visible_layers.contains(layer->getName())) { + odb::dbTech* tech = node.chip->getTech(); + if (!tech) { continue; } - - // Regular routing shapes (wires, vias) and BTerm shapes - if (vis.routing || vis.pins) { - for (const auto& shape : - search_->searchBoxShapes(block, layer, x_lo, y_lo, x_hi, y_hi)) { - const auto type = std::get<1>(shape); - if (type == Search::kBterm && !vis.pins) { - continue; - } - if (type == Search::kWire && !(vis.routing && vis.routing_segments)) { - continue; - } - if (type == Search::kVia && !(vis.routing && vis.routing_vias)) { - continue; - } - odb::dbNet* net = std::get<2>(shape); - if (seen_nets.contains(net)) { - continue; - } - const odb::Rect& box = std::get<0>(shape); - if (box.intersects(click_pt) && vis.isNetVisible(net)) { - seen_nets.insert(net); - results.push_back({net, net->getName(), "Net", net->getTermBBox()}); + odb::dbTransform inv_xfm = node.world_xfm; + inv_xfm.invert(); + odb::Point click_pt(dbu_x, dbu_y); + inv_xfm.apply(click_pt); + const int local_x = click_pt.x(); + const int local_y = click_pt.y(); + const int x_lo = local_x - margin; + const int y_lo = local_y - margin; + const int x_hi = local_x + margin; + const int y_hi = local_y + margin; + + // Map a local-frame rect back to world coordinates for the result + // bbox. dbTransform::apply(Rect&) returns the axis-aligned + // bounding box of the rotated rect, which is what the frontend + // wants for zoom-to-bbox. + auto toWorld = [&](odb::Rect r) { + node.world_xfm.apply(r); + return r; + }; + + // Search instances in this chiplet's block. + for (odb::dbInst* inst : + search_->searchInsts(block, x_lo, y_lo, x_hi, y_hi)) { + const odb::Rect bbox = inst->getBBox()->getBox(); + if (bbox.intersects(click_pt) && vis.isInstVisible(inst, sta_)) { + std::string label = inst->getName(); + if (!node.path.empty() && node.inst != nullptr) { + std::string prefixed = node.path; + prefixed += '/'; + prefixed += label; + label = std::move(prefixed); } + results.push_back({inst, label, "Inst", toWorld(bbox), true}); } } - // Special net vias - if (vis.special_nets && vis.srouting_vias) { - for (const auto& shape : - search_->searchSNetViaShapes(block, layer, x_lo, y_lo, x_hi, y_hi)) { - odb::dbNet* net = std::get<1>(shape); - if (seen_nets.contains(net)) { - continue; - } - const odb::Rect box = std::get<0>(shape)->getBox(); - if (box.intersects(click_pt) && vis.isNetVisible(net)) { - seen_nets.insert(net); - results.push_back({net, net->getName(), "Net", net->getTermBBox()}); + // Search nets via routing shapes on each layer. + for (odb::dbTechLayer* layer : tech->getLayers()) { + if (layer->getRoutingLevel() <= 0 + && layer->getType() != odb::dbTechLayerType::CUT) { + continue; + } + if (!visible_layers.empty() + && !visible_layers.contains(layer->getName())) { + continue; + } + + // Regular routing shapes (wires, vias) and BTerm shapes + if (vis.routing || vis.pins) { + for (const auto& shape : + search_->searchBoxShapes(block, layer, x_lo, y_lo, x_hi, y_hi)) { + const auto type = std::get<1>(shape); + if (type == Search::kBterm && !vis.pins) { + continue; + } + if (type == Search::kWire && !(vis.routing && vis.routing_segments)) { + continue; + } + if (type == Search::kVia && !(vis.routing && vis.routing_vias)) { + continue; + } + odb::dbNet* net = std::get<2>(shape); + if (seen_nets.contains(net)) { + continue; + } + const odb::Rect& box = std::get<0>(shape); + if (box.intersects(click_pt) && vis.isNetVisible(net)) { + seen_nets.insert(net); + results.push_back( + {net, net->getName(), "Net", toWorld(net->getTermBBox()), + false}); + } } } - } - // Special net shapes (segments/straps) - if (vis.special_nets && vis.srouting_segments) { - for (const auto& shape : - search_->searchSNetShapes(block, layer, x_lo, y_lo, x_hi, y_hi)) { - odb::dbNet* net = std::get<2>(shape); - if (seen_nets.contains(net)) { - continue; + // Special net vias + if (vis.special_nets && vis.srouting_vias) { + for (const auto& shape : search_->searchSNetViaShapes( + block, layer, x_lo, y_lo, x_hi, y_hi)) { + odb::dbNet* net = std::get<1>(shape); + if (seen_nets.contains(net)) { + continue; + } + const odb::Rect box = std::get<0>(shape)->getBox(); + if (box.intersects(click_pt) && vis.isNetVisible(net)) { + seen_nets.insert(net); + results.push_back( + {net, net->getName(), "Net", toWorld(net->getTermBBox()), + false}); + } } - const odb::Rect box = std::get<0>(shape)->getBox(); - if (box.intersects(click_pt) && vis.isNetVisible(net)) { - seen_nets.insert(net); - results.push_back({net, net->getName(), "Net", net->getTermBBox()}); + } + + // Special net shapes (segments/straps) + if (vis.special_nets && vis.srouting_segments) { + for (const auto& shape : + search_->searchSNetShapes(block, layer, x_lo, y_lo, x_hi, y_hi)) { + odb::dbNet* net = std::get<2>(shape); + if (seen_nets.contains(net)) { + continue; + } + const odb::Rect box = std::get<0>(shape)->getBox(); + if (box.intersects(click_pt) && vis.isNetVisible(net)) { + seen_nets.insert(net); + results.push_back( + {net, net->getName(), "Net", toWorld(net->getTermBBox()), + false}); + } } } } - } } + // Sort instances by area descending so larger instances (macros) come + // first; nets keep their per-chiplet insertion order behind insts. + std::ranges::stable_sort(results, [](const auto& a, const auto& b) { + if (a.is_inst != b.is_inst) { + return a.is_inst; + } + return a.bbox.area() > b.bbox.area(); + }); + debugPrint(logger_, utl::WEB, "select", 1, " selected={} (insts={}, nets={})", results.size(), - results.size() - seen_nets.size(), + std::ranges::count_if(results, + [](const auto& r) { return r.is_inst; }), seen_nets.size()); return results; } @@ -801,6 +943,80 @@ odb::dbBlock* TileGenerator::getBlock() const return chip ? chip->getBlock() : nullptr; } +namespace { + +// Recursive helper for collectChiplets. Records the current chip, +// then recurses into each dbChipInst with the accumulated world +// transform. See dbInst::getHierTransform() for the canonical +// concat order: child_local.concat(parent_world) yields the +// local-to-root transform. +void collectChipletsRec(odb::dbChip* chip, + odb::dbChipInst* inst, + const odb::dbTransform& parent_world_xfm, + const std::string& parent_path, + const int depth, + const int parent_global_z, + std::vector& out) +{ + if (!chip) { + return; + } + ChipletNode node; + node.chip = chip; + node.block = chip->getBlock(); + node.inst = inst; + node.depth = depth; + node.parent_path = parent_path; + if (inst != nullptr) { + odb::dbTransform local = inst->getTransform(); + local.concat(parent_world_xfm); + node.world_xfm = local; + node.name = inst->getName(); + node.path = parent_path + "." + node.name; + node.global_z = parent_global_z + inst->getLoc().z(); + } else { + node.world_xfm = parent_world_xfm; + if (node.block) { + node.name = node.block->getName(); + } else { + node.name = "top"; + } + node.path = node.name; + node.global_z = parent_global_z; + } + out.push_back(node); + + for (odb::dbChipInst* child : chip->getChipInsts()) { + collectChipletsRec(child->getMasterChip(), + child, + node.world_xfm, + node.path, + depth + 1, + node.global_z, + out); + } +} + +} // namespace + +std::vector collectChiplets(odb::dbChip* root) +{ + std::vector out; + if (!root) { + return out; + } + collectChipletsRec(root, nullptr, odb::dbTransform{}, std::string{}, 0, 0, out); + + std::stable_sort(out.begin(), out.end(), [](const ChipletNode& a, const ChipletNode& b) { + if (a.global_z != b.global_z) { + return a.global_z < b.global_z; + } + return a.depth < b.depth; + }); + + return out; +} + odb::dbChip* TileGenerator::getChip() const { return db_->getChip(); @@ -879,703 +1095,791 @@ std::vector TileGenerator::renderTileBuffer( { static_assert(sizeof(Color) == 4); constexpr int kBufferSize = kTileSizeInPixel * kTileSizeInPixel * 4; - std::vector image_buffer(kBufferSize, 0); - - // No design loaded — return blank tile. - if (!getBlock()) { - std::vector png_data; - lodepng::encode(png_data, image_buffer, kTileSizeInPixel, kTileSizeInPixel); - return png_data; + // The world (final) tile buffer. Inside the per-chiplet loop, a + // local alias `image_buffer` may instead refer to local_image_buffer + // when the chiplet's orient ≠ R0 (slow-path); the alias is composited + // back here at the end of each chiplet iteration via reverse mapping. + std::vector world_image_buffer(kBufferSize, 0); + + // No design loaded at all — return a blank raw RGBA buffer. The + // caller (generateTile) will PNG-encode it before sending to the + // browser. IMPORTANT: this contract returns *raw pixels*, never PNG + // bytes; an earlier version PNG-encoded here, which then caused + // lodepng to error out ("image too small to contain all pixels") + // when generateTile tried to encode the small PNG buffer as + // 256×256 RGBA. We test for getChip(), not getBlock(), because + // 3DBX/multi-die designs create a HIER design top chiplet that + // itself has no block — the actual geometry lives in dbChipInst + // master chips, which the chiplet loop below traverses. + if (!getChip()) { + return world_image_buffer; } // Per-layer colors mirror gui::DisplayControls so the GUI and web frontend // agree on which color belongs to which layer. const auto& layer_colors = getLayerColorMap(); - odb::dbTech* tech = db_->getTech(); - odb::dbTechLayer* tech_layer = tech->findLayer(layer.c_str()); - - Color color{.r = 200, .g = 200, .b = 200, .a = 180}; - if (tech_layer) { - const auto it = layer_colors.find(tech_layer); - if (it != layer_colors.end()) { - color = it->second; - } - } - const Color obs_color = color.lighter(); - // Determine our tile's bounding box in dbu coordinates. const double num_tiles_at_zoom = pow(2, z); if (x >= 0 && y >= 0 && x < num_tiles_at_zoom && y < num_tiles_at_zoom) { y = num_tiles_at_zoom - 1 - y; // flip const odb::Rect full_bounds = getBounds(); + // Guard against an empty/invalid design footprint. Without this, + // tile_dbu_size becomes 0 and `scale` blows up to infinity, which + // either produces garbage pixels or silently no-ops the render. + if (full_bounds.maxDXDY() <= 0) { + return world_image_buffer; + } const double tile_dbu_size = full_bounds.maxDXDY() / num_tiles_at_zoom; - const int dbu_x_min = full_bounds.xMin() + x * tile_dbu_size; - const int dbu_y_min = full_bounds.yMin() + y * tile_dbu_size; - const int dbu_x_max + const int dbu_x_min_world = full_bounds.xMin() + x * tile_dbu_size; + const int dbu_y_min_world = full_bounds.yMin() + y * tile_dbu_size; + const int dbu_x_max_world = full_bounds.xMin() + std::ceil((x + 1) * tile_dbu_size); - const int dbu_y_max + const int dbu_y_max_world = full_bounds.yMin() + std::ceil((y + 1) * tile_dbu_size); - const odb::Rect dbu_tile(dbu_x_min, dbu_y_min, dbu_x_max, dbu_y_max); + const odb::Rect dbu_tile_world( + dbu_x_min_world, dbu_y_min_world, dbu_x_max_world, dbu_y_max_world); const double scale = kTileSizeInPixel / tile_dbu_size; - odb::dbBlock* block = getBlock(); - - // Special "_modules" layer: draw filled module-colored rectangles - const bool modules_layer - = (layer == "_modules" && module_colors && !module_colors->empty()); - if (modules_layer) { - for (odb::dbInst* inst : search_->searchInsts( - block, dbu_x_min, dbu_y_min, dbu_x_max, dbu_y_max)) { - odb::Rect inst_bbox = inst->getBBox()->getBox(); - if (!dbu_tile.overlaps(inst_bbox)) { - continue; - } - if (inst->getMaster()->isFiller()) { - continue; - } - odb::dbModule* mod = inst->getModule(); - if (!mod) { - continue; - } - auto it = module_colors->find(mod->getId()); - if (it == module_colors->end()) { - continue; - } - const Color& c = it->second; - const int pxl - = std::max(0, (int) ((inst_bbox.xMin() - dbu_x_min) * scale)); - const int pyl - = std::max(0, (int) ((inst_bbox.yMin() - dbu_y_min) * scale)); - const int pxh = std::min( - 255, (int) std::ceil((inst_bbox.xMax() - dbu_x_min) * scale)); - const int pyh = std::min( - 255, (int) std::ceil((inst_bbox.yMax() - dbu_y_min) * scale)); - for (int iy = pyl; iy < pyh; ++iy) { - for (int ix = pxl; ix < pxh; ++ix) { - blendPixel(image_buffer, ix, 255 - iy, c); + // Per-chiplet rendering loop. Mirrors RenderThread::drawChips() in + // the Qt GUI: walks dbChip → dbChipInst → masterChip and draws each + // chiplet's block in its own frame, accumulating into the same tile + // image. The world tile rect is shifted into each chiplet's local + // frame for translation-only transforms; non-R0 orientations log + // and skip for v1 (followup work to support full transforms). + const std::vector& chiplet_nodes = chiplets(); + // The die-outline overlay only makes sense in multi-die designs; in + // single-chip layouts there is no chiplet to demarcate, and adding + // it caused regressions because every "expect transparent" test + // started seeing a gray frame. + const bool draw_die_outline = chiplet_nodes.size() > 1; + for (const ChipletNode& node : chiplet_nodes) { + if (!vis.isChipletVisible(node.path)) { + continue; + } + odb::dbBlock* block = node.block; + if (!block || !node.chip) { + continue; + } + odb::dbTech* tech = node.chip->getTech(); + if (!tech) { + continue; + } + // Translation-only fast path: the local tile is the world tile + // shifted by -offset, and pixel coordinates land in the same place + // because both shape coords and tile origin are in the same local + // frame. Non-R0 orientations need full per-shape transforms; for + // now we render them as if R0 (visible, but slightly misplaced). + std::vector local_image_buffer; + bool use_local = (node.world_xfm.getOrient() != odb::dbOrientType::R0); + if (use_local) { + local_image_buffer.resize(kBufferSize, 0); + } + // Alias the buffer the chiplet loop writes into. In the R0 + // fast-path it's the world buffer (so writes land directly). In + // the slow-path it's a per-chiplet local buffer that the + // reverse-mapping block at the end of this iteration composites + // back onto world_image_buffer. + auto& image_buffer + = use_local ? local_image_buffer : world_image_buffer; + + odb::Rect dbu_tile = dbu_tile_world; + if (use_local) { + odb::dbTransform inv_xfm = node.world_xfm; + inv_xfm.invert(); + inv_xfm.apply(dbu_tile); + } else { + const odb::Point xfm_off = node.world_xfm.getOffset(); + dbu_tile = odb::Rect(dbu_x_min_world - xfm_off.x(), + dbu_y_min_world - xfm_off.y(), + dbu_x_max_world - xfm_off.x(), + dbu_y_max_world - xfm_off.y()); + } + const int dbu_x_min = dbu_tile.xMin(); + const int dbu_y_min = dbu_tile.yMin(); + const int dbu_x_max = dbu_tile.xMax(); + const int dbu_y_max = dbu_tile.yMax(); + + // Mirrors RenderThread::drawChip() in the Qt GUI: outline the die + // boundary so the chiplet shape is visible regardless of which + // tech layer is active. Drawn once per layer-pass on every tile, + // but only in multi-die designs where the demarcation is useful. + if (draw_die_outline) { + const odb::Rect die = block->getDieArea(); + if (die.area() > 0) { + const int xl = die.xMin(); + const int yl = die.yMin(); + const int xh = die.xMax(); + const int yh = die.yMax(); + const int64_t pixel_xl = (int64_t) ((xl - dbu_x_min) * scale); + const int64_t pixel_yl = (int64_t) ((yl - dbu_y_min) * scale); + const int64_t pixel_xh = (int64_t) std::ceil((xh - dbu_x_min) * scale); + const int64_t pixel_yh = (int64_t) std::ceil((yh - dbu_y_min) * scale); + + const int loop_xl = std::clamp(pixel_xl, 0, 256); + const int loop_yl = std::clamp(pixel_yl, 0, 256); + const int loop_xh = std::clamp(pixel_xh, 0, 256); + const int loop_yh = std::clamp(pixel_yh, 0, 256); + + const int draw_xl = std::clamp(pixel_xl, 0, 255); + const int draw_yl = std::clamp(pixel_yl, 0, 255); + const int draw_xh = std::clamp(pixel_xh, 0, 255); + const int draw_yh = std::clamp(pixel_yh, 0, 255); + + constexpr Color die_outline{ + .r = 128, .g = 128, .b = 128, .a = 255}; + if (dbu_x_min <= xl && xl <= dbu_x_max) { + for (int iy = loop_yl; iy < loop_yh; ++iy) { + setPixel(image_buffer, draw_xl, 255 - iy, die_outline); + } + } + if (dbu_x_min <= xh && xh <= dbu_x_max) { + for (int iy = loop_yl; iy < loop_yh; ++iy) { + setPixel(image_buffer, draw_xh, 255 - iy, die_outline); + } + } + if (dbu_y_min <= yl && yl <= dbu_y_max) { + for (int ix = loop_xl; ix < loop_xh; ++ix) { + setPixel(image_buffer, ix, 255 - draw_yl, die_outline); + } + } + if (dbu_y_min <= yh && yh <= dbu_y_max) { + for (int ix = loop_xl; ix < loop_xh; ++ix) { + setPixel(image_buffer, ix, 255 - draw_yh, die_outline); + } } } } - } - // Special "_pins" layer: draw IO pin direction markers - const bool pins_layer = (layer == "_pins"); - if (pins_layer && vis.pins) { - const odb::Rect die_area = block->getDieArea(); - // Match GUI: scale markers to min(die, viewport) so they shrink - // when zoomed in (GUI renderThread.cpp:1598-1602). - const int die_max_dim = std::max(die_area.dx(), die_area.dy()); - const int tile_extent = static_cast(tile_dbu_size); - const int effective_dim = std::min(die_max_dim, tile_extent); - const int pin_max_size - = std::max(static_cast(kPinMarkerSizeRatio * effective_dim), - kMinPinMarkerSize); - const int qw = pin_max_size / 4; // quarter-width of marker - - // Show pin names when the full (die-relative) marker is large enough - // in pixels. pin_max_size shrinks with zoom, but the die-relative - // size grows as scale increases, so names appear when zoomed in. - const int die_pin_size - = std::max(static_cast(kPinMarkerSizeRatio * die_max_dim), - kMinPinMarkerSize); - const bool draw_pin_names - = (static_cast(die_pin_size * scale) >= kMinPinNameSizePixels); - const auto pin_label_font = fontAtlasGetFont(kPinLabelFontHeight); - - // Marker templates (same as GUI renderThread.cpp). - // Defined for "top edge" orientation; rotated per actual edge. - using Pts = std::vector; - const Pts in_marker{// arrow pointing into block - {qw, pin_max_size}, - {0, 0}, - {-qw, pin_max_size}, - {qw, pin_max_size}}; - const Pts out_marker{// arrow pointing out of block - {0, pin_max_size}, - {-qw, 0}, - {qw, 0}, - {0, pin_max_size}}; - const Pts bi_marker{// diamond - {0, 0}, - {-qw, pin_max_size / 2}, - {0, pin_max_size}, - {qw, pin_max_size / 2}, - {0, 0}}; - - // Iterate per-box like the GUI (each dbBox gets its own marker). - for (odb::dbBTerm* term : block->getBTerms()) { - // Respect net-type visibility (Power, Ground, etc.). - if (!vis.isNetVisible(term->getNet())) { - continue; + odb::dbTechLayer* tech_layer = tech->findLayer(layer.c_str()); + Color color{.r = 200, .g = 200, .b = 200, .a = 180}; + if (tech_layer) { + const auto it = layer_colors.find(tech_layer); + if (it != layer_colors.end()) { + color = it->second; } - for (odb::dbBPin* pin : term->getBPins()) { - const odb::dbPlacementStatus status = pin->getPlacementStatus(); - if (status == odb::dbPlacementStatus::NONE - || status == odb::dbPlacementStatus::UNPLACED) { + } + const Color obs_color = color.lighter(); + + // Special "_modules" layer: draw filled module-colored rectangles + const bool modules_layer + = (layer == "_modules" && module_colors && !module_colors->empty()); + if (modules_layer) { + for (odb::dbInst* inst : search_->searchInsts( + block, dbu_x_min, dbu_y_min, dbu_x_max, dbu_y_max)) { + odb::Rect inst_bbox = inst->getBBox()->getBox(); + if (!dbu_tile.overlaps(inst_bbox)) { + continue; + } + if (inst->getMaster()->isFiller()) { continue; } + odb::dbModule* mod = inst->getModule(); + if (!mod) { + continue; + } + auto it = module_colors->find(mod->getId()); + if (it == module_colors->end()) { + continue; + } + const Color& c = it->second; + const int pxl + = std::max(0, (int) ((inst_bbox.xMin() - dbu_x_min) * scale)); + const int pyl + = std::max(0, (int) ((inst_bbox.yMin() - dbu_y_min) * scale)); + const int pxh = std::min( + 255, (int) std::ceil((inst_bbox.xMax() - dbu_x_min) * scale)); + const int pyh = std::min( + 255, (int) std::ceil((inst_bbox.yMax() - dbu_y_min) * scale)); + for (int iy = pyl; iy < pyh; ++iy) { + for (int ix = pxl; ix < pxh; ++ix) { + blendPixel(image_buffer, ix, 255 - iy, c); + } + } + } + } - for (odb::dbBox* box : pin->getBoxes()) { - if (!box) { + // Special "_pins" layer: draw IO pin direction markers + const bool pins_layer = (layer == "_pins"); + if (pins_layer && vis.pins) { + const odb::Rect die_area = block->getDieArea(); + // Match GUI: scale markers to min(die, viewport) so they shrink + // when zoomed in (GUI renderThread.cpp:1598-1602). + const int die_max_dim = std::max(die_area.dx(), die_area.dy()); + const int tile_extent = static_cast(tile_dbu_size); + const int effective_dim = std::min(die_max_dim, tile_extent); + const int pin_max_size + = std::max(static_cast(kPinMarkerSizeRatio * effective_dim), + kMinPinMarkerSize); + const int qw = pin_max_size / 4; // quarter-width of marker + + // Show pin names when the full (die-relative) marker is large enough + // in pixels. pin_max_size shrinks with zoom, but the die-relative + // size grows as scale increases, so names appear when zoomed in. + const int die_pin_size + = std::max(static_cast(kPinMarkerSizeRatio * die_max_dim), + kMinPinMarkerSize); + const bool draw_pin_names + = (static_cast(die_pin_size * scale) >= kMinPinNameSizePixels); + const auto pin_label_font = fontAtlasGetFont(kPinLabelFontHeight); + + // Marker templates (same as GUI renderThread.cpp). + // Defined for "top edge" orientation; rotated per actual edge. + using Pts = std::vector; + const Pts in_marker{// arrow pointing into block + {qw, pin_max_size}, + {0, 0}, + {-qw, pin_max_size}, + {qw, pin_max_size}}; + const Pts out_marker{// arrow pointing out of block + {0, pin_max_size}, + {-qw, 0}, + {qw, 0}, + {0, pin_max_size}}; + const Pts bi_marker{// diamond + {0, 0}, + {-qw, pin_max_size / 2}, + {0, pin_max_size}, + {qw, pin_max_size / 2}, + {0, 0}}; + + // Iterate per-box like the GUI (each dbBox gets its own marker). + for (odb::dbBTerm* term : block->getBTerms()) { + // Respect net-type visibility (Power, Ground, etc.). + if (!vis.isNetVisible(term->getNet())) { + continue; + } + for (odb::dbBPin* pin : term->getBPins()) { + const odb::dbPlacementStatus status = pin->getPlacementStatus(); + if (status == odb::dbPlacementStatus::NONE + || status == odb::dbPlacementStatus::UNPLACED) { continue; } - // Skip pins on hidden tech layers. - if (vis.has_visible_layers) { - odb::dbTechLayer* box_layer = box->getTechLayer(); - if (box_layer - && !vis.visible_layers.contains(box_layer->getName())) { + for (odb::dbBox* box : pin->getBoxes()) { + if (!box) { continue; } - } - - const odb::Rect box_rect = box->getBox(); - // Layer color for this box. - Color marker_color{.r = 200, .g = 200, .b = 200, .a = 220}; - odb::dbTechLayer* pin_layer = box->getTechLayer(); - if (pin_layer) { - const auto it = layer_colors.find(pin_layer); - if (it != layer_colors.end()) { - marker_color = it->second; - marker_color.a = 220; + // Skip pins on hidden tech layers. + if (vis.has_visible_layers) { + odb::dbTechLayer* box_layer = box->getTechLayer(); + if (box_layer + && !vis.visible_layers.contains(box_layer->getName())) { + continue; + } } - } - // Center and edge distances from this specific box. - const odb::Point pin_center = box_rect.center(); - - const int dist_to_left - = std::abs(box_rect.xMin() - die_area.xMin()); - const int dist_to_right - = std::abs(box_rect.xMax() - die_area.xMax()); - const int dist_to_top = std::abs(box_rect.yMax() - die_area.yMax()); - const int dist_to_bot = std::abs(box_rect.yMin() - die_area.yMin()); - const std::array dists{ - dist_to_left, dist_to_right, dist_to_top, dist_to_bot}; - const int arg_min = static_cast( - std::distance(dists.begin(), std::ranges::min_element(dists))); - - odb::dbTransform xfm(pin_center); - if (arg_min == 0) { // left - xfm.setOrient(odb::dbOrientType::R90); - if (dist_to_left == 0) { - xfm.setOffset({die_area.xMin(), pin_center.y()}); - } - } else if (arg_min == 1) { // right - xfm.setOrient(odb::dbOrientType::R270); - if (dist_to_right == 0) { - xfm.setOffset({die_area.xMax(), pin_center.y()}); - } - } else if (arg_min == 2) { // top - // No rotation needed. - if (dist_to_top == 0) { - xfm.setOffset({pin_center.x(), die_area.yMax()}); - } - } else { // bottom - xfm.setOrient(odb::dbOrientType::MX); - if (dist_to_bot == 0) { - xfm.setOffset({pin_center.x(), die_area.yMin()}); + const odb::Rect box_rect = box->getBox(); + + // Layer color for this box. + Color marker_color{.r = 200, .g = 200, .b = 200, .a = 220}; + odb::dbTechLayer* pin_layer = box->getTechLayer(); + if (pin_layer) { + const auto it = layer_colors.find(pin_layer); + if (it != layer_colors.end()) { + marker_color = it->second; + marker_color.a = 220; + } } - } - // Select template based on IO direction. - const Pts* tmpl = &bi_marker; - const auto pin_dir = term->getIoType(); - if (pin_dir == odb::dbIoType::INPUT) { - tmpl = &in_marker; - } else if (pin_dir == odb::dbIoType::OUTPUT) { - tmpl = &out_marker; - } + // Center and edge distances from this specific box. + const odb::Point pin_center = box_rect.center(); + + const int dist_to_left + = std::abs(box_rect.xMin() - die_area.xMin()); + const int dist_to_right + = std::abs(box_rect.xMax() - die_area.xMax()); + const int dist_to_top + = std::abs(box_rect.yMax() - die_area.yMax()); + const int dist_to_bot + = std::abs(box_rect.yMin() - die_area.yMin()); + const std::array dists{ + dist_to_left, dist_to_right, dist_to_top, dist_to_bot}; + const int arg_min = static_cast(std::distance( + dists.begin(), std::ranges::min_element(dists))); + + odb::dbTransform xfm(pin_center); + if (arg_min == 0) { // left + xfm.setOrient(odb::dbOrientType::R90); + if (dist_to_left == 0) { + xfm.setOffset({die_area.xMin(), pin_center.y()}); + } + } else if (arg_min == 1) { // right + xfm.setOrient(odb::dbOrientType::R270); + if (dist_to_right == 0) { + xfm.setOffset({die_area.xMax(), pin_center.y()}); + } + } else if (arg_min == 2) { // top + // No rotation needed. + if (dist_to_top == 0) { + xfm.setOffset({pin_center.x(), die_area.yMax()}); + } + } else { // bottom + xfm.setOrient(odb::dbOrientType::MX); + if (dist_to_bot == 0) { + xfm.setOffset({pin_center.x(), die_area.yMin()}); + } + } - // Transform template to final marker polygon. - std::vector marker_pts; - marker_pts.reserve(tmpl->size()); - for (const auto& pt : *tmpl) { - odb::Point new_pt = pt; - xfm.apply(new_pt); - marker_pts.push_back(new_pt); - } - const odb::Polygon marker_poly(marker_pts); + // Select template based on IO direction. + const Pts* tmpl = &bi_marker; + const auto pin_dir = term->getIoType(); + if (pin_dir == odb::dbIoType::INPUT) { + tmpl = &in_marker; + } else if (pin_dir == odb::dbIoType::OUTPUT) { + tmpl = &out_marker; + } - // Only draw if marker intersects this tile. - const odb::Rect marker_bbox = marker_poly.getEnclosingRect(); - if (marker_bbox.overlaps(dbu_tile)) { - fillPolygon( - image_buffer, marker_poly, dbu_tile, scale, marker_color); - } + // Transform template to final marker polygon. + std::vector marker_pts; + marker_pts.reserve(tmpl->size()); + for (const auto& pt : *tmpl) { + odb::Point new_pt = pt; + xfm.apply(new_pt); + marker_pts.push_back(new_pt); + } + const odb::Polygon marker_poly(marker_pts); - // Draw the box rect itself (same as GUI painter.drawRect). - if (box_rect.overlaps(dbu_tile)) { - const odb::Rect overlap = box_rect.intersect(dbu_tile); - const odb::Rect draw = toPixels(scale, overlap, dbu_tile); - drawFilledRect(image_buffer, draw, marker_color); - } + // Only draw if marker intersects this tile. + const odb::Rect marker_bbox = marker_poly.getEnclosingRect(); + if (marker_bbox.overlaps(dbu_tile)) { + fillPolygon( + image_buffer, marker_poly, dbu_tile, scale, marker_color); + } - // Draw pin name label when zoomed in enough. - if (draw_pin_names && vis.pin_names) { - const std::string name = term->getName(); - const odb::Point anchor_pt = xfm.getOffset(); - const int text_w = getTextWidth(name, pin_label_font); - const int text_h = getTextHeight(pin_label_font); - const int text_margin_px = 3; - const bool rotated = (arg_min == 2 || arg_min == 3); - - // For rotated text, width/height swap. - const int block_w = rotated ? text_h : text_w; - const int block_h = rotated ? text_w : text_h; - - // Convert anchor to pixel coords. - const int anchor_px - = static_cast((anchor_pt.x() - dbu_tile.xMin()) * scale); - const int anchor_py_raw - = static_cast((anchor_pt.y() - dbu_tile.yMin()) * scale); - const int anchor_py = 255 - anchor_py_raw; - - // Position text outward (away from die center), matching the GUI. - const int marker_px = static_cast(pin_max_size * scale); - int px; - int py; - if (arg_min == 0) { // left — text to the left (outward) - px = anchor_px - marker_px - text_margin_px - text_w; - py = anchor_py - text_h / 2; - } else if (arg_min == 1) { // right — text to the right (outward) - px = anchor_px + marker_px + text_margin_px; - py = anchor_py - text_h / 2; - } else if (arg_min - == 2) { // top — rotated, above marker (outward) - px = anchor_px - block_w / 2; - py = anchor_py - marker_px - text_margin_px - block_h; - } else { // bottom — rotated, below marker (outward) - px = anchor_px - block_w / 2; - py = anchor_py + marker_px + text_margin_px; + // Draw the box rect itself (same as GUI painter.drawRect). + if (box_rect.overlaps(dbu_tile)) { + const odb::Rect overlap = box_rect.intersect(dbu_tile); + const odb::Rect draw = toPixels(scale, overlap, dbu_tile); + drawFilledRect(image_buffer, draw, marker_color); } - if (px > -block_w && px < kTileSizeInPixel && py > -block_h - && py < kTileSizeInPixel) { - const Color text_color{.r = marker_color.r, - .g = marker_color.g, - .b = marker_color.b, - .a = 255}; - if (rotated) { - drawTextRotated( - image_buffer, px, py, name, pin_label_font, text_color); - } else { - drawText( - image_buffer, px, py, name, pin_label_font, text_color); + // Draw pin name label when zoomed in enough. + if (draw_pin_names && vis.pin_names) { + const std::string name = term->getName(); + const odb::Point anchor_pt = xfm.getOffset(); + const int text_w = getTextWidth(name, pin_label_font); + const int text_h = getTextHeight(pin_label_font); + const int text_margin_px = 3; + const bool rotated = (arg_min == 2 || arg_min == 3); + + // For rotated text, width/height swap. + const int block_w = rotated ? text_h : text_w; + const int block_h = rotated ? text_w : text_h; + + // Convert anchor to pixel coords. + const int anchor_px = static_cast( + (anchor_pt.x() - dbu_tile.xMin()) * scale); + const int anchor_py_raw = static_cast( + (anchor_pt.y() - dbu_tile.yMin()) * scale); + const int anchor_py = 255 - anchor_py_raw; + + // Position text outward (away from die center), matching the + // GUI. + const int marker_px = static_cast(pin_max_size * scale); + int px; + int py; + if (arg_min == 0) { // left — text to the left (outward) + px = anchor_px - marker_px - text_margin_px - text_w; + py = anchor_py - text_h / 2; + } else if (arg_min + == 1) { // right — text to the right (outward) + px = anchor_px + marker_px + text_margin_px; + py = anchor_py - text_h / 2; + } else if (arg_min + == 2) { // top — rotated, above marker (outward) + px = anchor_px - block_w / 2; + py = anchor_py - marker_px - text_margin_px - block_h; + } else { // bottom — rotated, below marker (outward) + px = anchor_px - block_w / 2; + py = anchor_py + marker_px + text_margin_px; + } + + if (px > -block_w && px < kTileSizeInPixel && py > -block_h + && py < kTileSizeInPixel) { + const Color text_color{.r = marker_color.r, + .g = marker_color.g, + .b = marker_color.b, + .a = 255}; + if (rotated) { + drawTextRotated( + image_buffer, px, py, name, pin_label_font, text_color); + } else { + drawText( + image_buffer, px, py, name, pin_label_font, text_color); + } } } } } } } - } - // Special "_instances" layer: only draw instance borders, no routing - const bool instances_only = (layer == "_instances"); + // Special "_instances" layer: only draw instance borders, no routing + const bool instances_only = (layer == "_instances"); - // "_modules" and "_pins" layers handle their own drawing above; - // skip all other drawing (instances, routing, etc.) - if (!modules_layer && !pins_layer) { - const auto iterm_font = fontAtlasGetFont(kItermLabelFontHeight); - const int iterm_font_h = getTextHeight(iterm_font); + // "_modules" and "_pins" layers handle their own drawing above; + // skip all other drawing (instances, routing, etc.) + if (!modules_layer && !pins_layer) { + const auto iterm_font = fontAtlasGetFont(kItermLabelFontHeight); + const int iterm_font_h = getTextHeight(iterm_font); - // Draw instances - for (odb::dbInst* inst : search_->searchInsts( - block, dbu_x_min, dbu_y_min, dbu_x_max, dbu_y_max)) { - odb::Rect inst_bbox = inst->getBBox()->getBox(); - if (!dbu_tile.overlaps(inst_bbox)) { - continue; - } - odb::dbMaster* master = inst->getMaster(); + // Draw instances + for (odb::dbInst* inst : search_->searchInsts( + block, dbu_x_min, dbu_y_min, dbu_x_max, dbu_y_max)) { + odb::Rect inst_bbox = inst->getBBox()->getBox(); + if (!dbu_tile.overlaps(inst_bbox)) { + continue; + } + odb::dbMaster* master = inst->getMaster(); - if (!vis.isInstVisible(inst, sta_)) { - continue; - } - const int xl = inst_bbox.xMin(); - const int yl = inst_bbox.yMin(); - const int xh = inst_bbox.xMax(); - const int yh = inst_bbox.yMax(); - - const int pixel_xl = (int) ((xl - dbu_x_min) * scale); - const int pixel_yl = (int) ((yl - dbu_y_min) * scale); - const int pixel_xh = (int) std::ceil((xh - dbu_x_min) * scale); - const int pixel_yh = (int) std::ceil((yh - dbu_y_min) * scale); - - if (instances_only) { - // Draw the rectangle border (instances-only layer) - const Color gray{.r = 128, .g = 128, .b = 128, .a = 255}; - if (dbu_x_min <= xl && xl <= dbu_x_max) { - for (int iy = pixel_yl; iy < pixel_yh; ++iy) { - const int draw_y = (255 - iy); - setPixel(image_buffer, pixel_xl, draw_y, gray); - } + if (!vis.isInstVisible(inst, sta_)) { + continue; } - if (dbu_x_min <= xh && xh <= dbu_x_max) { - for (int iy = pixel_yl; iy < pixel_yh; ++iy) { - const int draw_y = (255 - iy); - setPixel(image_buffer, pixel_xh, draw_y, gray); + const int xl = inst_bbox.xMin(); + const int yl = inst_bbox.yMin(); + const int xh = inst_bbox.xMax(); + const int yh = inst_bbox.yMax(); + + const int64_t pixel_xl = (int64_t) ((xl - dbu_x_min) * scale); + const int64_t pixel_yl = (int64_t) ((yl - dbu_y_min) * scale); + const int64_t pixel_xh = (int64_t) std::ceil((xh - dbu_x_min) * scale); + const int64_t pixel_yh = (int64_t) std::ceil((yh - dbu_y_min) * scale); + + const int loop_xl = std::clamp(pixel_xl, 0, 256); + const int loop_yl = std::clamp(pixel_yl, 0, 256); + const int loop_xh = std::clamp(pixel_xh, 0, 256); + const int loop_yh = std::clamp(pixel_yh, 0, 256); + + const int draw_xl = std::clamp(pixel_xl, 0, 255); + const int draw_yl = std::clamp(pixel_yl, 0, 255); + const int draw_xh = std::clamp(pixel_xh, 0, 255); + const int draw_yh = std::clamp(pixel_yh, 0, 255); + + if (instances_only) { + // Draw the rectangle border (instances-only layer) + const Color gray{.r = 128, .g = 128, .b = 128, .a = 255}; + if (dbu_x_min <= xl && xl <= dbu_x_max) { + for (int iy = loop_yl; iy < loop_yh; ++iy) { + const int draw_y = (255 - iy); + setPixel(image_buffer, draw_xl, draw_y, gray); + } } - } - if (dbu_y_min <= yl && yl <= dbu_y_max) { - for (int ix = pixel_xl; ix < pixel_xh; ++ix) { - const int draw_y = (255 - pixel_yl); - setPixel(image_buffer, ix, draw_y, gray); + if (dbu_x_min <= xh && xh <= dbu_x_max) { + for (int iy = loop_yl; iy < loop_yh; ++iy) { + const int draw_y = (255 - iy); + setPixel(image_buffer, draw_xh, draw_y, gray); + } } - } - if (dbu_y_min <= yh && yh <= dbu_y_max) { - for (int ix = pixel_xl; ix < pixel_xh; ++ix) { - const int draw_y = (255 - pixel_yh); - setPixel(image_buffer, ix, draw_y, gray); + if (dbu_y_min <= yl && yl <= dbu_y_max) { + for (int ix = loop_xl; ix < loop_xh; ++ix) { + const int draw_y = (255 - draw_yl); + setPixel(image_buffer, ix, draw_y, gray); + } + } + if (dbu_y_min <= yh && yh <= dbu_y_max) { + for (int ix = loop_xl; ix < loop_xh; ++ix) { + const int draw_y = (255 - draw_yh); + setPixel(image_buffer, ix, draw_y, gray); + } } - } - // Draw instance name label when zoomed in enough. - // Font scales to ~40% of the smaller box dimension, clamped - // to [kMinInstNameFontPx, kMaxInstNameFontPx]. Text is - // elided from the left ("...suffix") to fit 90% of the - // available dimension, matching the Qt GUI's behavior. - if (vis.inst_names) { - const int box_px_w = pixel_xh - pixel_xl; - const int box_px_h = pixel_yh - pixel_yl; - const int box_px_min = std::min(box_px_w, box_px_h); - if (std::max(box_px_w, box_px_h) >= kMinInstNameBoxPx) { - const int font_px = std::clamp(static_cast(box_px_min * 0.4), - kMinInstNameFontPx, - kMaxInstNameFontPx); - const auto inst_font = fontAtlasGetFont(font_px); - const int font_h = getTextHeight(inst_font); - - // Skip if font would dominate the cell (> 50% of cross - // dimension), matching GUI's kNonCoreScaleLimit = 2.0. - if (2 * font_h <= box_px_min) { - constexpr Color name_color{ - .r = 255, .g = 255, .b = 0, .a = 220}; - const std::string full_name = inst->getName(); - const int full_w = getTextWidth(full_name, inst_font); - - // Rotate if taller than wide and text overflows (85%). - const bool rotate - = (box_px_h > box_px_w) && (full_w > box_px_w * 85 / 100); - - // Available width for text (90% of relevant dim). - const int avail - = rotate ? (box_px_h * 9 / 10) : (box_px_w * 9 / 10); - - // Elide from the left if text is too wide. Maintain a - // running prefix width so each candidate "..." + - // name.substr(skip) is evaluated in O(1) using - // textWidth(name.substr(skip)) - // = full_w - prefix_w - kern(name[skip-1], name[skip]) - // giving O(N) total instead of O(N^2). - std::string name = full_name; - int text_w = full_w; - if (text_w > avail && name.size() > 4) { - const int dots_w = getTextWidth("...", inst_font); - const size_t n = name.size(); - int prefix_w = 0; - for (size_t skip = 1; skip < n - 1; ++skip) { - prefix_w += inst_font.glyph(name[skip - 1]).advance; - if (skip >= 2) { - prefix_w - += inst_font.kern(name[skip - 2], name[skip - 1]); - } - const int suffix_w - = full_w - prefix_w - - inst_font.kern(name[skip - 1], name[skip]); - const int w - = dots_w + inst_font.kern('.', name[skip]) + suffix_w; - if (w <= avail) { - name = "..." + name.substr(skip); - text_w = w; - break; + // Draw instance name label when zoomed in enough. + // Font scales to ~40% of the smaller box dimension, clamped + // to [kMinInstNameFontPx, kMaxInstNameFontPx]. Text is + // elided from the left ("...suffix") to fit 90% of the + // available dimension, matching the Qt GUI's behavior. + if (vis.inst_names) { + const int box_px_w = (int)(pixel_xh - pixel_xl); + const int box_px_h = (int)(pixel_yh - pixel_yl); + const int box_px_min = std::min(box_px_w, box_px_h); + if (std::max(box_px_w, box_px_h) >= kMinInstNameBoxPx) { + const int font_px + = std::clamp(static_cast(box_px_min * 0.4), + kMinInstNameFontPx, + kMaxInstNameFontPx); + const auto inst_font = fontAtlasGetFont(font_px); + const int font_h = getTextHeight(inst_font); + + // Skip if font would dominate the cell (> 50% of cross + // dimension), matching GUI's kNonCoreScaleLimit = 2.0. + if (2 * font_h <= box_px_min) { + constexpr Color name_color{ + .r = 255, .g = 255, .b = 0, .a = 220}; + const std::string full_name = inst->getName(); + const int full_w = getTextWidth(full_name, inst_font); + + // Rotate if taller than wide and text overflows (85%). + const bool rotate + = (box_px_h > box_px_w) && (full_w > box_px_w * 85 / 100); + + // Available width for text (90% of relevant dim). + const int avail + = rotate ? (box_px_h * 9 / 10) : (box_px_w * 9 / 10); + + // Elide from the left if text is too wide. Maintain a + // running prefix width so each candidate "..." + + // name.substr(skip) is evaluated in O(1) using + // textWidth(name.substr(skip)) + // = full_w - prefix_w - kern(name[skip-1], name[skip]) + // giving O(N) total instead of O(N^2). + std::string name = full_name; + int text_w = full_w; + if (text_w > avail && name.size() > 4) { + const int dots_w = getTextWidth("...", inst_font); + const size_t n = name.size(); + int prefix_w = 0; + for (size_t skip = 1; skip < n - 1; ++skip) { + prefix_w += inst_font.glyph(name[skip - 1]).advance; + if (skip >= 2) { + prefix_w + += inst_font.kern(name[skip - 2], name[skip - 1]); + } + const int suffix_w + = full_w - prefix_w + - inst_font.kern(name[skip - 1], name[skip]); + const int w + = dots_w + inst_font.kern('.', name[skip]) + suffix_w; + if (w <= avail) { + name = "..." + name.substr(skip); + text_w = w; + break; + } } } - } - // Center of instance bbox in pixel coords. - const int cx = (pixel_xl + pixel_xh) / 2; - const int cy = 255 - (pixel_yl + pixel_yh) / 2; + // Center of instance bbox in pixel coords. + const int64_t cx = (pixel_xl + pixel_xh) / 2; + const int64_t cy = 255 - (pixel_yl + pixel_yh) / 2; - if (rotate) { - const int px = cx - font_h / 2; - const int py = cy - text_w / 2; - if (px > -font_h && px < kTileSizeInPixel && py > -text_w - && py < kTileSizeInPixel) { - drawTextRotated( - image_buffer, px, py, name, inst_font, name_color); - } - } else { - const int px = cx - text_w / 2; - const int py = cy - font_h / 2; - if (px > -text_w && px < kTileSizeInPixel && py > -font_h - && py < kTileSizeInPixel) { - drawText(image_buffer, px, py, name, inst_font, name_color); + if (rotate) { + const int64_t px = cx - font_h / 2; + const int64_t py = cy - text_w / 2; + if (px > -font_h && px < kTileSizeInPixel && py > -text_w + && py < kTileSizeInPixel) { + drawTextRotated( + image_buffer, (int)px, (int)py, name, inst_font, name_color); + } + } else { + const int64_t px = cx - text_w / 2; + const int64_t py = cy - font_h / 2; + if (px > -text_w && px < kTileSizeInPixel && py > -font_h + && py < kTileSizeInPixel) { + drawText( + image_buffer, (int)px, (int)py, name, inst_font, name_color); + } } } } } - } - } else { - // Layer-specific: obstructions and pins - if (vis.blockages) { - for (odb::dbPolygon* poly_obs : master->getPolygonObstructions()) { - if (tech_layer && poly_obs->getTechLayer() != tech_layer) { - continue; - } - odb::Polygon poly = poly_obs->getPolygon(); - inst->getTransform().apply(poly); - fillPolygon(image_buffer, poly, dbu_tile, scale, obs_color); - } - for (odb::dbBox* obs : master->getObstructions(false)) { - if (tech_layer && obs->getTechLayer() != tech_layer) { - continue; - } - odb::Rect box = obs->getBox(); - inst->getTransform().apply(box); - if (!box.overlaps(dbu_tile)) { - continue; + } else { + // Layer-specific: obstructions and pins + if (vis.blockages) { + for (odb::dbPolygon* poly_obs : + master->getPolygonObstructions()) { + if (tech_layer && poly_obs->getTechLayer() != tech_layer) { + continue; + } + odb::Polygon poly = poly_obs->getPolygon(); + inst->getTransform().apply(poly); + fillPolygon(image_buffer, poly, dbu_tile, scale, obs_color); } - const odb::Rect overlap = box.intersect(dbu_tile); - const odb::Rect draw = toPixels(scale, overlap, dbu_tile); - - drawFilledRect(image_buffer, draw, obs_color); - } - } - - if (vis.inst_pins) { - for (odb::dbMTerm* mterm : master->getMTerms()) { - for (odb::dbMPin* mpin : mterm->getMPins()) { - for (odb::dbPolygon* poly_geom : mpin->getPolygonGeometry()) { - if (tech_layer && poly_geom->getTechLayer() != tech_layer) { - continue; - } - odb::Polygon poly = poly_geom->getPolygon(); - inst->getTransform().apply(poly); - fillPolygon(image_buffer, poly, dbu_tile, scale, color); + for (odb::dbBox* obs : master->getObstructions(false)) { + if (tech_layer && obs->getTechLayer() != tech_layer) { + continue; } - for (odb::dbBox* geom : mpin->getGeometry(false)) { - if (tech_layer && geom->getTechLayer() != tech_layer) { - continue; - } - odb::Rect box = geom->getBox(); - inst->getTransform().apply(box); - if (!box.overlaps(dbu_tile)) { - continue; - } - const odb::Rect overlap = box.intersect(dbu_tile); - const odb::Rect draw = toPixels(scale, overlap, dbu_tile); - - drawFilledRect(image_buffer, draw, color); + odb::Rect box = obs->getBox(); + inst->getTransform().apply(box); + if (!box.overlaps(dbu_tile)) { + continue; } + const odb::Rect overlap = box.intersect(dbu_tile); + const odb::Rect draw = toPixels(scale, overlap, dbu_tile); + + drawFilledRect(image_buffer, draw, obs_color); } } - } - // Draw ITerm name labels when zoomed in and pins are visible. - if (vis.inst_pins && vis.inst_pin_names) { - constexpr Color iterm_label_color{ - .r = 255, .g = 255, .b = 0, .a = 220}; - const odb::dbTransform xfm = inst->getTransform(); - - for (odb::dbMTerm* mterm : master->getMTerms()) { - bool drawn = false; - for (odb::dbMPin* mpin : mterm->getMPins()) { - for (odb::dbBox* geom : mpin->getGeometry(false)) { - if (tech_layer && geom->getTechLayer() != tech_layer) { - continue; - } - odb::Rect box = geom->getBox(); - xfm.apply(box); - if (!box.overlaps(dbu_tile)) { - continue; + if (vis.inst_pins) { + for (odb::dbMTerm* mterm : master->getMTerms()) { + for (odb::dbMPin* mpin : mterm->getMPins()) { + for (odb::dbPolygon* poly_geom : mpin->getPolygonGeometry()) { + if (tech_layer && poly_geom->getTechLayer() != tech_layer) { + continue; + } + odb::Polygon poly = poly_geom->getPolygon(); + inst->getTransform().apply(poly); + fillPolygon(image_buffer, poly, dbu_tile, scale, color); } + for (odb::dbBox* geom : mpin->getGeometry(false)) { + if (tech_layer && geom->getTechLayer() != tech_layer) { + continue; + } + odb::Rect box = geom->getBox(); + inst->getTransform().apply(box); + if (!box.overlaps(dbu_tile)) { + continue; + } + const odb::Rect overlap = box.intersect(dbu_tile); + const odb::Rect draw = toPixels(scale, overlap, dbu_tile); - // Skip if pin box is too small in pixels. - const int box_px_w = static_cast(box.dx() * scale); - const int box_px_h = static_cast(box.dy() * scale); - if (box_px_w < kMinItermLabelBoxPx - && box_px_h < kMinItermLabelBoxPx) { - continue; + drawFilledRect(image_buffer, draw, color); } + } + } + } - const std::string name(mterm->getName()); - const int text_w = getTextWidth(name, iterm_font); - - // Center of pin box in pixel coords. - const odb::Point center = box.center(); - const int cx = static_cast((center.x() - dbu_tile.xMin()) - * scale); - const int cy = 255 - - static_cast( - (center.y() - dbu_tile.yMin()) * scale); - - // Rotate 90° if box is taller than wide and text overflows. - const bool rotate - = (box_px_h > box_px_w) && (text_w > box_px_w); + // Draw ITerm name labels when zoomed in and pins are visible. + if (vis.inst_pins && vis.inst_pin_names) { + constexpr Color iterm_label_color{ + .r = 255, .g = 255, .b = 0, .a = 220}; + const odb::dbTransform xfm = inst->getTransform(); + + for (odb::dbMTerm* mterm : master->getMTerms()) { + bool drawn = false; + for (odb::dbMPin* mpin : mterm->getMPins()) { + for (odb::dbBox* geom : mpin->getGeometry(false)) { + if (tech_layer && geom->getTechLayer() != tech_layer) { + continue; + } + odb::Rect box = geom->getBox(); + xfm.apply(box); + if (!box.overlaps(dbu_tile)) { + continue; + } - if (rotate) { - const int px = cx - iterm_font_h / 2; - const int py = cy - text_w / 2; - if (px > -iterm_font_h && px < kTileSizeInPixel - && py > -text_w && py < kTileSizeInPixel) { - drawTextRotated(image_buffer, - px, - py, - name, - iterm_font, - iterm_label_color); + // Skip if pin box is too small in pixels. + const int box_px_w = static_cast(box.dx() * scale); + const int box_px_h = static_cast(box.dy() * scale); + if (box_px_w < kMinItermLabelBoxPx + && box_px_h < kMinItermLabelBoxPx) { + continue; } - } else { - const int px = cx - text_w / 2; - const int py = cy - iterm_font_h / 2; - if (px > -text_w && px < kTileSizeInPixel - && py > -iterm_font_h && py < kTileSizeInPixel) { - drawText(image_buffer, - px, - py, - name, - iterm_font, - iterm_label_color); + + const std::string name(mterm->getName()); + const int text_w = getTextWidth(name, iterm_font); + + // Center of pin box in pixel coords. + const odb::Point center = box.center(); + const int cx = static_cast( + (center.x() - dbu_tile.xMin()) * scale); + const int cy = 255 + - static_cast( + (center.y() - dbu_tile.yMin()) * scale); + + // Rotate 90° if box is taller than wide and text overflows. + const bool rotate + = (box_px_h > box_px_w) && (text_w > box_px_w); + + if (rotate) { + const int px = cx - iterm_font_h / 2; + const int py = cy - text_w / 2; + if (px > -iterm_font_h && px < kTileSizeInPixel + && py > -text_w && py < kTileSizeInPixel) { + drawTextRotated(image_buffer, + px, + py, + name, + iterm_font, + iterm_label_color); + } + } else { + const int px = cx - text_w / 2; + const int py = cy - iterm_font_h / 2; + if (px > -text_w && px < kTileSizeInPixel + && py > -iterm_font_h && py < kTileSizeInPixel) { + drawText(image_buffer, + px, + py, + name, + iterm_font, + iterm_label_color); + } } - } - drawn = true; - break; // only label first geometry per pin - } - if (drawn) { - break; + drawn = true; + break; // only label first geometry per pin + } + if (drawn) { + break; + } } } } } } - } - // Draw routing shapes (wires, vias) and BTerm shapes on top of instances - if (!instances_only && tech_layer && (vis.routing || vis.pins)) { - for (const auto& shape : search_->searchBoxShapes(block, - tech_layer, - dbu_x_min, - dbu_y_min, - dbu_x_max, - dbu_y_max)) { - const auto type = std::get<1>(shape); - if (type == Search::kBterm && !vis.pins) { - continue; - } - if (type == Search::kWire && !(vis.routing && vis.routing_segments)) { - continue; - } - if (type == Search::kVia && !(vis.routing && vis.routing_vias)) { - continue; - } - odb::dbNet* net = std::get<2>(shape); - if (!vis.isNetVisible(net)) { - continue; - } - if (focus_net_ids && !focus_net_ids->empty() - && focus_net_ids->find(net->getId()) == focus_net_ids->end()) { - continue; - } - const odb::Rect& box = std::get<0>(shape); - if (!box.overlaps(dbu_tile)) { - continue; - } - const odb::Rect overlap = box.intersect(dbu_tile); - const odb::Rect draw = toPixels(scale, overlap, dbu_tile); - - drawFilledRect(image_buffer, draw, color); - } - } - - // Draw special net shapes (power/ground straps) on top of instances - if (!instances_only && tech_layer && vis.special_nets - && vis.srouting_segments) { - for (const auto& shape : search_->searchSNetShapes(block, - tech_layer, - dbu_x_min, - dbu_y_min, - dbu_x_max, - dbu_y_max)) { - odb::dbNet* snet = std::get<2>(shape); - if (!vis.isNetVisible(snet)) { - continue; - } - if (focus_net_ids && !focus_net_ids->empty() - && focus_net_ids->find(snet->getId()) == focus_net_ids->end()) { - continue; - } - const odb::Rect box = std::get<0>(shape)->getBox(); - if (!box.overlaps(dbu_tile)) { - continue; - } - const odb::Polygon& poly = std::get<1>(shape); - fillPolygon(image_buffer, poly, dbu_tile, scale, color); - } - } - - // Draw special net vias — decompose into individual cut boxes - if (!instances_only && tech_layer && vis.special_nets - && vis.srouting_vias) { - for (const auto& shape : search_->searchSNetViaShapes(block, - tech_layer, - dbu_x_min, - dbu_y_min, - dbu_x_max, - dbu_y_max)) { - odb::dbNet* via_net = std::get<1>(shape); - if (!vis.isNetVisible(via_net)) { - continue; - } - if (focus_net_ids && !focus_net_ids->empty() - && focus_net_ids->find(via_net->getId()) - == focus_net_ids->end()) { - continue; - } - odb::dbSBox* sbox = std::get<0>(shape); - std::vector via_boxes; - if (auto tech_via = sbox->getTechVia()) { - via_boxes.assign(tech_via->getBoxes().begin(), - tech_via->getBoxes().end()); - } else if (auto block_via = sbox->getBlockVia()) { - via_boxes.assign(block_via->getBoxes().begin(), - block_via->getBoxes().end()); - } - const odb::Point origin((sbox->xMin() + sbox->xMax()) / 2, - (sbox->yMin() + sbox->yMax()) / 2); - for (odb::dbBox* vbox : via_boxes) { - if (vbox->getTechLayer() != tech_layer) { + // Draw routing shapes (wires, vias) and BTerm shapes on top of + // instances + if (!instances_only && tech_layer && (vis.routing || vis.pins)) { + for (const auto& shape : search_->searchBoxShapes(block, + tech_layer, + dbu_x_min, + dbu_y_min, + dbu_x_max, + dbu_y_max)) { + const auto type = std::get<1>(shape); + if (type == Search::kBterm && !vis.pins) { + continue; + } + if (type == Search::kWire + && !(vis.routing && vis.routing_segments)) { + continue; + } + if (type == Search::kVia && !(vis.routing && vis.routing_vias)) { + continue; + } + odb::dbNet* net = std::get<2>(shape); + if (!vis.isNetVisible(net)) { + continue; + } + if (focus_net_ids && !focus_net_ids->empty() + && focus_net_ids->find(net->getId()) == focus_net_ids->end()) { continue; } - odb::Rect box = vbox->getBox(); - box.moveDelta(origin.x(), origin.y()); + const odb::Rect& box = std::get<0>(shape); if (!box.overlaps(dbu_tile)) { continue; } const odb::Rect overlap = box.intersect(dbu_tile); const odb::Rect draw = toPixels(scale, overlap, dbu_tile); + drawFilledRect(image_buffer, draw, color); } } - } - // Draw via enclosures from adjacent cut layers onto this metal layer. - // Vias are indexed by their cut layer in the search structure. When - // rendering a routing layer we look up the cut layers immediately above - // and below, search for vias there, and draw only the enclosure boxes - // that belong to the current routing layer. - if (!instances_only && tech_layer && vis.special_nets && vis.srouting_vias - && tech_layer->getType() == odb::dbTechLayerType::ROUTING) { - odb::dbTechLayer* adj_cuts[2] - = {tech_layer->getLowerLayer(), tech_layer->getUpperLayer()}; - for (odb::dbTechLayer* cut_layer : adj_cuts) { - if (!cut_layer || cut_layer->getType() != odb::dbTechLayerType::CUT) { - continue; + // Draw special net shapes (power/ground straps) on top of instances + if (!instances_only && tech_layer && vis.special_nets + && vis.srouting_segments) { + for (const auto& shape : search_->searchSNetShapes(block, + tech_layer, + dbu_x_min, + dbu_y_min, + dbu_x_max, + dbu_y_max)) { + odb::dbNet* snet = std::get<2>(shape); + if (!vis.isNetVisible(snet)) { + continue; + } + if (focus_net_ids && !focus_net_ids->empty() + && focus_net_ids->find(snet->getId()) == focus_net_ids->end()) { + continue; + } + const odb::Rect box = std::get<0>(shape)->getBox(); + if (!box.overlaps(dbu_tile)) { + continue; + } + const odb::Polygon& poly = std::get<1>(shape); + fillPolygon(image_buffer, poly, dbu_tile, scale, color); } + } + + // Draw special net vias — decompose into individual cut boxes + if (!instances_only && tech_layer && vis.special_nets + && vis.srouting_vias) { for (const auto& shape : search_->searchSNetViaShapes(block, - cut_layer, + tech_layer, dbu_x_min, dbu_y_min, dbu_x_max, @@ -1585,15 +1889,18 @@ std::vector TileGenerator::renderTileBuffer( continue; } if (focus_net_ids && !focus_net_ids->empty() - && !focus_net_ids->contains(via_net->getId())) { + && focus_net_ids->find(via_net->getId()) + == focus_net_ids->end()) { continue; } odb::dbSBox* sbox = std::get<0>(shape); - odb::dbSet via_boxes; + std::vector via_boxes; if (auto tech_via = sbox->getTechVia()) { - via_boxes = tech_via->getBoxes(); + via_boxes.assign(tech_via->getBoxes().begin(), + tech_via->getBoxes().end()); } else if (auto block_via = sbox->getBlockVia()) { - via_boxes = block_via->getBoxes(); + via_boxes.assign(block_via->getBoxes().begin(), + block_via->getBoxes().end()); } const odb::Point origin((sbox->xMin() + sbox->xMax()) / 2, (sbox->yMin() + sbox->yMax()) / 2); @@ -1612,230 +1919,355 @@ std::vector TileGenerator::renderTileBuffer( } } } - } - // Draw placement blockages (dbBlockage) on the _instances layer. - // Diagonal white hash lines in pixel space, with period anchored in dbu - // coordinates so the pattern is seamless across tile boundaries. - if (instances_only && vis.placement_blockages) { - const Color hash_color{.r = 255, .g = 255, .b = 255, .a = 180}; - constexpr int kPixelPeriod = 20; // pixels between line centers - constexpr int kLineWidth = 2; // pixels wide - for (odb::dbBlockage* blk : search_->searchBlockages( - block, dbu_x_min, dbu_y_min, dbu_x_max, dbu_y_max)) { - odb::Rect box = blk->getBBox()->getBox(); - if (!box.overlaps(dbu_tile)) { - continue; - } - const odb::Rect overlap = box.intersect(dbu_tile); - const odb::Rect draw = toPixels(scale, overlap, dbu_tile); - // Offset in absolute pixel coordinates for seamless tiling - const int ox = (int) (dbu_x_min * scale); - const int oy = (int) (dbu_y_min * scale); - for (int iy = draw.yMin(); iy < draw.yMax(); ++iy) { - for (int ix = draw.xMin(); ix < draw.xMax(); ++ix) { - if (((ix + ox) + (iy + oy)) % kPixelPeriod < kLineWidth) { - blendPixel(image_buffer, ix, 255 - iy, hash_color); + // Draw via enclosures from adjacent cut layers onto this metal layer. + // Vias are indexed by their cut layer in the search structure. When + // rendering a routing layer we look up the cut layers immediately above + // and below, search for vias there, and draw only the enclosure boxes + // that belong to the current routing layer. + if (!instances_only && tech_layer && vis.special_nets + && vis.srouting_vias + && tech_layer->getType() == odb::dbTechLayerType::ROUTING) { + odb::dbTechLayer* adj_cuts[2] + = {tech_layer->getLowerLayer(), tech_layer->getUpperLayer()}; + for (odb::dbTechLayer* cut_layer : adj_cuts) { + if (!cut_layer + || cut_layer->getType() != odb::dbTechLayerType::CUT) { + continue; + } + for (const auto& shape : search_->searchSNetViaShapes(block, + cut_layer, + dbu_x_min, + dbu_y_min, + dbu_x_max, + dbu_y_max)) { + odb::dbNet* via_net = std::get<1>(shape); + if (!vis.isNetVisible(via_net)) { + continue; + } + if (focus_net_ids && !focus_net_ids->empty() + && !focus_net_ids->contains(via_net->getId())) { + continue; + } + odb::dbSBox* sbox = std::get<0>(shape); + odb::dbSet via_boxes; + if (auto tech_via = sbox->getTechVia()) { + via_boxes = tech_via->getBoxes(); + } else if (auto block_via = sbox->getBlockVia()) { + via_boxes = block_via->getBoxes(); + } + const odb::Point origin((sbox->xMin() + sbox->xMax()) / 2, + (sbox->yMin() + sbox->yMax()) / 2); + for (odb::dbBox* vbox : via_boxes) { + if (vbox->getTechLayer() != tech_layer) { + continue; + } + odb::Rect box = vbox->getBox(); + box.moveDelta(origin.x(), origin.y()); + if (!box.overlaps(dbu_tile)) { + continue; + } + const odb::Rect overlap = box.intersect(dbu_tile); + const odb::Rect draw = toPixels(scale, overlap, dbu_tile); + drawFilledRect(image_buffer, draw, color); } } } } - } - // Draw routing obstructions (dbObstruction) on per-layer tiles. - // Same diagonal white hash lines. - if (!instances_only && tech_layer && vis.routing_obstructions) { - const Color hash_color{.r = 255, .g = 255, .b = 255, .a = 180}; - constexpr int kPixelPeriod = 20; - constexpr int kLineWidth = 2; - for (odb::dbObstruction* obs : search_->searchObstructions(block, - tech_layer, - dbu_x_min, - dbu_y_min, - dbu_x_max, - dbu_y_max)) { - odb::Rect box = obs->getBBox()->getBox(); - if (!box.overlaps(dbu_tile)) { - continue; - } - const odb::Rect overlap = box.intersect(dbu_tile); - const odb::Rect draw = toPixels(scale, overlap, dbu_tile); - const int ox = (int) (dbu_x_min * scale); - const int oy = (int) (dbu_y_min * scale); - for (int iy = draw.yMin(); iy < draw.yMax(); ++iy) { - for (int ix = draw.xMin(); ix < draw.xMax(); ++ix) { - if (((ix + ox) + (iy + oy)) % kPixelPeriod < kLineWidth) { - blendPixel(image_buffer, ix, 255 - iy, hash_color); + // Draw placement blockages (dbBlockage) on the _instances layer. + // Diagonal white hash lines in pixel space, with period anchored in dbu + // coordinates so the pattern is seamless across tile boundaries. + if (instances_only && vis.placement_blockages) { + const Color hash_color{.r = 255, .g = 255, .b = 255, .a = 180}; + constexpr int kPixelPeriod = 20; // pixels between line centers + constexpr int kLineWidth = 2; // pixels wide + for (odb::dbBlockage* blk : search_->searchBlockages( + block, dbu_x_min, dbu_y_min, dbu_x_max, dbu_y_max)) { + odb::Rect box = blk->getBBox()->getBox(); + if (!box.overlaps(dbu_tile)) { + continue; + } + const odb::Rect overlap = box.intersect(dbu_tile); + const odb::Rect draw = toPixels(scale, overlap, dbu_tile); + // Offset in absolute pixel coordinates for seamless tiling + const int ox = (int) (dbu_x_min * scale); + const int oy = (int) (dbu_y_min * scale); + for (int iy = draw.yMin(); iy < draw.yMax(); ++iy) { + for (int ix = draw.xMin(); ix < draw.xMax(); ++ix) { + if (((ix + ox) + (iy + oy)) % kPixelPeriod < kLineWidth) { + blendPixel(image_buffer, ix, 255 - iy, hash_color); + } } } } } - } - // Draw rows (and individual sites when zoomed in) on _instances layer. - if (instances_only && vis.rows) { - const Color row_color{ - .r = 60, .g = 180, .b = 60, .a = 180}; // green outlines - - // Lambda to draw a rectangle outline. - auto draw_outline = [&](const odb::Rect& rect) { - const odb::Rect draw = toPixels(scale, rect, dbu_tile); - for (int ix = draw.xMin(); ix <= draw.xMax(); ++ix) { - blendPixel(image_buffer, ix, 255 - draw.yMin(), row_color); - blendPixel(image_buffer, ix, 255 - draw.yMax(), row_color); - } - for (int iy = draw.yMin(); iy <= draw.yMax(); ++iy) { - blendPixel(image_buffer, draw.xMin(), 255 - iy, row_color); - blendPixel(image_buffer, draw.xMax(), 255 - iy, row_color); + // Draw routing obstructions (dbObstruction) on per-layer tiles. + // Same diagonal white hash lines. + if (!instances_only && tech_layer && vis.routing_obstructions) { + const Color hash_color{.r = 255, .g = 255, .b = 255, .a = 180}; + constexpr int kPixelPeriod = 20; + constexpr int kLineWidth = 2; + for (odb::dbObstruction* obs : + search_->searchObstructions(block, + tech_layer, + dbu_x_min, + dbu_y_min, + dbu_x_max, + dbu_y_max)) { + odb::Rect box = obs->getBBox()->getBox(); + if (!box.overlaps(dbu_tile)) { + continue; + } + const odb::Rect overlap = box.intersect(dbu_tile); + const odb::Rect draw = toPixels(scale, overlap, dbu_tile); + const int ox = (int) (dbu_x_min * scale); + const int oy = (int) (dbu_y_min * scale); + for (int iy = draw.yMin(); iy < draw.yMax(); ++iy) { + for (int ix = draw.xMin(); ix < draw.xMax(); ++ix) { + if (((ix + ox) + (iy + oy)) % kPixelPeriod < kLineWidth) { + blendPixel(image_buffer, ix, 255 - iy, hash_color); + } + } + } } - }; + } - for (const auto& [row_rect, row] : search_->searchRows( - block, dbu_x_min, dbu_y_min, dbu_x_max, dbu_y_max)) { - if (!row_rect.overlaps(dbu_tile)) { - continue; - } - odb::dbSite* site = row->getSite(); - if (site && !vis.isSiteVisible(site->getName())) { - continue; - } + // Draw rows (and individual sites when zoomed in) on _instances layer. + if (instances_only && vis.rows) { + const Color row_color{ + .r = 60, .g = 180, .b = 60, .a = 180}; // green outlines + + // Lambda to draw a rectangle outline. + auto draw_outline = [&](const odb::Rect& rect) { + const odb::Rect draw = toPixels(scale, rect, dbu_tile); + for (int ix = draw.xMin(); ix <= draw.xMax(); ++ix) { + blendPixel(image_buffer, ix, 255 - draw.yMin(), row_color); + blendPixel(image_buffer, ix, 255 - draw.yMax(), row_color); + } + for (int iy = draw.yMin(); iy <= draw.yMax(); ++iy) { + blendPixel(image_buffer, draw.xMin(), 255 - iy, row_color); + blendPixel(image_buffer, draw.xMax(), 255 - iy, row_color); + } + }; - // Always draw the row outline. - draw_outline(row_rect); - - // Draw individual sites when zoomed in enough (site >= 5px). - // Matches GUI nominalViewableResolution threshold. - if (site) { - int site_w = site->getWidth(); - int site_h = site->getHeight(); - - // Swap dimensions for rotated orientations. - switch (row->getOrient().getValue()) { - case odb::dbOrientType::R90: - case odb::dbOrientType::R270: - case odb::dbOrientType::MYR90: - case odb::dbOrientType::MXR90: - std::swap(site_w, site_h); - break; - default: - break; + for (const auto& [row_rect, row] : search_->searchRows( + block, dbu_x_min, dbu_y_min, dbu_x_max, dbu_y_max)) { + if (!row_rect.overlaps(dbu_tile)) { + continue; + } + odb::dbSite* site = row->getSite(); + if (site && !vis.isSiteVisible(site->getName())) { + continue; } - const int site_w_px = static_cast(site_w * scale); - if (site_w_px >= 5) { - odb::Point pt = row->getOrigin(); - const int spacing = row->getSpacing(); - const int count = row->getSiteCount(); - const bool horizontal - = (row->getDirection() == odb::dbRowDir::HORIZONTAL); - - for (int i = 0; i < count; ++i) { - const odb::Rect site_rect( - pt.x(), pt.y(), pt.x() + site_w, pt.y() + site_h); - if (site_rect.overlaps(dbu_tile)) { - draw_outline(site_rect); - } - if (horizontal) { - pt.addX(spacing); - } else { - pt.addY(spacing); + // Always draw the row outline. + draw_outline(row_rect); + + // Draw individual sites when zoomed in enough (site >= 5px). + // Matches GUI nominalViewableResolution threshold. + if (site) { + int site_w = site->getWidth(); + int site_h = site->getHeight(); + + // Swap dimensions for rotated orientations. + switch (row->getOrient().getValue()) { + case odb::dbOrientType::R90: + case odb::dbOrientType::R270: + case odb::dbOrientType::MYR90: + case odb::dbOrientType::MXR90: + std::swap(site_w, site_h); + break; + default: + break; + } + + const int site_w_px = static_cast(site_w * scale); + if (site_w_px >= 5) { + odb::Point pt = row->getOrigin(); + const int spacing = row->getSpacing(); + const int count = row->getSiteCount(); + const bool horizontal + = (row->getDirection() == odb::dbRowDir::HORIZONTAL); + + for (int i = 0; i < count; ++i) { + const odb::Rect site_rect( + pt.x(), pt.y(), pt.x() + site_w, pt.y() + site_h); + if (site_rect.overlaps(dbu_tile)) { + draw_outline(site_rect); + } + if (horizontal) { + pt.addX(spacing); + } else { + pt.addY(spacing); + } } } } } } - } - // Draw tracks on per-layer tiles - if (!instances_only && tech_layer - && (vis.tracks_pref || vis.tracks_non_pref)) { - odb::dbTrackGrid* grid = block->findTrackGrid(tech_layer); - debugPrint(logger_, - utl::WEB, - "tile", - 1, - "tracks: layer={} grid={} pref={} non_pref={}", - layer, - grid != nullptr, - vis.tracks_pref, - vis.tracks_non_pref); - if (grid) { - Color track_color = color; - track_color.a = 150; - const bool is_horizontal - = tech_layer->getDirection() == odb::dbTechLayerDir::HORIZONTAL; - - // X-direction tracks (vertical lines on screen) - // Preferred for vertical layers, non-preferred for horizontal layers - if ((!is_horizontal && vis.tracks_pref) - || (is_horizontal && vis.tracks_non_pref)) { - std::vector x_grid; - grid->getGridX(x_grid); - debugPrint(logger_, - utl::WEB, - "tile", - 1, - " x_tracks: count={} tile=[{},{},{},{}]", - x_grid.size(), - dbu_x_min, - dbu_y_min, - dbu_x_max, - dbu_y_max); - for (int tx : x_grid) { - if (tx < dbu_x_min || tx > dbu_x_max) { - continue; - } - const int px = static_cast((tx - dbu_x_min) * scale); - if (px >= 0 && px < kTileSizeInPixel) { - for (int py = 0; py < kTileSizeInPixel; ++py) { - blendPixel(image_buffer, px, py, track_color); + // Draw tracks on per-layer tiles + if (!instances_only && tech_layer + && (vis.tracks_pref || vis.tracks_non_pref)) { + odb::dbTrackGrid* grid = block->findTrackGrid(tech_layer); + debugPrint(logger_, + utl::WEB, + "tile", + 1, + "tracks: layer={} grid={} pref={} non_pref={}", + layer, + grid != nullptr, + vis.tracks_pref, + vis.tracks_non_pref); + if (grid) { + Color track_color = color; + track_color.a = 150; + const bool is_horizontal + = tech_layer->getDirection() == odb::dbTechLayerDir::HORIZONTAL; + + // X-direction tracks (vertical lines on screen) + // Preferred for vertical layers, non-preferred for horizontal + // layers + if ((!is_horizontal && vis.tracks_pref) + || (is_horizontal && vis.tracks_non_pref)) { + std::vector x_grid; + grid->getGridX(x_grid); + debugPrint(logger_, + utl::WEB, + "tile", + 1, + " x_tracks: count={} tile=[{},{},{},{}]", + x_grid.size(), + dbu_x_min, + dbu_y_min, + dbu_x_max, + dbu_y_max); + for (int tx : x_grid) { + if (tx < dbu_x_min || tx > dbu_x_max) { + continue; + } + const int px = static_cast((tx - dbu_x_min) * scale); + if (px >= 0 && px < kTileSizeInPixel) { + for (int py = 0; py < kTileSizeInPixel; ++py) { + blendPixel(image_buffer, px, py, track_color); + } } } } - } - // Y-direction tracks (horizontal lines on screen) - // Preferred for horizontal layers, non-preferred for vertical layers - if ((is_horizontal && vis.tracks_pref) - || (!is_horizontal && vis.tracks_non_pref)) { - std::vector y_grid; - grid->getGridY(y_grid); - debugPrint(logger_, - utl::WEB, - "tile", - 1, - " y_tracks: count={}", - y_grid.size()); - for (int ty : y_grid) { - if (ty < dbu_y_min || ty > dbu_y_max) { - continue; - } - const int py = 255 - static_cast((ty - dbu_y_min) * scale); - if (py >= 0 && py < kTileSizeInPixel) { - for (int px = 0; px < kTileSizeInPixel; ++px) { - blendPixel(image_buffer, px, py, track_color); + // Y-direction tracks (horizontal lines on screen) + // Preferred for horizontal layers, non-preferred for vertical + // layers + if ((is_horizontal && vis.tracks_pref) + || (!is_horizontal && vis.tracks_non_pref)) { + std::vector y_grid; + grid->getGridY(y_grid); + debugPrint(logger_, + utl::WEB, + "tile", + 1, + " y_tracks: count={}", + y_grid.size()); + for (int ty : y_grid) { + if (ty < dbu_y_min || ty > dbu_y_max) { + continue; + } + const int py = 255 - static_cast((ty - dbu_y_min) * scale); + if (py >= 0 && py < kTileSizeInPixel) { + for (int px = 0; px < kTileSizeInPixel; ++px) { + blendPixel(image_buffer, px, py, track_color); + } } } } } } - } - - } // end if (!modules_layer && !pins_layer) + } // end if (!modules_layer && !pins_layer) + + if (use_local) { + // Slow-path compositing for chiplets with non-R0 orientations. + // Forward-mapping (iterate the local buffer, write to world) + // leaves gaps when the rotation is non-identity because some + // world pixels never get a source. We do reverse-mapping + // instead: for each world destination pixel we map back into + // the local frame, sample the local buffer if present, and + // use blendPixel() to alpha-composite onto image_buffer. + odb::dbTransform inv_xfm = node.world_xfm; + inv_xfm.invert(); + for (int py_w = 0; py_w < kTileSizeInPixel; ++py_w) { + for (int px_w = 0; px_w < kTileSizeInPixel; ++px_w) { + // World pixel center → world DBU. + odb::Point pt(std::lround(dbu_x_min_world + (px_w + 0.5) / scale), + std::lround(dbu_y_min_world + + (kTileSizeInPixel - 1 - py_w + 0.5) + / scale)); + // World DBU → local DBU. + inv_xfm.apply(pt); + // Local DBU → local pixel. + const int px_l = std::floor((pt.x() - dbu_x_min) * scale); + const int py_l = kTileSizeInPixel - 1 + - std::floor((pt.y() - dbu_y_min) * scale); + if (px_l < 0 || px_l >= kTileSizeInPixel || py_l < 0 + || py_l >= kTileSizeInPixel) { + continue; + } + const int src_idx = (py_l * kTileSizeInPixel + px_l) * 4; + const unsigned char a_src = local_image_buffer[src_idx + 3]; + if (a_src == 0) { + continue; + } + const Color src_color{ + .r = local_image_buffer[src_idx + 0], + .g = local_image_buffer[src_idx + 1], + .b = local_image_buffer[src_idx + 2], + .a = a_src, + }; + blendPixel(world_image_buffer, px_w, py_w, src_color); + } + } + } + } // end per-chiplet for-loop + + // Overlays render once in world space, on top of all chiplets. + // Their geometry (timing paths, DRC rects, flight lines) is already + // expressed in world DBU and isn't tied to any single chiplet's + // local frame. route_guides keys on the top-chip tech layer. + odb::dbTech* world_tech = db_->getTech(); + odb::dbTechLayer* world_tech_layer + = world_tech ? world_tech->findLayer(layer.c_str()) : nullptr; + Color world_color{.r = 200, .g = 200, .b = 200, .a = 180}; + if (world_tech_layer) { + const auto it = layer_colors.find(world_tech_layer); + if (it != layer_colors.end()) { + world_color = it->second; + } + } if (!highlight_rects.empty() || !highlight_polys.empty()) { - drawHighlight( - image_buffer, highlight_rects, highlight_polys, dbu_tile, scale); + drawHighlight(world_image_buffer, + highlight_rects, + highlight_polys, + dbu_tile_world, + scale); } if (!colored_rects.empty()) { - drawColoredHighlight(image_buffer, colored_rects, layer, dbu_tile, scale); + drawColoredHighlight( + world_image_buffer, colored_rects, layer, dbu_tile_world, scale); } if (!flight_lines.empty()) { - drawFlightLines(image_buffer, flight_lines, dbu_tile, scale); - } - if (route_guide_net_ids && !route_guide_net_ids->empty() && tech_layer) { - drawRouteGuides( - image_buffer, *route_guide_net_ids, layer, color, dbu_tile, scale); + drawFlightLines( + world_image_buffer, flight_lines, dbu_tile_world, scale); + } + if (route_guide_net_ids && !route_guide_net_ids->empty() + && world_tech_layer) { + drawRouteGuides(world_image_buffer, + *route_guide_net_ids, + layer, + world_color, + dbu_tile_world, + scale); } if (vis.debug_renderers) { // The callback (installed by WebServer at startup) decides @@ -1843,15 +2275,16 @@ std::vector TileGenerator::renderTileBuffer( // the gui::Gui::get() access itself. Keeping Gui:: references // out of tile_generator means test executables that link libweb // don't transitively need gui.a / ord.a. - drawRendererOverlay(image_buffer, dbu_tile, scale, vis.debug_live); + drawRendererOverlay( + world_image_buffer, dbu_tile_world, scale, vis.debug_live); } } if (vis.debug) { - drawDebugOverlay(image_buffer, z, x, y); + drawDebugOverlay(world_image_buffer, z, x, y); } - return image_buffer; + return world_image_buffer; } std::vector TileGenerator::generateHeatMapTile( @@ -3170,6 +3603,48 @@ boost::json::object serializeTechResponse(const TileGenerator& gen) out["layer_hierarchy"] = buildLayerHierarchy(top_chip, gen, top_chip->getName(), "block"); } + + // Chiplet hierarchy. One JSON object per node in the dbChip / + // dbChipInst tree. Frontend uses these to populate the "Chiplets" + // section of the display-controls panel and to show the path of each + // selected object. See collectChiplets() for the traversal. + boost::json::array chiplets; + if (gen.getChip()) { + auto orientStr = [](const odb::dbOrientType& o) { + return std::string(odb::dbOrientType(o).getString()); + }; + for (const ChipletNode& node : gen.chiplets()) { + boost::json::object entry; + entry["path"] = node.path; + entry["name"] = node.name; + entry["depth"] = node.depth; + if (node.parent_path.empty()) { + entry["parent"] = nullptr; + } else { + entry["parent"] = node.parent_path; + } + if (node.chip && node.chip->getBlock()) { + entry["master"] = node.chip->getBlock()->getName(); + } else { + entry["master"] = ""; + } + if (node.block) { + odb::Rect b = node.block->getBBox()->getBox(); + boost::json::array local_bbox{b.xMin(), b.yMin(), b.xMax(), b.yMax()}; + entry["bbox_dbu_local"] = std::move(local_bbox); + odb::Rect bw = b; + node.world_xfm.apply(bw); + boost::json::array world_bbox{ + bw.xMin(), bw.yMin(), bw.xMax(), bw.yMax()}; + entry["bbox_dbu_world"] = std::move(world_bbox); + } + const odb::Point off = node.world_xfm.getOffset(); + entry["world_origin_dbu"] = boost::json::array{off.x(), off.y()}; + entry["orient"] = orientStr(node.world_xfm.getOrient()); + chiplets.emplace_back(std::move(entry)); + } + } + out["chiplets"] = std::move(chiplets); return out; } diff --git a/src/web/src/tile_generator.h b/src/web/src/tile_generator.h index be4eb40d2d5..e38da8fd372 100644 --- a/src/web/src/tile_generator.h +++ b/src/web/src/tile_generator.h @@ -20,6 +20,7 @@ #include "color.h" #include "glyph_cache.h" #include "odb/db.h" +#include "odb/dbTransform.h" #include "odb/geom.h" #include "web_painter.h" @@ -59,10 +60,38 @@ struct SelectionResult { std::any object; // dbInst*, dbNet*, etc. std::string name; - std::string type_name; // "Inst", "Net", etc. + std::string type_name; // "Inst", "Net", etc. — sent to the JSON API odb::Rect bbox; + // Fast-path tag for sort/count. type_name is a string so `selectAt` + // can serialize it later, but the sort comparator runs on every + // result pair — comparing two short strings ("Inst" / "Net") per + // comparison adds up. `is_inst` is set alongside type_name and + // dominates the sort. + bool is_inst = false; }; +// One node in the chiplet tree rooted at db->getChip(). The root has +// inst==nullptr and an identity world_xfm; descendants accumulate +// dbChipInst transforms top-down. See collectChiplets(). +struct ChipletNode +{ + odb::dbChip* chip = nullptr; + odb::dbBlock* block = nullptr; // chip->getBlock() + odb::dbChipInst* inst = nullptr; // null for root + odb::dbTransform world_xfm; // local-to-root transform + std::string path; // "top.soc_inst.subip" — unique + std::string parent_path; // path of the parent ("" for the root) + std::string name; // "top" or inst->getName() + int depth = 0; + int global_z = 0; +}; + +// Walk the dbChip → dbChipInst → masterChip hierarchy depth-first and +// return a flat list with each chiplet's accumulated world transform. +// Mirrors LayoutViewer::getChips() (Qt GUI) but adds transforms and +// stable hierarchical paths so the web renderer can place each chiplet. +std::vector collectChiplets(odb::dbChip* root); + struct TileVisibility { bool stdcells = true; @@ -158,6 +187,14 @@ struct TileVisibility std::set visible_layers; bool has_visible_layers = false; + // Per-chiplet visibility: when has_visible_chiplets is true, the tile + // renderer skips ChipletNodes whose `path` is not in this set. Empty + // set with the flag off renders every chiplet (default). Paths match + // ChipletNode::path produced by collectChiplets() (e.g. "top.soc_inst"). + std::set visible_chiplets; + bool has_visible_chiplets = false; + bool isChipletVisible(const std::string& path) const; + void parseFromJson(const boost::json::object& json); bool isNetVisible(odb::dbNet* net) const; @@ -214,6 +251,13 @@ class TileGenerator odb::dbChip* getChip() const; odb::dbTech* getTech() const; + // Cached, sorted list of chiplets reachable from db_->getChip(). + // The cache is invalidated by eagerInit() and rebuilt lazily on the + // next call. Hot-path call-sites (renderTileBuffer, getBounds, + // selectAt) read it on every tile / click; the free function + // `collectChiplets` is kept for tests and one-shot callers. + const std::vector& chiplets() const; + std::vector generateTile( const std::string& layer, int z, @@ -395,6 +439,13 @@ class TileGenerator mutable std::map> layer_colors_by_tech_; + // Cached chiplet traversal. See chiplets(). Invalidated in + // eagerInit() because that's where setTopChip() runs and the + // hierarchy may have changed. + mutable std::mutex chiplets_mutex_; + mutable std::vector chiplets_cache_; + mutable bool chiplets_cache_valid_ = false; + static constexpr int kTileSizeInPixel = 256; }; diff --git a/src/web/src/websocket-tile-layer.js b/src/web/src/websocket-tile-layer.js index 4b4f6662b77..cf5adec04a2 100644 --- a/src/web/src/websocket-tile-layer.js +++ b/src/web/src/websocket-tile-layer.js @@ -3,7 +3,36 @@ // Leaflet tile layer that fetches tiles via WebSocket. -export function createWebSocketTileLayer(visibility, visibleLayers) { +// `app` (third arg) is read lazily on every request so that +// app.visibleChiplets, populated by display-controls.js after the tech +// metadata arrives, is reflected in tile requests without rebuilding +// the layer. When null or absent the field is omitted and the server +// renders every chiplet (default). +export function createWebSocketTileLayer(visibility, visibleLayers, app) { + // Single source of truth for the tile-request payload. Both + // createTile (initial load) and refreshTiles (visibility change) + // call this so the wire format stays in sync — earlier copies of + // this snippet drifted when fields like visible_chiplets were + // added in only one place. + function buildTileRequest(coords, layerName) { + const vf = {}; + for (const [k, v] of Object.entries(visibility)) { + vf[k] = !!v; + } + const req = { + type: 'tile', + layer: layerName, + z: coords.z, + x: coords.x, + y: coords.y, + visible_layers: visibleLayers ? [...visibleLayers] : [], + ...vf, + }; + if (app && app.visibleChiplets instanceof Set) { + req.visible_chiplets = [...app.visibleChiplets]; + } + return req; + } return L.GridLayer.extend({ initialize: function(websocketManager, layerName, options) { this._websocketManager = websocketManager; @@ -35,23 +64,13 @@ export function createWebSocketTileLayer(visibility, visibleLayers) { } }; - const vf = {}; - for (const [k, v] of Object.entries(visibility)) { - vf[k] = !!v; - } // Store the request ID so _removeTile() can cancel it // when the tile is discarded (e.g. during zoom). tile._websocketRequestId = this._websocketManager.nextId; - this._websocketManager.request({ - type: 'tile', - layer: this._layerName, - z: coords.z, - x: coords.x, - y: coords.y, - visible_layers: visibleLayers ? [...visibleLayers] : [], - ...vf, - }).then(data => { + this._websocketManager.request( + buildTileRequest(coords, this._layerName) + ).then(data => { if (typeof data === 'string') { tile.src = data; // data URI from cache } else { @@ -69,11 +88,6 @@ export function createWebSocketTileLayer(visibility, visibleLayers) { refreshTiles: function() { if (!this._map) return; - const vf = {}; - for (const [k, v] of Object.entries(visibility)) { - vf[k] = !!v; - } - for (const key in this._tiles) { const tileInfo = this._tiles[key]; if (!tileInfo || !tileInfo.el) continue; @@ -86,18 +100,11 @@ export function createWebSocketTileLayer(visibility, visibleLayers) { this._websocketManager.cancel(tile._websocketRequestId); } - const requestId = this._websocketManager.nextId; - tile._websocketRequestId = requestId; + tile._websocketRequestId = this._websocketManager.nextId; - this._websocketManager.request({ - type: 'tile', - layer: this._layerName, - z: coords.z, - x: coords.x, - y: coords.y, - visible_layers: visibleLayers ? [...visibleLayers] : [], - ...vf, - }).then(data => { + this._websocketManager.request( + buildTileRequest(coords, this._layerName) + ).then(data => { if (tile.src && tile.src.startsWith('blob:')) { URL.revokeObjectURL(tile.src); } From 5295df5040e01f5bc88ea34a3c71b88c8ea8994f Mon Sep 17 00:00:00 2001 From: Jorge Ferreira Date: Wed, 6 May 2026 04:34:09 +0000 Subject: [PATCH 4/7] style(web): apply clang-format Signed-off-by: Jorge Ferreira --- src/web/src/tile_generator.cpp | 107 +++++++++++++++++++-------------- 1 file changed, 62 insertions(+), 45 deletions(-) diff --git a/src/web/src/tile_generator.cpp b/src/web/src/tile_generator.cpp index d324e894071..d503877fd1b 100644 --- a/src/web/src/tile_generator.cpp +++ b/src/web/src/tile_generator.cpp @@ -871,9 +871,11 @@ std::vector TileGenerator::selectAt( const odb::Rect& box = std::get<0>(shape); if (box.intersects(click_pt) && vis.isNetVisible(net)) { seen_nets.insert(net); - results.push_back( - {net, net->getName(), "Net", toWorld(net->getTermBBox()), - false}); + results.push_back({net, + net->getName(), + "Net", + toWorld(net->getTermBBox()), + false}); } } } @@ -889,9 +891,11 @@ std::vector TileGenerator::selectAt( const odb::Rect box = std::get<0>(shape)->getBox(); if (box.intersects(click_pt) && vis.isNetVisible(net)) { seen_nets.insert(net); - results.push_back( - {net, net->getName(), "Net", toWorld(net->getTermBBox()), - false}); + results.push_back({net, + net->getName(), + "Net", + toWorld(net->getTermBBox()), + false}); } } } @@ -907,9 +911,11 @@ std::vector TileGenerator::selectAt( const odb::Rect box = std::get<0>(shape)->getBox(); if (box.intersects(click_pt) && vis.isNetVisible(net)) { seen_nets.insert(net); - results.push_back( - {net, net->getName(), "Net", toWorld(net->getTermBBox()), - false}); + results.push_back({net, + net->getName(), + "Net", + toWorld(net->getTermBBox()), + false}); } } } @@ -925,15 +931,15 @@ std::vector TileGenerator::selectAt( return a.bbox.area() > b.bbox.area(); }); - debugPrint(logger_, - utl::WEB, - "select", - 1, - " selected={} (insts={}, nets={})", - results.size(), - std::ranges::count_if(results, - [](const auto& r) { return r.is_inst; }), - seen_nets.size()); + debugPrint( + logger_, + utl::WEB, + "select", + 1, + " selected={} (insts={}, nets={})", + results.size(), + std::ranges::count_if(results, [](const auto& r) { return r.is_inst; }), + seen_nets.size()); return results; } @@ -1005,14 +1011,16 @@ std::vector collectChiplets(odb::dbChip* root) if (!root) { return out; } - collectChipletsRec(root, nullptr, odb::dbTransform{}, std::string{}, 0, 0, out); + collectChipletsRec( + root, nullptr, odb::dbTransform{}, std::string{}, 0, 0, out); - std::stable_sort(out.begin(), out.end(), [](const ChipletNode& a, const ChipletNode& b) { - if (a.global_z != b.global_z) { - return a.global_z < b.global_z; - } - return a.depth < b.depth; - }); + std::stable_sort( + out.begin(), out.end(), [](const ChipletNode& a, const ChipletNode& b) { + if (a.global_z != b.global_z) { + return a.global_z < b.global_z; + } + return a.depth < b.depth; + }); return out; } @@ -1180,8 +1188,7 @@ std::vector TileGenerator::renderTileBuffer( // the slow-path it's a per-chiplet local buffer that the // reverse-mapping block at the end of this iteration composites // back onto world_image_buffer. - auto& image_buffer - = use_local ? local_image_buffer : world_image_buffer; + auto& image_buffer = use_local ? local_image_buffer : world_image_buffer; odb::Rect dbu_tile = dbu_tile_world; if (use_local) { @@ -1213,8 +1220,10 @@ std::vector TileGenerator::renderTileBuffer( const int yh = die.yMax(); const int64_t pixel_xl = (int64_t) ((xl - dbu_x_min) * scale); const int64_t pixel_yl = (int64_t) ((yl - dbu_y_min) * scale); - const int64_t pixel_xh = (int64_t) std::ceil((xh - dbu_x_min) * scale); - const int64_t pixel_yh = (int64_t) std::ceil((yh - dbu_y_min) * scale); + const int64_t pixel_xh + = (int64_t) std::ceil((xh - dbu_x_min) * scale); + const int64_t pixel_yh + = (int64_t) std::ceil((yh - dbu_y_min) * scale); const int loop_xl = std::clamp(pixel_xl, 0, 256); const int loop_yl = std::clamp(pixel_yl, 0, 256); @@ -1226,8 +1235,7 @@ std::vector TileGenerator::renderTileBuffer( const int draw_xh = std::clamp(pixel_xh, 0, 255); const int draw_yh = std::clamp(pixel_yh, 0, 255); - constexpr Color die_outline{ - .r = 128, .g = 128, .b = 128, .a = 255}; + constexpr Color die_outline{.r = 128, .g = 128, .b = 128, .a = 255}; if (dbu_x_min <= xl && xl <= dbu_x_max) { for (int iy = loop_yl; iy < loop_yh; ++iy) { setPixel(image_buffer, draw_xl, 255 - iy, die_outline); @@ -1544,8 +1552,10 @@ std::vector TileGenerator::renderTileBuffer( const int64_t pixel_xl = (int64_t) ((xl - dbu_x_min) * scale); const int64_t pixel_yl = (int64_t) ((yl - dbu_y_min) * scale); - const int64_t pixel_xh = (int64_t) std::ceil((xh - dbu_x_min) * scale); - const int64_t pixel_yh = (int64_t) std::ceil((yh - dbu_y_min) * scale); + const int64_t pixel_xh + = (int64_t) std::ceil((xh - dbu_x_min) * scale); + const int64_t pixel_yh + = (int64_t) std::ceil((yh - dbu_y_min) * scale); const int loop_xl = std::clamp(pixel_xl, 0, 256); const int loop_yl = std::clamp(pixel_yl, 0, 256); @@ -1591,8 +1601,8 @@ std::vector TileGenerator::renderTileBuffer( // elided from the left ("...suffix") to fit 90% of the // available dimension, matching the Qt GUI's behavior. if (vis.inst_names) { - const int box_px_w = (int)(pixel_xh - pixel_xl); - const int box_px_h = (int)(pixel_yh - pixel_yl); + const int box_px_w = (int) (pixel_xh - pixel_xl); + const int box_px_h = (int) (pixel_yh - pixel_yl); const int box_px_min = std::min(box_px_w, box_px_h); if (std::max(box_px_w, box_px_h) >= kMinInstNameBoxPx) { const int font_px @@ -1658,16 +1668,24 @@ std::vector TileGenerator::renderTileBuffer( const int64_t py = cy - text_w / 2; if (px > -font_h && px < kTileSizeInPixel && py > -text_w && py < kTileSizeInPixel) { - drawTextRotated( - image_buffer, (int)px, (int)py, name, inst_font, name_color); + drawTextRotated(image_buffer, + (int) px, + (int) py, + name, + inst_font, + name_color); } } else { const int64_t px = cx - text_w / 2; const int64_t py = cy - font_h / 2; if (px > -text_w && px < kTileSizeInPixel && py > -font_h && py < kTileSizeInPixel) { - drawText( - image_buffer, (int)px, (int)py, name, inst_font, name_color); + drawText(image_buffer, + (int) px, + (int) py, + name, + inst_font, + name_color); } } } @@ -2200,10 +2218,10 @@ std::vector TileGenerator::renderTileBuffer( for (int py_w = 0; py_w < kTileSizeInPixel; ++py_w) { for (int px_w = 0; px_w < kTileSizeInPixel; ++px_w) { // World pixel center → world DBU. - odb::Point pt(std::lround(dbu_x_min_world + (px_w + 0.5) / scale), - std::lround(dbu_y_min_world - + (kTileSizeInPixel - 1 - py_w + 0.5) - / scale)); + odb::Point pt( + std::lround(dbu_x_min_world + (px_w + 0.5) / scale), + std::lround(dbu_y_min_world + + (kTileSizeInPixel - 1 - py_w + 0.5) / scale)); // World DBU → local DBU. inv_xfm.apply(pt); // Local DBU → local pixel. @@ -2257,8 +2275,7 @@ std::vector TileGenerator::renderTileBuffer( world_image_buffer, colored_rects, layer, dbu_tile_world, scale); } if (!flight_lines.empty()) { - drawFlightLines( - world_image_buffer, flight_lines, dbu_tile_world, scale); + drawFlightLines(world_image_buffer, flight_lines, dbu_tile_world, scale); } if (route_guide_net_ids && !route_guide_net_ids->empty() && world_tech_layer) { From 6dd9b9dc40253847518536de587744aa4b11d4a8 Mon Sep 17 00:00:00 2001 From: Jorge Ferreira Date: Wed, 6 May 2026 04:45:46 +0000 Subject: [PATCH 5/7] refactor(web): use std::ranges::stable_sort as suggested by clang-tidy Signed-off-by: Jorge Ferreira --- src/web/src/tile_generator.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/web/src/tile_generator.cpp b/src/web/src/tile_generator.cpp index d503877fd1b..8ca1fd2aaed 100644 --- a/src/web/src/tile_generator.cpp +++ b/src/web/src/tile_generator.cpp @@ -1014,8 +1014,8 @@ std::vector collectChiplets(odb::dbChip* root) collectChipletsRec( root, nullptr, odb::dbTransform{}, std::string{}, 0, 0, out); - std::stable_sort( - out.begin(), out.end(), [](const ChipletNode& a, const ChipletNode& b) { + std::ranges::stable_sort( + out, [](const ChipletNode& a, const ChipletNode& b) { if (a.global_z != b.global_z) { return a.global_z < b.global_z; } From f97ce902b5f9c1eca38b3cb5711c3d32bbe1264b Mon Sep 17 00:00:00 2001 From: Jorge Ferreira Date: Wed, 6 May 2026 04:55:35 +0000 Subject: [PATCH 6/7] style(web): apply clang-format to ranges update Signed-off-by: Jorge Ferreira --- src/web/src/tile_generator.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/web/src/tile_generator.cpp b/src/web/src/tile_generator.cpp index 8ca1fd2aaed..5dec6a86541 100644 --- a/src/web/src/tile_generator.cpp +++ b/src/web/src/tile_generator.cpp @@ -1014,13 +1014,12 @@ std::vector collectChiplets(odb::dbChip* root) collectChipletsRec( root, nullptr, odb::dbTransform{}, std::string{}, 0, 0, out); - std::ranges::stable_sort( - out, [](const ChipletNode& a, const ChipletNode& b) { - if (a.global_z != b.global_z) { - return a.global_z < b.global_z; - } - return a.depth < b.depth; - }); + std::ranges::stable_sort(out, [](const ChipletNode& a, const ChipletNode& b) { + if (a.global_z != b.global_z) { + return a.global_z < b.global_z; + } + return a.depth < b.depth; + }); return out; } From e982c5ecbf14750a02d23dd1fee968ca9a717c70 Mon Sep 17 00:00:00 2001 From: Jorge Ferreira Date: Wed, 13 May 2026 23:49:53 +0000 Subject: [PATCH 7/7] feat(web,odb): update display controls for multiple tech Signed-off-by: Jorge Ferreira --- src/odb/src/3dblox/checker.cpp | 22 ++++--- src/web/src/3d-viewer-widget.js | 107 ++++++++++++++++++++++++++++---- src/web/src/drc-widget.js | 36 ++++++++++- src/web/src/inspector.js | 4 +- src/web/src/main.js | 2 + src/web/src/request_handler.cpp | 41 +++++++++++- src/web/src/style.css | 21 +++++++ src/web/src/tile_generator.cpp | 69 ++++++++++++++------ src/web/src/tile_generator.h | 1 + src/web/src/web.cpp | 5 +- 10 files changed, 257 insertions(+), 51 deletions(-) diff --git a/src/odb/src/3dblox/checker.cpp b/src/odb/src/3dblox/checker.cpp index 22067fd2633..f3d49546b5d 100644 --- a/src/odb/src/3dblox/checker.cpp +++ b/src/odb/src/3dblox/checker.cpp @@ -300,15 +300,19 @@ void Checker::checkBumpPhysicalAlignment(dbMarkerCategory* top_cat, if (!cat) { cat = dbMarkerCategory::createOrReplace(top_cat, "Bump Alignment"); } - auto* marker = dbMarker::create(cat); - marker->addSource(bump.bump_inst); - marker->addShape(Rect(p.x() - kBumpMarkerHalfSize, - p.y() - kBumpMarkerHalfSize, - p.x() + kBumpMarkerHalfSize, - p.y() + kBumpMarkerHalfSize)); - marker->setComment( - fmt::format("Bump is outside its parent region {}", - region.region_inst->getChipRegion()->getName())); + // dbMarker::create returns nullptr once the category hits its + // max_markers_ limit; skip the addSource/addShape/setComment + // chain to avoid a null-deref crash in that case. + if (auto* marker = dbMarker::create(cat)) { + marker->addSource(bump.bump_inst); + marker->addShape(Rect(p.x() - kBumpMarkerHalfSize, + p.y() - kBumpMarkerHalfSize, + p.x() + kBumpMarkerHalfSize, + p.y() + kBumpMarkerHalfSize)); + marker->setComment( + fmt::format("Bump is outside its parent region {}", + region.region_inst->getChipRegion()->getName())); + } } } } diff --git a/src/web/src/3d-viewer-widget.js b/src/web/src/3d-viewer-widget.js index 67ed16042eb..6fa17ea3025 100644 --- a/src/web/src/3d-viewer-widget.js +++ b/src/web/src/3d-viewer-widget.js @@ -3,7 +3,7 @@ import * as THREE from 'https://esm.sh/three@0.160.0'; -import {getThemeColors} from './theme.js'; +import {getThemeColors, setCookie} from './theme.js'; // Camera navigation tuning constants const kRotationSensitivity = 2.0; @@ -19,6 +19,7 @@ const kMinZNear = 10.0; const kZFarSafetyMargin = 10000; const kStackGapFactor = 0.15; const kDrcBlinkIntervalMs = 300; +const kDrcBlinkToggles = 6; const DEG2RAD = Math.PI / 180; // Distinct color palette for chiplets (saturated, good contrast in 3D) @@ -94,6 +95,9 @@ export class ThreeDViewerWidget { this._tooltip.className = 'three-d-tooltip'; this._canvasContainer.appendChild(this._tooltip); + this._optionsOverlay = this._createOptionsOverlay(); + this._canvasContainer.appendChild(this._optionsOverlay); + this._raycaster = new THREE.Raycaster(); this._scene = new THREE.Scene(); @@ -250,6 +254,11 @@ export class ThreeDViewerWidget { canvas.addEventListener('dblclick', (e) => { e.preventDefault(); + const markerId = this._pickDrcMarkerId(e); + if (markerId != null && this._app.drcWidget) { + this._app.drcWidget.highlightMarkerById(markerId, true); + return; + } this.resetCamera(); }); @@ -265,19 +274,45 @@ export class ThreeDViewerWidget { canvas.addEventListener('mouseleave', () => { this._hideTooltip(); }); } - _updateTooltip(event) { - if (!this._chipletMeshes.length) { - this._hideTooltip(); - return; - } + // Options overlay (top-left): toggles like "True Z" that affect how the + // scene is rendered. Mirrors app.useTrueZ so the cookie is the single + // source of truth across page reloads. + _createOptionsOverlay() { + const overlay = document.createElement('div'); + overlay.className = 'three-d-options'; + const trueZLabel = document.createElement('label'); + trueZLabel.title = 'Render chiplets at their true Z ' + + '(disable stacking offset for overlapping chiplets at same Z).'; + const trueZCb = document.createElement('input'); + trueZCb.type = 'checkbox'; + trueZCb.checked = !!this._app.useTrueZ; + trueZCb.addEventListener('change', () => { + this._app.useTrueZ = trueZCb.checked; + setCookie('or_use_true_z', trueZCb.checked ? '1' : '0'); + this.refreshSceneForOptions(); + }); + trueZLabel.appendChild(trueZCb); + trueZLabel.appendChild(document.createTextNode('True Z (no stacking)')); + overlay.appendChild(trueZLabel); + return overlay; + } + _setRaycasterFromEvent(event) { const canvas = this._renderer.domElement; const rect = canvas.getBoundingClientRect(); const ndc = new THREE.Vector2( ((event.clientX - rect.left) / rect.width) * 2 - 1, -(((event.clientY - rect.top) / rect.height) * 2 - 1)); - this._raycaster.setFromCamera(ndc, this._camera); + } + + _updateTooltip(event) { + if (!this._chipletMeshes.length) { + this._hideTooltip(); + return; + } + + this._setRaycasterFromEvent(event); const meshes = this._chipletMeshes.map((m) => m.mesh); const hits = this._raycaster.intersectObjects(meshes, false); if (hits.length === 0) { @@ -306,6 +341,16 @@ export class ThreeDViewerWidget { } } + _pickDrcMarkerId(event) { + if (!this._drcMeshGroup) return null; + const hitboxes = this._drcMeshGroup.children.filter( + c => c.userData && c.userData.isHitbox); + if (hitboxes.length === 0) return null; + this._setRaycasterFromEvent(event); + const hits = this._raycaster.intersectObjects(hitboxes, false); + return hits.length > 0 ? hits[0].object.userData.markerId : null; + } + // Quaternion-based rotation: axis perpendicular to mouse movement in // screen space, angle proportional to drag distance. _handleRotate(dx, dy) { @@ -580,6 +625,7 @@ export class ThreeDViewerWidget { buildScene(data) { if (this._destroyed) return; + this._lastSceneData = data; this._clearScene(); if (!data || !data.chiplets || data.chiplets.length === 0) @@ -626,8 +672,11 @@ export class ThreeDViewerWidget { } // Separate chiplets that share the same Z and overlap in XY by stacking - // them in slots. See computeZOffsetSlots(). - const zOffsets = computeZOffsetSlots(chiplets); + // them in slots. See computeZOffsetSlots(). When app.useTrueZ is on, skip + // the offset so chiplets render at their actual Z (may overlap). + const zOffsets = this._app.useTrueZ + ? new Array(chiplets.length).fill(0) + : computeZOffsetSlots(chiplets); chiplets.forEach((chiplet, idx) => { const width = chiplet.width / dbu; @@ -730,6 +779,10 @@ export class ThreeDViewerWidget { this._drcMaterial.dispose(); this._drcMaterial = null; } + if (this._drcHitboxMaterial) { + this._drcHitboxMaterial.dispose(); + this._drcHitboxMaterial = null; + } this._drcMeshGroup = null; this.render(); } @@ -764,6 +817,12 @@ export class ThreeDViewerWidget { depthWrite : false }); + this._drcHitboxMaterial = new THREE.MeshBasicMaterial({ + transparent : true, + opacity : 0, + depthWrite : false, + }); + for (const violation of violations) { const rects = (violation.rects && violation.rects.length > 0) ? violation.rects @@ -792,11 +851,15 @@ export class ThreeDViewerWidget { const boxGeometry = new THREE.BoxGeometry(width, height, currentDepth); const edgesGeometry = new THREE.EdgesGeometry(boxGeometry); - boxGeometry.dispose(); const line = new THREE.LineSegments(edgesGeometry, this._drcMaterial); line.position.set(cx, cy, cz); line.renderOrder = 999; this._drcMeshGroup.add(line); + + const hitbox = new THREE.Mesh(boxGeometry, this._drcHitboxMaterial); + hitbox.position.set(cx, cy, cz); + hitbox.userData = { markerId : violation.id, isHitbox : true }; + this._drcMeshGroup.add(hitbox); } } @@ -805,12 +868,30 @@ export class ThreeDViewerWidget { this.render(); let visible = true; + let toggles = 0; this._drcBlinkInterval = setInterval(() => { - if (this._drcMeshGroup && this._drcMaterial) { - visible = !visible; - this._drcMaterial.opacity = visible ? 1.0 : 0.2; + if (!this._drcMeshGroup || !this._drcMaterial) + return; + toggles += 1; + if (toggles >= kDrcBlinkToggles) { + this._drcMaterial.opacity = 1.0; this.render(); + clearInterval(this._drcBlinkInterval); + this._drcBlinkInterval = null; + return; } + visible = !visible; + this._drcMaterial.opacity = visible ? 1.0 : 0.2; + this.render(); }, kDrcBlinkIntervalMs); } + + refreshSceneForOptions() { + if (this._destroyed) + return; + if (this._lastSceneData) { + this.clearHighlightDRC(); + this.buildScene(this._lastSceneData); + } + } } diff --git a/src/web/src/drc-widget.js b/src/web/src/drc-widget.js index 4c9e9bbb2e3..fcf866a8cc4 100644 --- a/src/web/src/drc-widget.js +++ b/src/web/src/drc-widget.js @@ -307,10 +307,35 @@ export class DrcWidget { return row; } + highlightMarkerById(markerId, openInspector = false) { + const marker = this._findMarkerById(markerId); + if (marker) this._highlightMarker(marker, openInspector); + } + + _findMarkerById(markerId) { + const walk = (node) => { + if (!node) return null; + if (node.markers) { + for (const m of node.markers) { + if (m.id === markerId) return m; + } + } + if (node.subcategories) { + for (const sub of node.subcategories) { + const found = walk(sub); + if (found) return found; + } + } + return null; + }; + return walk(this._markerTree); + } + _highlightMarker(marker, openInspector = false) { this._app.websocketManager.request({ type: 'drc_highlight', - marker_id: marker.id + marker_id: marker.id, + open_inspector: openInspector, }).then(data => { if (data.ok && data.bbox) { marker.visited = true; @@ -321,8 +346,13 @@ export class DrcWidget { this._zoomToBBox(data.bbox); this._redrawAllLayers(); - if (openInspector && this._app.focusComponent) { - this._app.focusComponent('Inspector'); + if (openInspector) { + if (data.select_id != null && this._app.navigateInspector) { + this._app.navigateInspector(data.select_id); + } + if (this._app.focusComponent) { + this._app.focusComponent('Inspector'); + } } } }).catch(err => { diff --git a/src/web/src/inspector.js b/src/web/src/inspector.js index 1e1b3761046..2977eb74689 100644 --- a/src/web/src/inspector.js +++ b/src/web/src/inspector.js @@ -3,7 +3,7 @@ // Inspector panel — property tree, hover highlights, bbox display. -import { dbuToLatLng, dbuRectToBounds } from './coordinates.js'; +import { dbuRectToBounds } from './coordinates.js'; // SVG icons — distinct shapes so they're easy to tell apart at a glance. // Zoom to: magnifying glass with "+" (Material "zoom_in") @@ -475,5 +475,5 @@ export function createInspectorPanel(app, redrawAllLayers) { updateInspector(null); } - return { createInspector, updateInspector, highlightBBox }; + return { createInspector, updateInspector, highlightBBox, navigateInspector }; } diff --git a/src/web/src/main.js b/src/web/src/main.js index 1fe0330c632..081d7a7f61a 100644 --- a/src/web/src/main.js +++ b/src/web/src/main.js @@ -96,6 +96,7 @@ const app = { // display-controls.js once techData.chiplets arrives; null means // "render every chiplet" (single-chip designs). visibleChiplets: null, + useTrueZ: getCookie('or_use_true_z') === '1', heatMapData: null, activeHeatMap: '', heatMapLayer: null, @@ -546,6 +547,7 @@ const createInspector = inspector.createInspector; const updateInspector = inspector.updateInspector; const highlightBBox = inspector.highlightBBox; app.updateInspector = updateInspector; +app.navigateInspector = inspector.navigateInspector; function createBrowser(container) { new HierarchyBrowser(container, app, redrawAllLayers); diff --git a/src/web/src/request_handler.cpp b/src/web/src/request_handler.cpp index 299d921521e..de3f29c7900 100644 --- a/src/web/src/request_handler.cpp +++ b/src/web/src/request_handler.cpp @@ -2363,7 +2363,7 @@ void DRCHandler::refreshDRCOverlay(SessionState& state) constexpr int kDefaultMinBox = 200; min_box_ = kDefaultMinBox; if (block) { - odb::dbTech* tech = block->getDb()->getTech(); + odb::dbTech* tech = block->getTech(); if (tech) { for (odb::dbTechLayer* layer : tech->getLayers()) { if (layer->getType() == odb::dbTechLayerType::ROUTING) { @@ -2786,6 +2786,9 @@ WebSocketResponse DRCHandler::handleDRCHighlight(const WebSocketRequest& req, try { const int marker_id = static_cast(req.json.at("marker_id").as_int64()); + const bool open_inspector = req.json.contains("open_inspector") + ? req.json.at("open_inspector").as_bool() + : false; auto [block, chip] = getBlockAndChip(); odb::dbMarker* target = findMarkerById(state, chip, marker_id); @@ -2795,12 +2798,41 @@ WebSocketResponse DRCHandler::handleDRCHighlight(const WebSocketRequest& req, target->setVisited(true); odb::Rect bbox = target->getBBox(); - // Set highlight to the marker's bbox + // When the client requests inspector navigation, promote the marker to + // a canonical selectable so the existing `inspect` flow can populate + // the Inspector panel. Mirrors handleSelect's pattern (replace + // selectables, set current_inspected, clear navigation history) so + // back-navigation behaves the same as for instances/nets. + gui::Selected sel; + int marker_select_id = -1; + std::vector new_selectables; + if (open_inspector) { + sel = gui::DescriptorRegistry::instance()->makeSelected(target); + if (sel) { + marker_select_id = storeSelectable(new_selectables, sel); + } + } + { std::lock_guard lock(state.selection_mutex); state.highlight_rects.clear(); state.highlight_polys.clear(); - state.highlight_rects.push_back(bbox); + if (sel) { + state.hover_rects.clear(); + state.timing_rects.clear(); + state.timing_lines.clear(); + collectHighlightShapes( + sel, state.highlight_rects, state.highlight_polys); + state.current_inspected = sel; + state.navigation_history.clear(); + } else { + state.highlight_rects.push_back(bbox); + } + } + + if (sel) { + std::lock_guard lock(state.selectables_mutex); + state.selectables = std::move(new_selectables); } root["ok"] = 1; @@ -2810,6 +2842,9 @@ WebSocketResponse DRCHandler::handleDRCHighlight(const WebSocketRequest& req, if (odb::dbTechLayer* layer = target->getTechLayer()) { root["layer"] = std::string(layer->getName()); } + if (marker_select_id >= 0) { + root["select_id"] = marker_select_id; + } } else { // Clear highlight if marker_id is -1 (deselect) if (marker_id == -1) { diff --git a/src/web/src/style.css b/src/web/src/style.css index 4c8909e223d..f81b3006b19 100644 --- a/src/web/src/style.css +++ b/src/web/src/style.css @@ -1204,6 +1204,27 @@ html, body { display: none; } +.three-d-options { + position: absolute; + top: 8px; + left: 8px; + z-index: 100; + background: var(--bg-tooltip); + border: 1px solid var(--border-strong); + border-radius: 4px; + padding: 4px 8px; + font-size: 12px; + color: var(--fg-primary); + user-select: none; +} + +.three-d-options label { + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; +} + /* ─── Charts Widget ──────────────────────────────────────────────────── */ .charts-widget { diff --git a/src/web/src/tile_generator.cpp b/src/web/src/tile_generator.cpp index 5dec6a86541..f1ac007adba 100644 --- a/src/web/src/tile_generator.cpp +++ b/src/web/src/tile_generator.cpp @@ -596,7 +596,9 @@ std::vector TileGenerator::getLayers() const }; // Top-tech first so single-chip designs preserve the previous order. - collectFromTech(db_->getTech()); + // In multi-tech (3DBlox) designs getTech() returns nullptr; the chiplets() + // loop below contributes every layer through its own dbTech. + collectFromTech(getTech()); for (const ChipletNode& node : chiplets()) { if (node.chip) { collectFromTech(node.chip->getTech()); @@ -1122,9 +1124,10 @@ std::vector TileGenerator::renderTileBuffer( return world_image_buffer; } - // Per-layer colors mirror gui::DisplayControls so the GUI and web frontend - // agree on which color belongs to which layer. - const auto& layer_colors = getLayerColorMap(); + // Per-layer colors are resolved per chiplet below, using each chiplet's + // own dbTech. A single global getLayerColorMap() call returns an empty + // map in multi-tech (3DBlox) designs, which would paint every layer in + // the fallback gray. // Determine our tile's bounding box in dbu coordinates. const double num_tiles_at_zoom = pow(2, z); @@ -1172,12 +1175,23 @@ std::vector TileGenerator::renderTileBuffer( if (!tech) { continue; } + // Per-layer colors mirror gui::DisplayControls so the GUI and web + // frontend agree on which color belongs to which layer. Resolved per + // chiplet because each chiplet has its own dbTech in 3DBlox designs. + const auto& layer_colors = getLayerColorMap(tech); + // Translation-only fast path: the local tile is the world tile // shifted by -offset, and pixel coordinates land in the same place // because both shape coords and tile origin are in the same local // frame. Non-R0 orientations need full per-shape transforms; for // now we render them as if R0 (visible, but slightly misplaced). std::vector local_image_buffer; + // We only branch on the 2D part of the orient. 3DBlox "MZ" + // (mirror about Z) is stored as {orient_2d=R0, mirror_z_=true} in + // dbOrientType3D / dbTransform; in the XY plane that's the + // identity, so the R0 fast-path produces correct pixels. If + // future renderers need to react to mirror_z_ (e.g. flipped pin + // labels or 3D viewer parity) this branch is the place. bool use_local = (node.world_xfm.getOrient() != odb::dbOrientType::R0); if (use_local) { local_image_buffer.resize(kBufferSize, 0); @@ -2251,16 +2265,24 @@ std::vector TileGenerator::renderTileBuffer( // Overlays render once in world space, on top of all chiplets. // Their geometry (timing paths, DRC rects, flight lines) is already // expressed in world DBU and isn't tied to any single chiplet's - // local frame. route_guides keys on the top-chip tech layer. - odb::dbTech* world_tech = db_->getTech(); - odb::dbTechLayer* world_tech_layer - = world_tech ? world_tech->findLayer(layer.c_str()) : nullptr; + // local frame. route_guides keys on the top-chip tech layer; in + // multi-tech (3DBlox) designs there is no single top tech, so we + // search every tech for the requested layer name and use that tech's + // color map. Color world_color{.r = 200, .g = 200, .b = 200, .a = 180}; - if (world_tech_layer) { - const auto it = layer_colors.find(world_tech_layer); - if (it != layer_colors.end()) { + bool world_layer_found = false; + for (odb::dbTech* world_tech : db_->getTechs()) { + odb::dbTechLayer* world_tech_layer = world_tech->findLayer(layer.c_str()); + if (!world_tech_layer) { + continue; + } + world_layer_found = true; + const auto& world_layer_colors = getLayerColorMap(world_tech); + const auto it = world_layer_colors.find(world_tech_layer); + if (it != world_layer_colors.end()) { world_color = it->second; } + break; } if (!highlight_rects.empty() || !highlight_polys.empty()) { drawHighlight(world_image_buffer, @@ -2277,7 +2299,7 @@ std::vector TileGenerator::renderTileBuffer( drawFlightLines(world_image_buffer, flight_lines, dbu_tile_world, scale); } if (route_guide_net_ids && !route_guide_net_ids->empty() - && world_tech_layer) { + && world_layer_found) { drawRouteGuides(world_image_buffer, *route_guide_net_ids, layer, @@ -3572,8 +3594,6 @@ boost::json::object buildLayerHierarchy(odb::dbChip* chip, boost::json::object serializeTechResponse(const TileGenerator& gen) { boost::json::object out; - const auto& layer_colors = gen.getLayerColorMap(); - odb::dbTech* tech = gen.getTech(); boost::json::array layers; for (const auto& name : gen.getLayers()) { @@ -3581,16 +3601,25 @@ boost::json::object serializeTechResponse(const TileGenerator& gen) } out["layers"] = std::move(layers); + // Flat layer_colors array aligned with out["layers"]. In multi-tech + // (3DBlox) designs gen.getTech() is nullptr; resolve each name in the + // first tech that defines it, mirroring how the world overlay above + // looks up its color. The main frontend prefers layer_hierarchy.color, + // but this fallback keeps clients that consume the flat array correct. boost::json::array layer_color_arr; for (const auto& name : gen.getLayers()) { Color c{.r = 200, .g = 200, .b = 200, .a = 180}; - if (tech) { - if (odb::dbTechLayer* layer = tech->findLayer(name.c_str())) { - const auto it = layer_colors.find(layer); - if (it != layer_colors.end()) { - c = it->second; - } + for (odb::dbTech* t : gen.getDb()->getTechs()) { + odb::dbTechLayer* layer = t->findLayer(name.c_str()); + if (!layer) { + continue; } + const auto& cmap = gen.getLayerColorMap(t); + auto it = cmap.find(layer); + if (it != cmap.end()) { + c = it->second; + } + break; } layer_color_arr.emplace_back(boost::json::array{ static_cast(c.r), static_cast(c.g), static_cast(c.b)}); diff --git a/src/web/src/tile_generator.h b/src/web/src/tile_generator.h index e38da8fd372..3b1b5ce5ef7 100644 --- a/src/web/src/tile_generator.h +++ b/src/web/src/tile_generator.h @@ -250,6 +250,7 @@ class TileGenerator odb::dbBlock* getBlock() const; odb::dbChip* getChip() const; odb::dbTech* getTech() const; + odb::dbDatabase* getDb() const { return db_; } // Cached, sorted list of chiplets reachable from db_->getChip(). // The cache is invalidated by eagerInit() and rebuilt lazily on the diff --git a/src/web/src/web.cpp b/src/web/src/web.cpp index f583870a1ca..0da5722a972 100644 --- a/src/web/src/web.cpp +++ b/src/web/src/web.cpp @@ -438,7 +438,10 @@ void WebSocketSession::on_accept(beast::error_code ec) // Only send refresh if there's actually a design to render. // Without this guard, eagerInit returns instantly when no block is // loaded and the push races with async_accept (Beast soft_mutex crash). - if (!self->generator_->getBlock()) { + // We gate on the dbChip (not dbBlock) so 3DBlox multi-tech designs + // — whose top chip is HIER and has no dbBlock — still register the + // chip observer and send the refresh notification. + if (!self->generator_->getChip()) { return; }