"use strict"; import { Platform } from "../baseplatform"; import { PLATFORMS, setKeyboardFromMap, AnimationTimer, RasterVideo, Keys, makeKeycodeMap } from "../emu"; import { SampleAudio } from "../audio"; var VERILOG_PRESETS = [ {id:'clock_divider.v', name:'Clock Divider'}, {id:'hvsync_generator.v', name:'Video Sync Generator'}, {id:'test_hvsync.v', name:'Test Pattern'}, {id:'7segment.v', name:'7-Segment Decoder'}, {id:'digits10.v', name:'Bitmapped Digits'}, {id:'scoreboard.v', name:'Scoreboard'}, {id:'ball_absolute.v', name:'Ball Motion (absolute position)'}, {id:'ball_slip_counter.v', name:'Ball Motion (slipping counter)'}, {id:'paddles.v', name:'Paddle Inputs'}, {id:'ball_paddle.v', name:'Brick Smash Game'}, {id:'ram1.v', name:'RAM Text Display'}, {id:'sprite_bitmap.v', name:'Sprite Bitmaps'}, {id:'sprite_renderer.v', name:'Sprite Rendering'}, {id:'racing_game.v', name:'Racing Game'}, {id:'sprite_rotation.v', name:'Sprite Rotation'}, {id:'tank.v', name:'Tank Game'}, {id:'sound_generator.v', name:'Sound Generator'}, {id:'lfsr.v', name:'Linear Feedback Shift Register'}, {id:'starfield.v', name:'Scrolling Starfield'}, {id:'cpu8.v', name:'Simple 8-Bit CPU'}, {id:'racing_game_cpu.v', name:'Racing Game with CPU'}, {id:'framebuffer.v', name:'Frame Buffer'}, {id:'tile_renderer.v', name:'Tile Renderer'}, {id:'sprite_scanline_renderer.v', name:'Sprite Scanline Renderer'}, {id:'cpu16.v', name:'16-Bit CPU'}, {id:'cpu_platform.v', name:'CPU Platform'}, {id:'test2.asm', name:'16-bit ASM Game'}, ]; var VERILOG_KEYCODE_MAP = makeKeycodeMap([ [Keys.VK_LEFT, 0, 0x1], [Keys.VK_RIGHT, 0, 0x2], [Keys.VK_UP, 0, 0x4], [Keys.VK_DOWN, 0, 0x8], [Keys.VK_SPACE, 0, 0x10], [Keys.VK_SHIFT, 0, 0x20], [Keys.VK_A, 1, 0x1], [Keys.VK_D, 1, 0x2], [Keys.VK_W, 1, 0x4], [Keys.VK_S, 1, 0x8], [Keys.VK_Z, 1, 0x10], [Keys.VK_X, 1, 0x20], [Keys.VK_1, 2, 0x1], [Keys.VK_2, 2, 0x2], [Keys.VK_5, 2, 0x4], [Keys.VK_6, 2, 0x8], [Keys.VK_7, 2, 0x10], ]); var vl_finished = false; var vl_stopped = false; // TODO: these have to be global var VL_UL = this.VL_UL = function(x) { return x|0; } var VL_ULL = this.VL_ULL = function(x) { return x|0; } var VL_TIME_Q = this.VL_TIME_Q = function() { return (new Date().getTime())|0; } /// Return true if data[bit] set var VL_BITISSET_I = this.VL_BITISSET_I = function(data,bit) { return (data & (VL_UL(1)< VL_EXTENDS_II(x,lbits,rhs)) ? 1 : 0; } var VL_LTES_III = this.VL_LTES_III = function(x,lbits,y,lhs,rhs) { return (VL_EXTENDS_II(x,lbits,lhs) <= VL_EXTENDS_II(x,lbits,rhs)) ? 1 : 0; } var VL_GTES_III = this.VL_GTES_III = function(x,lbits,y,lhs,rhs) { return (VL_EXTENDS_II(x,lbits,lhs) >= VL_EXTENDS_II(x,lbits,rhs)) ? 1 : 0; } var VL_MODDIV_III = this.VL_MODDIV_III = function(lbits,lhs,rhs) { return (((rhs)==0)?0:(lhs)%(rhs)); } var VL_MODDIVS_III = this.VL_MODDIVS_III = function(lbits,lhs,rhs) { return (((rhs)==0)?0:(lhs)%(rhs)); } var VL_REDXOR_32 = this.VL_REDXOR_32 = function(r) { r=(r^(r>>1)); r=(r^(r>>2)); r=(r^(r>>4)); r=(r^(r>>8)); r=(r^(r>>16)); return r; } var VL_WRITEF = this.VL_WRITEF = console.log; // TODO: $write var vl_finish = this.vl_finish = function(filename,lineno,hier) { console.log("Finished at " + filename + ":" + lineno, hier); vl_finished = true; } var vl_stop = this.vl_stop = function(filename,lineno,hier) { console.log("Stopped at " + filename + ":" + lineno, hier); vl_stopped = true; } var VL_RAND_RESET_I = this.VL_RAND_RESET_I = function(bits) { return 0 | Math.floor(Math.random() * (1< 100) { vl_fatal("Verilated model didn't converge"); } } if (__VclockLoop > maxVclockLoop) { maxVclockLoop = __VclockLoop; console.log("Graph took " + maxVclockLoop + " iterations to stabilize"); } totalTicks++; } this._eval_initial_loop = function(vlSymsp) { vlSymsp.TOPp = this; vlSymsp.__Vm_didInit = true; this._eval_initial(vlSymsp); vlSymsp.__Vm_activity = true; var __VclockLoop = 0; var __Vchange=1; while (__Vchange) { this._eval_settle(vlSymsp); this._eval(vlSymsp); __Vchange = this._change_request(vlSymsp); if (++__VclockLoop > 100) { vl_fatal("Verilated model didn't DC converge"); } } } } var VerilogPlatform = function(mainElement, options) { var self = this; var video, audio; var useAudio = false; var videoWidth = 292; var videoHeight = 256; var maxVideoLines = 262+40; // vertical hold var idata, timer, timerCallback; var gen; var cyclesPerFrame = (256+23+7+23)*262; // 4857480/60 Hz var current_output; var paddle_x = 0; var paddle_y = 0; var switches = [0,0,0]; var inspect_obj, inspect_sym; var inspect_data = new Uint32Array(videoWidth * videoHeight); var scope_time_x = 0; // scope cursor var scope_x_offset = 0; var scope_y_offset = 0; var scope_index_offset = 0; var scope_max_y = 0; var scope_y_top = 0; var scope_a = 0; // used for transitions var scopeWidth = videoWidth; var scopeHeight = videoHeight; var scopeImageData; var sdata; // scope data var module_name; var yposlist = []; var lasty = []; var lastval = []; var trace_ports; var trace_signals; var trace_buffer; var trace_index; var mouse_pressed; var dirty = false; this.getPresets = function() { return VERILOG_PRESETS; } var RGBLOOKUP = [ 0xff222222, 0xff2222ff, 0xff22ff22, 0xff22ffff, 0xffff2222, 0xffff22ff, 0xffffff22, 0xffffffff, 0xff999999, 0xff9999ff, 0xff99ff99, 0xff99ffff, 0xffff9999, 0xffff99ff, 0xffffff99, 0xff666666, ]; var debugCond; function vidtick() { gen.tick2(); if (useAudio) audio.feedSample(gen.spkr*(1.0/255.0), 1); if (debugCond && debugCond()) debugCond = null; } function updateInspectionFrame() { useAudio = false; if (inspect_obj && inspect_sym) { var COLOR_BIT_OFF = 0xffff6666; var COLOR_BIT_ON = 0xffff9999; var i = videoWidth; for (var y=0; y videoWidth*2) { framehsync = false; framex = 0; framey++; gen.hpaddle = framey > paddle_x ? 1 : 0; gen.vpaddle = framey > paddle_y ? 1 : 0; } if (framey > maxVideoLines || gen.vsync) { framevsync = true; framey = 0; framex = 0; frameidx = 0; gen.hpaddle = 0; gen.vpaddle = 0; } else { var wasvsync = framevsync; framevsync = false; if (sync && wasvsync) return; // exit when vsync ends } } } function updateVideoFrame() { useAudio = (audio != null); debugCond = self.getDebugCallback(); gen.switches_p1 = switches[0]; gen.switches_p2 = switches[1]; gen.switches_gen = switches[2]; var fps = self.getFrameRate(); // darken the previous frame? if (fps < 45) { var mask = fps > 5 ? 0xe7ffffff : 0x7fdddddd; for (var i=0; i 45; var trace = fps < 0.02; updateVideoFrameCycles(cyclesPerFrame * fps/60 + 1, sync, trace); //if (trace) displayTraceBuffer(); self.restartDebugState(); gen.__unreset(); refreshVideoFrame(); } function refreshVideoFrame() { updateInspectionFrame(); updateAnimateScope(); updateInspectionPostFrame(); } function updateFrame() { if (!gen) return; if (gen.vsync !== undefined && gen.hsync !== undefined && gen.rgb !== undefined) updateVideoFrame(); else updateScopeFrame(); } function refreshFrame() { if (!gen) return; if (gen.vsync !== undefined && gen.hsync !== undefined && gen.rgb !== undefined) refreshVideoFrame(); else refreshScopeOverlay(trace_ports); } function updateAnimateScope() { var fps = self.getFrameRate(); var trace = fps < 0.02; var ctx = video.getContext(); if (scope_a > 0.01) { ctx.fillStyle = "black"; ctx.fillRect(0, 0, videoWidth, videoHeight); var vidyoffset = Math.round(scope_a*(-framey+videoHeight/6)); video.updateFrame(0, vidyoffset, 0, 0, videoWidth, videoHeight); ctx.fillStyle = "white"; ctx.fillRect(framex, framey+vidyoffset, 1, 1); scope_index_offset = (trace_index - trace_signals.length*scopeWidth + trace_buffer.length) % trace_buffer.length; scope_x_offset = 0; refreshScopeOverlay(trace_signals); } else { video.updateFrame(); scope_index_offset = 0; } // smooth transition scope_a = scope_a * 0.9 + (trace?1.0:0.0) * 0.1; scope_y_top = (1 - scope_a*0.7) * videoHeight - (1 - scope_a) * scope_y_offset; } function displayTraceBuffer() { var skip = trace_signals.length; var src = trace_index; for (var dest=0; dest= trace_buffer.length) trace_index = 0; } } function fillTraceBuffer(count) { var max_index = Math.min(trace_buffer.length - trace_ports.length, trace_index + count); while (trace_index < max_index) { gen.clk ^= 1; gen.eval(); snapshotTrace(false); dirty = true; } gen.__unreset(); } function shadowText(ctx, txt, x, y) { ctx.shadowColor = "black"; ctx.shadowBlur = 0; ctx.shadowOffsetY = -1; ctx.shadowOffsetX = 0; ctx.fillText(txt, x, y); ctx.shadowOffsetY = 1; ctx.shadowOffsetX = 0; ctx.fillText(txt, x, y); ctx.shadowOffsetY = 0; ctx.shadowOffsetX = -1; ctx.fillText(txt, x, y); ctx.shadowOffsetY = 0; ctx.shadowOffsetX = 1; ctx.fillText(txt, x, y); ctx.shadowOffsetX = 0; } function updateScopeFrame() { fillTraceBuffer(Math.floor(videoWidth/4) * trace_ports.length); if (!dirty) return; dirty = false; scope_y_top = 0; refreshScopeOverlay(trace_ports); } function refreshScopeOverlay(arr) { if (!sdata) { scopeImageData = video.getContext().createImageData(scopeWidth,scopeHeight); sdata = new Uint32Array(scopeImageData.data.buffer); } var COLOR_BLACK = 0xff000000; var COLOR_SIGNAL = 0xff22ff22; var COLOR_BORDER = 0xff662222; var COLOR_TRANS_SIGNAL = 0xff226622; var COLOR_BLIP_SIGNAL = 0xff226622; sdata.fill(0xff000000); var jstart = scope_x_offset * arr.length + scope_index_offset; var j = jstart; for (var x=0; x1 ? v.len*2+8 : 8; var y2 = y1+ys; var z = trace_buffer[j++]; if (j >= trace_buffer.length) j = 0; var y = Math.round(y2 - ys*((z-lo)/hi)); yposlist[i] = y2 + scope_y_top; var ly = lasty[i]; if (x > 0 && ly != y) { var dir = ly < y ? 1 : -1; while ((ly += dir) != y && ly >= y1 && ly <= y2) { sdata[x + ly * scopeWidth] = COLOR_TRANS_SIGNAL; } } sdata[x + y * scopeWidth] = lastval[i]==z ? COLOR_SIGNAL : COLOR_BLIP_SIGNAL; lasty[i] = y; lastval[i] = z; y1 += ys+yb; } } scope_max_y = y1 - scope_y_offset; video.getContext().putImageData(scopeImageData, 0, scope_y_top); // draw labels var ctx = video.getContext(); for (var i=0; i videoHeight) continue; var v = arr[i]; var name = v.name; ctx.fillStyle = name == inspect_sym ? "yellow" : "white"; name = name.replace(/__DOT__/g,'.'); name = name.replace(module_name+'.',''); ctx.textAlign = 'left'; ctx.fillStyle = "white"; shadowText(ctx, name, 1, yposlist[i]); if (scope_time_x > 0) { ctx.textAlign = 'right'; var value = (arr.length * scope_time_x + i + jstart) % trace_buffer.length; shadowText(ctx, ""+trace_buffer[value], videoWidth-1, yp); } } // draw scope line & label if (scope_time_x > 0) { ctx.fillStyle = "cyan"; shadowText(ctx, ""+(scope_time_x+scope_x_offset), (scope_time_x>10)?(scope_time_x-2):(scope_time_x+20), videoHeight-2); ctx.fillRect(scope_time_x, 0, 1, 4000); } // scroll left/right if (scope_time_x >= videoWidth && scope_x_offset < (trace_buffer.length / arr.length) - videoWidth) { scope_x_offset += 1 + (scope_time_x - videoWidth); dirty = true; } else if (scope_time_x < 0 && scope_x_offset > 0) { scope_x_offset = Math.max(0, scope_x_offset + scope_time_x); dirty = true; } } function clamp(minv,maxv,v) { return (v < minv) ? minv : (v > maxv) ? maxv : v; } this.start = function() { video = new RasterVideo(mainElement,videoWidth,videoHeight); video.create(); var ctx = video.getContext(); ctx.font = "8px TinyFont"; ctx.fillStyle = "white"; ctx.textAlign = "left"; setKeyboardFromMap(video, switches, VERILOG_KEYCODE_MAP); var vcanvas = $(video.canvas); vcanvas.mousemove(function(e) { var new_x = Math.floor(e.offsetX * video.canvas.width / vcanvas.width() - 20); var new_y = Math.floor(e.offsetY * video.canvas.height / vcanvas.height() - 20); if (mouse_pressed) { scope_y_offset = clamp(Math.min(0,-scope_max_y+videoHeight), 0, scope_y_offset + new_y - paddle_y); scope_time_x = Math.floor(e.offsetX * video.canvas.width / vcanvas.width() - 16); dirty = true; refreshFrame(); } paddle_x = clamp(8, 240, new_x); paddle_y = clamp(8, 240, new_y); }); vcanvas.mousedown(function(e) { scope_time_x = Math.floor(e.offsetX * video.canvas.width / vcanvas.width() - 16); mouse_pressed = true; //if (e.target.setCapture) e.target.setCapture(); // TODO: pointer capture dirty = true; refreshFrame(); }); vcanvas.mouseup(function(e) { mouse_pressed = false; //if (e.target.setCapture) e.target.releaseCapture(); // TODO: pointer capture dirty = true; refreshFrame(); }); vcanvas.keydown(function(e) { switch (e.keyCode) { case 37: scope_time_x--; dirty=true; refreshFrame(); break; case 39: scope_time_x++; dirty=true; refreshFrame(); break; } }); idata = video.getFrameData(); timerCallback = function() { if (!self.isRunning()) return; gen.switches = switches[0]; updateFrame(); }; trace_buffer = new Uint32Array(0x10000); self.setFrameRate(60); } this.printErrorCodeContext = function(e, code) { if (e.lineNumber && e.message) { var lines = code.split('\n'); var s = e.message + '\n'; for (var i=0; i e.lineNumber-5 && i < e.lineNumber+5) { s += lines[i] + '\n'; } } console.log(s); } } this.loadROM = function(title, output) { var mod; if (output.code) { // is code identical? if (current_output && current_output.code == output.code) { } else { try { mod = new Function('base', output.code); } catch (e) { this.printErrorCodeContext(e, output.code); throw e; } // compile Verilog code var base = new VerilatorBase(); gen = new mod(base); //$.extend(gen, base); gen.__proto__ = base; current_output = output; module_name = output.name ? output.name.substr(1) : "top"; trace_ports = current_output.ports; trace_signals = current_output.ports.concat(current_output.signals); trace_index = 0; // power on module this.poweron(); } } // replace program ROM, if using the assembler if (output.program_rom && output.program_rom_variable) { if (gen[output.program_rom_variable]) { if (gen[output.program_rom_variable].length != output.program_rom.length) alert("ROM size mismatch -- expected " + gen[output.program_rom_variable].length + " got " + output.program_rom.length); else gen[output.program_rom_variable] = output.program_rom; } else { alert("No program_rom variable found (" + output.program_rom_variable + ")"); } this.reset(); } // restart audio restartAudio(); } function restartAudio() { // stop/start audio var hasAudio = gen && gen.spkr !== undefined && frameRate > 1; if (audio && !hasAudio) { audio.stop(); audio = null; } else if (!audio && hasAudio) { audio = new SampleAudio(cyclesPerFrame * self.getFrameRate()); if (self.isRunning()) audio.start(); } } this.isRunning = function() { return timer && timer.isRunning(); } this.pause = function() { timer.stop(); if (audio) audio.stop(); } this.resume = function() { timer.start(); if (audio) audio.start(); } var frameRate = 0; this.setFrameRate = function(rateHz) { frameRate = rateHz; var fps = Math.min(60, rateHz*cyclesPerFrame); if (!timer || timer.frameRate != fps) { var running = this.isRunning(); if (timer) timer.stop(); timer = new AnimationTimer(fps, timerCallback); if (running) timer.start(); } if (audio) { audio.stop(); audio = null; } restartAudio(); } this.getFrameRate = function() { return frameRate; } this.poweron = function() { gen._ctor_var_reset(); this.reset(); } this.reset = function() { gen.__reset(); trace_index = scope_x_offset = 0; if (trace_buffer) trace_buffer.fill(0); dirty = true; if (video) video.setRotate(gen.rotate ? -90 : 0); } this.tick = function() { gen.tick2(); } this.getToolForFilename = function(fn) { if (fn.endsWith("asm")) return "jsasm"; else return "verilator"; } this.getDefaultExtension = function() { return ".v"; }; this.inspect = function(name) { if (!gen) return; if (name && !name.match(/^\w+$/)) return; var val = gen[name]; if (val === undefined && current_output.code) { var re = new RegExp("(\\w+__DOT__" + name + ")\\b", "gm"); var m = re.exec(current_output.code); if (m) { name = m[1]; val = gen[name]; } } if (typeof(val) === 'number') { inspect_obj = gen; inspect_sym = name; } else { inspect_obj = inspect_sym = null; } dirty = true; } // DEBUGGING this.saveState = function() { return {T:gen.ticks(), o:$.extend(true, {}, gen)}; } this.loadState = function(state) { gen = $.extend(true, gen, state.o); gen.setTicks(state.T); } var onBreakpointHit; var debugCondition; var debugSavedState = null; var debugBreakState = null; var debugTargetClock = 0; this.setDebugCondition = function(debugCond) { if (debugSavedState) { this.loadState(debugSavedState); } else { debugSavedState = this.saveState(); } debugCondition = debugCond; debugBreakState = null; this.resume(); } this.restartDebugState = function() { if (debugCondition && !debugBreakState) { debugSavedState = this.saveState(); if (debugTargetClock > 0) debugTargetClock -= debugSavedState.T; debugSavedState.T = 0; this.loadState(debugSavedState); } } this.getDebugCallback = function() { return debugCondition; } this.setupDebug = function(callback) { onBreakpointHit = callback; } this.clearDebug = function() { debugSavedState = null; debugBreakState = null; debugTargetClock = 0; onBreakpointHit = null; debugCondition = null; } this.breakpointHit = function(targetClock) { debugTargetClock = targetClock; debugBreakState = this.saveState(); console.log("Breakpoint at clk", debugBreakState.T); this.pause(); if (onBreakpointHit) { onBreakpointHit(debugBreakState); } } this.wasBreakpointHit = function() { return debugBreakState != null; } this.step = function() { var self = this; this.setDebugCondition(function() { if (gen.ticks() > debugTargetClock) { self.breakpointHit(gen.ticks()); return true; } return false; }); } this.runToVsync = function() { var self = this; this.setDebugCondition(function() { if (gen.vsync && gen.ticks() > debugTargetClock+2000) { self.breakpointHit(gen.ticks()); return true; } return false; }); } this.stepBack = function() { var self = this; var prevState; var prevClock; this.setDebugCondition(function() { var debugClock = gen.ticks(); if (debugClock >= debugTargetClock && prevState) { self.loadState(prevState); self.breakpointHit(prevClock); return true; } else if (debugClock >= debugTargetClock-2 && debugClock < debugTargetClock) { prevState = self.saveState(); prevClock = debugClock; } return false; }); } this.runEval = function(evalfunc) { var self = this; this.setDebugCondition(function() { if (gen.ticks() > debugTargetClock) { if (evalfunc(gen)) { self.breakpointHit(gen.ticks()); return true; } } return false; }); } }; //////////////// PLATFORMS['verilog'] = VerilogPlatform;