diff --git a/litex/build/sim/core/Makefile b/litex/build/sim/core/Makefile index 8eda365b7..62b7a1f25 100644 --- a/litex/build/sim/core/Makefile +++ b/litex/build/sim/core/Makefile @@ -14,7 +14,7 @@ ifeq ($(UNAME_S),Darwin) else CC ?= gcc CFLAGS += -ggdb - LDFLAGS += -lpthread -Wl,--no-as-needed -ljson-c -lz -lm -lstdc++ -Wl,--no-as-needed -ldl -levent + LDFLAGS += -lpthread -Wl,--no-as-needed -ljson-c -lz -lm -lstdc++ -Wl,--no-as-needed -ldl -levent -lSDL2 endif CFLAGS += -Wall -$(OPT_LEVEL) $(if $(COVERAGE), -DVM_COVERAGE) $(if $(TRACE_FST), -DTRACE_FST) diff --git a/litex/build/sim/core/modules/Makefile b/litex/build/sim/core/modules/Makefile index 4d44c72b0..d46147b34 100644 --- a/litex/build/sim/core/modules/Makefile +++ b/litex/build/sim/core/modules/Makefile @@ -1,5 +1,5 @@ include ../variables.mak -MODULES = xgmii_ethernet ethernet serial2console serial2tcp clocker spdeeprom gmii_ethernet +MODULES = xgmii_ethernet ethernet serial2console serial2tcp clocker spdeeprom gmii_ethernet video .PHONY: $(MODULES) $(EXTRA_MOD_LIST) all: $(MODULES) $(EXTRA_MOD_LIST) diff --git a/litex/build/sim/core/modules/video/Makefile b/litex/build/sim/core/modules/video/Makefile new file mode 100644 index 000000000..0c69b5b9e --- /dev/null +++ b/litex/build/sim/core/modules/video/Makefile @@ -0,0 +1,2 @@ +include ../../variables.mak +include $(SRC_DIR)/modules/rules.mak diff --git a/litex/build/sim/core/modules/video/sim_fb.c b/litex/build/sim/core/modules/video/sim_fb.c new file mode 100644 index 000000000..79c5c10f5 --- /dev/null +++ b/litex/build/sim/core/modules/video/sim_fb.c @@ -0,0 +1,58 @@ +// Simple framebuffer windows for visualizations +// Copyright (C) 2022 Victor Suarez Rovere + +#include +#include "sim_fb.h" + +bool fb_init(unsigned width, unsigned height, bool vsync, fb_handle_t *handle) +{ + if(SDL_Init(SDL_INIT_VIDEO) < 0) + return false; + + handle->win = SDL_CreateWindow("LiteX Sim Video Window", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, SDL_WINDOW_SHOWN); + if (!handle->win) + return false; + + handle->renderer = SDL_CreateRenderer(handle->win, -1, vsync ? SDL_RENDERER_ACCELERATED | SDL_RENDERER_TARGETTEXTURE | SDL_RENDERER_PRESENTVSYNC : 0); + if (!handle->renderer) + return false; + + handle->texture = SDL_CreateTexture(handle->renderer, SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_TARGET, width, height); + if (!handle->texture) + return false; + + return true; +} + +bool fb_should_quit(void) +{ + SDL_Event event; + while(SDL_PollEvent(&event)) + { + switch(event.type) + { + case SDL_QUIT: + return true; + case SDL_KEYDOWN: + if(event.key.keysym.sym == SDLK_ESCAPE) + return true; + } + } + return false; +} + +void fb_update(fb_handle_t *handle, const void *buf, size_t stride_bytes) +{ + SDL_UpdateTexture(handle->texture, NULL, buf, stride_bytes); + SDL_RenderCopy(handle->renderer, handle->texture, NULL, NULL); + SDL_RenderPresent(handle->renderer); +} + +void fb_deinit(fb_handle_t *handle) +{ + SDL_DestroyTexture(handle->texture); + SDL_DestroyRenderer(handle->renderer); + SDL_DestroyWindow(handle->win); + handle->win = NULL; + SDL_Quit(); +} diff --git a/litex/build/sim/core/modules/video/sim_fb.h b/litex/build/sim/core/modules/video/sim_fb.h new file mode 100644 index 000000000..3feb34576 --- /dev/null +++ b/litex/build/sim/core/modules/video/sim_fb.h @@ -0,0 +1,34 @@ +// Copyright (C) 2022 Victor Suarez Rovere + +#ifndef __SIM_FB_H__ +#define __SIM_FB_H__ + +struct SDL_Window; +struct SDL_Renderer; +struct SDL_Texture; + +typedef struct +{ + SDL_Window* win; + SDL_Renderer* renderer; + SDL_Texture* texture; +} fb_handle_t; + +bool fb_init(unsigned width, unsigned height, bool vsync, fb_handle_t *handle); +void fb_update(fb_handle_t *handle, const void *buf, size_t stride_bytes); +void fb_deinit(fb_handle_t *handle); +bool fb_should_quit(void); + +#ifdef __cplusplus +extern "C" { +#endif +uint64_t SDL_GetPerformanceCounter(void); +uint64_t SDL_GetPerformanceFrequency(void); +#ifdef __cplusplus +} +#endif + +inline uint64_t higres_ticks() { return SDL_GetPerformanceCounter(); } +inline uint64_t higres_ticks_freq() { return SDL_GetPerformanceFrequency(); } + +#endif //__SIM_FB_H__ diff --git a/litex/build/sim/core/modules/video/video.c b/litex/build/sim/core/modules/video/video.c new file mode 100644 index 000000000..9a3df407f --- /dev/null +++ b/litex/build/sim/core/modules/video/video.c @@ -0,0 +1,205 @@ +// Copyright (c) 2023 Victor Suarez Rovere +// Copyright (c) LiteX developers +// FIXME: add license + +#include +#include +#include +#include "error.h" +#include +#include +#include +#include +#include + +#include "modules.h" +#include "sim_fb.c" //from https://github.com/suarezvictor/CflexHDL/blob/main/src/sim_fb.c + +struct session_s { + char *hsync; + char *vsync; + char *de; + char *valid; + char *pix_clk; + uint8_t *r; + uint8_t *g; + uint8_t *b; + short hres, vres; + short x, y; + unsigned frame, stride; + uint8_t *buf, *pbuf; + fb_handle_t fb; +}; + +static int litex_sim_module_pads_get(struct pad_s *pads, char *name, void **signal) +{ + int ret = RC_OK; + void *sig = NULL; + int i; + + if(!pads || !name || !signal) { + ret=RC_INVARG; + goto out; + } + + i = 0; + while(pads[i].name) { + if(!strcmp(pads[i].name, name)) { + sig = (void*)pads[i].signal; + break; + } + i++; + } + +out: + *signal = sig; + return ret; +} + +static int videosim_start(void *b) +{ + printf("[video] loaded (%p)\n", (struct event_base *)b); + return RC_OK; +} + +static int videosim_new(void **sess, char *args) +{ + int ret = RC_OK; + struct session_s *s = NULL; + + if(!sess) { + ret = RC_INVARG; + goto out; + } + + s = (struct session_s*) malloc(sizeof(struct session_s)); + if(!s) { + ret=RC_NOENMEM; + goto out; + } + memset(s, 0, sizeof(struct session_s)); + +out: + *sess = (void*) s; + return ret; +} + +static int videosim_add_pads(void *sess, struct pad_list_s *plist) +{ + int ret = RC_OK; + struct session_s *s = (struct session_s*) sess; + struct pad_s *pads; + + if(!sess || !plist) { + ret = RC_INVARG; + goto out; + } + pads = plist->pads; + if(!strcmp(plist->name, "vga")) { + litex_sim_module_pads_get(pads, "hsync", (void**)&s->hsync); + litex_sim_module_pads_get(pads, "vsync", (void**)&s->vsync); + litex_sim_module_pads_get(pads, "de", (void**)&s->de); + litex_sim_module_pads_get(pads, "valid", (void**)&s->valid); + litex_sim_module_pads_get(pads, "r", (void**)&s->r); + litex_sim_module_pads_get(pads, "g", (void**)&s->g); + litex_sim_module_pads_get(pads, "b", (void**)&s->b); + char *clk_pad = NULL; + litex_sim_module_pads_get(pads, "clk", (void**) &clk_pad); + if(clk_pad != NULL) + s->pix_clk = clk_pad; //overrides sys_clk if previously set + } + + if(!strcmp(plist->name, "sys_clk")) + { + if(!s->pix_clk) //not selected if vga clk was already used + litex_sim_module_pads_get(pads, "sys_clk", (void**) &s->pix_clk); + } + +out: + return ret; +} + +static uint8_t *alloc_buf(unsigned short hres, unsigned short vres) +{ + return (uint8_t *) malloc(hres*vres*sizeof(uint32_t)); +} + +static int videosim_tick(void *sess, uint64_t time_ps) { + static clk_edge_state_t edge; + struct session_s *s = (struct session_s*)sess; + + if(!clk_pos_edge(&edge, *s->pix_clk)) { + return RC_OK; + } + + if(*s->vsync) + { + if(s->y != 0) //new frame + { + if(!s->vres) + { + s->vres = s->y; + s->buf = alloc_buf(s->hres, s->vres); + //printf("[video] start, resolution %dx%d\n", s->hres, s->vres); + fb_init(s->hres, s->vres, false, &s->fb); + s->stride = s->hres*sizeof(uint32_t); + } + s->y = 0; + s->pbuf = s->buf; + ++s->frame; + } + } + + if(*s->de) + { + if(s->pbuf) + { + if(*s->valid) //mitigate underflow + { + *s->pbuf++ = *s->r; + *s->pbuf++ = *s->g; + *s->pbuf++ = *s->b; + s->pbuf++; + } + } + if(*s->valid) + s->x = s->x + 1; + } + else if(s->x != 0) + { + if(s->buf) //update each horizontal line + { + if(fb_should_quit()) + { + fb_deinit(&s->fb); + exit(1); //FIXME: end gracefully + } + fb_update(&s->fb, s->buf, s->stride); + s->pbuf = s->buf + s->y*s->stride; + } + + s->hres = s->x; //this is set many times until settled + if(!s->hres) //avoid initial counting + s->y = 0; + s->y = s->y + 1; + s->x = 0; + } + + return RC_OK; +} + +static struct ext_module_s ext_mod = { + "video", + videosim_start, + videosim_new, + videosim_add_pads, + NULL, + videosim_tick +}; + +int litex_sim_ext_module_init(int (*register_module) (struct ext_module_s *)) +{ + int ret = RC_OK; + ret = register_module(&ext_mod); + return ret; +} diff --git a/litex/tools/litex_sim.py b/litex/tools/litex_sim.py index c1cd7fb6a..764e5268b 100755 --- a/litex/tools/litex_sim.py +++ b/litex/tools/litex_sim.py @@ -6,6 +6,7 @@ # Copyright (c) 2015-2020 Florent Kermarrec # Copyright (c) 2020 Antmicro # Copyright (c) 2017 Pierre-Olivier Vauboin +# Copyright (c) 2023 Victor Suarez Rovere # SPDX-License-Identifier: BSD-2-Clause import sys @@ -124,6 +125,17 @@ _io = [ Subsignal("oe", Pins(32)), Subsignal("o", Pins(32)), Subsignal("i", Pins(32)), + ), + + # Video + ("vga", 0, + Subsignal("hsync", Pins(1)), + Subsignal("vsync", Pins(1)), + Subsignal("de", Pins(1)), + Subsignal("valid", Pins(1)), + Subsignal("r", Pins(8)), + Subsignal("g", Pins(8)), + Subsignal("b", Pins(8)), ) ] @@ -133,6 +145,36 @@ class Platform(SimPlatform): def __init__(self): SimPlatform.__init__(self, "SIM", _io) +# Video +from litex.soc.cores.video import video_data_layout, video_timing_layout +class VideoPHYModel(Module, AutoCSR): + def __init__(self, pads, clock_domain="sys"): + self.sink = sink = stream.Endpoint(video_data_layout) + + # # # + + # Always ack Sink, no backpressure. + self.comb += sink.ready.eq(1) + + # Drive Clk. + if hasattr(pads, "clk"): + self.comb += pads.clk.eq(ClockSignal(clock_domain)) + + # Drive Controls. + self.comb += pads.valid.eq(1) #may be overriden with underflow from the framebuffer + self.comb += pads.de.eq(sink.de) + self.comb += pads.hsync.eq(sink.hsync) + self.comb += pads.vsync.eq(sink.vsync) + + # Drive Datas. + cbits = len(pads.r) + cshift = (8 - cbits) + for i in range(cbits): + self.comb += pads.r[i].eq(sink.r[cshift + i] & sink.de) + self.comb += pads.g[i].eq(sink.g[cshift + i] & sink.de) + self.comb += pads.b[i].eq(sink.b[cshift + i] & sink.de) + + # Simulation SoC ----------------------------------------------------------------------------------- class SimSoC(SoCCore): @@ -155,6 +197,8 @@ class SimSoC(SoCCore): with_spi_flash = False, spi_flash_init = [], with_gpio = False, + with_video_framebuffer = False, + with_video_terminal = False, sim_debug = False, trace_reset_on = False, **kwargs): @@ -291,6 +335,18 @@ class SimSoC(SoCCore): self.gpio = GPIOTristate(platform.request("gpio"), with_irq=True) self.irq.add("gpio", use_loc_if_exists=True) + # Video Framebuffer ------------------------------------------------------------------------ + if with_video_framebuffer: + video_pads = platform.request("vga") + self.submodules.videophy = VideoPHYModel(video_pads) + self.add_video_framebuffer(phy=self.videophy, timings="640x480@60Hz", format="rgb888") + self.videophy.comb += video_pads.valid.eq(~self.video_framebuffer.underflow) + + # Video Terminal --------------------------------------------------------------------------- + if with_video_terminal: + self.submodules.videophy = VideoPHYModel(platform.request("vga")) + self.add_video_terminal(phy=self.videophy, timings="640x480@60Hz") + # Simulation debugging ---------------------------------------------------------------------- if sim_debug: platform.add_debug(self, reset=1 if trace_reset_on else 0) @@ -400,6 +456,10 @@ def sim_args(parser): # Analyzer. parser.add_argument("--with-analyzer", action="store_true", help="Enable Analyzer support.") + # Video. + parser.add_argument("--with-video-framebuffer", action="store_true", help="Enable Video Framebuffer.") + parser.add_argument("--with-video-terminal", action="store_true", help="Enable Video Terminal.") + # Debug/Waveform. parser.add_argument("--sim-debug", action="store_true", help="Add simulation debugging modules.") parser.add_argument("--gtkwave-savefile", action="store_true", help="Generate GTKWave savefile.") @@ -476,6 +536,10 @@ def main(): if args.with_i2c: sim_config.add_module("spdeeprom", "i2c") + # Video. + if args.with_video_framebuffer or args.with_video_terminal: + sim_config.add_module("video", "vga") + # SoC ------------------------------------------------------------------------------------------ soc = SimSoC( with_sdram = args.with_sdram, @@ -488,6 +552,8 @@ def main(): with_sdcard = args.with_sdcard, with_spi_flash = args.with_spi_flash, with_gpio = args.with_gpio, + with_video_framebuffer = args.with_video_framebuffer, + with_video_terminal = args.with_video_terminal, sim_debug = args.sim_debug, trace_reset_on = int(float(args.trace_start)) > 0 or int(float(args.trace_end)) > 0, spi_flash_init = None if args.spi_flash_init is None else get_mem_data(args.spi_flash_init, endianness="big"),