initial commit

This commit is contained in:
Alessandro Mauri 2025-10-26 00:03:45 +02:00
commit 4def485a6d
17 changed files with 574 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
build
resources/shaders/compiled/**

12
.gitmodules vendored Normal file
View File

@ -0,0 +1,12 @@
[submodule "lib/ugui.c3l"]
path = lib/ugui.c3l
url = https://git.alemauri.eu/alema/ugui.c3l.git
[submodule "lib/schrift.c3l"]
path = lib/schrift.c3l
url = https://git.alemauri.eu/alema/schrift.c3l.git
[submodule "lib/ugui_sdl.c3l"]
path = lib/ugui_sdl.c3l
url = https://git.alemauri.eu/alema/ugui_sdl.c3l.git
[submodule "lib/sdl3.c3l"]
path = lib/sdl3.c3l
url = https://git.alemauri.eu/alema/sdl3.c3l.git

3
Makefile Normal file
View File

@ -0,0 +1,3 @@
all:
make -C resources/shaders
c3c build -g

16
example-vm.json Normal file
View File

@ -0,0 +1,16 @@
{
"name" : "Alpine Linux",
"disk" : "$HOME/Documents/alpine.qcow2",
"memory" : "1G",
"processors" : "2",
"qemu" : "qemu-system-x86_64",
"parameters" : [
"enable-kvm",
[ "net", "nic" ],
[ "net", "user" ],
[ "display", "sdl" ],
[ "vga", "qxl" ],
[ "monitor", "stdio" ],
[ "virtfs", "local,path=/home/ale/Documents/Projects/toughpad-alpine,mount_tag=shared,security_model=mapped-xattr,id=shared" ],
]
}

1
lib/schrift.c3l Submodule

@ -0,0 +1 @@
Subproject commit 64d168f53b0fe70bda77e2ed7bec35f3f0f1af78

1
lib/sdl3.c3l Submodule

@ -0,0 +1 @@
Subproject commit e7356df5d1d0c22a6bba822bda6994062b0b75d7

1
lib/ugui.c3l Submodule

@ -0,0 +1 @@
Subproject commit 4f7fa7d50c16db3acac020cdd7204e28d42efdf2

1
lib/ugui_sdl.c3l Submodule

@ -0,0 +1 @@
Subproject commit 050624fd67c2d80190114c7774ccfa64b0f449b9

20
project.json Normal file
View File

@ -0,0 +1,20 @@
{
"langrev": "1",
"warnings": ["no-unused"],
"dependency-search-paths": ["lib"],
"dependencies": ["ugui", "ugui_sdl3"],
"features": ["DEBUG_POINTER"],
"authors": ["Alessandro Mauri <ale@shitposting.expert>"],
"version": "0.1.0",
"sources": ["src/**"],
"output": "build",
"target": "linux-x64",
"targets": {
"ugui": {
"type": "executable"
}
},
"safe": true,
"opt": "O1",
"debug-info": "full"
}

BIN
resources/hack-nerd.ttf Normal file

Binary file not shown.

View File

@ -0,0 +1,33 @@
SOURCE_DIR := ./source
COMPILED_DIR := ./compiled
SOURCE_FILES := $(wildcard $(SOURCE_DIR)/*.glsl)
COMPILED_FILES := $(patsubst $(SOURCE_DIR)/%.glsl,$(COMPILED_DIR)/%.spv,$(SOURCE_FILES))
all: $(COMPILED_FILES)
@echo "Compiling shaders from $(SOURCE_DIR) -> $(COMPILED_DIR)"
$(COMPILED_DIR)/%.spv: $(SOURCE_DIR)/%.glsl
@mkdir -p $(COMPILED_DIR)
@stage=$$(basename $< .glsl | cut -d. -f2); \
if [ "$$stage" = "frag" ] || [ "$$stage" = "vert" ]; then \
echo "$$stage $(notdir $<) > $(notdir $@)"; \
glslc -O0 -g -fshader-stage=$$stage $< -o $@; \
else \
echo "Skipping $<: unsupported stage $$stage"; \
fi
$(COMPILED_DIR):
mkdir -p $(COMPILED_DIR)
.PHONY: clean
clean:
rm -rf $(COMPILED_DIR)
.PHONY: tree
tree:
tree $(COMPILED_DIR)
.PHONY: compile_all
compile_all: clean all tree

View File

@ -0,0 +1,118 @@
#version 450
/* Combined fragment shader to render UGUI commands */
// type values, these are the same as in renderer.c3
const uint TYPE_RECT = 0;
const uint TYPE_FONT = 1;
const uint TYPE_SPRITE = 2;
const uint TYPE_MSDF = 3;
// viewport size
layout(set = 3, binding = 0) uniform Viewport {
ivec2 view;
};
// textures
layout(set = 2, binding = 0) uniform sampler2D font_atlas;
layout(set = 2, binding = 1) uniform sampler2D sprite_atlas;
// inputs
layout(location = 0) in vec4 in_color;
layout(location = 1) in vec2 in_uv;
layout(location = 2) in vec4 in_quad_size;
layout(location = 3) in float in_radius;
layout(location = 4) flat in uint in_type;
// outputs
layout(location = 0) out vec4 fragColor;
// SDF for a rounded rectangle given the centerpoint, half size and radius, all in pixels
float sdf_rr(vec2 p, vec2 half_size, float radius) {
vec2 q = abs(p) - half_size + radius;
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius;
}
const float PX_RANGE = 4.0f;
float screen_px_range(vec2 uv, sampler2D tx) {
vec2 unit_range = vec2(PX_RANGE)/vec2(textureSize(tx, 0));
vec2 texel_size = vec2(1.0)/fwidth(uv);
return max(0.5*dot(unit_range, texel_size), 1.0);
}
float median(float r, float g, float b) {
return max(min(r, g), min(max(r, g), b));
}
// main for TYPE_RECT, draw a rouded rectangle with a SDF
void rect_main()
{
vec2 centerpoint = in_quad_size.xy + in_quad_size.zw * 0.5;
vec2 half_size = in_quad_size.zw * 0.5;
float distance = sdf_rr(vec2(gl_FragCoord) - centerpoint, half_size, in_radius);
float alpha = 1.0 - smoothstep(0.0, 1.5, distance);
fragColor = vec4(in_color.rgb, in_color.a * alpha);
}
// main for TYPE_SPRITE, draws a sprite sampled from an atlas
void sprite_main()
{
ivec2 ts = textureSize(sprite_atlas, 0);
vec2 fts = vec2(ts);
vec2 real_uv = in_uv.xy / fts;
fragColor = texture(sprite_atlas, real_uv);
}
// main for TYPE_FONT, draws a character sampled from an atlas that contains only the alpha channel
void font_main()
{
ivec2 ts = textureSize(font_atlas, 0);
vec2 fts = vec2(ts);
vec2 real_uv = in_uv.xy / fts;
vec4 opacity = texture(font_atlas, real_uv);
fragColor = vec4(in_color.rgb, in_color.a*opacity.r);
}
// main for TYPE_MSDF, draws a sprite that is stored as a multi-channel SDF
void msdf_main() {
ivec2 ts = textureSize(sprite_atlas, 0);
vec2 fts = vec2(ts);
vec2 real_uv = in_uv.xy / fts;
vec3 msd = texture(sprite_atlas, real_uv).rgb;
float sd = median(msd.r, msd.g, msd.b);
float distance = screen_px_range(real_uv, sprite_atlas)*(sd - 0.5);
float opacity = clamp(distance + 0.5, 0.0, 1.0);
fragColor = in_color * opacity;
}
// shader main
void main()
{
switch (in_type) {
case TYPE_RECT:
rect_main();
break;
case TYPE_FONT:
font_main();
break;
case TYPE_SPRITE:
sprite_main();
break;
case TYPE_MSDF:
msdf_main();
break;
default:
// ERROR, invalid type, return magenta
fragColor = vec4(1.0, 0.0, 1.0, 1.0);
}
}

