From: Stefan Reinauer Date: Fri, 12 Jun 2026 16:02:54 +0000 (-0700) Subject: od-unix: add Scale2x GLSL scaler to the shader pipeline X-Git-Url: https://gitweb.unchartedbackwaters.co.uk/w/?a=commitdiff_plain;h=a71132b90a2562d7e3a66515db04d1c086b2c242;p=francis%2Fwinuae.git od-unix: add Scale2x GLSL scaler to the shader pipeline 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). --- diff --git a/README_unix.md b/README_unix.md index 37092b87..e3808993 100644 --- a/README_unix.md +++ b/README_unix.md @@ -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. diff --git a/od-unix/config.cpp b/od-unix/config.cpp index b8e52969..c2e9a56e 100644 --- a/od-unix/config.cpp +++ b/od-unix/config.cpp @@ -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++) { diff --git a/od-unix/qt/launcher.cpp b/od-unix/qt/launcher.cpp index 7d250726..316679ec 100644 --- a/od-unix/qt/launcher.cpp +++ b/od-unix/qt/launcher.cpp @@ -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)) { } } diff --git a/od-unix/video.h b/od-unix/video.h index bd1b4a47..736ac28a 100644 --- a/od-unix/video.h +++ b/od-unix/video.h @@ -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 */ diff --git a/od-unix/video_sdl.cpp b/od-unix/video_sdl.cpp index c8a0ee4d..10d59a22 100644 --- a/od-unix/video_sdl.cpp +++ b/od-unix/video_sdl.cpp @@ -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("u_scanline")); s_gl_uniform_blur_noise = p_glGetUniformLocation(program, reinterpret_cast("u_blur_noise")); s_gl_uniform_frame = p_glGetUniformLocation(program, reinterpret_cast("u_frame")); + s_gl_uniform_scaler = p_glGetUniformLocation(program, reinterpret_cast("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)) { diff --git a/tools/unix-smoke-config-roundtrip.sh b/tools/unix-smoke-config-roundtrip.sh index 20858a71..ceb418c6 100755 --- a/tools/unix-smoke-config-roundtrip.sh +++ b/tools/unix-smoke-config-roundtrip.sh @@ -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" <