]> gitweb.unchartedbackwaters.co.uk Git - francis/winuae.git/commitdiff
od-unix: add Scale2x GLSL scaler to the shader pipeline
authorStefan Reinauer <stefan.reinauer@coreboot.org>
Fri, 12 Jun 2026 16:02:54 +0000 (09:02 -0700)
committerStefan Reinauer <stefan.reinauer@coreboot.org>
Tue, 7 Jul 2026 20:59:15 +0000 (13:59 -0700)
The Filter page's filter selector only offered "None" (GitHub issue
#4): the generic gfx_filter config option cannot carry named shaders
outside the Windows Direct3D path, and the Unix GL presenter had no
scaler stage.

Carry the selection as a new unix.gfx_shader target option (parsed and
saved in od-unix/config.cpp, no generic code changes) and implement
Scale2x/EPX as a stage in the GL presenter's fragment shader, selected
through a uniform. The scaler forces nearest-neighbour sampling, runs
before the color/scanline/noise stages, and applies to the emulation
frame only, leaving the status line untouched. The launcher's Filter
selector offers Scale2x when the shader pipeline is available, and the
value round-trips through the config (covered by the roundtrip smoke).

README_unix.md
od-unix/config.cpp
od-unix/qt/launcher.cpp
od-unix/video.h
od-unix/video_sdl.cpp
tools/unix-smoke-config-roundtrip.sh

index 37092b872b61f4fed1d1abdcd200cde2e84894cd..e380899383d2117e234d5a237cae0c75cd9e257c 100644 (file)
@@ -571,7 +571,7 @@ export WINUAE_SMOKE_LOG=/tmp/winuae_unix_smoke.log
 - Non-gamepad SDL joysticks expose their native axes, hats, and buttons; hats also map to joystick directions by default.
 - Press `F12` to open the integrated Qt settings UI during emulation. The button row matches the Windows GUI: `OK` applies the edited configuration and resumes (floppy/CD changes are picked up live, including ejects), `Reset` applies the configuration and hard-resets the Amiga, `Restart` quits the running Amiga and returns to the launcher, `Quit` exits the emulator, and `Cancel` resumes without applying changes.
 - The screenshot file input event and integrated runtime Qt Output-page button save under the configured Screenshots directory. Unix uses PNG when libpng is found at configure time and falls back to BMP otherwise. On macOS, libpng must also pass the configured deployment-target check; a newer Homebrew libpng is skipped so release builds stay compatible with the selected minimum macOS. SDL3 builds can also copy the screenshot event output to the host clipboard as BMP data, and clipboard sharing can exchange PNG plus macOS TIFF image data when the matching native codecs are available. Unix screenshots now cover autoclip, palette-indexed PNG when the framebuffer has 256 or fewer colors, continuous screenshot directories, and the savestate thumbnail byte helper. The Output page can also start internal DIB RGB AVI capture, PCM-in-AVI audio capture, and WAV audio capture.
-- SDL3 builds can enable an OpenGL shader presenter with `WINUAE_UNIX_WITH_OPENGL_SHADER_PIPELINE`. When OpenGL is available, the Filter page's portable color, blur, noise, scanline, bilinear, and geometry controls are applied by a native GLSL path. Direct3D shader preset files, mask/overlay chains, HDR, and Metal/Vulkan backends are still future work.
+- SDL3 builds can enable an OpenGL shader presenter with `WINUAE_UNIX_WITH_OPENGL_SHADER_PIPELINE`. When OpenGL is available, the Filter page's portable color, blur, noise, scanline, bilinear, and geometry controls are applied by a native GLSL path, and the Filter selector offers named GLSL scalers (currently Scale2x/EPX, stored as the `unix.gfx_shader` config option; it forces nearest-neighbour sampling and applies to the emulation frame only). Direct3D shader preset files, mask/overlay chains, HDR, and Metal/Vulkan backends are still future work.
 - CHD hardfile and CD image support is built by default. CHD FLAC codecs require libFLAC that is compatible with the configured macOS deployment target; otherwise CHD remains enabled without FLAC-compressed CD codecs.
 - Hold `End` and press `F1`-`F4` to change DF0:-DF3:, `F5` to change the CD image, and `F6` to restore a state. Hold `Shift` with those shortcuts to eject the matching floppy/CD image or save state, matching the Windows key map.
 - On MacBook or compact Apple keyboards, `End` is usually `Fn`/Globe + `Right Arrow`. Depending on macOS keyboard settings, function keys may also need `Fn`/Globe, so the MacBook form is `Fn`/Globe + `Right Arrow`, then `F1`-`F6` or `Shift` + `F1`-`F6`. Enabling "Use F1, F2, etc. keys as standard function keys" in macOS makes these closer to the Windows chords.
index b8e529695b024b7e04f52dab4eab7d491d9e06c8..c2e9a56e3b18b9e7c1b6726e6f82844349757b2b 100644 (file)
@@ -39,6 +39,7 @@ static TCHAR path_saveimage[MAX_DPATH];
 static TCHAR path_ripper[MAX_DPATH];
 static TCHAR path_data[MAX_DPATH];
 static TCHAR path_rom[MAX_DPATH];
+static TCHAR unix_gfx_shader[64] = _T("none");
 static TCHAR uaeserial_ports[UNIX_UAESERIAL_MAX_UNITS][256];
 
 static std::string trim_copy(const std::string &s)
@@ -517,11 +518,22 @@ int target_parse_option(struct uae_prefs *p, const TCHAR *option, const TCHAR *v
         || parse_rom_path_option(p, option, value)) {
         return 1;
     }
+    if (!_tcsicmp(option, _T("gfx_shader"))) {
+        uae_tcslcpy(unix_gfx_shader, value && value[0] ? value : _T("none"),
+            sizeof unix_gfx_shader / sizeof(TCHAR));
+        return 1;
+    }
     return 0;
 }
 
+const TCHAR *unix_gfx_shader_option(void)
+{
+    return unix_gfx_shader;
+}
+
 void target_save_options(struct zfile *f, struct uae_prefs *p)
 {
+    cfgfile_target_dwrite_str(f, _T("gfx_shader"), unix_gfx_shader);
     cfgfile_target_dwrite_str(f, _T("serial_port"), p->sername[0] ? p->sername : _T("none"));
     cfgfile_target_dwrite_str_escape(f, _T("parallel_port"), p->prtname[0] ? p->prtname : _T("none"));
     for (int i = 0; i < UNIX_UAESERIAL_MAX_UNITS; i++) {
index 7d250726cbe8bcc4312d1590e6867f18c738d914..316679ec52adb4bf415322466797a9a05a4db4d1 100644 (file)
@@ -2544,7 +2544,8 @@ static const ConfigChoice filterTargetChoices[] = {
 };
 
 static const ConfigChoice filterModeChoices[] = {
-    { "None", "none" }
+    { "None", "none" },
+    { "Scale2x (EPX)", "scale2x" }
 };
 
 static const ConfigChoice filterModeHChoices[] = {
@@ -15431,6 +15432,7 @@ private:
             settings.insert(filterKey(QStringLiteral("gfx_filter_scanlineoffset"), i), QString::number(state.scanlineOffset));
         }
         settings.insert(QStringLiteral("gfx_filter_enable_lace"), filterStateFromUi(2).enable ? QStringLiteral("1") : QStringLiteral("0"));
+        settings.insert(QStringLiteral("unix.gfx_shader"), filterStateFromUi(0).filter);
         for (int i = 0; i < MaxCdSlots; i++) {
             const QString value = cdSlotConfigValue(cdSlotState(i));
             /* Slot 0 is always written, empty included, so ejecting the CD
@@ -15915,6 +15917,7 @@ private:
             QStringLiteral("gfx_filter_scanlinelevel_lace"),
             QStringLiteral("gfx_filter_scanlineoffset_lace"),
             QStringLiteral("gfx_filter_enable_lace"),
+            QStringLiteral("unix.gfx_shader"),
             QStringLiteral("cdimage0"),
             QStringLiteral("cdimage1"),
             QStringLiteral("cdimage2"),
@@ -17098,6 +17101,12 @@ private:
             displayKeepAspect->setChecked(configBoolValue(value));
         } else if (key == QStringLiteral("gfx_ntscpixels")) {
             filterNtscPixels->setChecked(configBoolValue(value));
+        } else if (key == QStringLiteral("unix.gfx_shader")) {
+            const QString shader = value.trimmed();
+            filterStates[0].filter = shader.isEmpty() ? QStringLiteral("none") : shader;
+            if (currentFilterTarget == 0) {
+                loadFilterStateToUi(0);
+            }
         } else if (applyFilterSetting(key, value)) {
         }
     }
index bd1b4a47c2ca02b55a73468b939f21acce5e3790..736ac28a84ed22aaa1030f5bcac4a488a02bc66b 100644 (file)
@@ -44,5 +44,7 @@ void unix_video_get_desktop(int *dw, int *dh, int *x, int *y, int *w, int *h);
 void unix_video_set_mouse_grab(bool grab);
 bool unix_video_get_mouse_grab(void);
 void unix_video_toggle_mouse_grab(void);
+/* Named shader/scaler selected through the unix.gfx_shader config option. */
+const TCHAR *unix_gfx_shader_option(void);
 
 #endif /* WINUAE_OD_UNIX_VIDEO_H */
index c8a0ee4dbf499c6142d9606b045a40b66b12f38c..10d59a22539f11f66fae23a055aa28bd91144cf0 100644 (file)
@@ -55,6 +55,7 @@ static int s_gl_uniform_adjust = -1;
 static int s_gl_uniform_scanline = -1;
 static int s_gl_uniform_blur_noise = -1;
 static int s_gl_uniform_frame = -1;
+static int s_gl_uniform_scaler = -1;
 static unsigned int s_gl_frame_counter;
 #endif
 static bool s_setup_done;
@@ -1072,13 +1073,40 @@ static bool unix_gl_build_program(void)
         "uniform vec4 u_scanline;\n"
         "uniform vec4 u_blur_noise;\n"
         "uniform float u_frame;\n"
+        "uniform float u_scaler;\n"
         "varying vec2 v_tex;\n"
         "float rand(vec2 co) {\n"
         "    return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);\n"
         "}\n"
+        "bool ceq(vec4 a, vec4 b) {\n"
+        "    return all(lessThan(abs(a.rgb - b.rgb), vec3(1.0 / 255.0)));\n"
+        "}\n"
+        "vec4 scale2x_sample(vec2 uv, vec2 texel) {\n"
+        "    vec2 p = uv * u_source_size;\n"
+        "    vec2 q = step(vec2(0.5), fract(p));\n"
+        "    vec2 base = (floor(p) + vec2(0.5)) * texel;\n"
+        "    vec4 e = texture2D(u_tex, base);\n"
+        "    vec4 up = texture2D(u_tex, base + vec2(0.0, -texel.y));\n"
+        "    vec4 dn = texture2D(u_tex, base + vec2(0.0, texel.y));\n"
+        "    vec4 lt = texture2D(u_tex, base + vec2(-texel.x, 0.0));\n"
+        "    vec4 rt = texture2D(u_tex, base + vec2(texel.x, 0.0));\n"
+        "    vec4 hn = mix(lt, rt, q.x);\n"
+        "    vec4 hf = mix(rt, lt, q.x);\n"
+        "    vec4 vn = mix(up, dn, q.y);\n"
+        "    vec4 vf = mix(dn, up, q.y);\n"
+        "    if (ceq(vn, hn) && !ceq(hn, vf) && !ceq(vn, hf)) {\n"
+        "        return hn;\n"
+        "    }\n"
+        "    return e;\n"
+        "}\n"
         "void main(void) {\n"
         "    vec2 texel = 1.0 / max(u_source_size, vec2(1.0));\n"
-        "    vec4 color = texture2D(u_tex, v_tex);\n"
+        "    vec4 color;\n"
+        "    if (u_scaler > 0.5) {\n"
+        "        color = scale2x_sample(v_tex, texel);\n"
+        "    } else {\n"
+        "        color = texture2D(u_tex, v_tex);\n"
+        "    }\n"
         "    float blur = clamp(u_blur_noise.x, 0.0, 1.0);\n"
         "    if (blur > 0.001) {\n"
         "        vec4 sum = color * 4.0;\n"
@@ -1135,6 +1163,7 @@ static bool unix_gl_build_program(void)
     s_gl_uniform_scanline = p_glGetUniformLocation(program, reinterpret_cast<const GLchar *>("u_scanline"));
     s_gl_uniform_blur_noise = p_glGetUniformLocation(program, reinterpret_cast<const GLchar *>("u_blur_noise"));
     s_gl_uniform_frame = p_glGetUniformLocation(program, reinterpret_cast<const GLchar *>("u_frame"));
+    s_gl_uniform_scaler = p_glGetUniformLocation(program, reinterpret_cast<const GLchar *>("u_scaler"));
     return true;
 }
 
@@ -1240,13 +1269,15 @@ static bool unix_gl_ensure_status_texture(int width)
 }
 
 static void unix_gl_draw_texture(GLuint texture, const SDL_FRect &dst, bool shader,
-    const struct gfx_filterdata *filter, int source_width, int source_height)
+    const struct gfx_filterdata *filter, int source_width, int source_height,
+    int scaler = 0)
 {
     glBindTexture(GL_TEXTURE_2D, texture);
     if (shader) {
         p_glUseProgram(s_gl_program);
         p_glUniform1i(s_gl_uniform_texture, 0);
         p_glUniform2f(s_gl_uniform_source_size, (GLfloat)source_width, (GLfloat)source_height);
+        p_glUniform1f(s_gl_uniform_scaler, (GLfloat)scaler);
 
         float luminance = filter ? filter->gfx_filter_luminance / 1000.0f : 0.0f;
         float contrast = filter ? 1.0f + filter->gfx_filter_contrast / 1000.0f : 1.0f;
@@ -1350,10 +1381,14 @@ static void unix_gl_present(const struct unix_video_frame *frame, const struct g
         return;
     }
 
+    /* Scale2x samples discrete texels; it requires nearest filtering. */
+    const TCHAR *shader_name = unix_gfx_shader_option();
+    const int scaler = shader_name && !_tcsicmp(shader_name, _T("scale2x")) ? 1 : 0;
+    const bool bilinear = !scaler && filter && filter->gfx_filter_bilinear;
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
-        filter && filter->gfx_filter_bilinear ? GL_LINEAR : GL_NEAREST);
+        bilinear ? GL_LINEAR : GL_NEAREST);
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
-        filter && filter->gfx_filter_bilinear ? GL_LINEAR : GL_NEAREST);
+        bilinear ? GL_LINEAR : GL_NEAREST);
 
     glViewport(0, 0, output_width, output_height);
     glMatrixMode(GL_PROJECTION);
@@ -1368,7 +1403,7 @@ static void unix_gl_present(const struct unix_video_frame *frame, const struct g
     glScissor(layout.frame_clip.x,
         output_height - layout.frame_clip.y - layout.frame_clip.h,
         layout.frame_clip.w, layout.frame_clip.h);
-    unix_gl_draw_texture(s_gl_texture, layout.frame_dst, true, filter, frame->width, frame->height);
+    unix_gl_draw_texture(s_gl_texture, layout.frame_dst, true, filter, frame->width, frame->height, scaler);
     glDisable(GL_SCISSOR_TEST);
 
     if (unix_gl_ensure_status_texture(frame->width)) {
index 20858a71859db616f47384d1ad4efd3dab5b6a9a..ceb418c6a4be605704c7e60b522f1631e94f0e14 100755 (executable)
@@ -87,10 +87,11 @@ megachipmem_size=32
 mbresmem_size=16
 floppy0=$WORK/boot.adf
 nr_floppies=2
+unix.gfx_shader=scale2x
 EOF
 
 run_case a4000 "$WORK/a4000.uae" \
-    "cpu_model fpu_model cpu_24bit_addressing cachesize chipset chipmem_size fastmem_size z3mem_size megachipmem_size mbresmem_size floppy0 nr_floppies"
+    "cpu_model fpu_model cpu_24bit_addressing cachesize chipset chipmem_size fastmem_size z3mem_size megachipmem_size mbresmem_size floppy0 nr_floppies unix.gfx_shader"
 
 cat > "$WORK/a500.uae" <<EOF
 config_description=Roundtrip A500