Tab System and the Application Tab
This page is the Design Document for Tab 0 — the un-closable Application Tab — and the longer-term Chrome-style tab strip it enables. To be referred by AI for coding as well as humans. It is a plan; sections marked as decisions are locked, defaults are changeable until first implementation.
The feature: today the application starts 3 engineering tabs, each with its own engineering
thread. We add a 4th tab at tab index 0 that has no file name — its tab button shows only the
Devanagari application title विश्वकर्मा. It cannot be closed, cannot be extracted into another
window, and has no engineering thread: everything inside it is owned and serviced by the main
UI thread. It hosts 8 fixed, un-closable views (sub-tabs): Launcher, Profile, Settings, Support,
Peer Chat, Documentation, Common Geometry, and Stats. Its memory group (group 0) is reserved as
the future home of the immutable instancing geometry masters. Once tab 0 exists, a later phase
removes the OS title bar so the window becomes nearly identical to the Google Chrome tab system:
[विश्वकर्मा][Tab][Tab][+] ................ [─][□][✕].
Current state (anchor points in code)
Main.cppwWinMain: creates tabs 0..2 frominitialTabNames[](~line 744), publishes a 3-entry tab list, then launches 3 engineering threadsविश्वकर्मा(i)fori = 0..2(~line 826).nextTabSlotstarts at 3.CreateEngineeringTab()(Main.cpp~line 199): ‘+’ button path; takes the next slot, publishes, spawns the engineering thread.RequestCloseEngineeringTab()(Main.cpp~line 249): refuses whentabCount <= 1; picks a replacement tab, retargets windows, queuesACTION_TYPE::CLOSE_TABto the tab’s own thread.- Tab band rendering:
UserInterface.cppBuildUIOverlay(~line 1067 onward). Every tab button gets anxclose button; label text is ASCII-filtered (if (ch <= 0x7F), ~line 1183), so a DevanagarifileNamewould currently render blank. Drag-down extracts a tab (kExtractTabUIAction) unless it is the window’s last hosted tab. - Sub-tab (view) machinery: fixed 128-slot
subTabs[]perDATASETTABwith a double-buffered published index list (विश्वकर्मा.h~line 157). Slots are engineering-thread owned, written understorageObjectsMutex, read lock-free by render threads. Open/activate/close live inविश्वकर्मा.cpp(OpenInternalSubTab~line 394). - Content per window:
RenderCompositor-DirectX12.cpp(~line 250) branches Page2D vs Scene3D on the active sub-tab’s container type, then draws the UI overlay on top. - Input:
WndProc(Main.cpp) pushes scene/keyboard input into the active tab’suserInputQueue; ribbon commands resolve their target viaGetActiveTabForUIAction(). - The comment at
विश्वकर्मा.h~line 245 (“Tab 0 id default application launch screen tab. It can’t be closed.”) already records this intent — this document implements it.
Locked design decisions
- Tab 0 reuses
DATASETTABslotallTabs[0]. No parallel structure. Every existing reader (tab band, sub-tab band, compositor, data tree) keeps working on the same arrays and published lists.IsApplicationTab(tabIndex)is simplytabIndex == kApplicationTabIdwithconstexpr uint16_t kApplicationTabId = 0;. - No engineering thread, UI thread is the single writer. Tab 0’s
DATASETTABfields are written only by the main UI thread (startup init +ProcessPendingUIActions). Render threads read exactly as they do for other tabs (acquire loads of published lists). The 8 view slots are filled and published once at startup, before render threads launch, and never change afterward — the published sub-tab list of tab 0 is immutable, so there is nothing to race on. OnlyactiveInternalSubTabMemoryIdchanges at runtime (UI thread writes on view activation, same 1-frame-staleness read contract as everywhere else). - Tab 0’s queues are never drained — so nothing may ever push to them.
userInputQueueandtodoCPUQueueexist (the constructor makes them) but have no consumer. Guards (see Guard Inventory) stopWndProcandPushSystemTodoToTab()from pushing; otherwise memory grows unbounded. This is the single most important correctness rule of this feature. - Views are identified by reserved synthetic container IDs. Real container memory IDs come
from
GetNewTempID()which counts up from 1, so low constants would collide. Reserve the top range:constexpr uint64_t kAppViewContainerIdBase = 0xFFFFFFFFFFFFFF00ULL;andAppViewKind1..8 maps tokAppViewContainerIdBase + kind.InternalSubTabis not modified;containerTypestaysObjectType::Unknownand helpersIsAppViewContainerId(uint64_t)/AppViewKindForContainerId(uint64_t)derive the kind. Storage schema is untouched — these IDs are never persisted. - Single new file pair
ApplicationTab.h/ApplicationTab.cpphosts everything: theAppViewKindenum, synthetic ID helpers,InitializeApplicationTab(),BuildApplicationTabOverlay()(per-view stub panels), and later the per-view real content. Bifurcate into per-view files only when a view outgrows a screenful of code. - Engineering tabs shift to slots 1..3; memory group 0 is reserved.
tabNodoubles as the CPU memory group number (new (tab->tabNo)placement allocation), so tab 0 keeping group 0 reserves that group for the future immutable instancing masters.nextTabSlotstarts at 4. - The Devanagari title renders as an SVG word-mark icon, not shaped text. विश्वकर्मा needs
complex shaping (conjunct श्व, rakar र्मा) that the codepoint→glyph MSDF atlas cannot do, and
the tab-label path filters to ASCII anyway. Author the word once as vector paths (text→path in
Inkscape from
Fonts/NotoSansDevanagari), commit as a new icon inwebsite/static/SVGIconsplus oneSVGIconManifest.hline, and draw it withPushIconin tab 0’s button — the exact mechanism the launch splash already uses for the logo (kSplashLogoCodepoint,UserInterface.cpp~line 921). Full Devanagari text shaping is explicitly out of scope here. - Un-closable and un-movable, everywhere. No
xon the tab button, close requests refused at theRequestCloseEngineeringTablevel (defense in depth), no drag-out extraction, no drag-merge into another window.hostWindowSlotstays 0 forever; secondary tab-host windows never show tab 0. Closing the last engineering tab is now allowed — the main window falls back to the Application Tab, Chrome-style. - Views are un-closable and (for now) un-extractable. The sub-tab band draws no
xfor tab 0’s views and ignores drag-out. View activation for tab 0 is handled directly on the UI thread insideProcessPendingUIActions(writeactiveInternalSubTabMemoryId), since there is no engineering thread to servicekActivateUIAction.
The 8 application views
| # | AppViewKind |
Band title | MVP content (Phase 3) | Eventual content |
|---|---|---|---|---|
| 1 | Launcher |
Launcher | Stub panel | Recent local files + online servers; click opens a new engineering tab |
| 2 | Profile |
Profile | Stub panel | Login details, badges, achievements (AccountManager identity) |
| 3 | Settings |
Settings | Stub panel | Application-level settings |
| 4 | Support |
Support | Stub panel | Direct chat with support team (server: mv-server.ramshanker.in) |
| 5 | PeerChat |
Peer Chat | Stub panel | IPMessenger-style LAN chat for the local office |
| 6 | Documentation |
Docs | Stub panel | Local copy of the online documentation (this website) |
| 7 | CommonGeometry |
Geometry | Stub panel | Display of the frequently-used / instanced master geometry elements |
| 8 | Stats |
Stats | Real data from day 1 | Tab count, window count, CPU memory pages, GPU memory pages, and whatever else is cheap to read |
Stats ships real numbers in the MVP because it is nearly free (publishedTabCount,
publishedWindowCount are atomics; CPU/GPU page counts need only small read-only counters
exposed from MemoryManagerCPU / MemoryManagerGPU-DirectX12) and it proves the whole
UI-thread-owned view pattern end to end.
Naming note: the request said “Comment Geometry elements”. This document interprets it as Common Geometry — the display of the immutable instancing masters described below. If the intent was annotation/markup elements (clouds, leaders, comments), rename the view; nothing else in the design changes.
Startup sequence changes (Main.cpp wWinMain)
- Keep the existing per-tab init loop but run it for slots 0..3: slot 0 gets
InitializeApplicationTab(allTabs[0])(from the new file), slots 1..3 get the existinginitialTabNames[]treatment (names unchanged).gpu.InitD3DPerTabandInitCad2DTabResourcesstill run for tab 0 — empty but valid GPU state keeps every downstream path (matrix tables, compositor) untouched. InitializeApplicationTabfills:tabID = tabNo = 0,fileName = L"विश्वकर्मा"(used by window-title contexts; the band draws the word-mark icon instead),dataTreeView.isVisible = false, then opens the 8 view slots — slotk-1getscontainerType = Unknown,containerMemoryId = kAppViewContainerIdBase + k, ASCIItitlefrom the table above, stateSUBTAB_OPEN— and publishes the 8-entry list.activeInternalSubTabMemoryId= Launcher. No lock needed: render threads do not exist yet.- Publish tab list
{0, 1, 2, 3}, count 4.nextTabSlot = 4. - Engineering thread launch loop becomes
for (int i = 1; i <= 3; ++i). Tab 0 gets no thread. mainWindow.activeTabIndex: default 1 during development (engineering demo content stays front on every run); switch the default to 0 (Launcher) once the Launcher has real content.
Guard inventory
Every place that must learn about the Application Tab. Each is one small IsApplicationTab(...)
check; together they make “no thread + un-closable” airtight.
| Where | Guard |
|---|---|
WndProc (Main.cpp, all tab->userInputQueue->push sites) |
Skip the push when the routed tab is tab 0 (queue has no consumer — Decision 3). The per-window uiInput snapshot path is untouched, so all overlay UI (buttons, text fields) keeps working. |
PushSystemTodoToTab() (Main.cpp ~line 89) |
Early-return for tab 0 (same reason). Single choke point for all ribbon/system todos. |
RequestCloseEngineeringTab() |
Replace the tabCount <= 1 guard with tabID == kApplicationTabId refusal. Closing the last engineering tab is now legal; the existing replacement logic already lands the main window on tab 0 (it is always in the published list, hosted by window 0). |
Tab band (UserInterface.cpp ~line 1126) |
Tab 0: draw word-mark icon instead of label; skip the x button and its hit test; never set pressedTabId (no drag-out). |
ExtractTabToNewWindow / drag-merge paths |
Refuse tab 0; hostWindowSlot stays 0. |
Sub-tab band (UserInterface.cpp ~line 1400) |
When the active tab is 0: skip the x button/hit-test and drag-out (pressedSubTabSlot stays -1). Activation click still emits kActivateUIAction. |
ProcessPendingUIActions (Main.cpp ~line 375) |
kActivateUIAction for tab 0: set allTabs[0].activeInternalSubTabMemoryId = p2 directly on the UI thread. kCloseUIAction / kExtractUIAction / kCloseViewWindowUIAction for tab 0: ignore. |
GetActiveTabForUIAction() (Main.cpp ~line 123) |
Never return tab 0 — return nullptr when the action originated there. All engineering ribbon commands (save, create sphere, import…) then no-op on the Application Tab through their existing null checks. Commands that do not need a tab (e.g. PROJECT_OPEN → OpenStorageFileInNewTab) keep working. |
Shutdown / CleanupReleasedTabs |
No change needed — tab 0 never sets closeRequested, owns no engineering thread to join, and never enters the GPU-release handshake. Verify only. |
Not guards, but consequences worth knowing: tab 0 has no camera orbit, no random geometry generation, no pick servicing (no pick requests are ever queued for it), and its data tree is hidden. The compositor’s Scene3D branch runs against empty geometry and draws only the sky gradient behind the view panel — harmless, zero compositor changes in the MVP.
Rendering the application views
BuildUIOverlay (UI overlay, render thread, immediate-mode — same model as the ribbon and the
properties pane): when window.activeTabIndex == kApplicationTabId, call
ApplicationTab::BuildApplicationTabOverlay(...) after the bands are drawn. It:
- Draws one opaque full-content panel (from
topUITotalHeightPxdown) in the UI background color — this is why the compositor needs no changes. - Switches on
AppViewKindForContainerId(allTabs[0].activeInternalSubTabMemoryId)and draws the active view: MVP = centered view name + one line of placeholder text; Stats = live numbers. - Receives the same
UIInputsnapshot as the rest of the overlay, so future views (Launcher file list, Settings toggles) get clicks and text input for free, and commit their effects viaPushUIActionhandled on the UI thread — never via tab queues.
Threading recap: render thread draws and hit-tests; durable state changes travel through
PushUIAction → ProcessPendingUIActions (UI thread), which is already the pattern for every
band button today.
Devanagari tab title (word-mark details)
- New SVG: the word विश्वकर्मा converted to paths (no
<text>element — the embedded rasterizer handles paths only), committed underwebsite/static/SVGIconswith a manifest line inSVGIconManifest.h, ID per the existing hash-style convention. - The icon atlas rasterizes square cells (
RenderEmbeddedSVGIcons(int pixelSize)), so author the word inside a square viewBox occupying a horizontal middle strip. In the tab band, draw the icon as a square quad larger than the band height, centered vertically on the band: the strip lands inside the band, the overflow above/below is fully transparent pixels, and the ribbon — drawn after the band — paints over the lower overflow anyway. No renderer changes. - Fallback that MVP may ship with: the existing logo icon (
IconForID(1u)), swapped for the word-mark by changing one constant. Do not block Phase 1 on SVG authoring. - If the strip trick looks bad at small DPI, the alternative is teaching the atlas one
non-square cell (
RenderedSVGIconalready carries independent width/height) — deferred.
Phases
Phase 1 — Tab 0 exists, un-closable, engineering tabs renumbered
Startup changes, all guards from the inventory, word-mark (or logo fallback) tab button.
No views yet (empty published sub-tab list is fine — band renders nothing).
Verify: app starts with 4 tabs; tab 0 shows the icon and no x; clicking tab 0 shows the
empty sky-gradient content; engineering tabs 1..3 behave exactly as before (create, close,
extract, multi-window); + creates slot 4+; closing all engineering tabs leaves the app on tab
0 and the app stays healthy; opening/saving files from tab 0 either works (open) or no-ops
(save); a debug counter proves tab 0’s queues stay empty across a full session.
Phase 2 — The 8 un-closable views
InitializeApplicationTab publishes the 8 slots; sub-tab band guards; UI-thread activation.
Verify: all 8 buttons visible with no x; clicking each switches
activeInternalSubTabMemoryId; drag-out does nothing; engineering tabs’ own sub-tabs
(Scene3D/Page2D) still open/close/extract normally.
Phase 3 — Stub panels + real Stats view
BuildApplicationTabOverlay with the opaque panel, per-view stubs, Stats with live counters.
Verify: each view renders its name; Stats numbers change when tabs/windows open and close and
when geometry allocates pages in engineering tabs.
Future phases (designed direction, not scheduled)
Instancing / Common Geometry masters
Tab 0’s memory group 0 hosts the frequently-used geometry elements at all instancing levels.
Created once at startup on the main thread before the copy/render threads consume them, then
immutable — the same create-before-publish reasoning that makes the view slots race-free, applied
to geometry pages. The CommonGeometry view becomes their visual catalog. Engineering tabs will
reference these masters by ID for instanced draws; the graphics-pipeline side (instance buffers,
per-instance transforms) is its own future design document section in graphics.md.
Chrome-style frameless window
Remove the OS title bar; final band layout [विश्वकर्मा][tabs...][+] ... [─][□][✕].
Known ingredients: WM_NCCALCSIZE client-area extension (a commented prototype already sits in
WndProc, Main.cpp ~line 1060), WM_NCHITTEST returning HTCAPTION for empty band area (drag
to move), HTMINBUTTON/HTMAXBUTTON/HTCLOSE for the three drawn window controls (and Win11
snap-layout flyout via HTMAXBUTTON), maximized-state inset correction, and drawing the three
controls at the band’s right edge in BuildUIOverlay. This phase only starts after tab 0 has
shipped, because the band then carries the application identity that the title bar used to.
File touch list (Phases 1–3)
| File | Change |
|---|---|
code-core/ApplicationTab.h / .cpp |
New. Everything from Decision 5. |
code-core/Main.cpp |
Startup sequence, thread launch loop 1..3, guards in WndProc, PushSystemTodoToTab, RequestCloseEngineeringTab, GetActiveTabForUIAction, ProcessPendingUIActions. |
code-core/UserInterface.cpp |
Tab-band tab-0 branch (icon, no x, no drag); sub-tab-band tab-0 branch; BuildApplicationTabOverlay call. |
code-core/विश्वकर्मा.h |
kApplicationTabId, IsApplicationTab() (or these live in ApplicationTab.h and are included where needed). |
code-core/SVGIconManifest.h + website/static/SVGIcons/ |
Word-mark icon entry + SVG file. |
code-core/MemoryManagerCPU.h / MemoryManagerGPU-DirectX12.h |
Tiny read-only page counters for the Stats view (only if not already readable). |
Project file (Vishwakarma.vcxproj) |
Add the new .h/.cpp pair. |
Open questions and changeable defaults
- Startup active tab: default engineering tab 1 during development; flips to tab 0 when the Launcher is real.
- Ribbon on tab 0: MVP keeps the full ribbon drawn (uniform layout; commands no-op via the
GetActiveTabForUIActionguard). A contextual ribbon (or none) for the Application Tab is a later decision. - Data tree on tab 0: hidden by default (
dataTreeView.isVisible = false). - View 7 name: “Common Geometry” per the naming note above — confirm or rename.
- Extracting tab-0 views into their own windows (e.g. Docs on a second monitor): refused in
the MVP; the machinery (
subTabHostWindowSlots) would allow it later since these views render purely from the UI overlay. - Recent-files persistence for the Launcher: file format and location (likely next to the AccountManager identity data) — decide when the Launcher gets real content.