View File

@ -0,0 +1,47 @@
#version 450
/* Combined vertex shader to render UGUI commands */
// Viewport size in pixels
layout(set = 1, binding = 0) uniform Viewport {
ivec2 view;
};
// inputs
layout(location = 0) in ivec2 in_position;
layout(location = 1) in ivec4 in_attr; // quad x,y,w,h
layout(location = 2) in ivec4 in_uv;
layout(location = 3) in uvec4 in_color;
layout(location = 4) in uint in_type;
// outputs
layout(location = 0) out vec4 out_color;
layout(location = 1) out vec2 out_uv;
layout(location = 2) out vec4 out_quad_size;
layout(location = 3) out float out_radius;
layout(location = 4) out uint out_type;
void main()
{
// vertex position
ivec2 px_pos = in_attr.xy + in_position.xy * in_attr.zw;
vec2 clip_pos;
clip_pos.x = float(px_pos.x)*2.0 / view.x - 1.0;
clip_pos.y = -(float(px_pos.y)*2.0 / view.y - 1.0);
gl_Position = vec4(clip_pos, 0.0, 1.0);
// color output
out_color = vec4(in_color) / 255.0;
// uv output. only useful if the type is SPRITE
vec2 px_uv = in_uv.xy + in_position.xy * in_uv.zw;
out_uv = vec2(px_uv);
// quad size and radius output, only useful if type is RECT
out_quad_size = vec4(in_attr);
out_radius = float(abs(in_uv.x));
// type output
out_type = in_type;
}

