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)