/******************************************************************************** * * * This file is part of IfcOpenShell. * * * * IfcOpenShell is free software: you can redistribute it and/or modify * * it under the terms of the Lesser GNU General Public License as published by * * the Free Software Foundation, either version 3.0 of the License, or * * (at your option) any later version. * * * * IfcOpenShell is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * Lesser GNU General Public License for more details. * * * * You should have received a copy of the Lesser GNU General Public License * * along with this program. If not, see . * * * ********************************************************************************/ #ifndef VIEWPORTWINDOW_H #define VIEWPORTWINDOW_H #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE class QTimer; QT_END_NAMESPACE #include #include #include #include #include #include #include #include #include #include #include "BvhAccel.h" #include "InstancedGeometry.h" #include "OverlayRenderer.h" #include "Selection.h" #include "SidecarCache.h" // Matches GL_DRAW_INDIRECT_BUFFER layout for glMultiDrawElementsIndirect. struct DrawElementsIndirectCommand { uint32_t count; uint32_t instanceCount; uint32_t firstIndex; uint32_t baseVertex; uint32_t baseInstance; }; // Per-model GPU state for the instanced render path. // // VBO: local-coord interleaved verts (pos3 + normal3 + color1_packed) — 28 B. // EBO: mesh-local indices (uint32). // meshes[]: per-unique-representation metadata; indexed by local_mesh_id. // instances[]: CPU-side per-instance records; sorted by mesh_id at finalize. // ssbo: InstanceGpu[]; populated at finalize. // // A model is drawable once `finalized == true`. struct ModelGpuData { GLuint vao = 0; GLuint vbo = 0; GLuint ebo = 0; GLuint ssbo = 0; GLuint mesh_info_ssbo = 0; // MeshGpu[] — per-mesh quantization basis size_t mesh_info_capacity = 0; // bytes size_t vbo_capacity = 0; size_t ebo_capacity = 0; size_t ssbo_capacity = 0; // bytes size_t vbo_used = 0; size_t ebo_used = 0; uint32_t vertex_count = 0; // total (across all meshes) uint32_t total_triangles = 0; std::vector meshes; std::vector instances; // unsorted // 1:1 with instances[] — true when the instance transform has // det < 0 (a reflection). Reflected instances need their // triangle winding treated as reversed so GL_CULL_FACE culls // the correct side. std::vector instance_reflected; uint32_t ssbo_instance_count = 0; // Stats snapshot from the last cullAndUploadVisible call. Cached so we // can report the same numbers on skipped-cull frames (see // have_cached_cull_ on ViewportWindow) without iterating the per-model // scratch array again. uint32_t cached_visible_objects = 0; uint32_t cached_visible_triangles = 0; // Per-instance world AABB + BVH (built at finalize). The BVH is the // same ordering as `instances`; bvh_items[i] corresponds to instances[i]. std::vector bvh_items; ModelBvh bvh; // Dynamic visible-instance index buffer (std430, binding = 1). // Re-uploaded each frame from visible_flat_. GLuint visible_ssbo = 0; size_t visible_ssbo_capacity = 0; // bytes // GL_DRAW_INDIRECT_BUFFER of DrawElementsIndirectCommand[], one per // non-empty mesh. Re-uploaded each frame. GLuint indirect_buffer = 0; size_t indirect_capacity = 0; // bytes uint32_t indirect_command_count = 0; // total valid commands this frame uint32_t indirect_forward_count = 0; // first N are CCW-winding draws // Per-model cull scratch — owned by the model so each cull job runs // without sharing mutable state. Four buckets = {fwd, rev} × {LOD0, LOD1}. std::vector> vis_fwd_lod0; std::vector> vis_fwd_lod1; std::vector> vis_rev_lod0; std::vector> vis_rev_lod1; std::vector visible_flat; std::vector indirect_scratch; std::vector dirty_meshes; bool finalized = false; bool hidden = false; // Per-model federation-pipeline matrices in metres. Default identity // → no per-model contribution to the composed transform. See // Federation.h for the full pipeline composition order. Eigen::Matrix4d coordinate_operation_meters = Eigen::Matrix4d::Identity(); Eigen::Matrix4d model_transformation_meters = Eigen::Matrix4d::Identity(); }; // Rendering is event-driven: render() runs only when QEvent::UpdateRequest // is delivered, posted via requestUpdate(). An idle scene costs zero CPU. // INVARIANT: every public mutator that changes what should be on screen // (camera, selection, model lifecycle, visibility) MUST call requestUpdate() // before returning, or the viewport will go silently stale. class ViewportWindow : public QWindow { Q_OBJECT public: explicit ViewportWindow(QWindow* parent = nullptr); ~ViewportWindow(); // Streaming ingress. void uploadMeshChunk(const MeshChunk& chunk); void uploadInstanceChunk(const InstanceChunk& chunk); // Called once all chunks for a model have arrived: sorts instances by // mesh_id, assigns each mesh its contiguous range, and uploads the // instance SSBO. The model becomes drawable. void finalizeModel(uint32_t model_id); void resetScene(); // Snapshot the finalised model into a SidecarData struct for caching. // Vertices + indices are read back from the GPU; meshes/instances come // from the CPU-side vectors. Leaves `elements` and `string_table` empty // for the caller to fill in. bool snapshotModel(uint32_t model_id, SidecarData& out) const; // Restore a finalised model from a cached SidecarData struct. Replaces // any existing state for model_id and marks it drawable. void applyCachedModel(uint32_t model_id, SidecarData data); // After buildLods() has extended sd.indices + populated lod1_* fields, // push just the appended index slice + the refreshed mesh metadata onto // the live GPU state for model_id. VBO / SSBO / instance array are left // alone; only the EBO grows and m.meshes is replaced. No-op if the // model isn't finalised on the viewport. void applyLodExtension(uint32_t model_id, const SidecarData& sd); void hideModel(uint32_t model_id); void showModel(uint32_t model_id); void removeModel(uint32_t model_id); // Debug helper: walk to the currently selected object's instance and // qInfo a sample vertex (decoded from the VBO), the placement // transformation matrix, and the global matrix // (CoordinateOperation · placement_transformation), in metres. void printSelectedObjectCoords(); // Mesh-local CPU triangles read back from the VBO/EBO. Positions are // dequantised against the mesh's local AABB; units match the streamer // (metres). Indices are mesh-local (0..vertex_count-1). struct MeshTriangles { std::vector positions; // 3 * vertex_count std::vector indices; // 3 * triangle_count }; // Lazy GPU readback of one mesh's triangles. Fails if model_id / // mesh_id aren't live, the model isn't finalised, or GL isn't ready. // Stalls the GL pipeline for the readback — call from the main thread // and not from inside render(). bool readbackMeshTriangles(uint32_t model_id, uint32_t mesh_id, MeshTriangles& out); // Pure CPU lookup: object_id → owning model + mesh + raw streamer // placement matrix (column-major, pre-CoordinateOperation / // FederatedFalseOrigin / ModelTransformation). struct InstanceLookup { uint32_t model_id = 0; uint32_t mesh_id = 0; float placement_transformation[16]{}; }; bool findInstance(uint32_t object_id, InstanceLookup& out) const; // Pick + resolve to mesh-local space. Runs pickSurfaceAt to get the // world-space hit, then inverts the instance's composed transform // (FederatedFalseOrigin · ModelTransformation · CoordinateOperation // · placement_transformation) to express the hit in the mesh's own // coordinates — what readbackMeshTriangles returns. Returns false if // the click missed geometry or its object_id has no live instance. struct MeshLocalPick { uint32_t object_id = 0; uint32_t model_id = 0; uint32_t mesh_id = 0; float mesh_local[3] = {0, 0, 0}; float world_pos[3] = {0, 0, 0}; float world_normal[3]= {0, 0, 0}; // The instance's composed (FederatedFalseOrigin · ModelTransformation // · CoordinateOperation · placement_transformation) matrix in // column-major form. Mesh-local positions × this = world. Caching // by callers becomes stale if any federation matrix is later edited. float composed_transform[16] = {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1}; }; bool pickMeshLocalAt(int x, int y, MeshLocalPick& out); // CPU raycast against the per-model BVHs. Walks the BVH for each // finalised model, transforms the world ray into mesh-local space // for each candidate instance, reads back its triangles, and runs // Möller-Trumbore against them. Returns the closest hit overall. // // `dir` MUST be a unit vector — distance is reported as the t value // along the ray, which equals world distance only when |dir|=1. // Stalls the GL pipeline once per unique (model, mesh) candidate // because triangle data is read back lazily; budget ~1ms for // typical BIM scenes. struct RaycastHit { uint32_t object_id = 0; float distance = 0.0f; float world_pos[3] = {0, 0, 0}; float world_normal[3]= {0, 0, 0}; }; bool raycast(const float origin[3], const float dir[3], RaycastHit& out); // Measurement tool modes. While any tool is active, LMB clicks emit // surfacePickedInTool with the click coordinates (instead of swapping // object selection); the app interprets them per-tool. Esc exits the // active tool. Backspace/Delete in length mode emits // toolBackspacePressed for "remove last point" semantics. enum class ToolMode { None, Area, Length }; Q_ENUM(ToolMode) void toggleAreaTool(); void toggleLengthTool(); void setToolMode(ToolMode mode); ToolMode toolMode() const { return tool_mode_; } // Replace the overlay highlight-triangle list rendered after the main // pass. `world_xyz` is 3 floats per vertex, 3 verts per triangle, in // world space. Empty disables the overlay. Triggers a viewport // update so the change becomes visible immediately. void setHighlightTriangles(const std::vector& world_xyz, float r, float g, float b, float a); // Replace the overlay line groups. Each group has its own segments // + style (color/halo/width/dash) — see OverlayRenderer::LineGroup. // Empty disables every line. void setOverlayLines(const std::vector& groups); // Replace the overlay-point list (3 floats per point, world space). // pixel_size is the inner-disc diameter in physical pixels; when // stroke_a > 0 the sprite is enlarged by 2*stroke_extra to draw a // halo around the inner disc — proper outlined points via the // sprite shader (no two-pass glPointSize trickery). void setOverlayPoints(const std::vector& world_xyz, float r, float g, float b, float a, float pixel_size, float stroke_r, float stroke_g, float stroke_b, float stroke_a, float stroke_extra); // Replace the world-anchored label list — projected to screen space // and drawn via QPainter inside the same overlay pass as the HUD. // Used today for per-segment length readouts. void setOverlayLabels(const std::vector& labels); // Multi-line HUD text drawn via QPainter on top of the GL surface at // the end of each frame. Empty hides the HUD. Newlines split the // string into separate rows in the same translucent box. void setHudText(const QString& text); // Federation pipeline: composed instance transform = // FederatedFalseOrigin · ModelTransformation · CoordinateOperation // · placement_transformation // setFederatedFalseOrigin affects every model; the per-model setters // affect a single model. Each setter rewrites the SSBO, recomputes // world AABBs, rebuilds the BVH, and posts an update. Defaults are // identity, so until a setter is called the composed transform equals // placement_transformation. void setFederatedFalseOrigin(const Eigen::Matrix4d& matrix_meters); void setModelCoordinateOperation(uint32_t model_id, const Eigen::Matrix4d& matrix_meters); void setModelTransformation(uint32_t model_id, const Eigen::Matrix4d& matrix_meters); // Selection. The viewport owns a SelectionState that tracks both // the multi-set and the "active" (last single-clicked) id. External // callers (MainWindow / tree sync) drive selection through it. SelectionState& selection() { return selection_; } const SelectionState& selection() const { return selection_; } // Convenience: replace the selection with {id} (or clear if id==0). // Used by callers that have not yet been ported off the single-id API. void setSelectedObjectId(uint32_t id); uint32_t pickObjectAt(int x, int y); // Render the pick pass and collect every distinct non-zero object_id // covered by the pixels inside `rect` (logical coords, top-left origin). // Used by the box-select drag. Returns an empty set if rect is empty // or off-surface. std::unordered_set picksInRect(const QRect& rect); // Extended pick: returns the object id, world-space hit point, and // world-space surface normal at (x, y). Renders the same pick pass as // pickObjectAt but reads back from two extra color attachments // (world_pos in RGB32F, world_normal in RGB16F). Returns false if the // click missed all geometry; the out-params are then untouched. bool pickSurfaceAt(int x, int y, uint32_t& object_id_out, QVector3D& world_pos_out, QVector3D& world_normal_out); // Section planes — fragment-shader clipping with up to MaxSectionPlanes // active. Each plane clips the half-space dot(n, p) + d > 0. addSection- // PlaneAtSurface auto-flips the normal toward the camera so the first // click immediately cuts away the camera-facing side. static constexpr int MaxSectionPlanes = 8; struct SectionPlane { QVector3D n; // unit world-space normal QVector3D origin; // point on the plane — the gizmo's anchor float d; // = -dot(n, origin); kept in sync with origin }; int sectionPlaneCount() const { return int(section_planes_.size()); } bool addSectionPlaneAtSurface(const QVector3D& point, const QVector3D& normal); void removeSectionPlane(int index); void clearSectionPlanes(); // Section tool: when active, LMB on geometry creates a new plane (using // pickSurfaceAt for hit-point + normal); LMB on an existing plane's // arrow gizmo selects + drags it; Delete removes the selected plane; // Esc or another K-press exits the tool. void toggleSectionTool(); bool sectionToolActive() const { return section_tool_active_; } // Projection: orthographic vs perspective. In ortho mode the visible // box is sized to match what the perspective camera would show at the // pivot's distance, so toggling at any zoom level keeps the framing. void toggleProjection(); bool projectionOrtho() const { return projection_ortho_; } // Snap the camera to a canonical axis-aligned view. Yaw/pitch are // clamped according to the orbit convention; target and distance are // preserved (the user explicitly asked for a rotate-only behavior). void setStandardView(float yaw_deg, float pitch_deg); void setCamera(float tx, float ty, float tz, float dist, float yaw, float pitch); void setBenchmarkFrames(int n); QString cameraString() const; // Move camera_target_ to the selected object's world-AABB centroid and // dolly camera_distance_ so the object's bounding sphere fits the // current viewport. Yaw/pitch are preserved. No-op if no object is // selected or its AABB is unknown. void focusOnSelectedObject(); // Frame the union of all finalized models. No-op if the scene is empty. void viewAll(); struct CameraState { QVector3D target; float distance; float yaw; // degrees float pitch; // degrees }; CameraState cameraState() const; struct FrameStats { float fps; float frame_time_ms; uint32_t total_objects; uint32_t visible_objects; uint32_t total_triangles; uint32_t visible_triangles; uint32_t unique_meshes; uint32_t gl_draw_calls; // actual glMultiDrawElementsIndirect issues per frame uint32_t indirect_sub_draws; // total commands packed into those indirect buffers }; signals: void objectPicked(uint32_t object_id); void initialized(); void frameStatsUpdated(const ViewportWindow::FrameStats& stats); // Emitted instead of objectPicked when any measurement tool is active. // The app branches on toolMode() to decide what to do, calls // pickMeshLocalAt(x, y, ...) for hit details, and accumulates. // modifiers carries Qt::KeyboardModifiers at click time (Alt etc.). void surfacePickedInTool(int x, int y, int modifiers); // Emitted whenever the active tool changes (incl. on→off transitions). // The app uses this to reset accumulator state on entry/exit. void toolModeChanged(ViewportWindow::ToolMode mode); // Backspace/Delete pressed while a tool is active. Used by the length // tool to remove the last point; other tools may ignore it. void toolBackspacePressed(); protected: void exposeEvent(QExposeEvent* event) override; void resizeEvent(QResizeEvent* event) override; void keyPressEvent(QKeyEvent* event) override; void keyReleaseEvent(QKeyEvent* event) override; bool event(QEvent* event) override; private: enum class PendingOpType { UploadMeshChunk, UploadInstanceChunk, FinalizeModel, ApplyCachedModel, ApplyLodExtension, ResetScene, HideModel, ShowModel, RemoveModel, }; struct PendingOperation { PendingOpType type; MeshChunk mesh_chunk; InstanceChunk instance_chunk; SidecarData sidecar_data; uint32_t model_id = 0; }; void initGL(); void flushPendingOperations(); void enqueuePendingOperation(PendingOperation op); void render(); void renderPickPass(); void renderAxisGizmo(); void renderPivotIndicator(); void renderSectionPlanes(); void buildSectionPlaneGizmo(); // Post-process edge enhancement: resolve MSAA depth into a single- // sample texture, then run a fullscreen pass that detects sharp // depth-laplacian peaks and darkens the colour buffer there. Catches // silhouettes and overlapping-surface boundaries as faint dark lines. void renderEdgePass(); // Returns the index of the section plane whose arrow gizmo is under // (x, y), or -1 if none. Screen-space line-segment distance test. int hitTestSectionGizmo(int x, int y) const; // Update section_planes_[section_drag_index_] from the current cursor // position by projecting the move onto the plane's normal axis in // screen space. void updateSectionDrag(int x, int y); void updateCamera(); // Geometry queries used by focusOnSelectedObject() / viewAll(). Both // return false when nothing matched (caller should leave the camera // alone). Bounds are world-space AABBs. bool computeObjectAabb(uint32_t object_id, QVector3D& mn, QVector3D& mx) const; bool computeSceneAabb(QVector3D& mn, QVector3D& mx) const; // Re-aim the orbit camera so the bounding sphere of [mn, mx] just fits // vertically and horizontally within the current FOV, with `padding` // headroom (1.0 = tight). Yaw/pitch are preserved; only target and // distance change. void frameAabb(const QVector3D& mn, const QVector3D& mx, float padding); void buildShaders(); void buildAxisGizmo(); void buildPivotIndicator(); // Show/hide the orbit-pivot marker. hide_after_ms > 0 starts a single-shot // timer that auto-hides — used by the wheel handler to give the marker a // short afterglow after zoom. Drag-based callers pass 0 and toggle // visibility manually on press/release. void setPivotIndicatorVisible(bool visible, int hide_after_ms = 0); void setupVaoLayout(GLuint vao, GLuint vbo, GLuint ebo); // Resolve the default framebuffer's MSAA depth into a single-sample // texture, read it back, and max-reduce a mip pyramid on the CPU. The // resulting pyramid is stored in hiz_pyramid_ along with the VP matrix // used to draw it; next frame's cullAndUploadVisible can test AABBs // against it. Synchronous readback — at 256×128 the cost is sub-ms // and not a measured bottleneck; Phase 3D's compute-shader cull will // eliminate the readback entirely. void buildHizPyramid(); // True if the AABB is fully occluded by the previous frame's depth. // Returns false when the HiZ is invalid, the AABB crosses the near // plane, or the projection falls outside NDC. bool aabbOccludedByHiz(const float mn[3], const float mx[3]) const; bool growModelVbo(ModelGpuData& m, size_t needed_total); bool growModelEbo(ModelGpuData& m, size_t needed_total); bool growModelSsbo(ModelGpuData& m, size_t needed_total); ModelGpuData& getOrCreateModel(uint32_t model_id); // Frustum-cull m's instances (BVH if available, else linear scan), // build the per-mesh DrawElementsIndirectCommand array + flat visible // list, and upload both to m.indirect_buffer / m.visible_ssbo. // // `min_pixel_radius` controls contribution culling: instances (and BVH // subtrees) whose projected bounding-sphere radius would be below this // many pixels are dropped. 0 = disabled (all frustum-visible kept), // which is what the pick pass uses so clickable targets aren't filtered. void cullAndUploadVisible(ModelGpuData& m, const float planes[6][4], float focal_px, float min_pixel_radius); // Thread-safe: CPU-only cull (frustum + contribution + HiZ + bucketing + // emit). Writes survivors into m.vis_* / m.visible_flat / m.indirect_scratch // and sets m.indirect_forward_count / m.indirect_command_count / // m.cached_visible_*. Touches no GL state and no ViewportWindow mutable // state other than the atomic counters below — safe to run on a worker. void cullModelCpu(ModelGpuData& m, const float planes[6][4], float focal_px, float min_pixel_radius); // Main-thread only: uploads m.visible_flat / m.indirect_scratch into the // model's SSBO + indirect buffer, growing them if needed. void uploadCullResults(ModelGpuData& m); // Compose // FederatedFalseOrigin · ModelTransformation · CoordinateOperation // · placement_transformation // for one instance: writes inst.transform (composed, float) and // recomputes inst.world_aabb_* from the composed transform + the // mesh's local AABB. Maths runs in double; narrow to float at the end. void composeInstanceFromPlacement(InstanceCpu& inst, const ModelGpuData& m) const; // Walk every instance of `model_id`, recompose `transform` from // `placement_transformation` + current stage matrices, recompute world // AABBs, re-upload the InstanceGpu SSBO, refresh instance_reflected, // and rebuild the BVH. Posts an update. No-op if model_id is unknown // or GL isn't initialised yet. void recomposeAndUploadModel(uint32_t model_id); // Mouse interaction void handleMousePress(QMouseEvent* event); void handleMouseRelease(QMouseEvent* event); void handleMouseMove(QMouseEvent* event); void handleWheel(QWheelEvent* event); // FPS/fly mode. Toggled with Shift+F; exits on any mouse click or Esc. // While active, WASD translates the camera in view-space, Q/E moves // world-down/up, mouse rotates the view (cursor hidden + recentered each // move), Shift accelerates, and the wheel scales the base move speed // instead of zooming. The underlying orbit state is preserved: movement // translates camera_target_ and rotation re-pins it so camera_eye_ stays // put, so exiting drops the user back into orbit at the same viewpoint. // // Movement is integrated inside render() using wall-clock dt and the // next frame is requestUpdate()'d while any movement key is held — that // way one long frame only produces a single catch-up step instead of // also missing a QTimer tick. enum class CameraMode { Orbit, Fps }; void enterFpsMode(); void exitFpsMode(); void fpsIntegrate(); // called from render() void recenterFpsCursor(); QOpenGLContext* context_ = nullptr; QOpenGLFunctions_4_5_Core* gl_ = nullptr; bool gl_initialized_ = false; std::deque pending_ops_; // Shaders GLuint main_program_ = 0; GLuint pick_program_ = 0; GLuint axis_program_ = 0; // Axis gizmo GLuint axis_vao_ = 0; GLuint axis_vbo_ = 0; // Orbit-pivot indicator: 1 center vertex + (N+1) rim vertices on the unit // circle (last == first to close the triangle fan). Rendered as a screen- // space disc at camera_target_, visible only while the user is navigating. GLuint pivot_program_ = 0; GLuint pivot_vao_ = 0; GLuint pivot_vbo_ = 0; int pivot_rim_count_ = 0; // Per-model GPU data std::unordered_map models_gpu_; // FederatedFalseOrigin matrix, in metres. Default identity → no // contribution to the composed transform. See Federation.h. Eigen::Matrix4d federated_false_origin_meters_ = Eigen::Matrix4d::Identity(); // Pick framebuffer. Three color attachments: // 0: R32UI — object_id // 1: RGB32F — world position at hit // 2: RGB16F — world normal at hit (already flipped for reflections in // the vertex shader) GLuint pick_fbo_ = 0; GLuint pick_color_tex_ = 0; GLuint pick_pos_tex_ = 0; GLuint pick_normal_tex_ = 0; GLuint pick_depth_rbo_ = 0; int pick_width_ = 0; int pick_height_ = 0; // HiZ occlusion culling (Phase 3C). // // Each frame after the main draw we blit the MSAA depth buffer down // into a single-sample depth texture (hiz_fbo_ / hiz_depth_tex_), then // glReadPixels it into hiz_depth_readback_. We max-reduce that into a // mip pyramid (hiz_pyramid_) and remember the VP matrix used // (hiz_vp_ + hiz_vp_valid_) so next frame's cull can test AABBs // against a slightly-stale depth. Skipped for the pick pass and when // IFC_NO_HIZ=1. GLuint hiz_downsample_program_ = 0; GLuint hiz_downsample_vao_ = 0; GLuint hiz_fbo_ = 0; GLuint hiz_depth_tex_ = 0; GLuint hiz_resolve_fbo_ = 0; // full-size single-sample resolve GLuint hiz_resolve_depth_tex_ = 0; int hiz_resolve_w_ = 0; int hiz_resolve_h_ = 0; int hiz_base_w_ = 0; int hiz_base_h_ = 0; std::vector hiz_depth_readback_; // hiz_base_w_ * hiz_base_h_ floats std::vector hiz_pyramid_; // concatenated mip levels std::vector hiz_mip_offset_; // into hiz_pyramid_ std::vector hiz_mip_w_; std::vector hiz_mip_h_; QMatrix4x4 hiz_vp_; bool hiz_vp_valid_ = false; std::atomic hiz_reject_count_{0}; // per-frame stat // Cull-phase timers. Accumulated across all frames in the current // 1-second stats window; divided by frame_count_ at print time to // give per-frame average ms. Reset each window. Lets us see where // CPU time actually goes: bucket clears vs BVH traversal vs emit vs // GPU upload. // Atomic so parallel cull workers can fetch_add into them without // contending on a lock. clr/trv/emt are SUMS across all worker threads // for the frame — they describe total CPU work, not wall-clock. The // wall counter is measured once around the dispatch block in render() // and is what actually determines frame time. std::atomic cull_clear_ns_{0}; std::atomic cull_traverse_ns_{0}; std::atomic cull_emit_ns_{0}; std::atomic cull_upload_ns_{0}; uint64_t cull_wall_ns_ = 0; // main-thread only uint32_t cull_skipped_frames_ = 0; // Skip cullAndUploadVisible + buildHizPyramid when the camera and scene // haven't changed since the last cull. The existing per-model // indirect_buffer / visible_ssbo are still correct and just get // redrawn. Invalidated by any function that mutates models_gpu_. QMatrix4x4 last_cull_view_; QMatrix4x4 last_cull_proj_; bool have_cached_cull_ = false; // Motion-adaptive contribution culling. During camera motion, use a // larger pixel-radius threshold to aggressively cull small objects. // When the camera stops, re-cull once at the base threshold. bool last_cull_was_motion_ = false; // Benchmark mode: render N frames, collect stats, then exit. int benchmark_total_ = 0; int benchmark_count_ = 0; int benchmark_warmup_ = 5; float benchmark_yaw_start_ = 0.0f; float benchmark_yaw_speed_ = 0.5f; // degrees per frame std::vector benchmark_frame_times_; // Per-frame stats uint32_t visible_triangles_ = 0; uint32_t visible_objects_ = 0; uint32_t gl_draw_calls_ = 0; uint32_t indirect_sub_draws_ = 0; // Camera QVector3D camera_target_{0, 0, 0}; QVector3D camera_eye_{0, 0, 0}; // world-space eye, set in updateCamera float camera_distance_ = 50.0f; float camera_yaw_ = 45.0f; float camera_pitch_ = 30.0f; float camera_fov_y_deg_ = 45.0f; bool projection_ortho_ = false; QMatrix4x4 view_matrix_; QMatrix4x4 proj_matrix_; // Mouse Qt::MouseButton active_button_ = Qt::NoButton; QPoint last_mouse_pos_; // Pivot indicator visibility — true while drag-navigating, or briefly // after a wheel notch (the timer auto-clears it). bool pivot_indicator_visible_ = false; QTimer* pivot_indicator_hide_timer_ = nullptr; // FPS/fly mode state. fps_keys_held_ tracks WASD/QE/Shift between // press+release; fps_last_tick_ gates dt inside render(). // fps_ignore_next_mouse_move_ swallows the synthetic MouseMove that // QCursor::setPos() generates after we recenter. CameraMode camera_mode_ = CameraMode::Orbit; QSet fps_keys_held_; float fps_move_speed_ = 5.0f; // m/s at speed=1 QElapsedTimer fps_last_tick_; bool fps_ignore_next_mouse_move_ = false; // Selection — set + active id + per-object_id flags SSBO (binding=3). SelectionState selection_; // LMB-press state. press_pick_id_ caches the object hit at press // time so the release path can apply click semantics without a // second pick pass. box_select_armed_ is set only when the press // landed on empty space — a subsequent drag past the click // threshold then promotes to box_select_active_, so a small wobble // on a click doesn't accidentally box-select. Modifiers captured // at press-time decide commit semantics: plain replace, Shift add, // Ctrl remove. uint32_t press_pick_id_ = 0; bool box_select_armed_ = false; bool box_select_active_ = false; QPoint box_select_start_pos_; QPoint box_select_current_pos_; Qt::KeyboardModifiers box_select_press_mods_ = Qt::NoModifier; // Active measurement tool: see ToolMode / surfacePickedInTool. ToolMode tool_mode_ = ToolMode::None; // Renders any client-supplied overlay primitives (highlight triangles // today; lines/points/labels later) in their own pass after the main // geometry, with depth-test on / depth-write off. OverlayRenderer overlay_renderer_; // Active section planes. Uploaded as uniform array each frame to the // main + pick programs; capped at MaxSectionPlanes. std::vector section_planes_; bool section_tool_active_ = false; int section_plane_selected_ = -1; bool section_drag_active_ = false; int section_drag_index_ = -1; QVector3D section_drag_start_origin_; QPoint section_drag_start_mouse_; // Push the current plane list into a freshly-bound program. No-op if // the program does not declare u_clip_count / u_clip_planes. void uploadClipPlaneUniforms(GLuint program); // GL resources for the per-plane visualization (quad outline + arrow). GLuint plane_program_ = 0; GLuint plane_vao_ = 0; GLuint plane_vbo_ = 0; int plane_quad_offset_ = 0; int plane_quad_count_ = 0; int plane_arrow_offset_ = 0; int plane_arrow_count_ = 0; // Edge-enhancement pass resources. edge_depth_tex_ is a single-sample // resolve target the size of the window; we blit the default FB depth // into it each frame, then sample it from the fullscreen edge shader. GLuint edge_program_ = 0; GLuint edge_depth_fbo_ = 0; GLuint edge_depth_tex_ = 0; GLuint edge_vao_ = 0; // empty VAO for fullscreen-triangle draw int edge_w_ = 0; int edge_h_ = 0; // FPS smoothing int frame_count_ = 0; float accumulated_time_ = 0.0f; float last_fps_ = 0.0f; }; #endif // VIEWPORTWINDOW_H