From 97ded8826329a5e578620364dac50b674566fe69 Mon Sep 17 00:00:00 2001 From: Lloyd Ramseyer Date: Fri, 1 Nov 2024 19:07:13 -0700 Subject: [PATCH] Candidate for 1.1.9 (Not released to Marketplace) --- CHANGELOG.md | 6 +- README.md | 11 ++-- media/vaporview.js | 85 +++++----------------------- package.json | 4 +- src/extension.ts | 131 +++++++++++++++++++++++++++++--------------- src/lib.rs | 37 +++++++++++-- wit/filehandler.wit | 1 + 7 files changed, 148 insertions(+), 127 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c222b90..c75b3d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,11 @@ # Next release (Version TBD) -- Removed checkboxes for scope \[module\] items in netlist viewer to reduce confusion - File parsing uses the [wellen](https://github.com/ekiwi/wellen/tree/new-api) library compiled to wasm. Benefits include: - FST and GHW file support - - Improves file parsing speed - - Improves memory efficiency (over storing everything in JS objects) + - Improves file parsing speed and memory efficiency (over storing everything in JS objects) +- Removed checkboxes for scope \[module\] items in netlist viewer to reduce confusion +- Variables loaded into viewer show up before waveform data is loaded as a better visual acknowledgement to user action # 1.1.0 diff --git a/README.md b/README.md index 253285a..4272dbc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # VaporView -VaporView is an open source VCD waveform viewer extension for Visual Studio Code designed for FPGA/RTL developers. +VaporView is an open source waveform viewer extension for Visual Studio Code ![](https://github.com/Lramseyer/vaporview/blob/main/readme_assets/overview.png?raw=true) @@ -8,7 +8,9 @@ VaporView is an open source VCD waveform viewer extension for Visual Studio Code ## Waveform Viewer -VaporView automatically opens .vcd files in the waveform viewer. In the viewer, you can: +Supports VCD and FST waveform dump formats + +VaporView automatically opens the waveform files in a viewer, where you can: - Add, remove, and rearrange signals - Pan and zoom in on the view - Place and move markers @@ -142,8 +144,7 @@ New for 1.1.0 - Added large VCD file support. Contents of VCD files are statical - Removed checkboxes for scope \[module\] items in netlist viewer to reduce confusion - File parsing uses the [wellen](https://github.com/ekiwi/wellen/tree/new-api) library compiled to wasm. Benefits include: - FST and GHW file support - - Improves file parsing speed - - Improves memory efficiency (over storing everything in JS objects) + - Improves file parsing speed and memory efficiency (over storing everything in JS objects) ## Planned Features @@ -175,4 +176,6 @@ This extension was written by one person, with a full time job that doesn't invo # Acknowledgements +This project uses the [wellen](https://github.com/ekiwi/wellen/tree/new-api) library compiled to WASM for file parsing and back-end data management. + Thanks to my coworkers for their encouragement, feature requests, bug reports, and contribution of VCD files that made this project possible! \ No newline at end of file diff --git a/media/vaporview.js b/media/vaporview.js index 898c796..9019a63 100644 --- a/media/vaporview.js +++ b/media/vaporview.js @@ -2182,7 +2182,6 @@ goToNextTransition = function (direction, edge) { } case 'add-variable': { // Handle rendering a signal, e.g., render the signal based on message content - //console.log(message); let signalId = message.signalId; @@ -2202,8 +2201,7 @@ goToNextTransition = function (direction, edge) { if (waveformData[signalId]) { - console.log('signal already exists'); - + // console.log('signal already exists'); updateWaveformInCache([message.netlistId]); renderLabelsPanels(); @@ -2220,8 +2218,7 @@ goToNextTransition = function (direction, edge) { totalChunks: 0 }; - console.log('signal data not found, fetching data'); - + //console.log('signal data not found, fetching data'); vscode.postMessage({ command: 'fetchTransitionData', signalId: signalId, @@ -2234,48 +2231,6 @@ goToNextTransition = function (direction, edge) { break; } - case 'update-waveform': { - let signalId = message.signalId; - let netlistId = message.netlistId; - let transitionData = message.transitionData; - let signalWidth = netlistData[netlistId].signalWidth; - let numberFormat = netlistData[netlistId].numberFormat; - let nullValue = "X".repeat(signalWidth); - - if (transitionData[0][0] !== 0) { - transitionData.unshift([0, nullValue]); - } - if (transitionData[transitionData.length - 1][0] !== timeStop) { - transitionData.push([timeStop, nullValue]); - } - waveformData[signalId] = { - transitionData: transitionData, - signalWidth: signalWidth, - textWidth: getValueTextWidth(signalWidth, numberFormat), - }; - - // Create ChunkStart array - waveformData[signalId].chunkStart = new Array(chunkCount).fill(transitionData.length); - let chunkIndex = 0; - for (let i = 0; i < transitionData.length; i++) { - while (transitionData[i][0] >= chunkTime * chunkIndex) { - waveformData[signalId].chunkStart[chunkIndex] = i; - chunkIndex++; - } - } - waveformData[signalId].chunkStart[0] = 1; - waveformDataTemp[signalId] = undefined; - - updateWaveformInCache([netlistId]); - renderLabelsPanels(); - - updatePending = true; - updateContentArea(leftOffset, getBlockNum()); - contentArea.style.height = (40 + (28 * displayedSignals.length)) + "px"; - handleSignalSelect(netlistId); - - break; - } case 'update-waveform-chunk': { // Newer command used for fetching transition data in chunks @@ -2386,31 +2341,6 @@ goToNextTransition = function (direction, edge) { break; } - case 'setMarker': { - //console.log('setting marker'); - // Handle setting the marker, e.g., update the marker position - handleMarkerSet(message.time, 0); - break; - } - case 'setSelectedSignal': { - // Handle setting the selected signal, e.g., update the selected signal - handleSignalSelect(message.netlistId); - break; - } - case 'getSelectionContext': { - - sendWebviewContext('response'); - //vscode.postMessage({type: 'context', context: displaySignalContext}); - break; - } - case 'getContext': { - sendWebviewContext('response'); - break; - } - case 'copyWaveDrom': { - copyWaveDrom(); - break; - } case 'setWaveDromClock': { waveDromClock = { netlistId: message.netlistId, @@ -2418,6 +2348,17 @@ goToNextTransition = function (direction, edge) { }; break; } + case 'getSelectionContext': { + sendWebviewContext('response'); + //vscode.postMessage({type: 'context', context: displaySignalContext}); + break; + } + // Handle setting the marker, e.g., update the marker position + case 'setMarker': {handleMarkerSet(message.time, 0); break; } + // Handle setting the selected signal, e.g., update the selected signal + case 'setSelectedSignal': {handleSignalSelect(message.netlistId); break; } + case 'getContext': {sendWebviewContext('response'); break;} + case 'copyWaveDrom': {copyWaveDrom(); break;} } }); diff --git a/package.json b/package.json index 9e62777..3b29e64 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,10 @@ "Other" ], "keywords": [ - "VCD", "Waveform", + "VCD", + "FST", + "Logic Simulation", "Logic Analyzer" ], "engines": { diff --git a/src/extension.ts b/src/extension.ts index 2083ca7..b6fc744 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -17,15 +17,10 @@ interface VaporviewDocumentDelegate { } let wasmModule: WebAssembly.Module; -const BASE_CHUNK_TIME_WINDOW = 512; -const TIME_INDEX = 0; -const VALUE_INDEX = 1; - type WaveformTopMetadata = { fileName: string; fileSize: number; fd: number; - waveformsStartOffset: number; timeTableLoaded: boolean; moduleCount: number; netlistIdCount: number; @@ -38,7 +33,7 @@ type WaveformTopMetadata = { timeUnit: string; }; -export type NetlistIdTable = Map; +export type NetlistIdTable = NetlistIdRef[]; export type TransitionData = [number, number | string]; export type SignalId = number; export type NetlistId = number; @@ -70,7 +65,7 @@ export function createScope(name: string, type: string, path: string, netlistId: return module; } -export function createVar(name: string, type: string, path: string, netlistId: NetlistId, signalId: SignalId, width: number) { +export function createVar(name: string, type: string, path: string, netlistId: NetlistId, signalId: SignalId, width: number, msb: number, lsb: number) { const variable = new NetlistItem(name, type, width, signalId, netlistId, name, path, [], vscode.TreeItemCollapsibleState.None, vscode.TreeItemCheckboxState.Unchecked); const typename = type.toLocaleLowerCase(); if ((typename === 'wire') || (typename === 'reg')) { @@ -104,9 +99,15 @@ export class VaporviewDocument extends vscode.Disposable implements vscode.Custo document.metadata.fileName = uri.fsPath; document.metadata.fileSize = stats.size; - await document.wasmApi.loadfst(stats.size, document.metadata.fd); - document.wasmApi.readbody(document.metadata.fd); + await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: "Parsing Netlist", + cancellable: false + }, async () => { + await document.wasmApi.loadfst(stats.size, document.metadata.fd); + }); + document._readBody(); return document; } @@ -115,7 +116,7 @@ export class VaporviewDocument extends vscode.Disposable implements vscode.Custo // Hierarchy public treeData: NetlistItem[] = []; public displayedSignals: NetlistItem[] = []; - private _netlistIdTable: NetlistIdTable = new Map(); + private _netlistIdTable: NetlistIdTable = []; // Wasm private readonly _delegate: VaporviewDocumentDelegate; public _wasmWorker: Worker; @@ -127,13 +128,12 @@ export class VaporviewDocument extends vscode.Disposable implements vscode.Custo fileName: "", fileSize: 0, fd: 0, - waveformsStartOffset: 0, timeTableLoaded: false, moduleCount: 0, netlistIdCount: 0, signalIdCount: 0, timeEnd: 0, - chunkTime: BASE_CHUNK_TIME_WINDOW, + chunkTime: 128, chunkCount: 0, timeScale: 1, defaultZoom: 1, @@ -167,12 +167,16 @@ export class VaporviewDocument extends vscode.Disposable implements vscode.Custo }, setscopetop: (name: string, id: number, tpe: string) => { - console.log(name); - console.log(id); - console.log(tpe); - const scope = createScope(name, tpe, "", id); this.treeData.push(scope); + this._netlistIdTable[id] = {netlistItem: scope, displayedItem: undefined, signalId: 0}; + }, + setmetadata: (scopecount: number, varcount: number, timescale: number, timeunit: string) => { + this.metadata.moduleCount = scopecount; + this.metadata.netlistIdCount = varcount; + this.metadata.timeScale = timescale; + this.metadata.timeUnit = timeunit; + this._netlistIdTable = new Array(varcount); }, setchunksize: (chunksize: bigint, timeend: bigint) => { this.setChunkSize(Number(chunksize)); @@ -192,7 +196,6 @@ export class VaporviewDocument extends vscode.Disposable implements vscode.Custo }); } }; - // The implementation of the log function that is called from WASM public get uri() { return this._uri; } @@ -203,6 +206,16 @@ export class VaporviewDocument extends vscode.Disposable implements vscode.Custo this.wasmApi = await filehandler._.bind(this.service, wasmModule, wasmWorker); } + private _readBody() { + vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: "Parsing Waveforms", + cancellable: false + }, async (progress) => { + await this.wasmApi.readbody(); + }); + } + public onWebviewReady(webviewPanel: vscode.WebviewPanel) { console.log("onWebviewReady"); this.webviewPanel = webviewPanel; @@ -226,10 +239,10 @@ export class VaporviewDocument extends vscode.Disposable implements vscode.Custo } public setNetlistIdTable(netlistId: NetlistId, displayedSignalViewRef: NetlistItem | undefined) { - const viewRef = this._netlistIdTable.get(netlistId); + const viewRef = this._netlistIdTable[netlistId]; if (viewRef === undefined) {return;} viewRef.displayedItem = displayedSignalViewRef; - this._netlistIdTable.set(netlistId, viewRef); + this._netlistIdTable[netlistId] = viewRef; } public setChunkSize(minTimeStemp: number) { @@ -324,7 +337,7 @@ class WaveformViewerProvider implements vscode.CustomReadonlyEditorProvider = new vscode.EventEmitter(); readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; private wasmApi: any = undefined; + private netlistIdTable: NetlistIdTable = []; public setCheckboxState(netlistItem: NetlistItem, checkboxState: vscode.TreeItemCheckboxState) { netlistItem.checkboxState = checkboxState; @@ -1213,6 +1220,7 @@ export class NetlistTreeDataProvider implements vscode.TreeDataProvider { - if (element) {return this.getChildrenExternal(element);} // Return the children of the selected element - else {return Promise.resolve(this.treeData);} // Return the top-level netlist items + return this.getChildrenExternal(element); } - async getChildrenExternal(element: NetlistItem): Promise { + private bitRangeString(msb: number, lsb: number): string { + if (msb < 0 || lsb < 0) {return "";} + if (msb === lsb) {return "[" + msb + "]";} + return "[" + msb + ":" + lsb + "]"; + } + + async getChildrenExternal(element: NetlistItem | undefined): Promise { + if (!element) {return Promise.resolve(this.treeData);} // Return the top-level netlist items if (!this.wasmApi) {return Promise.resolve([]);} if (element.children.length > 0) {return Promise.resolve(element.children);} @@ -1242,14 +1260,15 @@ export class NetlistTreeDataProvider implements vscode.TreeDataProvider 0) { - console.log("calling getscopes for " + modulePath + " with start index " + startIndex); + //console.log("calling getscopes for " + modulePath + " with start index " + startIndex); const children = await this.wasmApi.getchildren(element.netlistId, startIndex); const childItems = JSON.parse(children); //console.log(children); - console.log(childItems); + //console.log(childItems); itemsRemaining = childItems.remainingItems; startIndex += childItems.totalReturned; @@ -1257,12 +1276,40 @@ export class NetlistTreeDataProvider implements vscode.TreeDataProvider { - result.push(createVar(child.name, child.type, modulePath, child.netlistId, child.signalId, child.width)); + // Need to handle the case where we get a variable with the same name but + // different bit ranges. + const field = this.bitRangeString(child.msb, child.lsb); + const name = child.name + " " + field; + const varItem = createVar(name, child.type, modulePath, child.netlistId, child.signalId, child.width, child.msb, child.lsb); + if (varTable[child.name] === undefined) { + varTable[child.name] = [varItem]; + } else { + varTable[child.name].push(varItem); + } + this.netlistIdTable[child.netlistId] = {netlistItem: varItem, displayedItem: undefined, signalId: child.signalId}; }); callLimit--; if (callLimit <= 0) {break;} } + + for (const [key, value] of Object.entries(varTable)) { + if ((value as NetlistItem[]).length === 1) { + result.push((value as NetlistItem[])[0]); + } else { + const varList = value as NetlistItem[]; + const bitList: NetlistItem[] = []; + const busList: NetlistItem[] = []; + varList.forEach((varItem) => { + if (varItem.width === 1) {bitList.push(varItem);} + else {busList.push(varItem);} + }); + busList.forEach((busItem) => { + result.push(busItem); + }); + } + } + element.children = result; return Promise.resolve(element.children); } @@ -1494,10 +1541,8 @@ export async function activate(context: vscode.ExtensionContext) { // Add or remove signal commands context.subscriptions.push(vscode.commands.registerCommand('vaporview.removeSignal', (e) => { - if (e.netlistId) { + if (e.netlistId !== undefined) { viewerProvider.removeSignalFromDocument(e.netlistId); - - console.log(e); } })); diff --git a/src/lib.rs b/src/lib.rs index 655f9ae..b105b25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ wit_bindgen::generate!({ use std::io::{self, Read, Seek, SeekFrom}; use lazy_static::lazy_static; use std::sync::Mutex; -use wellen::{simple, FileFormat, GetItem, Hierarchy, HierarchyItem, ScopeRef, ScopeType, VarRef, SignalRef, SignalSource, TimeTable}; +use wellen::{simple, FileFormat, GetItem, Hierarchy, HierarchyItem, ScopeRef, ScopeType, VarRef, SignalRef, SignalSource, TimeTable, TimescaleUnit}; use wellen::viewers::{HeaderResult, read_header_from_bytes, read_body, ReadBodyContinuation}; use wellen::LoadOptions; @@ -126,6 +126,30 @@ impl Guest for Filecontext { let hierarchy = global_hierarchy.as_ref().unwrap(); + // count the number of scopes and vars + let scope_count = hierarchy.iter_scopes().count() as u32; + let var_count = hierarchy.iter_vars().count() as u32; + let time_scale_data = hierarchy.timescale(); + let time_unit = match time_scale_data { + Some(scale) => { + match scale.unit { + TimescaleUnit::FemtoSeconds => "fs".to_string(), + TimescaleUnit::PicoSeconds => "ps".to_string(), + TimescaleUnit::NanoSeconds => "ns".to_string(), + TimescaleUnit::MicroSeconds => "us".to_string(), + TimescaleUnit::MilliSeconds => "ms".to_string(), + TimescaleUnit::Seconds => "s".to_string(), + TimescaleUnit::Unknown => "s".to_string() + } + }, + None => "s".to_string(), + }; + let time_scale = match time_scale_data { + Some(scale) => scale.factor, + None => 1, + } as u32; + setmetadata(scope_count, var_count, time_scale, time_unit.as_str()); + for s in hierarchy.scopes() { let scope = hierarchy.get(s); log(&format!("ID: {:?} Scope: {:?}", s, scope)); @@ -242,7 +266,14 @@ impl Guest for Filecontext { let tpe = format!("{:?}", var.var_type()); let width = var.length().unwrap_or(0); let signal_ref = var.signal_ref().index(); - let var_string = format!("{{\"name\": {:?},\"netlistId\": {:?},\"signalId\": {:?},\"type\": {:?},\"width\": {:?}}}", name, id, signal_ref, tpe, width); + let mut msb: i32 = -1; + let mut lsb: i32 = -1; + let bits = var.index(); + match bits { + Some(b) => {msb = b.msb() as i32; lsb = b.lsb() as i32;}, + None => {} + } + let var_string = format!("{{\"name\": {:?},\"netlistId\": {:?},\"signalId\": {:?},\"type\": {:?},\"width\": {:?}, \"msb\": {:?}, \"lsb\": {:?}}}", name, id, signal_ref, tpe, width, msb, lsb); items_returned += 1; return_length += (var_string.len() as u32) + 1; @@ -327,8 +358,6 @@ impl Guest for Filecontext { } } - - } diff --git a/wit/filehandler.wit b/wit/filehandler.wit index acbc631..b727408 100644 --- a/wit/filehandler.wit +++ b/wit/filehandler.wit @@ -33,6 +33,7 @@ world filehandler { import fsread: func(fd: u32, offset: u64, length: u32) -> list; import getsize: func(fd: u32) -> u64; import setscopetop: func(name: string, id: u32, tpe: string); + import setmetadata: func (scopecount: u32, varcount: u32, timescale: u32, timeunit: string); import setchunksize: func(chunksize: u64, timeend: u64); import sendtransitiondatachunk: func(signalid: u32, totalchunks: u32, chunknum: u32, data: string);