diff --git a/.ci/Jenkinsfile b/.ci/Jenkinsfile index 26ee36593..d7b2f94aa 100644 --- a/.ci/Jenkinsfile +++ b/.ci/Jenkinsfile @@ -25,7 +25,7 @@ def buildBranch = env.JOB_BASE_NAME.contains('-') ? 1 : 0 def osArchs = [ 'Windows': ['32', '64'], 'Linux': ['x86', 'x86_64', 'arm32', 'arm64'], - 'macOS': ['x86_64+arm64'] + 'macOS': ['x86_64+x86_64h+arm64'] ] def osFlags = [ @@ -261,12 +261,13 @@ pipeline { osArchs.each { os, thisOsArchs -> def combinations = [:] thisOsArchs.each { arch -> - def thisArchDynarecs = dynarecArchs[arch.toLowerCase()] + def archSlug = arch.replace('+x86_64h', '') /* all instances of arch except the one passed to -b */ + def thisArchDynarecs = dynarecArchs[archSlug.toLowerCase()] if (!thisArchDynarecs) thisArchDynarecs = ['NoDR'] thisArchDynarecs.each { dynarec -> presets.each { preset -> - def combination = "$os $arch $dynarec $preset" + def combination = "$os $archSlug $dynarec $preset" combinations[combination] = { catchError(buildResult: 'FAILURE', stageResult: 'SUCCESS') { retry(10) { @@ -278,11 +279,11 @@ pipeline { /* Switch to output directory. */ dir("${env.WORKSPACE_TMP}/output") { /* Run build process. */ - def packageName = "${env.JOB_BASE_NAME}${dynarecSlugs[dynarec]}${presetSlugs[preset]}-$os-$arch$buildSuffix" + def packageName = "${env.JOB_BASE_NAME}${dynarecSlugs[dynarec]}${presetSlugs[preset]}-$os-$archSlug$buildSuffix" def ret = -1 - def archName = archNames[arch] + def archName = archNames[archSlug] if (os == 'macOS') - archName = archNamesMac[arch] + archName = archNamesMac[archSlug] dir("${dynarecNames[dynarec]}/$os - $archName") { ret = runBuild("-b \"$packageName\" \"$arch\" ${presetFlags[preset]} ${dynarecFlags[dynarec]} ${osFlags[os]} $buildFlags") } diff --git a/.ci/build.sh b/.ci/build.sh index b5edae736..a9639c3fc 100644 --- a/.ci/build.sh +++ b/.ci/build.sh @@ -37,10 +37,17 @@ # build_arch x86_64 (or arm64) # universal_archs (blank) # ui_interactive no -# macosx_deployment_target 10.13 (for x86_64, or 11.0 for arm64) +# macosx_deployment_target 10.13 (for x86_64, 10.14 for Qt Vulkan, or 11.0 for arm64) # - For universal building on Apple Silicon hardware, install native MacPorts on the default # /opt/local and Intel MacPorts on /opt/intel, then tell build.sh to build for "x86_64+arm64" -# - port is called through sudo to manage dependencies; make sure it is configured +# - Qt Vulkan support through MoltenVK requires 10.14 while we target 10.13. We deal with that +# (at least for now) by abusing the x86_64h universal slice to branch Haswell and newer Macs +# into a Vulkan-enabled but 10.14+ binary, with older ones opting for a 10.13-compatible, +# non-Vulkan binary. With this approach, the only machines that miss out on Vulkan despite +# supporting Metal are Ivy Bridge ones as well as GPU-upgraded Mac Pros. For building that +# Vulkan binary, install another Intel MacPorts on /opt/x86_64h, then use the "x86_64h" +# architecture when invoking build.sh (either standalone or as part of an universal build) +# - port and sed are called through sudo to manage dependencies; make sure those are configured # as NOPASSWD in /etc/sudoers if you're doing unattended builds # @@ -401,10 +408,10 @@ then args= [ $strip -ne 0 ] && args="-t $args" case $arch_universal in # workaround: force new dynarec on for ARM - arm32 | arm64) cmake_flags_extra="-D NEW_DYNAREC=ON";; - *) cmake_flags_extra=;; + arm*) cmake_flags_extra="-D NEW_DYNAREC=ON";; + *) cmake_flags_extra=;; esac - zsh -lc 'exec "'"$0"'" -n -b "universal part" "'"$arch_universal"'" '"$args""$cmake_flags"' '"$cmake_flags_extra" + zsh -lc 'exec "'"$0"'" -n -b "universal slice" "'"$arch_universal"'" '"$args""$cmake_flags"' '"$cmake_flags_extra" status=$? if [ $status -eq 0 ] @@ -538,8 +545,8 @@ then # Switch into the correct architecture if required. case $arch in - x86_64) arch_mac="i386";; - *) arch_mac="$arch";; + x86_64*) arch_mac="i386";; + *) arch_mac="$arch";; esac if [ "$(arch)" != "$arch" -a "$(arch)" != "$arch_mac" ] then @@ -560,17 +567,33 @@ then [ "$arch" = "x86_64" -a -e "/opt/intel/bin/port" ] && macports="/opt/intel" export PATH="$macports/bin:$macports/sbin:$macports/libexec/qt5/bin:$PATH" - # Install dependencies only if we're in a new build and/or architecture. - if check_buildtag "$(arch)" + # Enable MoltenVK on x86_64h and arm64, but not on x86_64. + # The rationale behind that is explained on the big comment up top. + moltenvk=0 + if [ "$arch" != "x86_64" ] + then + moltenvk=1 + cmake_flags_extra="$cmake_flags_extra -D MOLTENVK=ON -D \"MOLTENVK_INCLUDE_DIR=$macports\"" + fi + + # Install dependencies only if we're in a new build and/or MacPorts prefix. + if check_buildtag "$(basename "$macports")" then # Install dependencies. echo [-] Installing dependencies through MacPorts sudo "$macports/bin/port" selfupdate + if [ $moltenvk -ne 0 ] + then + # Patch Qt to enable Vulkan support where supported. + qt5_portfile="$macports/var/macports/sources/rsync.macports.org/macports/release/tarballs/ports/aqua/qt5/Portfile" + sudo sed -i -e 's/-no-feature-vulkan/-feature-vulkan/g' "$qt5_portfile" + sudo sed -i -e 's/configure.env-append MAKE=/configure.env-append VULKAN_SDK=${prefix} MAKE=/g' "$qt5_portfile" + fi sudo "$macports/bin/port" install $(cat .ci/dependencies_macports.txt) # Save build tag to skip this later. Doing it here (once everything is # in place) is important to avoid potential issues with retried builds. - save_buildtag "$(arch)" + save_buildtag "$(basename "$macports")" else echo [-] Not installing dependencies again @@ -697,7 +720,7 @@ rm -rf build # Add ARCH to skip the arch_detect process. case $arch in 32 | x86) cmake_flags_extra="$cmake_flags_extra -D ARCH=i386";; - 64 | x86_64) cmake_flags_extra="$cmake_flags_extra -D ARCH=x86_64";; + 64 | x86_64*) cmake_flags_extra="$cmake_flags_extra -D ARCH=x86_64";; ARM32 | arm32) cmake_flags_extra="$cmake_flags_extra -D ARCH=arm";; ARM64 | arm64) cmake_flags_extra="$cmake_flags_extra -D ARCH=arm64";; *) cmake_flags_extra="$cmake_flags_extra -D \"ARCH=$arch\"";; @@ -778,7 +801,7 @@ fi # Determine Discord Game SDK architecture. case $arch in 32) arch_discord="x86";; - 64) arch_discord="x86_64";; + 64 | x86_64*) arch_discord="x86_64";; arm64 | ARM64) arch_discord="aarch64";; *) arch_discord="$arch";; esac @@ -844,6 +867,50 @@ then unzip -j "$discord_zip" "lib/$arch_discord/discord_game_sdk.dylib" -d "archive_tmp/"*".app/Contents/Frameworks" [ ! -e "archive_tmp/"*".app/Contents/Frameworks/discord_game_sdk.dylib" ] && echo [!] No Discord Game SDK for architecture [$arch_discord] + # Hack to convert x86_64 binaries to x86_64h when building that architecture. + if [ "$arch" = "x86_64h" ] + then + find archive_tmp -type f | while IFS= read line + do + # Parse and patch a fat header (0xCAFEBABE, big endian) first. + macho_offset=0 + if [ "$(dd if="$line" bs=1 count=4 status=none)" = "$(printf '\xCA\xFE\xBA\xBE')" ] + then + # Get the number of fat architectures. + fat_archs=$(($(dd if="$line" bs=1 skip=4 count=4 status=none | rev | tr -d '\n' | od -An -vtu4))) + + # Go through fat architectures. + fat_offset=8 + for fat_arch in $(seq 1 $fat_archs) + do + # Check CPU type. + if [ "$(dd if="$line" bs=1 skip=$fat_offset count=4 status=none)" = "$(printf '\x01\x00\x00\x07')" ] + then + # Change CPU subtype in the fat header from ALL (0x00000003) to H (0x00000008). + printf '\x00\x00\x00\x08' | dd of="$line" bs=1 seek=$((fat_offset + 4)) count=4 conv=notrunc status=none + + # Save offset for this architecture's Mach-O header. + macho_offset=$(($(dd if="$line" bs=1 skip=$((fat_offset + 8)) count=4 status=none | rev | tr -d '\n' | od -An -vtu4))) + + # Stop looking for the x86_64 slice. + break + fi + + # Move on to the next architecture. + fat_offset=$((fat_offset + 20)) + done + fi + + # Now patch a 64-bit Mach-O header (0xFEEDFACF, little endian), either at + # the beginning or as a sub-header within a fat binary as parsed above. + if [ "$(dd if="$line" bs=1 seek=$macho_offset count=8 status=none)" = "$(printf '\xCF\xFA\xED\xFE\x07\x00\x00\x01')" ] + then + # Change CPU subtype in the Mach-O header from ALL (0x00000003) to H (0x00000008). + printf '\x08\x00\x00\x00' | dd of="$line" bs=1 seek=$((macho_offset + 8)) count=4 conv=notrunc status=none + fi + done + fi + # Sign app bundle, unless we're in an universal build. [ $skip_archive -eq 0 ] && codesign --force --deep -s - "archive_tmp/"*".app" elif [ "$BUILD_TAG" = "precondition" ] diff --git a/.ci/dependencies_macports.txt b/.ci/dependencies_macports.txt index 88270b4da..b78331f9e 100644 --- a/.ci/dependencies_macports.txt +++ b/.ci/dependencies_macports.txt @@ -4,7 +4,10 @@ ninja freetype libsdl2 libpng +openal-soft FAudio rtmidi +vulkan-headers +MoltenVK qt5 wget diff --git a/CMakeLists.txt b/CMakeLists.txt index 542d446cb..4e49ac784 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,7 +40,7 @@ if(MUNT_EXTERNAL) endif() project(86Box - VERSION 3.7.1 + VERSION 3.8 DESCRIPTION "Emulator of x86-based systems" HOMEPAGE_URL "https://86box.net" LANGUAGES C CXX) @@ -165,6 +165,9 @@ cmake_dependent_option(XL24 "ATI VGA Wonder XL24 (ATI-28800-6)" # Ditto but for Qt if(QT) option(USE_QT6 "Use Qt6 instead of Qt5" OFF) + if(APPLE) + option(MOLTENVK "Use MoltenVK libraries for Vulkan support on macOS. Requires a Vulkan-enabled QT." OFF) + endif() endif() # Determine the build type diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 428d5b521..01de9a473 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,6 +14,9 @@ # Copyright 2020-2022 David Hrdlička. # Copyright 2021 dob205. # +if(APPLE) + set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) +endif() add_executable(86Box 86box.c config.c log.c random.c timer.c io.c acpi.c apm.c dma.c ddma.c discord.c nmi.c pic.c pit.c pit_fast.c port_6x.c port_92.c ppi.c pci.c @@ -83,11 +86,16 @@ if(APPLE) # Force using the newest library if it's installed by homebrew set(CMAKE_FIND_FRAMEWORK LAST) - # setting our compilation target to macOS 10.15 Catalina if targetting Qt6, macOS 10.13 High Sierra otherwise + # setting our compilation target to macOS 10.15 Catalina if targeting Qt6, + # macOS 10.14 Mojave for vulkan support, 10.13 High Sierra otherwise if (USE_QT6) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15") else() - set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13") + if(MOLTENVK) + set(CMAKE_OSX_DEPLOYMENT_TARGET "10.14") + else() + set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13") + endif() endif() endif() diff --git a/src/config.c b/src/config.c index 0c6c0e28b..55dd5dff2 100644 --- a/src/config.c +++ b/src/config.c @@ -1829,6 +1829,15 @@ load_floppy_and_cdrom_drives(void) sprintf(temp, "cdrom_%02i_iso_path", c + 1); config_delete_var(cat, temp); + + for (int i = 0; i < MAX_PREV_IMAGES; i++) { + cdrom[c].image_history[i] = (char *) calloc(MAX_IMAGE_PATH_LEN + 1, sizeof(char)); + sprintf(temp, "cdrom_%02i_image_history_%02i", c + 1, i + 1); + p = config_get_string(cat, temp, NULL); + if(p) { + sprintf(cdrom[c].image_history[i], "%s", p); + } + } } } @@ -3137,6 +3146,15 @@ save_floppy_and_cdrom_drives(void) } else { config_set_string(cat, temp, cdrom[c].image_path); } + + for (int i = 0; i < MAX_PREV_IMAGES; i++) { + sprintf(temp, "cdrom_%02i_image_history_%02i", c + 1, i + 1); + if((cdrom[c].image_history[i] == 0) || strlen(cdrom[c].image_history[i]) == 0) { + config_delete_var(cat, temp); + } else { + config_set_string(cat, temp, cdrom[c].image_history[i]); + } + } } delete_section_if_empty(cat); diff --git a/src/cpu/cpu.c b/src/cpu/cpu.c index 0127270e9..f91dbdb4d 100644 --- a/src/cpu/cpu.c +++ b/src/cpu/cpu.c @@ -1391,7 +1391,7 @@ cpu_set(void) } if (is386) { -#ifdef USE_DYNAREC +#if defined(USE_DYNAREC) && !defined(USE_GDBSTUB) if (cpu_use_dynarec) cpu_exec = exec386_dynarec; else diff --git a/src/gdbstub.c b/src/gdbstub.c index 5140d6c54..b63b13f72 100644 --- a/src/gdbstub.c +++ b/src/gdbstub.c @@ -114,7 +114,8 @@ typedef struct _gdbstub_client_ { struct sockaddr_in addr; char packet[16384], response[16384]; - int has_packet, waiting_stop, packet_pos, response_pos; + int has_packet: 1, first_packet_received: 1, ida_mode: 1, waiting_stop: 1, + packet_pos, response_pos; event_t *processed_event, *response_event; @@ -133,7 +134,6 @@ typedef struct _gdbstub_breakpoint_ { struct _gdbstub_breakpoint_ *next; } gdbstub_breakpoint_t; -#define ENABLE_GDBSTUB_LOG 1 #ifdef ENABLE_GDBSTUB_LOG int gdbstub_do_log = ENABLE_GDBSTUB_LOG; @@ -152,15 +152,15 @@ gdbstub_log(const char *fmt, ...) # define gdbstub_log(fmt, ...) #endif -static x86seg *segment_regs[] = { &cpu_state.seg_cs, &cpu_state.seg_ss, &cpu_state.seg_ds, &cpu_state.seg_es, &cpu_state.seg_fs, &cpu_state.seg_gs }; -static uint32_t *cr_regs[] = { &cpu_state.CR0.l, &cr2, &cr3, &cr4 }; -static void *fpu_regs[] = { &cpu_state.npxc, &cpu_state.npxs, NULL, &x87_pc_seg, &x87_pc_off, &x87_op_seg, &x87_op_off }; -static const char target_xml[] = /* QEMU gdb-xml/i386-32bit.xml with modifications (described in comments) */ +static x86seg *segment_regs[] = { &cpu_state.seg_cs, &cpu_state.seg_ss, &cpu_state.seg_ds, &cpu_state.seg_es, &cpu_state.seg_fs, &cpu_state.seg_gs }; +static uint32_t *cr_regs[] = { &cpu_state.CR0.l, &cr2, &cr3, &cr4 }; +static void *fpu_regs[] = { &cpu_state.npxc, &cpu_state.npxs, NULL, &x87_pc_seg, &x87_pc_off, &x87_op_seg, &x87_op_off }; +static char target_xml[] = /* QEMU gdb-xml/i386-32bit.xml with modifications (described in comments) */ // clang-format off "" "" "" - "i8086" /* start in 16-bit mode to work around known GDB bug preventing 32->16 switching */ + "" /* patched in here (length must be kept) */ "" "" "" @@ -753,6 +753,15 @@ gdbstub_client_packet(gdbstub_client_t *client) client->response_pos = 0; client->packet_pos = 1; + /* Handle IDA-specific hacks. */ + if (!client->first_packet_received) { + client->first_packet_received = 1; + if (!strcmp(client->packet, "qSupported:xmlRegisters=i386,arm,mips")) { + gdbstub_log("GDB Stub: Enabling IDA mode\n"); + client->ida_mode = 1; + } + } + /* Parse command. */ switch (client->packet[0]) { case '?': /* stop reason */ @@ -982,10 +991,21 @@ e14: if (!strcmp(client->response, "read")) { /* Read the transfer annex. */ client->packet_pos += gdbstub_client_read_string(client, client->response, sizeof(client->response) - 1, ':') + 1; - if (!strcmp(client->response, "target.xml")) - p = (char *) target_xml; - else + if (!strcmp(client->response, "target.xml")) { + /* Patch architecture for IDA. */ + p = strstr(target_xml, ""); + if (p) { + if (client->ida_mode) + memcpy(p, "i386 ", 35); /* make IDA not complain about i8086 being unknown */ + else + memcpy(p, "i8086 ", 35); /* start in 16-bit mode to work around known GDB bug preventing 32->16 switching */ + } + + /* Send target XML. */ + p = target_xml; + } else { p = NULL; + } /* Stop if the file wasn't found. */ if (!p) { diff --git a/src/include/86box/86box.h b/src/include/86box/86box.h index 28c370e78..fce923e4f 100644 --- a/src/include/86box/86box.h +++ b/src/include/86box/86box.h @@ -31,6 +31,10 @@ #define NVR_PATH "nvr" #define SCREENSHOT_PATH "screenshots" +/* Recently used images */ +#define MAX_PREV_IMAGES 4 +#define MAX_IMAGE_PATH_LEN 256 + /* Default language 0xFFFF = from system, 0x409 = en-US */ #define DEFAULT_LANGUAGE 0x0409 diff --git a/src/include/86box/cdrom.h b/src/include/86box/cdrom.h index f9040a4ed..4daad5821 100644 --- a/src/include/86box/cdrom.h +++ b/src/include/86box/cdrom.h @@ -39,6 +39,8 @@ #define CD_TOC_SESSION 1 #define CD_TOC_RAW 2 +#define CD_IMAGE_HISTORY 4 + #define BUF_SIZE 32768 #define CDROM_IMAGE 200 @@ -110,6 +112,8 @@ typedef struct cdrom { char image_path[1024], prev_image_path[1024]; + char *image_history[CD_IMAGE_HISTORY]; + uint32_t sound_on, cdrom_capacity, pad, seek_pos, seek_diff, cd_end; diff --git a/src/include_make/86box/version.h b/src/include_make/86box/version.h index 4fccf12f7..098c120f0 100644 --- a/src/include_make/86box/version.h +++ b/src/include_make/86box/version.h @@ -20,12 +20,12 @@ #define EMU_NAME "86Box" #define EMU_NAME_W LSTR(EMU_NAME) -#define EMU_VERSION "3.7.1" +#define EMU_VERSION "3.8" #define EMU_VERSION_W LSTR(EMU_VERSION) #define EMU_VERSION_EX "3.50" /* frozen due to IDE re-detection behavior on Windows */ #define EMU_VERSION_MAJ 3 -#define EMU_VERSION_MIN 7 -#define EMU_VERSION_PATCH 1 +#define EMU_VERSION_MIN 8 +#define EMU_VERSION_PATCH 0 #define EMU_BUILD_NUM 0 @@ -40,7 +40,7 @@ #define EMU_ROMS_URL "https://github.com/86Box/roms/releases/latest" #define EMU_ROMS_URL_W LSTR(EMU_ROMS_URL) #ifdef RELEASE_BUILD -# define EMU_DOCS_URL "https://86box.readthedocs.io/en/v3.7/" +# define EMU_DOCS_URL "https://86box.readthedocs.io/en/v3.8/" #else # define EMU_DOCS_URL "https://86box.readthedocs.io" #endif diff --git a/src/machine/m_at_socket7.c b/src/machine/m_at_socket7.c index 043faddda..1fb4abfdc 100644 --- a/src/machine/m_at_socket7.c +++ b/src/machine/m_at_socket7.c @@ -332,8 +332,8 @@ machine_at_5ivg_init(const machine_t *model) pci_init(PCI_CONFIG_TYPE_1); pci_register_slot(0x00, PCI_CARD_NORTHBRIDGE, 0, 0, 0, 0); - pci_register_slot(0x11, PCI_CARD_NORMAL, 2, 3, 4, 1); - pci_register_slot(0x12, PCI_CARD_NORMAL, 1, 2, 3, 4); + pci_register_slot(0x11, PCI_CARD_NORMAL, 1, 2, 3, 4); + pci_register_slot(0x12, PCI_CARD_NORMAL, 2, 3, 4, 1); pci_register_slot(0x13, PCI_CARD_NORMAL, 3, 4, 1, 2); pci_register_slot(0x07, PCI_CARD_SOUTHBRIDGE, 0, 0, 0, 0); device_add(&i430vx_device); diff --git a/src/qt/CMakeLists.txt b/src/qt/CMakeLists.txt index 1db01301d..0b7ac7092 100644 --- a/src/qt/CMakeLists.txt +++ b/src/qt/CMakeLists.txt @@ -166,6 +166,9 @@ add_library(ui STATIC qt_mcadevicelist.cpp qt_mcadevicelist.ui + qt_mediahistorymanager.cpp + qt_mediahistorymanager.hpp + ../qt_resources.qrc ) @@ -206,6 +209,18 @@ endif() if (APPLE) target_sources(ui PRIVATE macos_event_filter.mm) + if(MOLTENVK) + find_path(MOLTENVK_INCLUDE "vulkan/vulkan.h" PATHS "/opt/homebrew/opt/molten-vk/libexec/include" "/usr/local/opt/molten-vk/libexec/include" ${MOLTENVK_INCLUDE_DIR}) + if (NOT MOLTENVK_INCLUDE) + message(FATAL_ERROR "Could not find vulkan/vulkan.h. If the headers are installed please use -DMOLTENVK_INCLUDE_DIR=/path/to/headers") + endif() + target_include_directories(ui PRIVATE ${MOLTENVK_INCLUDE}) + find_library(MOLTENVK_LIB MoltenVK) + if (NOT MOLTENVK_LIB) + message(FATAL_ERROR "Could not find MoltenVK library") + endif() + target_link_libraries(ui PRIVATE "${MOLTENVK_LIB}") + endif() endif() if (WIN32) @@ -282,6 +297,7 @@ if (APPLE AND CMAKE_MACOSX_BUNDLE) set(prefix "86Box.app/Contents") set(INSTALL_RUNTIME_DIR "${prefix}/MacOS") set(INSTALL_CMAKE_DIR "${prefix}/Resources") + set(INSTALL_LIB_DIR "${prefix}/Frameworks") # using the install_qt5_plugin to add Qt plugins into the macOS app bundle install_qt5_plugin("Qt${QT_MAJOR}::QCocoaIntegrationPlugin" QT_PLUGINS ${prefix}) @@ -314,6 +330,16 @@ if (APPLE AND CMAKE_MACOSX_BUNDLE) COMMAND ${CMAKE_INSTALL_NAME_TOOL} -add_rpath \"@executable_path/../Frameworks/\" \"\${CMAKE_INSTALL_PREFIX_ABSOLUTE}/${INSTALL_RUNTIME_DIR}/86Box\") ") + if(MOLTENVK) + install(CODE " + execute_process( + COMMAND bash -c \"set -e + echo \\\"-- Creating vulkan dylib symlink for QT (libVulkan.dylib -> libMoltenVK.dylib)\\\" + cd \${CMAKE_INSTALL_PREFIX_ABSOLUTE}/${INSTALL_LIB_DIR} + ln -sf libMoltenVK.dylib libVulkan.dylib + \") + ") + endif() endif() if (UNIX AND NOT APPLE AND NOT HAIKU) diff --git a/src/qt/qt_mediahistorymanager.cpp b/src/qt/qt_mediahistorymanager.cpp new file mode 100644 index 000000000..2c08f66f9 --- /dev/null +++ b/src/qt/qt_mediahistorymanager.cpp @@ -0,0 +1,326 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* Media history management module +* +* +* +* Authors: cold-brewed +* +* Copyright 2022 The 86Box development team +*/ + + +#include +#include +#include +#include +#include + +#include "86box/cdrom.h" +#include "qt_mediahistorymanager.hpp" + +namespace ui { + +MediaHistoryManager::MediaHistoryManager() { + initializeImageHistory(); + deserializeAllImageHistory(); + initialDeduplication(); + +} + +MediaHistoryManager::~MediaHistoryManager() += default; + +master_list_t & +MediaHistoryManager::blankImageHistory(master_list_t &initialized_master_list) const +{ + for ( const auto device_type : ui::AllSupportedMediaHistoryTypes ) { + device_media_history_t device_media_history; + // Loop for all possible media devices + for (int device_index = 0 ; device_index < maxDevicesSupported(device_type); device_index++) { + device_index_list_t indexing_list; + device_media_history[device_index] = indexing_list; + // Loop for each history slot + for (int slot_index = 0; slot_index < max_images; slot_index++) { + device_media_history[device_index].append(QString()); + } + } + initialized_master_list.insert(device_type, device_media_history); + } + return initialized_master_list; +} + + +const device_index_list_t& +MediaHistoryManager::getHistoryListForDeviceIndex(int index, ui::MediaType type) +{ + if (master_list.contains(type)) { + if ((index >= 0 ) && (index < master_list[type].size())) { + return master_list[type][index]; + } else { + qWarning("Media device index %i for device type %s was requested but index %i is out of range (valid range: >= 0 && < %i)", + index, mediaTypeToString(type).toUtf8().constData(), index, master_list[type].size()); + } + } + // Failure gets an empty list + return empty_device_index_list; +} + +void MediaHistoryManager::setHistoryListForDeviceIndex(int index, ui::MediaType type, device_index_list_t history_list) +{ + master_list[type][index] = std::move(history_list); +} + +QString +MediaHistoryManager::getImageForSlot(int index, int slot, ui::MediaType type) +{ + QString image_name; + device_index_list_t device_history = getHistoryListForDeviceIndex(index, type); + if ((slot >= 0) && (slot < device_history.size())) { + image_name = device_history[slot]; + } else { + qWarning("Media history slot %i, index %i for device type %s was requested but slot %i is out of range (valid range: >= 0 && < %i, device_history.size() is %i)", + slot, index, mediaTypeToString(type).toUtf8().constData(), slot, maxDevicesSupported(type), device_history.size()); + } + return image_name; +} + +// These are hardcoded since we can't include the various +// header files where they are defined (e.g., fdd.h, mo.h). +// However, all in ui::MediaType support 4 except cassette. +int MediaHistoryManager::maxDevicesSupported(ui::MediaType type) +{ + return type == ui::MediaType::Cassette ? 1 : 4; + +} + +void MediaHistoryManager::deserializeImageHistoryType(ui::MediaType type) +{ + for (int device = 0; device < maxDevicesSupported(type); device++) { + char **device_history_ptr = getEmuHistoryVarForType(type, device); + if(device_history_ptr == nullptr) { + // Device not supported, return and do not deserialize. + // This will leave the image listing at the default initialization state + // from the ui side (this class) + continue; + } + for ( int slot = 0; slot < MAX_PREV_IMAGES; slot++) { + master_list[type][device][slot] = device_history_ptr[slot]; + } + } +} +void MediaHistoryManager::deserializeAllImageHistory() +{ + for ( const auto device_type : ui::AllSupportedMediaHistoryTypes ) { + deserializeImageHistoryType(device_type); + } +} +void MediaHistoryManager::serializeImageHistoryType(ui::MediaType type) +{ + for (int device = 0; device < maxDevicesSupported(type); device++) { + char **device_history_ptr = getEmuHistoryVarForType(type, device); + if(device_history_ptr == nullptr) { + // Device not supported, return and do not serialize. + // This will leave the image listing at the current state, + // and it will not be saved on the emu side + continue; + } + for ( int slot = 0; slot < MAX_PREV_IMAGES; slot++) { + strncpy(device_history_ptr[slot], master_list[type][device][slot].toUtf8().constData(), MAX_IMAGE_PATH_LEN); + + } + } +} + +void MediaHistoryManager::serializeAllImageHistory() +{ + for ( const auto device_type : ui::AllSupportedMediaHistoryTypes ) { + serializeImageHistoryType(device_type); + } +} + +void MediaHistoryManager::initialDeduplication() +{ + + QString current_image; + // Perform initial dedup if an image is loaded + for ( const auto device_type : ui::AllSupportedMediaHistoryTypes ) { + for (int device_index = 0; device_index < maxDevicesSupported(device_type); device_index++) { + device_index_list_t device_history = getHistoryListForDeviceIndex(device_index, device_type); + switch (device_type) { + case ui::MediaType::Optical: + current_image = cdrom[device_index].image_path; + break; + default: + continue; + break; + } + deduplicateList(device_history, QVector (1, current_image)); + // Fill in missing, if any + int missing = MAX_PREV_IMAGES - device_history.size(); + if(missing) { + for (int i = 0; i < missing; i++) { + device_history.push_back(QString()); + } + } + setHistoryListForDeviceIndex(device_index, device_type, device_history); + } + } +} + +char ** MediaHistoryManager::getEmuHistoryVarForType(ui::MediaType type, int index) +{ + switch (type) { + case ui::MediaType::Optical: + return &cdrom[index].image_history[0]; + default: + return nullptr; + + } +} + +device_index_list_t & +MediaHistoryManager::deduplicateList(device_index_list_t &device_history, const QVector& filenames) +{ + QVector items_to_delete; + for (auto &list_item_path : device_history) { + if(list_item_path.isEmpty()) { + continue ; + } + for (const auto& path_to_check : filenames) { + if(path_to_check.isEmpty()) { + continue ; + } + QString adjusted_path = pathAdjustSingle(path_to_check); + int match = QString::localeAwareCompare(list_item_path, adjusted_path); + if (match == 0) { + items_to_delete.append(list_item_path); + } + } + } + // Remove by name rather than index because the index would change + // after each removal + for (const auto& path: items_to_delete) { + device_history.removeAll(path); + } + return device_history; +} + +void MediaHistoryManager::addImageToHistory(int index, ui::MediaType type, const QString& image_name, const QString& new_image_name) +{ + device_index_list_t device_history = getHistoryListForDeviceIndex(index, type); + QVector files_to_check; + + files_to_check.append(image_name); + files_to_check.append(new_image_name); + device_history = deduplicateList(device_history, files_to_check); + + + if (!image_name.isEmpty()) { + device_history.push_front(image_name); + } + + // Pop any extras + if ((device_history.size() > MAX_PREV_IMAGES)) { + device_history.pop_back(); + } + + // Fill in missing, if any + int missing = MAX_PREV_IMAGES - device_history.size(); + if(missing) { + for (int i = 0; i < missing; i++) { + device_history.push_back(QString()); + } + } + + device_history = removeMissingImages(device_history); + device_history = pathAdjustFull(device_history); + + setHistoryListForDeviceIndex(index, type, device_history); + serializeImageHistoryType(type); +} + +QString MediaHistoryManager::mediaTypeToString(ui::MediaType type) +{ + QMetaEnum qme = QMetaEnum::fromType(); + return qme.valueToKey(static_cast(type)); +} + +QString +MediaHistoryManager::pathAdjustSingle(QString checked_path) +{ + QString current_usr_path = getUsrPath(); + QFileInfo file_info(checked_path); + if (file_info.filePath().isEmpty() || current_usr_path.isEmpty() || file_info.isRelative()) { + return checked_path; + } + if (file_info.filePath().startsWith(current_usr_path)) { + checked_path = file_info.filePath().remove(current_usr_path); + } + return checked_path; +} + +device_index_list_t & +MediaHistoryManager::pathAdjustFull(device_index_list_t &device_history) +{ + for (auto &checked_path : device_history) { + checked_path = pathAdjustSingle(checked_path); + } + return device_history; +} +QString MediaHistoryManager::getUsrPath() +{ + QString current_usr_path(usr_path); + // Ensure `usr_path` has a trailing slash + return current_usr_path.endsWith("/") ? current_usr_path : current_usr_path.append("/"); +} +device_index_list_t & +MediaHistoryManager::removeMissingImages(device_index_list_t &device_history) +{ + for (auto &checked_path : device_history) { + QFileInfo file_info(checked_path); + if (file_info.filePath().isEmpty()) { + continue; + } + // For this check, explicitly prepend `usr_path` to relative paths to account for $CWD platform variances + QFileInfo absolute_path = file_info.isRelative() ? getUsrPath().append(file_info.filePath()) : file_info; + if(!absolute_path.exists()) { + qWarning("Image file %s does not exist - removing from history", qPrintable(file_info.filePath())); + checked_path = ""; + } + } + return device_history; +} + +void MediaHistoryManager::initializeImageHistory() +{ + auto initial_master_list = getMasterList(); + setMasterList(blankImageHistory(initial_master_list)); +} + +const master_list_t & +MediaHistoryManager::getMasterList() const +{ + return master_list; +} + +void +MediaHistoryManager::setMasterList(const master_list_t &masterList) +{ + master_list = masterList; +} + +void +MediaHistoryManager::clearImageHistory() +{ + initializeImageHistory(); + serializeAllImageHistory(); +} + +} // ui \ No newline at end of file diff --git a/src/qt/qt_mediahistorymanager.hpp b/src/qt/qt_mediahistorymanager.hpp new file mode 100644 index 000000000..0a69aa100 --- /dev/null +++ b/src/qt/qt_mediahistorymanager.hpp @@ -0,0 +1,139 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* Header for the media history management module +* +* +* +* Authors: cold-brewed +* +* Copyright 2022 The 86Box development team +*/ + +#ifndef QT_MEDIAHISTORYMANAGER_HPP +#define QT_MEDIAHISTORYMANAGER_HPP + +#include +#include +#include + +#include + +extern "C" { +#include <86box/86box.h> +} + +// This macro helps give us the required `qHash()` function in order to use the +// enum as a hash key +#define QHASH_FOR_CLASS_ENUM(T) \ +inline uint qHash(const T &t, uint seed) { \ + return ::qHash(static_cast::type>(t), seed); \ +} + +typedef QVector device_index_list_t; +typedef QHash> device_media_history_t; + + +namespace ui { + Q_NAMESPACE + + enum class MediaType { + Floppy, + Optical, + Zip, + Mo, + Cassette + }; + // This macro allows us to do a reverse lookup of the enum with `QMetaEnum` + Q_ENUM_NS(MediaType) + + QHASH_FOR_CLASS_ENUM(MediaType) + + typedef QHash master_list_t; + + // Used to iterate over all supported types when preparing data structures + // Also useful to indicate which types support history + static const MediaType AllSupportedMediaHistoryTypes[] = { + MediaType::Optical + }; + + class MediaHistoryManager { + + public: + MediaHistoryManager(); + virtual ~MediaHistoryManager(); + + // Get the image name for a particular slot, + // index, and type combination + QString getImageForSlot(int index, int slot, ui::MediaType type); + + // Add an image to history + void addImageToHistory(int index, ui::MediaType type, const QString& image_name, const QString& new_image_name); + + // Convert the enum value to a string + static QString mediaTypeToString(ui::MediaType type); + + // Clear out the image history + void clearImageHistory(); + + + private: + int max_images = MAX_PREV_IMAGES; + + // Main hash of hash of vector of strings + master_list_t master_list; + const master_list_t &getMasterList() const; + void setMasterList(const master_list_t &masterList); + + device_index_list_t index_list, empty_device_index_list; + + // Return a blank, initialized image history list + master_list_t &blankImageHistory(master_list_t &initialized_master_list) const; + + // Initialize the image history + void initializeImageHistory(); + + // Max number of devices supported by media type + static int maxDevicesSupported(ui::MediaType type); + + // Serialize the data back into the C array + // on the emu side + void serializeImageHistoryType(ui::MediaType type); + void serializeAllImageHistory(); + + // Deserialize the data from C array on the emu side + // for the ui side + void deserializeImageHistoryType(ui::MediaType type); + void deserializeAllImageHistory(); + + // Get emu history variable for a device type + static char** getEmuHistoryVarForType(ui::MediaType type, int index); + + // Get or set the history for a specific device/index combo + const device_index_list_t &getHistoryListForDeviceIndex(int index, ui::MediaType type); + void setHistoryListForDeviceIndex(int index, ui::MediaType type, device_index_list_t history_list); + + // Remove missing image files from history list + static device_index_list_t &removeMissingImages(device_index_list_t &device_history); + + // If an absolute path is contained within `usr_path`, convert to a relative path + static device_index_list_t &pathAdjustFull(device_index_list_t &device_history); + static QString pathAdjustSingle(QString checked_path); + + // Deduplicate history entries + static device_index_list_t &deduplicateList(device_index_list_t &device_history, const QVector& filenames); + void initialDeduplication(); + + // Gets the `usr_path` from the emu side and appends a + // trailing slash if necessary + static QString getUsrPath(); + }; + +} // ui + +#endif // QT_MEDIAHISTORYMANAGER_HPP diff --git a/src/qt/qt_mediamenu.cpp b/src/qt/qt_mediamenu.cpp index 0050da900..ba52074cf 100644 --- a/src/qt/qt_mediamenu.cpp +++ b/src/qt/qt_mediamenu.cpp @@ -52,6 +52,7 @@ extern "C" { #include "qt_newfloppydialog.hpp" #include "qt_util.hpp" #include "qt_deviceconfig.hpp" +#include "qt_mediahistorymanager.hpp" std::shared_ptr MediaMenu::ptr; @@ -120,8 +121,11 @@ void MediaMenu::refresh(QMenu *parentMenu) { menu->addAction(tr("&Mute"), [this, i]() { cdromMute(i); })->setCheckable(true); menu->addSeparator(); menu->addAction(tr("&Image..."), [this, i]() { cdromMount(i); })->setCheckable(false); - cdromReloadPos = menu->children().count(); - menu->addAction(tr("&Reload previous image"), [this, i]() { cdromReload(i); }); + menu->addSeparator(); + for (int slot = 0; slot < MAX_PREV_IMAGES; slot++) { + cdromImageHistoryPos[slot] = menu->children().count(); + menu->addAction(QString::asprintf(tr("Image %i").toUtf8().constData(), slot), [this, i, slot]() { cdromReload(i, slot); })->setCheckable(false); + } menu->addSeparator(); cdromImagePos = menu->children().count(); menu->addAction(tr("E&ject"), [this, i]() { cdromEject(i); })->setCheckable(false); @@ -170,6 +174,7 @@ void MediaMenu::refresh(QMenu *parentMenu) { netMenus[i] = menu; nicUpdateMenu(i); }); + parentMenu->addAction(tr("Clear image history"), [this]() { clearImageHistory(); }); } void MediaMenu::cassetteNewImage() { @@ -404,14 +409,13 @@ void MediaMenu::cdromMount(int i, const QString &filename) } else { ui_sb_update_icon_state(SB_CDROM | i, 1); } + mhm.addImageToHistory(i, ui::MediaType::Optical, cdrom[i].prev_image_path, cdrom[i].image_path); cdromUpdateMenu(i); ui_sb_update_tip(SB_CDROM | i); config_save(); } void MediaMenu::cdromMount(int i) { - QString dir; - QFileInfo fi(cdrom[i].image_path); auto filename = QFileDialog::getOpenFileName( parentWidget, @@ -430,22 +434,60 @@ void MediaMenu::cdromMount(int i) { } void MediaMenu::cdromEject(int i) { + mhm.addImageToHistory(i, ui::MediaType::Optical, cdrom[i].image_path, QString()); cdrom_eject(i); cdromUpdateMenu(i); ui_sb_update_tip(SB_CDROM | i); } -void MediaMenu::cdromReload(int i) { - cdrom_reload(i); - cdromUpdateMenu(i); - ui_sb_update_tip(SB_CDROM | i); +void MediaMenu::cdromReload(int index, int slot) { + QString filename = mhm.getImageForSlot(index, slot, ui::MediaType::Optical); + cdromMount(index, filename.toUtf8().constData()); + cdromUpdateMenu(index); + ui_sb_update_tip(SB_CDROM | index); +} + +void MediaMenu::updateImageHistory(int index, int slot, ui::MediaType type) { + QMenu* menu; + QAction* imageHistoryUpdatePos; + QString image_path; + QObjectList children; + + switch (type) { + case ui::MediaType::Optical: + if (!cdromMenus.contains(index)) + return; + menu = cdromMenus[index]; + children = menu->children(); + imageHistoryUpdatePos = dynamic_cast(children[cdromImageHistoryPos[slot]]); + image_path = mhm.getImageForSlot(index, slot, type); + break; + case ui::MediaType::Floppy: + if (!floppyMenus.contains(index)) + return; + menu = floppyMenus[index]; + children = menu->children(); + imageHistoryUpdatePos = dynamic_cast(children[floppyImageHistoryPos[slot]]); + image_path = mhm.getImageForSlot(index, slot, type); + break; + default: + pclog("History not yet implemented for media type %s\n", qPrintable(mhm.mediaTypeToString(type))); + return; + } + + QFileInfo fi(image_path); + imageHistoryUpdatePos->setText(QString::asprintf(tr("%s").toUtf8().constData(), fi.fileName().isEmpty() ? tr("previous image").toUtf8().constData() : fi.fileName().toUtf8().constData())); + imageHistoryUpdatePos->setVisible(!fi.fileName().isEmpty()); +} + +void MediaMenu::clearImageHistory() { + mhm.clearImageHistory(); + ui_sb_update_panes(); } void MediaMenu::cdromUpdateMenu(int i) { QString name = cdrom[i].image_path; - QString prev_name = cdrom[i].prev_image_path; QFileInfo fi(cdrom[i].image_path); - QFileInfo fi_prev(cdrom[i].prev_image_path); if (!cdromMenus.contains(i)) return; @@ -459,9 +501,8 @@ void MediaMenu::cdromUpdateMenu(int i) { imageMenu->setEnabled(!name.isEmpty()); imageMenu->setText(QString::asprintf(tr("Eject %s").toUtf8().constData(), name.isEmpty() ? QString().toUtf8().constData() : fi.fileName().toUtf8().constData())); - auto* prevMenu = dynamic_cast(childs[cdromReloadPos]); - prevMenu->setText(QString::asprintf(tr("Reload %s").toUtf8().constData(), prev_name.isEmpty() ? tr("previous image").toUtf8().constData() : fi_prev.fileName().toUtf8().constData())); - prevMenu->setVisible(name.isEmpty() && cdrom[i].prev_host_drive != 0); + for (int slot = 0; slot < MAX_PREV_IMAGES; slot++) + updateImageHistory(i, slot, ui::MediaType::Optical); QString busName = tr("Unknown Bus"); switch (cdrom[i].bus_type) { diff --git a/src/qt/qt_mediamenu.hpp b/src/qt/qt_mediamenu.hpp index de892d73c..4503c1b93 100644 --- a/src/qt/qt_mediamenu.hpp +++ b/src/qt/qt_mediamenu.hpp @@ -3,7 +3,11 @@ #include #include #include +#include "qt_mediahistorymanager.hpp" +extern "C" { +#include <86box/86box.h> +} class QMenu; class MediaMenu : QObject @@ -40,7 +44,9 @@ public: void cdromMount(int i); void cdromMount(int i, const QString& filename); void cdromEject(int i); - void cdromReload(int i); + void cdromReload(int index, int slot); + void updateImageHistory(int index, int slot, ui::MediaType type); + void clearImageHistory(); void cdromUpdateMenu(int i); void zipNewImage(int i); @@ -72,6 +78,7 @@ private: QMap netMenus; QString getMediaOpenDirectory(); + ui::MediaHistoryManager mhm; int cassetteRecordPos; int cassettePlayPos; @@ -87,6 +94,8 @@ private: int cdromMutePos; int cdromReloadPos; int cdromImagePos; + int cdromImageHistoryPos[MAX_PREV_IMAGES]; + int floppyImageHistoryPos[MAX_PREV_IMAGES]; int zipEjectPos; int zipReloadPos; diff --git a/src/qt/qt_platform.cpp b/src/qt/qt_platform.cpp index 8a41769a9..019d38cf4 100644 --- a/src/qt/qt_platform.cpp +++ b/src/qt/qt_platform.cpp @@ -167,7 +167,13 @@ plat_fopen(const char *path, const char *mode) FILE * plat_fopen64(const char *path, const char *mode) { +#if defined(Q_OS_MACOS) or defined(Q_OS_LINUX) + QFileInfo fi(path); + QString filename = fi.isRelative() ? usr_path + fi.filePath() : fi.filePath(); + return fopen(filename.toUtf8().constData(), mode); +#else return fopen(QString::fromUtf8(path).toLocal8Bit(), mode); +#endif } int diff --git a/src/unix/assets/86Box.spec b/src/unix/assets/86Box.spec index 585e1aa17..c6378dddf 100644 --- a/src/unix/assets/86Box.spec +++ b/src/unix/assets/86Box.spec @@ -15,7 +15,7 @@ %global romver v3.7 Name: 86Box -Version: 3.7.1 +Version: 3.8 Release: 1%{?dist} Summary: Classic PC emulator License: GPLv2+ @@ -117,5 +117,5 @@ popd %{_datadir}/%{name}/roms %changelog -* Tue Aug 02 2022 Robert de Rooy 3.7.1-1 +* Tue Aug 30 2022 Robert de Rooy 3.8-1 - Bump release diff --git a/src/unix/assets/net.86box.86Box.metainfo.xml b/src/unix/assets/net.86box.86Box.metainfo.xml index a4458cfad..21e84b16a 100644 --- a/src/unix/assets/net.86box.86Box.metainfo.xml +++ b/src/unix/assets/net.86box.86Box.metainfo.xml @@ -10,7 +10,7 @@ net.86box.86Box.desktop - + diff --git a/vcpkg.json b/vcpkg.json index 19dd09354..0bf171db8 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,6 +1,6 @@ { "name": "86box", - "version-string": "3.7.1", + "version-string": "3.8", "homepage": "https://86box.net/", "documentation": "http://86box.readthedocs.io/", "license": "GPL-2.0-or-later",