From 89fafc48da6fba6d63619e7c4ac72cf9f44e03dd Mon Sep 17 00:00:00 2001 From: leeliu103 Date: Fri, 30 Jan 2026 19:33:50 +0000 Subject: [PATCH] Add Show in Linear Layout projection for Shared Layout tab Co-Authored-By: Claude (claude-opus-4.5) --- index.html | 1 + src/tabs/SharedLayoutTab.test.ts | 120 +++++++++++++++++++++++++++++++ src/tabs/SharedLayoutTab.ts | 65 +++++++++++++++++ 3 files changed, 186 insertions(+) diff --git a/index.html b/index.html index b87c54b..3ec8799 100644 --- a/index.html +++ b/index.html @@ -241,6 +241,7 @@

Bank Info

+
diff --git a/src/tabs/SharedLayoutTab.test.ts b/src/tabs/SharedLayoutTab.test.ts index 9fb0526..ded8128 100644 --- a/src/tabs/SharedLayoutTab.test.ts +++ b/src/tabs/SharedLayoutTab.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' +import { layoutProjectionBus, LINEAR_LAYOUT_TAB_ID } from '../integration/LayoutProjectionBus' type RendererStub = { render: ReturnType @@ -173,6 +174,7 @@ const setupDom = () => { +
@@ -257,6 +259,124 @@ describe('SharedLayoutTab', () => { expect(segmentsText).toBe('--') }) + it('toggles the Show in Linear Layout button based on validation state changes', () => { + new SharedLayoutTab('shared-layout') + const button = document.getElementById('shared-show-linear-layout') as HTMLButtonElement + expect(button.disabled).toBe(false) + + const rowsInput = document.getElementById('shared-rows') as HTMLInputElement + rowsInput.value = '63' + rowsInput.dispatchEvent(new Event('input')) + expect(button.disabled).toBe(true) + + rowsInput.value = '64' + rowsInput.dispatchEvent(new Event('input')) + expect(button.disabled).toBe(false) + }) + + it('initializes with the Show in Linear Layout button disabled when invalid', () => { + const rowsInput = document.getElementById('shared-rows') as HTMLInputElement + rowsInput.value = '63' + + new SharedLayoutTab('shared-layout') + + const button = document.getElementById('shared-show-linear-layout') as HTMLButtonElement + expect(button.disabled).toBe(true) + }) + + describe('linear layout projection', () => { + it('publishes a logical view snapshot when requested', () => { + new SharedLayoutTab('shared-layout') + const button = document.getElementById('shared-show-linear-layout') as HTMLButtonElement + const publishSpy = vi.spyOn(layoutProjectionBus, 'publish') + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + button?.click() + + expect(publishSpy).toHaveBeenCalledWith( + expect.objectContaining({ + sourceTabId: 'shared-layout', + targetTabId: LINEAR_LAYOUT_TAB_ID, + }) + ) + + const snapshot = publishSpy.mock.calls[0]?.[0]?.snapshot as { + metadata?: { sourceLayoutType?: string; description?: string } + inputDimensions?: Array<{ name: string }> + outputDimensions?: Array<{ name: string }> + } + expect(snapshot?.metadata?.sourceLayoutType).toBe('shared-layout') + expect(snapshot?.metadata?.description).toContain('Logical View') + expect(snapshot?.inputDimensions?.map((dim) => dim.name)).toContain('offset') + expect(snapshot?.outputDimensions?.map((dim) => dim.name)).toEqual(['dim0', 'dim1']) + + warnSpy.mockRestore() + publishSpy.mockRestore() + }) + + it('publishes a bank view snapshot when selected', () => { + new SharedLayoutTab('shared-layout') + const viewSelect = document.getElementById('shared-view-mode') as HTMLSelectElement + viewSelect.value = 'bank' + + const button = document.getElementById('shared-show-linear-layout') as HTMLButtonElement + const publishSpy = vi.spyOn(layoutProjectionBus, 'publish') + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + button?.click() + + const snapshot = publishSpy.mock.calls[0]?.[0]?.snapshot as { + metadata?: { description?: string } + outputDimensions?: Array<{ name: string }> + } + expect(snapshot?.metadata?.description).toContain('Bank View') + expect(snapshot?.outputDimensions?.map((dim) => dim.name)).toEqual(['segment', 'bank']) + + warnSpy.mockRestore() + publishSpy.mockRestore() + }) + + it('alerts when the projection collapses all dimensions', () => { + new SharedLayoutTab('shared-layout') + const rowsInput = document.getElementById('shared-rows') as HTMLInputElement + const colsInput = document.getElementById('shared-cols') as HTMLInputElement + const vecSelect = document.getElementById('shared-vec') as HTMLSelectElement + const perPhaseSelect = document.getElementById('shared-per-phase') as HTMLSelectElement + const maxPhaseSelect = document.getElementById('shared-max-phase') as HTMLSelectElement + rowsInput.value = '1' + colsInput.value = '1' + vecSelect.value = '1' + perPhaseSelect.value = '1' + maxPhaseSelect.value = '1' + + const button = document.getElementById('shared-show-linear-layout') as HTMLButtonElement + const publishSpy = vi.spyOn(layoutProjectionBus, 'publish') + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + button?.click() + + expect(window.alert).toHaveBeenCalledWith( + 'Unable to derive a linear layout because all input and output dimensions are size 1.' + ) + expect(publishSpy).not.toHaveBeenCalled() + + warnSpy.mockRestore() + publishSpy.mockRestore() + }) + + it('warns when the Show in Linear Layout button is missing', () => { + document.getElementById('shared-show-linear-layout')?.remove() + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + new SharedLayoutTab('shared-layout') + + expect(warnSpy).toHaveBeenCalledWith( + 'Show in Linear Layout button not found in SharedLayoutTab.' + ) + warnSpy.mockRestore() + }) + }) + describe('hover tooltips', () => { const triggerHover = ( tab: SharedLayoutTab, diff --git a/src/tabs/SharedLayoutTab.ts b/src/tabs/SharedLayoutTab.ts index db8e6a9..e09fb90 100644 --- a/src/tabs/SharedLayoutTab.ts +++ b/src/tabs/SharedLayoutTab.ts @@ -3,6 +3,7 @@ import { renderSharedControls } from '../ui/renderSharedControls' import { CanvasRenderer, type CellInfo, type SubCellLayout } from '../visualization/CanvasRenderer' import { ParameterForm } from '../ui/ParameterForm' import type { BlockLayoutParams } from '../validation/InputValidator' +import type { SnapshotFilterResult } from '../core/filterSnapshotDimensions' import { assignBank, computeBankInfo, @@ -27,6 +28,7 @@ interface BankInfoElements { } export class SharedLayoutTab extends CanvasTab { + private readonly tabId: string private readonly form: ParameterForm private readonly validator = new SharedLayoutValidator() private readonly bankInfoElements: BankInfoElements @@ -90,6 +92,7 @@ export class SharedLayoutTab extends CanvasTab { super(elements) + this.tabId = tabId this.bankInfoElements = { bankCount, bankSize, @@ -105,6 +108,11 @@ export class SharedLayoutTab extends CanvasTab { validateParams: (params) => this.validator.validate(params), }) + const linearLayoutButton = this.setupShowInLinearLayoutButton() + if (linearLayoutButton) { + this.monitorShowInLinearLayoutButton(linearLayoutButton) + } + this.form.onParamsChange((params) => { this.refreshVisualization(params) }) @@ -198,6 +206,63 @@ export class SharedLayoutTab extends CanvasTab { this.hideTooltip() } + private setupShowInLinearLayoutButton(): HTMLButtonElement | null { + this.setupLinearLayoutProjection({ + buttonSelector: '#shared-show-linear-layout', + missingButtonWarning: 'Show in Linear Layout button not found in SharedLayoutTab.', + sourceTabId: this.tabId, + shouldProject: () => this.form.validate(), + buildSnapshot: () => { + const params = this.form.getParams() + return this.buildProjectionSnapshot(params) + }, + errorLogContext: 'Shared Layout', + errorAlertMessage: 'Failed to convert Shared Layout into Linear Layout. See console for details.', + }) + + return this.root.querySelector('#shared-show-linear-layout') + } + + private monitorShowInLinearLayoutButton(button: HTMLButtonElement): void { + this.form.onValidationChange((isValid) => { + button.disabled = !isValid + }) + } + + private buildProjectionSnapshot(params: SharedLayoutUiParams): SnapshotFilterResult { + const description = this.buildProjectionDescription(params) + + if (params.viewMode === 'bank') { + const bankLayout = createSharedBankLayout(params.tensorShape, params.elementBits) + const colorInputDimension = bankLayout.bankSpan > 1 ? 'half' : 'offset' + return this.prepareLinearLayoutSnapshot(bankLayout.layout, { + sourceLayoutType: 'shared-layout', + sourceTabId: this.tabId, + tensorShape: [bankLayout.rowCount, bankLayout.bankCount], + generatedAt: new Date().toISOString(), + description, + colorInputDimension, + }) + } + + const logicalLayout = createSharedLayout(params) + return this.prepareLinearLayoutSnapshot(logicalLayout.layout, { + sourceLayoutType: 'shared-layout', + sourceTabId: this.tabId, + tensorShape: [...params.tensorShape], + generatedAt: new Date().toISOString(), + description, + colorInputDimension: 'offset', + }) + } + + private buildProjectionDescription(params: SharedLayoutUiParams): string { + const viewLabel = params.viewMode === 'bank' ? 'Bank View' : 'Logical View' + const swizzleLabel = params.swizzleMode === 'amdRotating' ? 'AMD Rotating' : 'Swizzled' + const elementLabel = params.viewMode === 'bank' ? `, ${params.elementBits}-bit` : '' + return `Projection from Shared Layout (${viewLabel}${elementLabel}, ${swizzleLabel})` + } + private resolveBankSegment(cellInfo: CellInfo): { bank: number; segment: number } { const offset = this.getCellOffset(cellInfo) const base = assignBank(offset, this.currentElementBits)