feat: jpeg image rendering, desktop backgrounds, fix memory regression
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# Makefile for imageviewer (standalone JPEG viewer) on ZenithOS
|
||||
# Copyright (c) 2026 Daniel Hammer
|
||||
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
# ---- Toolchain ----
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
|
||||
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
else
|
||||
CXX := g++
|
||||
endif
|
||||
|
||||
# ---- Paths ----
|
||||
|
||||
JPEG_LIB := ../../lib/libjpeg
|
||||
LIBC_LIB := ../../lib/libc
|
||||
LIBC_INC := ../../include/libc
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
|
||||
# ---- Compiler flags ----
|
||||
|
||||
CXXFLAGS := \
|
||||
-std=gnu++20 \
|
||||
-g -O2 -pipe \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wno-unused-parameter \
|
||||
-Wno-unused-function \
|
||||
-nostdinc \
|
||||
-ffreestanding \
|
||||
-fno-stack-protector \
|
||||
-fno-stack-check \
|
||||
-fno-PIC \
|
||||
-fno-rtti \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-m64 \
|
||||
-march=x86-64 \
|
||||
-msse \
|
||||
-msse2 \
|
||||
-mno-red-zone \
|
||||
-mcmodel=small \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(LIBC_INC) \
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||
|
||||
# ---- Linker flags ----
|
||||
|
||||
LDFLAGS := \
|
||||
-nostdlib \
|
||||
-static \
|
||||
-Wl,--build-id=none \
|
||||
-Wl,--gc-sections \
|
||||
-Wl,-m,elf_x86_64 \
|
||||
-z max-page-size=0x1000 \
|
||||
-T $(LINK_LD)
|
||||
|
||||
# ---- Libraries ----
|
||||
|
||||
LIBS := $(JPEG_LIB)/libjpeg.a $(LIBC_LIB)/liblibc.a
|
||||
|
||||
# ---- Source files ----
|
||||
|
||||
SRCS := main.cpp stb_truetype_impl.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
# ---- Target ----
|
||||
|
||||
TARGET := $(BINDIR)/os/imageviewer.elf
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LIBS) $(LINK_LD) Makefile
|
||||
mkdir -p $(BINDIR)/os
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
@@ -0,0 +1,418 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* ZenithOS Image Viewer - standalone Window Server process
|
||||
* Displays JPEG images with pan via mouse drag, scroll wheel, and arrow keys
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <zenith/syscall.h>
|
||||
#include <zenith/string.h>
|
||||
#include <zenith/heap.h>
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/truetype.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <gui/stb_image.h>
|
||||
}
|
||||
|
||||
using namespace gui;
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int INIT_W = 800;
|
||||
static constexpr int INIT_H = 600;
|
||||
static constexpr int STATUS_H = 24;
|
||||
static constexpr int PAN_STEP = 40;
|
||||
static constexpr int FONT_SIZE = 13;
|
||||
|
||||
static constexpr Color BG_COLOR = Color::from_rgb(0x30, 0x30, 0x30);
|
||||
static constexpr Color STATUS_BG = Color::from_rgb(0x24, 0x24, 0x24);
|
||||
static constexpr Color STATUS_TEXT = Color::from_rgb(0xCC, 0xCC, 0xCC);
|
||||
static constexpr Color ERR_COLOR = Color::from_rgb(0xCC, 0x33, 0x33);
|
||||
|
||||
// ============================================================================
|
||||
// App state
|
||||
// ============================================================================
|
||||
|
||||
static int g_win_w = INIT_W;
|
||||
static int g_win_h = INIT_H;
|
||||
|
||||
// Image data
|
||||
static uint32_t* g_image = nullptr; // ARGB pixel buffer
|
||||
static int g_img_w = 0;
|
||||
static int g_img_h = 0;
|
||||
|
||||
// Pan offset (image position relative to viewport)
|
||||
static int g_pan_x = 0;
|
||||
static int g_pan_y = 0;
|
||||
|
||||
// Mouse drag state
|
||||
static bool g_dragging = false;
|
||||
static int g_drag_start_x = 0;
|
||||
static int g_drag_start_y = 0;
|
||||
static int g_drag_pan_x = 0;
|
||||
static int g_drag_pan_y = 0;
|
||||
|
||||
// File info
|
||||
static char g_filename[128] = {};
|
||||
static char g_status[256] = {};
|
||||
static bool g_load_ok = false;
|
||||
|
||||
// Font
|
||||
static TrueTypeFont* g_font = nullptr;
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
|
||||
static void px_fill(uint32_t* px, int bw, int bh,
|
||||
int x, int y, int w, int h, Color c) {
|
||||
uint32_t v = c.to_pixel();
|
||||
int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y;
|
||||
int x1 = x + w > bw ? bw : x + w;
|
||||
int y1 = y + h > bh ? bh : y + h;
|
||||
for (int row = y0; row < y1; row++)
|
||||
for (int col = x0; col < x1; col++)
|
||||
px[row * bw + col] = v;
|
||||
}
|
||||
|
||||
// Extract basename from a VFS path (e.g. "0:/photos/cat.jpg" -> "cat.jpg")
|
||||
static const char* basename(const char* path) {
|
||||
const char* last = path;
|
||||
for (const char* p = path; *p; p++) {
|
||||
if (*p == '/') last = p + 1;
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
// Format an integer into a decimal string
|
||||
static void int_to_str(char* buf, int val) {
|
||||
if (val == 0) { buf[0] = '0'; buf[1] = '\0'; return; }
|
||||
bool neg = val < 0;
|
||||
if (neg) val = -val;
|
||||
char tmp[16];
|
||||
int i = 0;
|
||||
while (val > 0 && i < 15) {
|
||||
tmp[i++] = '0' + (val % 10);
|
||||
val /= 10;
|
||||
}
|
||||
int j = 0;
|
||||
if (neg) buf[j++] = '-';
|
||||
while (i > 0) buf[j++] = tmp[--i];
|
||||
buf[j] = '\0';
|
||||
}
|
||||
|
||||
static void str_append(char* dst, const char* src, int maxlen) {
|
||||
int len = zenith::slen(dst);
|
||||
int i = 0;
|
||||
while (src[i] && len + i < maxlen - 1) {
|
||||
dst[len + i] = src[i];
|
||||
i++;
|
||||
}
|
||||
dst[len + i] = '\0';
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Image loading
|
||||
// ============================================================================
|
||||
|
||||
static bool load_image(const char* path) {
|
||||
int fd = zenith::open(path);
|
||||
if (fd < 0) {
|
||||
zenith::strcpy(g_status, "Error: could not open file");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t size = zenith::getsize(fd);
|
||||
if (size == 0 || size > 16 * 1024 * 1024) {
|
||||
zenith::close(fd);
|
||||
zenith::strcpy(g_status, "Error: file too large or empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t* filedata = (uint8_t*)zenith::malloc(size);
|
||||
if (!filedata) {
|
||||
zenith::close(fd);
|
||||
zenith::strcpy(g_status, "Error: out of memory");
|
||||
return false;
|
||||
}
|
||||
|
||||
int bytes_read = zenith::read(fd, filedata, 0, size);
|
||||
zenith::close(fd);
|
||||
|
||||
if (bytes_read <= 0) {
|
||||
zenith::mfree(filedata);
|
||||
zenith::strcpy(g_status, "Error: could not read file");
|
||||
return false;
|
||||
}
|
||||
|
||||
int w, h, channels;
|
||||
unsigned char* rgb = stbi_load_from_memory(filedata, bytes_read, &w, &h, &channels, 3);
|
||||
zenith::mfree(filedata);
|
||||
|
||||
if (!rgb) {
|
||||
zenith::strcpy(g_status, "Error: ");
|
||||
const char* reason = stbi_failure_reason();
|
||||
if (reason) str_append(g_status, reason, 256);
|
||||
else str_append(g_status, "unknown decode error", 256);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert RGB to ARGB
|
||||
g_image = (uint32_t*)zenith::malloc((uint64_t)w * h * 4);
|
||||
if (!g_image) {
|
||||
stbi_image_free(rgb);
|
||||
zenith::strcpy(g_status, "Error: out of memory for image");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < w * h; i++) {
|
||||
uint8_t r = rgb[i * 3 + 0];
|
||||
uint8_t g = rgb[i * 3 + 1];
|
||||
uint8_t b = rgb[i * 3 + 2];
|
||||
g_image[i] = 0xFF000000u | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
|
||||
}
|
||||
|
||||
stbi_image_free(rgb);
|
||||
|
||||
g_img_w = w;
|
||||
g_img_h = h;
|
||||
|
||||
// Build status string: "filename.jpg (1920 x 1080)"
|
||||
const char* name = basename(path);
|
||||
zenith::strncpy(g_filename, name, 127);
|
||||
|
||||
zenith::strcpy(g_status, g_filename);
|
||||
str_append(g_status, " (", 256);
|
||||
char num[16];
|
||||
int_to_str(num, w);
|
||||
str_append(g_status, num, 256);
|
||||
str_append(g_status, " x ", 256);
|
||||
int_to_str(num, h);
|
||||
str_append(g_status, num, 256);
|
||||
str_append(g_status, ")", 256);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Pan clamping
|
||||
// ============================================================================
|
||||
|
||||
static void clamp_pan() {
|
||||
int view_h = g_win_h - STATUS_H;
|
||||
|
||||
// If image fits in viewport, center it
|
||||
if (g_img_w <= g_win_w) {
|
||||
g_pan_x = (g_win_w - g_img_w) / 2;
|
||||
} else {
|
||||
// Clamp so image edges don't leave the viewport
|
||||
if (g_pan_x > 0) g_pan_x = 0;
|
||||
if (g_pan_x < g_win_w - g_img_w) g_pan_x = g_win_w - g_img_w;
|
||||
}
|
||||
|
||||
if (g_img_h <= view_h) {
|
||||
g_pan_y = (view_h - g_img_h) / 2;
|
||||
} else {
|
||||
if (g_pan_y > 0) g_pan_y = 0;
|
||||
if (g_pan_y < view_h - g_img_h) g_pan_y = view_h - g_img_h;
|
||||
}
|
||||
}
|
||||
|
||||
static void center_image() {
|
||||
int view_h = g_win_h - STATUS_H;
|
||||
g_pan_x = (g_win_w - g_img_w) / 2;
|
||||
g_pan_y = (view_h - g_img_h) / 2;
|
||||
clamp_pan();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Rendering
|
||||
// ============================================================================
|
||||
|
||||
static void render(uint32_t* pixels) {
|
||||
int view_h = g_win_h - STATUS_H;
|
||||
|
||||
// Fill background
|
||||
px_fill(pixels, g_win_w, g_win_h, 0, 0, g_win_w, view_h, BG_COLOR);
|
||||
|
||||
// Draw image
|
||||
if (g_image && g_load_ok) {
|
||||
for (int row = 0; row < g_img_h; row++) {
|
||||
int dy = g_pan_y + row;
|
||||
if (dy < 0 || dy >= view_h) continue;
|
||||
for (int col = 0; col < g_img_w; col++) {
|
||||
int dx = g_pan_x + col;
|
||||
if (dx < 0 || dx >= g_win_w) continue;
|
||||
pixels[dy * g_win_w + dx] = g_image[row * g_img_w + col];
|
||||
}
|
||||
}
|
||||
} else if (!g_load_ok && g_font) {
|
||||
// Show error message centered
|
||||
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
|
||||
20, view_h / 2 - 8, g_status, ERR_COLOR, 15);
|
||||
}
|
||||
|
||||
// Status bar
|
||||
px_fill(pixels, g_win_w, g_win_h, 0, view_h, g_win_w, STATUS_H, STATUS_BG);
|
||||
if (g_font) {
|
||||
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
|
||||
8, view_h + (STATUS_H - FONT_SIZE) / 2, g_status, STATUS_TEXT, FONT_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Entry point
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void _start() {
|
||||
// Get file path from arguments
|
||||
char filepath[512] = {};
|
||||
int arglen = zenith::getargs(filepath, sizeof(filepath));
|
||||
if (arglen <= 0) {
|
||||
zenith::strcpy(filepath, "");
|
||||
}
|
||||
|
||||
// Load font
|
||||
auto load_font = [](const char* path) -> TrueTypeFont* {
|
||||
TrueTypeFont* f = (TrueTypeFont*)zenith::malloc(sizeof(TrueTypeFont));
|
||||
if (!f) return nullptr;
|
||||
zenith::memset(f, 0, sizeof(TrueTypeFont));
|
||||
if (!f->init(path)) { zenith::mfree(f); return nullptr; }
|
||||
return f;
|
||||
};
|
||||
g_font = load_font("0:/fonts/Roboto-Medium.ttf");
|
||||
|
||||
// Determine window title from filename
|
||||
char title[64] = "Image Viewer";
|
||||
if (filepath[0]) {
|
||||
const char* name = basename(filepath);
|
||||
if (name[0]) zenith::strncpy(title, name, 63);
|
||||
}
|
||||
|
||||
// Create window
|
||||
Zenith::WinCreateResult wres;
|
||||
if (zenith::win_create(title, INIT_W, INIT_H, &wres) < 0 || wres.id < 0)
|
||||
zenith::exit(1);
|
||||
|
||||
int win_id = wres.id;
|
||||
uint32_t* pixels = (uint32_t*)(uintptr_t)wres.pixelVa;
|
||||
|
||||
// Load image
|
||||
if (filepath[0]) {
|
||||
g_load_ok = load_image(filepath);
|
||||
} else {
|
||||
zenith::strcpy(g_status, "No file specified");
|
||||
g_load_ok = false;
|
||||
}
|
||||
|
||||
// Center image initially
|
||||
if (g_load_ok) center_image();
|
||||
|
||||
// Initial render
|
||||
render(pixels);
|
||||
zenith::win_present(win_id);
|
||||
|
||||
// Event loop
|
||||
while (true) {
|
||||
Zenith::WinEvent ev;
|
||||
int r = zenith::win_poll(win_id, &ev);
|
||||
|
||||
if (r < 0) break;
|
||||
|
||||
if (r == 0) {
|
||||
zenith::sleep_ms(16);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Close event
|
||||
if (ev.type == 3) break;
|
||||
|
||||
// Resize event
|
||||
if (ev.type == 2) {
|
||||
g_win_w = ev.resize.w;
|
||||
g_win_h = ev.resize.h;
|
||||
pixels = (uint32_t*)(uintptr_t)zenith::win_resize(win_id, g_win_w, g_win_h);
|
||||
if (g_load_ok) clamp_pan();
|
||||
render(pixels);
|
||||
zenith::win_present(win_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Key event
|
||||
if (ev.type == 0 && ev.key.pressed) {
|
||||
bool redraw = false;
|
||||
|
||||
// Q or Escape to close
|
||||
if (ev.key.ascii == 'q' || ev.key.ascii == 'Q' || ev.key.scancode == 0x01) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Arrow keys to pan
|
||||
if (ev.key.scancode == 0x48) { g_pan_y += PAN_STEP; redraw = true; } // Up
|
||||
if (ev.key.scancode == 0x50) { g_pan_y -= PAN_STEP; redraw = true; } // Down
|
||||
if (ev.key.scancode == 0x4B) { g_pan_x += PAN_STEP; redraw = true; } // Left
|
||||
if (ev.key.scancode == 0x4D) { g_pan_x -= PAN_STEP; redraw = true; } // Right
|
||||
|
||||
// Home key to re-center
|
||||
if (ev.key.scancode == 0x47) { center_image(); redraw = true; }
|
||||
|
||||
if (redraw && g_load_ok) {
|
||||
clamp_pan();
|
||||
render(pixels);
|
||||
zenith::win_present(win_id);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mouse event
|
||||
if (ev.type == 1) {
|
||||
bool redraw = false;
|
||||
bool left_now = ev.mouse.buttons & 1;
|
||||
bool left_prev = ev.mouse.prev_buttons & 1;
|
||||
|
||||
// Start drag
|
||||
if (left_now && !left_prev) {
|
||||
g_dragging = true;
|
||||
g_drag_start_x = ev.mouse.x;
|
||||
g_drag_start_y = ev.mouse.y;
|
||||
g_drag_pan_x = g_pan_x;
|
||||
g_drag_pan_y = g_pan_y;
|
||||
}
|
||||
|
||||
// Continue drag
|
||||
if (left_now && g_dragging) {
|
||||
g_pan_x = g_drag_pan_x + (ev.mouse.x - g_drag_start_x);
|
||||
g_pan_y = g_drag_pan_y + (ev.mouse.y - g_drag_start_y);
|
||||
redraw = true;
|
||||
}
|
||||
|
||||
// End drag
|
||||
if (!left_now && g_dragging) {
|
||||
g_dragging = false;
|
||||
}
|
||||
|
||||
// Scroll wheel for vertical pan
|
||||
if (ev.mouse.scroll != 0) {
|
||||
g_pan_y += ev.mouse.scroll * PAN_STEP;
|
||||
redraw = true;
|
||||
}
|
||||
|
||||
if (redraw && g_load_ok) {
|
||||
clamp_pan();
|
||||
render(pixels);
|
||||
zenith::win_present(win_id);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if (g_image) zenith::mfree(g_image);
|
||||
zenith::win_destroy(win_id);
|
||||
zenith::exit(0);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* stb_truetype_impl.cpp
|
||||
* Single compilation unit for stb_truetype in ZenithOS freestanding environment
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <zenith/heap.h>
|
||||
#include <zenith/string.h>
|
||||
#include <gui/stb_math.h>
|
||||
|
||||
// Override all stb_truetype dependencies before including the implementation
|
||||
|
||||
#define STBTT_ifloor(x) ((int) stb_floor(x))
|
||||
#define STBTT_iceil(x) ((int) stb_ceil(x))
|
||||
#define STBTT_sqrt(x) stb_sqrt(x)
|
||||
#define STBTT_pow(x,y) stb_pow(x,y)
|
||||
#define STBTT_fmod(x,y) stb_fmod(x,y)
|
||||
#define STBTT_cos(x) stb_cos(x)
|
||||
#define STBTT_acos(x) stb_acos(x)
|
||||
#define STBTT_fabs(x) stb_fabs(x)
|
||||
|
||||
#define STBTT_malloc(x,u) ((void)(u), zenith::malloc(x))
|
||||
#define STBTT_free(x,u) ((void)(u), zenith::mfree(x))
|
||||
|
||||
#define STBTT_memcpy(d,s,n) zenith::memcpy(d,s,n)
|
||||
#define STBTT_memset(d,v,n) zenith::memset(d,v,n)
|
||||
|
||||
#define STBTT_strlen(x) zenith::slen(x)
|
||||
|
||||
#define STBTT_assert(x) ((void)(x))
|
||||
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include <gui/stb_truetype.h>
|
||||
Reference in New Issue
Block a user