88
resources/style.css Normal file
View File

@ -0,0 +1,88 @@
div {
bg: #282828;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #6c19ca8f;
accent: #fabd2fff;
}
separator {
bg: #fbf1c7ff;
size: 1;
padding: 0 5 0 5;
}
button {
margin: 2;
border: 2;
padding: 2;
radius: 10;
size: 32;
bg: #3c3836ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #504945ff;
}
checkbox {
margin: 2;
border: 2;
padding: 1;
radius: 10;
size: 20;
bg: #3c3836ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #fabd2fff;
}
toggle {
margin: 2;
border: 2;
padding: 1;
radius: 10;
size: 20;
bg: #3c3836ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #fabd2fff;
}
slider {
margin: 2;
padding: 2;
border: 1;
radius: 4;
size: 8;
bg: #3c3836ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #fabd2fff;
}
scrollbar {
padding: 2;
size: 8;
bg: #45858842;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #fabd2fff;
}
text-box {
bg: #4a4543ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #8f3f61a8;
border: 1;
padding: 4;
margin: 2;
}

BIN
resources/tick_sdf.qoi Normal file

Binary file not shown.

85
src/conf.c3 Normal file
View File

@ -0,0 +1,85 @@
import std::io;
import std::io::file;
import std::io::path;
import std::os::env;
import std::core::mem::allocator;
import std::collections::object;
import std::collections::list;
alias StrList = list::List{String};
fn Path? String.to_expanded_path(&str, Allocator allocator)
{
@pool() {
StrList l;
l.tinit();
Path str_path = str.to_tpath()!;
l.push(str_path.basename());
Path? p = str_path.parent();
while (true) {
Path? x;
if (try p) {
String n = p.basename();
// env variable
if (n.starts_with("$")) {
l.push(env::tget_var(n[1..]))!;
} else {
l.push(n);
}
x = p.parent();
} else {
break;
}
p = x;
}
Path path = path::temp("")!;
foreach_r (idx, s: l) {
path = path.tappend(s)!;
}
return path::new(allocator, path.str_view(), path.env);
};
}
fn StrList get_qemu_cmdline(Allocator allocator, Object* conf)
{
// parse the disk path
String disk = conf.get_string("disk")!!;
Path disk_path = disk.to_expanded_path(mem)!!;
defer disk_path.free();
// compose the command line for the vm
StrList cmd;
cmd.init(allocator);
// first the executable
cmd.push(conf.get_string("qemu"))!!;
// then the drive
cmd.push("-drive");
cmd.push(string::format(allocator, "file=%s", disk_path.str_view()));
//cmd.push(string::format(allocator, "file=%s,format=%s", disk_path.str_view(), disk_path.extension()))!!;
// memory
cmd.push("-m");
cmd.push(conf.get_string("memory"))!!;
// processors
cmd.push("-smp");
cmd.push(conf.get_string("processors") ?? "1");
// then all the parameters
Object* parameters = conf.get("parameters")!!;
for (usz i = 0; i < parameters.get_len(); i++) {
Object* p = parameters.get_at(i);
if (p.is_string()) {
cmd.push(string::format(allocator, "-%s", p.s));
} else if (p.is_indexable()) {
cmd.push(string::format(allocator, "-%s", p.get_string_at(0)))!!;
cmd.push(p.get_string_at(1))!!;
}
}
return cmd;
}

146
src/main.c3 Normal file
View File

