- 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.
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)
|| 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++) {
};
static const ConfigChoice filterModeChoices[] = {
- { "None", "none" }
+ { "None", "none" },
+ { "Scale2x (EPX)", "scale2x" }
};
static const ConfigChoice filterModeHChoices[] = {
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
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"),
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)) {
}
}
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;
"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"
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;
}
}
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;
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);
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)) {