Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ gsplus_screenshot_*.png
.agents/
skills-lock.json

# Absolute trash
.DS_Store
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ Some features that are a part of the SDL3 build:
- **Drag-and-drop disk loading** - drop a disk image on the window and GSplus
guesses the slot from its size.
- **CRT scanline simulator** - `-scanline <0-100>`, Shift+F11 to toggle.
- **Curved CRT effect** - `-crt 1` with `-crtcurve` / `-crtmask`, Ctrl+F11 to
toggle. Screen curvature, RGB phosphor mask, bloom, and vignette; composes
with the scanline simulator.
- **Curved CRT effect** - `-crt 1` with `-crtcurve` / `-crtmask` / `-crtglow`
/ `-crtvignette`, Ctrl+F11 to toggle. Screen curvature, RGB phosphor mask,
bloom, and vignette; composes with the scanline simulator.
- **One cross-platform build** - the same SDL3 app and CMake build on macOS,
Linux, and Windows, with prebuilt downloads for each.

Expand Down
9 changes: 6 additions & 3 deletions gsplus/src/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ int g_nohwaccel = 0; /* force the software renderer */
int g_scanline_simulator = 0; /* CRT scanline overlay intensity, 0-100 (0=off) */
int g_crt = 0; /* curved CRT effect (curvature+mask+glow+vignette) */
int g_crt_curve = 2; /* CRT screen curvature amount, 0-100 (0=flat) */
int g_crt_mask = 5; /* CRT phosphor-mask strength, 0-100 (0=off, subtle) */
int g_crt_mask = 2; /* CRT phosphor-mask strength, 0-100 (0=off, subtle) */
int g_crt_glow = 8; /* CRT glow/bloom strength, 0-100 (0=off) */
int g_crt_vignette = 5; /* CRT corner darkening, 0-100 (0=off, flat brightness) */
int g_hblur = 0; /* horizontal linear blur, 0-100 (0=off, sharp) */
int g_vblur = 0; /* vertical linear blur, 0-100 (0=off, sharp) */
Expand Down Expand Up @@ -399,6 +400,7 @@ Cfg_menu g_cfg_sdl_video_menu[] = {
{ "CRT Effect (curve+mask+glow),0,Off,1,On", &g_crt, "crt", 0, CFGTYPE_INT },
{ "CRT Curvature 0-100", &g_crt_curve, "crtcurve", 0, CFGTYPE_INT },
{ "CRT Phosphor Mask 0-100", &g_crt_mask, "crtmask", 0, CFGTYPE_INT },
{ "CRT Glow 0-100", &g_crt_glow, "crtglow", 0, CFGTYPE_INT },
{ "CRT Vignette 0-100", &g_crt_vignette, "crtvignette", 0, CFGTYPE_INT },
{ "Horizontal Blur 0-100", &g_hblur, "hblur", 0, CFGTYPE_INT },
{ "Vertical Blur 0-100", &g_vblur, "vblur", 0, CFGTYPE_INT },
Expand Down Expand Up @@ -1158,10 +1160,11 @@ cfg_int_update(int *iptr, int new_val)

old_val = *iptr;
if((iptr == &g_scanline_simulator) || (iptr == &g_crt_curve) ||
(iptr == &g_crt_mask) || (iptr == &g_crt_vignette) ||
(iptr == &g_crt_mask) || (iptr == &g_crt_glow) ||
(iptr == &g_crt_vignette) ||
(iptr == &g_hblur) || (iptr == &g_vblur)) {
// Scanline intensity, CRT curvature, phosphor-mask strength,
// vignette and horizontal/vertical blur are
// glow, vignette and horizontal/vertical blur are
// 0-100 percentages (0=off/flat, 100=max). Clamp here so they can't
// go negative or above 100 from any path: typed edits, +/- arrows,
// or a hand-edited config file.
Expand Down
89 changes: 62 additions & 27 deletions gsplus/src/sdl_driver.c
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@ typedef struct {
* tiled over the output in host pixels */
int mask_for; /* g_crt_mask the mask was filled for */
SDL_Vertex *crt_verts; /* warped curvature mesh vertices */
SDL_Vertex *glow_verts; /* same mesh, glow-strength colors */
int *crt_idx; /* mesh triangle indices */
int crt_nverts;
int crt_nidx;
int crt_curve_for; /* g_crt_curve the mesh was built for */
int crt_vig_for; /* g_crt_vignette the mesh was built for */
int crt_glow_for; /* g_crt_glow the mesh was built for */
word32 *data; /* dest buffer for video_out_data() */
int active;
int width_req; /* current logical width (pixels) */
Expand All @@ -82,6 +84,7 @@ extern int g_scanline_simulator; /* CRT scanline overlay intensity, 0-100 */
extern int g_crt; /* curved CRT effect on/off */
extern int g_crt_curve; /* CRT screen curvature, 0-100 */
extern int g_crt_mask; /* CRT phosphor-mask strength, 0-100 */
extern int g_crt_glow; /* CRT glow/bloom strength, 0-100 */
extern int g_crt_vignette; /* CRT corner darkening, 0-100 */
extern int g_hblur; /* horizontal linear blur, 0-100 */
extern int g_vblur; /* vertical linear blur, 0-100 */
Expand All @@ -106,7 +109,6 @@ static int g_screenshot_requested = 0; /* set by Shift+F12, serviced at frame en
#define CRT_MASK_STRIPE_PX 2 /* target phosphor stripe width in device
* (physical screen) pixels */
#define CRT_GLOW_DIV 3 /* glow downsample factor (blur radius) */
#define CRT_GLOW_LEVEL 90 /* additive bloom strength, 0-255 */

/* Widths of the 2-row scanline tile and the 1-row phosphor-mask tile. Any
* width works (they're tiled across the output; the mask just needs a multiple
Expand Down Expand Up @@ -250,19 +252,22 @@ sdl_fill_mask(Window_info *win)
static void
sdl_build_crt_mesh(Window_info *win, int w, int h)
{
int i, j, nx, ny, n, idx, vig_strength;
float curve, vignette, u, v, px, py, wx, wy, r2, vig;
int i, j, nx, ny, n, idx, vig_strength, glow_strength;
float curve, vignette, glow, u, v, px, py, wx, wy, r2, vig;

nx = CRT_MESH_COLS + 1;
ny = CRT_MESH_ROWS + 1;
free(win->crt_verts);
free(win->glow_verts);
free(win->crt_idx);
win->crt_verts = malloc((size_t)nx * ny * sizeof(SDL_Vertex));
win->glow_verts = malloc((size_t)nx * ny * sizeof(SDL_Vertex));
win->crt_idx = malloc((size_t)CRT_MESH_COLS * CRT_MESH_ROWS * 6 *
sizeof(int));
if(!win->crt_verts || !win->crt_idx) {
free(win->crt_verts); win->crt_verts = NULL;
free(win->crt_idx); win->crt_idx = NULL;
if(!win->crt_verts || !win->glow_verts || !win->crt_idx) {
free(win->crt_verts); win->crt_verts = NULL;
free(win->glow_verts); win->glow_verts = NULL;
free(win->crt_idx); win->crt_idx = NULL;
return;
}

Expand All @@ -275,6 +280,12 @@ sdl_build_crt_mesh(Window_info *win, int w, int h)
if(vig_strength > 100) { vig_strength = 100; }
vignette = (float)vig_strength * 0.01f;

/* 0-100 glow knob -> 0..1 additive bloom strength (0 = off). */
glow_strength = g_crt_glow;
if(glow_strength < 0) { glow_strength = 0; }
if(glow_strength > 100) { glow_strength = 100; }
glow = (float)glow_strength * 0.01f;

n = 0;
for(j = 0; j < ny; j++) {
for(i = 0; i < nx; i++) {
Expand All @@ -297,6 +308,15 @@ sdl_build_crt_mesh(Window_info *win, int w, int h)
win->crt_verts[n].color.g = vig;
win->crt_verts[n].color.b = vig;
win->crt_verts[n].color.a = 1.0f;
/* Glow pass: same mesh, but the bloom strength lives in
* the vertex colors. SDL_RenderGeometry ignores
* SDL_SetTextureColorMod (only SDL_RenderTexture applies
* it), so modulating the glow texture doesn't work --
* the strength must be baked in here. */
win->glow_verts[n] = win->crt_verts[n];
win->glow_verts[n].color.r = vig * glow;
win->glow_verts[n].color.g = vig * glow;
win->glow_verts[n].color.b = vig * glow;
n++;
}
}
Expand All @@ -320,6 +340,7 @@ sdl_build_crt_mesh(Window_info *win, int w, int h)
win->crt_nidx = idx;
win->crt_curve_for = g_crt_curve;
win->crt_vig_for = g_crt_vignette;
win->crt_glow_for = g_crt_glow;
}

/* (Re)create the offscreen targets + mesh at the given logical size. */
Expand Down Expand Up @@ -472,10 +493,11 @@ sdl_video_init(void)
video_set_red_mask(0xff0000);
video_set_green_mask(0x00ff00);
video_set_blue_mask(0x0000ff);
/* Mark framebuffer pixels opaque (alpha 0xff), like the mac driver does.
* Without this every pixel has alpha 0, which makes the CRT glow pass
* (SDL_BLENDMODE_ADD scales source RGB by source alpha) contribute
* nothing, silently disabling the bloom half of the CRT effect. */
/* Mark framebuffer pixels opaque (alpha 0xff), like the mac driver
* does, so the backbuffer/screenshots carry a consistent alpha. (Note
* the core only applies this in its scaled copy paths; nothing in the
* render path may depend on framebuffer alpha -- the glow pass learned
* this the hard way and now adds RGB independent of alpha.) */
video_set_alpha_mask(0xff000000);
video_set_palette();

Expand Down Expand Up @@ -1191,9 +1213,11 @@ sdl_render_crt(Window_info *win)
int w = win->width_req;
int h = win->main_height;

/* Rebuild the mesh if the curvature or vignette knob changed at runtime. */
/* Rebuild the mesh if the curvature, vignette or glow knob changed at
* runtime. */
if((win->crt_curve_for != g_crt_curve) ||
(win->crt_vig_for != g_crt_vignette)) {
(win->crt_vig_for != g_crt_vignette) ||
(win->crt_glow_for != g_crt_glow)) {
sdl_build_crt_mesh(win, w, h);
if(!win->crt_verts) {
return;
Expand All @@ -1204,20 +1228,19 @@ sdl_render_crt(Window_info *win)
SDL_SetRenderLogicalPresentation(r, 0, 0,
SDL_LOGICAL_PRESENTATION_DISABLED);
SDL_SetRenderTarget(r, win->crt_target);
/* Clear to transparent black first: the blur path accumulates additively, so
* it needs a cleared target. Crucially the alpha is 0, not 255: without blur,
* crt_target is a plain (NONE) copy of the framebuffer texture, whose alpha is
* 0 -- which leaves the later glow pass (it ADD-blends scaled by source alpha)
* dormant. Clearing to opaque here would instead switch the glow on only when
* blur is enabled, washing the picture out. Matching alpha 0 keeps the CRT look
* identical with blur on or off. */
/* Clear to black first: the blur path accumulates additively, so it
* needs a cleared target. The alpha value doesn't matter -- the glow
* pass adds source RGB directly, independent of alpha (see below). */
SDL_SetRenderDrawColor(r, 0, 0, 0, 0);
SDL_RenderClear(r);
sdl_render_framebuffer(win, w, h);

/* --- Pass 2: downscale crt_target -> glow_target (LINEAR = blur). --- */
SDL_SetRenderTarget(r, win->glow_target);
SDL_RenderTexture(r, win->crt_target, NULL, NULL);
/* --- Pass 2: downscale crt_target -> glow_target (LINEAR = blur).
* Skipped entirely at glow 0 (as is the additive draw below). --- */
if(g_crt_glow > 0) {
SDL_SetRenderTarget(r, win->glow_target);
SDL_RenderTexture(r, win->crt_target, NULL, NULL);
}

/* --- Pass 3: warp onto the curve in the window, then add the glow. --- */
SDL_SetRenderTarget(r, NULL);
Expand All @@ -1232,11 +1255,23 @@ sdl_render_crt(Window_info *win)
SDL_RenderGeometry(r, win->crt_target, win->crt_verts, win->crt_nverts,
win->crt_idx, win->crt_nidx);

SDL_SetTextureBlendMode(win->glow_target, SDL_BLENDMODE_ADD);
SDL_SetTextureColorMod(win->glow_target, CRT_GLOW_LEVEL, CRT_GLOW_LEVEL,
CRT_GLOW_LEVEL);
SDL_RenderGeometry(r, win->glow_target, win->crt_verts, win->crt_nverts,
win->crt_idx, win->crt_nidx);
/* Additive bloom. SDL_BLENDMODE_ADD scales the source by its alpha,
* which here is whatever alpha the framebuffer copy happened to carry
* (opaque via the scaled video_out_data paths, 0 via the 1:1 path or
* after the additive blur) -- making the bloom silently switch on and
* off with window size and blur settings. Use a custom blend that adds
* the source RGB directly, so the glow never depends on alpha. The
* strength is in glow_verts' colors (see sdl_build_crt_mesh). */
if(g_crt_glow > 0) {
SDL_SetTextureBlendMode(win->glow_target,
SDL_ComposeCustomBlendMode(
SDL_BLENDFACTOR_ONE, SDL_BLENDFACTOR_ONE,
SDL_BLENDOPERATION_ADD,
SDL_BLENDFACTOR_ZERO, SDL_BLENDFACTOR_ONE,
SDL_BLENDOPERATION_ADD));
SDL_RenderGeometry(r, win->glow_target, win->glow_verts,
win->crt_nverts, win->crt_idx, win->crt_nidx);
}

/* Mask and scanlines go on last, in host-pixel space over the warped
* image, so they stay at fixed screen-pixel sizes regardless of window
Expand Down
4 changes: 3 additions & 1 deletion gsplus/src/video.c
Original file line number Diff line number Diff line change
Expand Up @@ -2356,14 +2356,16 @@ video_out_data(void *vptr, Kimage *kimage_ptr, int out_width_act,
return video_out_data_scaled(vptr, kimage_ptr, out_width_act,
rectptr);
} else {
// Apply g_alpha_mask here too, so 1:1 output carries the same
// alpha as the scaled paths below
out_wptr = (word32 *)vptr;
for(i = 0; i < height; i++) {
eff_y = rectptr->y + i;
wptr = kimage_ptr->wptr + (eff_y * a2_width_full) + x;
out_wptr = ((word32 *)vptr) +
(eff_y * out_width_act) + x;
for(j = 0; j < width; j++) {
*out_wptr++ = *wptr++;
*out_wptr++ = *wptr++ | g_alpha_mask;
}
}
}
Expand Down
Loading