Skip to content

Comments

feat(gui): Enhance 3D Viewer with filled faces, shading, and selectio…#9532

Open
openroad-ci wants to merge 1 commit intoThe-OpenROAD-Project:masterfrom
The-OpenROAD-Project-staging:test-feature-3D-checker
Open

feat(gui): Enhance 3D Viewer with filled faces, shading, and selectio…#9532
openroad-ci wants to merge 1 commit intoThe-OpenROAD-Project:masterfrom
The-OpenROAD-Project-staging:test-feature-3D-checker

Conversation

@openroad-ci
Copy link
Collaborator

fix the issue #9412

Visual Improvements (3D Viewer): * Upgraded rendering from wireframes to solid polygons for real chiplet volume.

Added Z-depth sorting (Painter's Algorithm) for proper occlusion.

Implemented face-normal shading for 3D depth perception.

Changed the default color palette from Yellow to Teal.

Selection Synchronization: * Mapped 3D faces to database objects (odb::dbObject*).

Selections are now fully synchronized across the 3D Viewer, Inspector, DRC Viewer, and MainWindow (Layout Viewer).

Stability (Bugfix): * Fixed a std::out_of_range crash in renderThread.cpp.

Replaced unsafe map::at() calls with .find() to safely handle new or unregistered layout objects during asynchronous rendering.

…n sync

- Fix map::at out of range exception in renderThread
- Apply clang-format to chiplet3DWidget, mainWindow, and renderThread

Signed-off-by: Jorge Ferreira <jorge.ferreira@precisioninno.com>
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request introduces significant visual enhancements to the 3D Viewer, including filled polygons, depth sorting (Painter's Algorithm), and flat shading. It also implements selection synchronization between the 3D Viewer and other GUI components. A stability fix for an asynchronous rendering crash in renderThread.cpp is included. While the visual improvements are valuable, there are several performance concerns in the new rendering logic, particularly regarding redundant vertex transformations and inefficient selection set processing within the paintEvent. Additionally, a regression was identified where connection region checks in the 3D Blox checker were disabled. Adherence to C++ coding standards (using C++ casts instead of C-style casts) and avoiding magic numbers should also be addressed.

Comment on lines 233 to 236
void Checker::checkConnectionRegions(dbMarkerCategory* top_cat,
const UnfoldedModel& model)
{
auto describe = [](const UnfoldedRegion* r, dbMarker* marker) {
marker->addSource(r->region_inst);
marker->addShape(Rect(r->cuboid.xMin(),
r->cuboid.yMin(),
r->cuboid.xMax(),
r->cuboid.yMax()));
return fmt::format("{}/{} (faces {})",
r->parent_chip->name,
r->region_inst->getChipRegion()->getName(),
sideToString(r->effective_side));
};
int count = 0;
dbMarkerCategory* cat = nullptr;
for (const auto& conn : model.getConnections()) {
if (!isValid(conn)) {
if (!cat) {
cat = dbMarkerCategory::createOrReplace(top_cat, "Connection regions");
}
auto* marker = dbMarker::create(cat);
marker->addSource(conn.connection);
std::string msg = fmt::format("Invalid connection {}: {} to {}",
conn.connection->getName(),
describe(conn.top_region, marker),
describe(conn.bottom_region, marker));
marker->setComment(msg);
logger_->warn(utl::ODB, 207, msg);
count++;
}
}
if (count > 0) {
logger_->warn(utl::ODB, 273, "Found {} invalid connections", count);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The logic for checkConnectionRegions has been completely removed, leaving an empty function. This check is essential for validating 3D Blox connections (XY overlap, mating surfaces, thickness). Disabling this check is a significant regression in the checker's functionality.

for (int j = 0; j < 4; ++j) {
const uint32_t idx = indices_faces_[i + j];
if (idx < vertices_.size()) {
const QVector3D view_pos = modelView * vertices_[idx].position;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Transforming vertices inside the face loop is inefficient as vertices are shared between multiple faces. It is better to transform all vertices once into a temporary buffer before starting the face loop to avoid redundant matrix-vector multiplications.

if (obj) {
odb::dbBlock* chiplet_block = nullptr;
if (obj->getObjectType() == odb::dbChipObj) {
chiplet_block = ((odb::dbChip*) obj)->getBlock();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using C-style casts. Use static_cast for better type safety and clarity when the object type is already known.

            chiplet_block = static_cast<odb::dbChip*>(obj)->getBlock();

}

// Check selection set
for (const auto& sel : selection_) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Iterating through the entire selection_ set for every face of every chiplet in every frame is O(N_faces * N_selection) and can lead to performance issues if the selection set is large. Consider pre-calculating a set of selected odb::dbObject* pointers at the beginning of paintEvent to allow for O(1) lookups inside the loop.

Comment on lines +300 to +333
auto* marker = std::any_cast<odb::dbMarker*>(any_obj);
// Check ownership
if (chiplet_block) {
auto* category = marker->getCategory();
if (category) {
auto* parent = category->getParent();
if (parent == chiplet_block) {
is_highlighted = true;
break;
}
}
}
// Check sources
for (odb::dbObject* src : marker->getSources()) {
if (src == obj) {
is_highlighted = true;
break;
}
// Check name match for ChipInst vs Inst
if (obj->getObjectType() == odb::dbChipInstObj
&& src->getObjectType() == odb::dbInstObj) {
const auto* chip_inst = (odb::dbChipInst*) obj;
const auto* inst = (odb::dbInst*) src;
if (chip_inst && inst
&& chip_inst->getName() == inst->getName()) {
is_highlighted = true;
break;
}
}
}
if (is_highlighted) {
break;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for determining if a dbMarker selection applies to a chiplet is complex and duplicated for both the selection set and the focus object. This logic should be encapsulated in a helper function to improve maintainability and reduce code duplication.

References
  1. Encapsulate the logic for calculating master symmetry in a helper function to avoid code duplication.


// Transform to View Space
for (auto& p : current_poly) {
p = modelView * p;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The vertices are being transformed by modelView again here, even though they were already transformed for depth sorting in paintEvent. Passing view-space coordinates to drawFace3D would avoid redundant matrix-vector multiplications.

// 1. Fixed Light Direction (in View Space)
// We assume light comes from top-right-front relative to camera.
// Normalized vector: (1, 1, 1) -> (0.577, 0.577, 0.577)
const QVector3D light_dir = QVector3D(0.5f, 0.5f, 1.0f).normalized();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using hardcoded magic numbers for the light direction. Define it as a named constant.

References
  1. Define tunable parameters as named constants instead of using hardcoded magic numbers.

int end_col);
bool isSegmentEmpty(std::vector<std::vector<bool>>& grid,
int start_row,
int tart_col,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Typo in parameter name: tart_col should be start_col to match the implementation and maintain consistency.

Suggested change
int tart_col,
int start_col,

if (expanded_vicinity.total() > current_vicinity.total()
|| expanded_vicinity == current_vicinity) {
current_vicinity = expanded_vicinity;
&& isSegmentEmpty(grid, start_row, start_col, end_row, end_col)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Checking the entire segment from start_row to end_row is redundant because the previous rows were already verified to be empty in previous iterations of the while loop. Only the new row end_row needs to be checked for efficiency.

Suggested change
&& isSegmentEmpty(grid, start_row, start_col, end_row, end_col)) {
&& isSegmentEmpty(grid, end_row, start_col, end_row, end_col)) {
References
  1. When analyzing code within a loop, consider the entire loop structure. Operations at the beginning of the loop (e.g., journalBegin) can affect the logic within the loop body, potentially making similar calls inside the loop redundant.

Comment on lines +824 to +830
auto obs_filter = [this](const ShapePtr& other) -> bool {
if (other->shapeType() != Shape::GRID_OBS) {
return true;
}
const GridObsShape* shape = static_cast<GridObsShape*>(other.get());
return !shape->belongsTo(this);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Helper logic should be defined as a free function in a namespace rather than as a local lambda within a function to improve reusability and adhere to the project's coding standards.

References
  1. Helper logic should be defined as a free function in a namespace rather than as a local lambda within a function.

@github-actions
Copy link
Contributor

clang-tidy review says "All clean, LGTM! 👍"

Comment on lines +253 to +262
// Sort back-to-front (smaller Z in view space is further away if looking down
// -Z, but standard OpenGL view is looking down -Z. View space Z usually is
// negative in front of camera. So "further away" means more negative. Wait,
// let's check coordinate system.
// Standard: Camera at origin looking at -Z.
// Objects at Z=-10 are further than Z=-5.
// We want to draw -10 first. So sort ascending Z (more negative first).
std::ranges::stable_sort(
sorted_faces,
[](const FaceDepth& a, const FaceDepth& b) { return a.depth < b.depth; });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a great way to implement depth based rendering. It's not always possible to sort triangles by depth. Example:

Image

Instead you want to build a Z-buffer, which you store the depth of the closest pixel you've seen so far. If you see another triangle with a depth deeper than the current value in the zbuffer you continue.

https://haqr.eu/tinyrenderer/z-buffer/

@QuantamHD
Copy link
Collaborator

It looks like some of this code might have come from an LLM, overall it looks pretty good, but I would suggest restructuring the rendering code into a vertex and fragment shader. Right now you're mixing the two in the drawFaces3D function.

In most rendering pipelines you want a separation of concerns.

@QuantamHD
Copy link
Collaborator

Another reason to use the Z-Buffer is to ensure that the grid is correctly rendered with depth. Right now it looks like the grid is below both chips, but the reality is the grid is between the two chips.

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants