Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ <h3>Bank Info</h3>
</div>
</div>
</div>
<button type="button" id="shared-show-linear-layout">Show in Linear Layout</button>
</form>

<div class="controls" data-controls></div>
Expand Down
120 changes: 120 additions & 0 deletions src/tabs/SharedLayoutTab.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof vi.fn>
Expand Down Expand Up @@ -173,6 +174,7 @@ const setupDom = () => {
</select>
</label>
</div>
<button type="button" id="shared-show-linear-layout">Show in Linear Layout</button>
</form>
<div id="shared-validation-errors"></div>
<div id="shared-validation-warnings"></div>
Expand Down Expand Up @@ -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,
Expand Down
65 changes: 65 additions & 0 deletions src/tabs/SharedLayoutTab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -27,6 +28,7 @@ interface BankInfoElements {
}

export class SharedLayoutTab extends CanvasTab {
private readonly tabId: string
private readonly form: ParameterForm<SharedLayoutUiParams>
private readonly validator = new SharedLayoutValidator()
private readonly bankInfoElements: BankInfoElements
Expand Down Expand Up @@ -90,6 +92,7 @@ export class SharedLayoutTab extends CanvasTab {

super(elements)

this.tabId = tabId
this.bankInfoElements = {
bankCount,
bankSize,
Expand All @@ -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)
})
Expand Down Expand Up @@ -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<HTMLButtonElement>('#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)
Expand Down