@ -0,0 +1,146 @@
import std::io;
import std::io::file;
import std::collections::object;
import std::os::process;
import std::encoding::json;
import std::time;
import conf;
import ugui;
import ugui::sdl::ren;
const char[*] VS_PATH = "resources/shaders/compiled/ugui.vert.spv";
const char[*] FS_PATH = "resources/shaders/compiled/ugui.frag.spv";
const char[*] STYLESHEET_PATH = "resources/style.css";
const bool LIMIT_FPS = true;
const bool VSYNC = true;
fn int main(String[] args)
{
String path = args[1];
File file = file::open(path, "r")!!;
// load configuration in memory as an object
Object* conf = json::parse(mem, &file)!!;
defer conf.free();
// UI initialization
ArenaAllocator arena;
char[] arena_mem = mem::new_array(char, 1024*1024);
defer (void)mem::free(arena_mem);
arena.init(arena_mem);
ugui::Ctx ui;
ui.init(&arena)!!;
defer ui.free();
ren::Renderer ren;
ren.init("Qemu Manager", 800, 600, VSYNC);
defer ren.free();
ui.input_window_size(800, 600)!!;
ui.load_font(&arena, "font1", "resources/hack-nerd.ttf", 16)!!;
ren.font_atlas_id = ui.get_font_id("font1");
Atlas* font_atlas = ui.get_font_atlas("font1")!!;
ren.new_texture("font1", JUST_ALPHA, font_atlas.buffer, font_atlas.width, font_atlas.height);
ui.sprite_atlas_create("icons", AtlasType.ATLAS_R8G8B8A8, 512, 512)!!;
ui.import_sprite_file_qoi("tick", "resources/tick_sdf.qoi", SpriteType.SPRITE_MSDF)!!;
ren.sprite_atlas_id = ui.get_sprite_atlas_id("icons");
Atlas* sprite_atlas = &(ui.sprite_atlas.atlas);
ren.new_texture("icons", FULL_COLOR, sprite_atlas.buffer, sprite_atlas.width, sprite_atlas.height);
ren.load_spirv_shader_from_file("UGUI_PIPELINE", VS_PATH, FS_PATH, 2, 0);
ren.create_pipeline("UGUI_PIPELINE", RECT);
ui.import_style_from_file(STYLESHEET_PATH);
ren::pre(ren.win);
// End UI initialization
StrList cmd = conf::get_qemu_cmdline(mem, conf);
defer cmd.free();
String vm_name = conf.get_string("name")!!;
String vm_disk = conf.get_string("disk").to_expanded_path(tmem).str_view()!!;
String vm_disk_size = bytes_to_human_readable(tmem, file::get_size(vm_disk))!!;
bool vm_on = false;
SubProcess vm_proc;
defer (void)vm_proc.join();
bool quit;
Clock sleep_clock;
while (!quit) {
sleep_clock.mark();
quit = ui.handle_events()!!;
vm_on = vm_proc.is_running()!!;
/* Start UI Handling */
ui.frame_begin()!!;
if (ui.check_key_combo(ugui::KMOD_CTRL, "q")) quit = true;
ui.@div(ugui::@grow(), ugui::@grow(), COLUMN) {
ui.@div(ugui::@grow(), ugui::@fit(20)) {
ui.text("Machine:")!!;
ui.separator(ugui::@grow(), ugui::@grow())!!;
ui.text(vm_name)!!;
}!!;
ui.hor_line()!!;
ui.@div(ugui::@grow(), ugui::@grow(), COLUMN, scroll_y: true) {
ui.text(string::tformat("disk: %s (%s)", vm_disk, vm_disk_size))!!;
ui.text(string::tformat("runner: %s", conf.get_string("qemu")))!!;
ui.text(string::tformat("memory: %s", conf.get_string("memory")))!!;
ui.text(string::tformat("processors: %s", conf.get_string("processors")))!!;
}!!;
ui.hor_line()!!;
ui.@div(ugui::@grow(), ugui::@fit(), ROW) {
if (!vm_on) {
if (ui.button("START")!!.mouse_release) {
vm_proc = process::create(cmd.array_view(), {.inherit_stdio=true, .inherit_environment=true})!!;
vm_on = true;
}
} else {
if (ui.button("STOP")!!.mouse_release) {
vm_proc.terminate()!!;
vm_on = false;
}
}
}!!;
}!!;
ui.frame_end()!!;
/* End UI Handling */
/* Start UI Drawing */
ren.begin_render(true);
ren.render_ugui(&ui.cmd_queue);
ren.end_render();
/* End Drawing */
// wait for the next event, timeout after 100ms
int timeout = LIMIT_FPS ? (int)(100.0-sleep_clock.mark().to_ms()-0.5) : 0;
if (ui.skip_frame) timeout = 0;
ren::wait_events(timeout);
}
return 0;
}
fn String bytes_to_human_readable(Allocator allocator, usz bytes, int dp = 1) {
static usz thresh = 1024;
static String[] units = {"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"};
double b = bytes;
int u = 0;
for (; b > thresh && u < units.len; u++) {
b /= thresh;
}
return string::format(allocator, "%.*f %s", dp, b, units[u]);
}