I have been GTKWave user for long until i stumbled on Surfer. funny enough, I knew about it reading cocotb commits which added support for starting Surfer or gtkwave on generated vcd.

Installation Link to heading

We need Cargo and rustc obviously to build surfer as it is written in rust. That said, this is needed if you are building from source. There are Surfer binaries for all platforms if you just want to use it.

curl https://sh.rustup.rs -sSf | sh
git clone git@gitlab.com:surfer-project/surfer.git
cd surfer
git submodule update --init --recursive
cargo build --release

Surfer Link to heading

Digging deeper into surfer code surfer/src/main.rs, There are 2 main things happening surver (cool pun!) loading the vcd and exposing to API’s ui which webasm. The first snippet shows the call to server_main to start the server with the file, port and token shared between server and frontend.

        if let Some(Commands::Server { port, token, file }) = args.command {
            let default_port = 8911; // FIXME: make this more configurable
            let res = runtime.block_on(surver::server_main(
                port.unwrap_or(default_port),
                token,
                file,
                None,
            ));
            return res;
        }

The second snippet shows how it starts the UI by calling eframe which seems like the official lib over egui to create GUI applications in rust.

        let options = eframe::NativeOptions {
            viewport: egui::ViewportBuilder::default()
                .with_title("Surfer")
                .with_inner_size(Vec2::new(
                    state.user.config.layout.window_width as f32,
                    state.user.config.layout.window_height as f32,
                )),
            ..Default::default()
        };

        eframe::run_native("Surfer", options, Box::new(|cc| Ok(run_egui(cc, state)?))).unwrap();

Surver Link to heading

Looking at the server at surver/src/main.rs, surver can be called separately from surfer. This is the snippet and the print logs when called.

fn main() -> Result<()> {

    let runtime = tokio::runtime::Builder::new_current_thread()
        .worker_threads(1)
        .enable_all()
        .build()
        .unwrap();

    // parse arguments
    let args = Args::parse();
    let default_port = 8911; // FIXME: make this more configurable
    runtime.block_on(surver::server_main(
        args.port.unwrap_or(default_port),
        args.token,
        args.wave_file,
        None,
    ))
}
% target/release/surver examples/picorv32.vcd       
[INFO] Loaded header of examples/picorv32.vcd in 1.532375ms
[INFO] Starting server on 127.0.0.1:8911. To use:
[INFO] 1. Setup an ssh tunnel: -L 8911:localhost:8911
[INFO]    The correct command may be: ssh -L 8911:localhost:8911 aa@AMBA.local 
[INFO] 2. Start Surfer: surfer http://127.0.0.1:8911/qw 
[INFO] or, if the host is directly accessible:
[INFO] 1. Start Surfer: surfer http://MBA.local:8911/qw 
[INFO] Loaded body in 3.039667ms

Ok, Let’s look at the server. Starting from surver/src/server.rs, where server_main is defined. it takes couple of arguments the most important are token and filename.

pub async fn server_main(
    port: u16,
    token: Option<String>,
    filename: String,
    started: Option<ServerStartedFlag>,
) -> Result<()> {
    // if no token was provided, we generate one
    let token = token.unwrap_or_else(|| {
        // generate a random ASCII token
        repeat_with(fastrand::alphanumeric)
            .take(RAND_TOKEN_LEN)
            .collect()
    });

There it calls loader to load vcs and starts http server on that port.

    std::thread::spawn(move || loader(shared_2, header_result.body, state_2, rx));
  // create listener and serve it
    let listener = TcpListener::bind(&addr).await?;

    // we have started the server
    if let Some(started) = started {
        started.store(true, Ordering::SeqCst);
    }

    // main server loop
    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);

        let state = state.clone();
        let shared = shared.clone();
        let tx = tx.clone();
        tokio::task::spawn(async move {
            let service =
                service_fn(move |req| handle(state.clone(), shared.clone(), tx.clone(), req));
            if let Err(e) = http1::Builder::new().serve_connection(io, service).await {
                error!("server error: {}", e);
            }
        });
    }
/// Thread that loads the body and signals.
fn loader<R: BufRead + Seek + Sync + Send + 'static>(
    shared: Arc<ReadOnly>,
    body_cont: viewers::ReadBodyContinuation<R>,
    state: Arc<RwLock<State>>,
    rx: std::sync::mpsc::Receiver<SignalRequest>,
) -> Result<()> {

There is main handle function that process the requests to http server. For example, get_hierarchy branch call the function get_hierarchy and prepares response.

async fn handle(
    state: Arc<RwLock<State>>,
    shared: Arc<ReadOnly>,
    tx: Sender<SignalRequest>,
    req: Request<hyper::body::Incoming>,
) -> Result<Response<Full<Bytes>>> {
        ("get_hierarchy", []) => {
            let body = get_hierarchy(shared)?;
            Response::builder()
                .status(StatusCode::OK)
                .default_header()
                .body(Full::from(body))
        }
fn get_hierarchy(shared: Arc<ReadOnly>) -> Result<Vec<u8>> {
    let mut raw = BINCODE_OPTIONS.serialize(&shared.file_format)?;
    let mut raw2 = BINCODE_OPTIONS.serialize(&shared.hierarchy)?;
    raw.append(&mut raw2);
    let compressed = lz4_flex::compress_prepend_size(&raw);
    info!(
        "Sending hierarchy. {} raw, {} compressed.",
        bytesize::ByteSize::b(raw.len() as u64),
        bytesize::ByteSize::b(compressed.len() as u64)
    );
    Ok(compressed)
}

webasm UI Link to heading

I didn’t know you can write UI with rust but it seems there is framework called egui which is called from run_egui defined in libsurfer/src/lib.rs.

pub fn run_egui(cc: &CreationContext, mut state: SystemState) -> Result<Box<dyn App>> {
    let ctx_arc = Arc::new(cc.egui_ctx.clone());
    *EGUI_CONTEXT.write().unwrap() = Some(ctx_arc.clone());
    state.context = Some(ctx_arc.clone());
    cc.egui_ctx
        .set_visuals_of(egui::Theme::Dark, state.get_visuals());
    cc.egui_ctx
        .set_visuals_of(egui::Theme::Light, state.get_visuals());
    #[cfg(not(target_arch = "wasm32"))]
    if state.user.config.wcp.autostart {
        state.start_wcp_server(Some(state.user.config.wcp.address.clone()), false);
    }
    setup_custom_font(&cc.egui_ctx);
    Ok(Box::new(state))
}

There is too much code to read on UI side but i wanted to see how the UI calls the API (provided by Surver). I found some wrapper functions to do that. For example, get_hierarchy in libsurfer/src/remote/client.rs returns the hierarch by called {server}/get_hierarchy.

pub async fn get_hierarchy(server: String) -> Result<HierarchyResponse> {
    let client = reqwest::Client::new();
    let response = client.get(format!("{server}/get_hierarchy")).send().await?;
    check_response(&server, &response)?;
    let compressed = response.bytes().await?;
    let raw = lz4_flex::decompress_size_prepended(&compressed)?;
    let mut reader = std::io::Cursor::new(raw);
    // first we read a value, expecting there to be more bytes
    let opts = BINCODE_OPTIONS.allow_trailing_bytes();
    let file_format: wellen::FileFormat = opts.deserialize_from(&mut reader)?;
    // the last value should consume all remaining bytes
    let hierarchy: wellen::Hierarchy = BINCODE_OPTIONS.deserialize_from(&mut reader)?;
    Ok(HierarchyResponse {
        hierarchy,
        file_format,
    })
}