From 9198c2bd58d31310a324ad0180c57e503c630d0f Mon Sep 17 00:00:00 2001 From: Vas Crabb Date: Sat, 19 Jan 2019 17:34:43 +1100 Subject: More UI stuff: * Split up the different parts of ICO loading in the menus (locating files, scaling, drawing, etc.) * Added icon support to software selection menu * Added support for more ICO file variants, including PNG-in-ICO (new DIB parser is overkill for ICO but I can factor it out for BMP loading at some point) * Added favourites filter for software menus - includes software that's favourited on any system, so GBC includes DMG favourties and vice versa * Eliminated unnecessary member variables and O(n) walks in software selection menu * Made the menus' cached texture structures a bit more efficient --- scripts/src/mame/frontend.lua | 1 + src/emu/fileio.cpp | 2 +- src/frontend/mame/ui/icorender.cpp | 781 +++++++++++++++++++++++++++++++++++++ src/frontend/mame/ui/icorender.h | 227 +---------- src/frontend/mame/ui/menu.cpp | 4 +- src/frontend/mame/ui/menu.h | 22 +- src/frontend/mame/ui/selgame.cpp | 183 +++------ src/frontend/mame/ui/selgame.h | 13 +- src/frontend/mame/ui/selmenu.cpp | 239 ++++++++++-- src/frontend/mame/ui/selmenu.h | 36 +- src/frontend/mame/ui/selsoft.cpp | 256 +++++++----- src/frontend/mame/ui/selsoft.h | 38 +- src/frontend/mame/ui/utils.cpp | 19 + src/frontend/mame/ui/utils.h | 1 + src/frontend/mame/ui/widgets.cpp | 13 +- src/frontend/mame/ui/widgets.h | 12 +- src/lib/util/corefile.cpp | 22 +- src/lib/util/corefile.h | 21 +- 18 files changed, 1347 insertions(+), 543 deletions(-) create mode 100644 src/frontend/mame/ui/icorender.cpp diff --git a/scripts/src/mame/frontend.lua b/scripts/src/mame/frontend.lua index 60e955f8773..c8f21daa4a6 100644 --- a/scripts/src/mame/frontend.lua +++ b/scripts/src/mame/frontend.lua @@ -137,6 +137,7 @@ files { MAME_DIR .. "src/frontend/mame/ui/defimg.ipp", MAME_DIR .. "src/frontend/mame/ui/dirmenu.cpp", MAME_DIR .. "src/frontend/mame/ui/dirmenu.h", + MAME_DIR .. "src/frontend/mame/ui/icorender.cpp", MAME_DIR .. "src/frontend/mame/ui/icorender.h", MAME_DIR .. "src/frontend/mame/ui/inifile.cpp", MAME_DIR .. "src/frontend/mame/ui/inifile.h", diff --git a/src/emu/fileio.cpp b/src/emu/fileio.cpp index 3ec686266c8..bbd1d931f30 100644 --- a/src/emu/fileio.cpp +++ b/src/emu/fileio.cpp @@ -96,7 +96,7 @@ bool path_iterator::next(std::string &buffer, const char *name) if (name) { // compute the full pathname - if (!buffer.empty() && (*buffer.rbegin() != *PATH_SEPARATOR)) + if (!buffer.empty() && !util::is_directory_separator(buffer.back())) buffer.append(PATH_SEPARATOR); buffer.append(name); } diff --git a/src/frontend/mame/ui/icorender.cpp b/src/frontend/mame/ui/icorender.cpp new file mode 100644 index 00000000000..6f50d63f076 --- /dev/null +++ b/src/frontend/mame/ui/icorender.cpp @@ -0,0 +1,781 @@ +// license:BSD-3-Clause +// copyright-holders:Maurizio Petrarota, Victor Laskin +/*************************************************************************** + + ui/icorender.h + + Windows icon file parser. + + Previously based on code by Victor Laskin (victor.laskin@gmail.com) + http://vitiy.info/Code/ico.cpp + + TODO: + * Add variant that loads all images from the file + * Allow size hint for choosing best candidate + * Allow selecting amongst candidates based on colour depth + +***************************************************************************/ + +#include "emu.h" +#include "icorender.h" + +#include "util/png.h" + +#include +#include +#include +#include + +// need to set LOG_OUTPUT_STREAM because there's no logerror outside devices +#define LOG_OUTPUT_STREAM std::cerr + +#define LOG_GENERAL (1U << 0) +#define LOG_DIB (1U << 1) + +//#define VERBOSE (LOG_GENERAL | LOG_DIB) + +#include "logmacro.h" + + +namespace ui { + +namespace { + +// DIB compression schemes +enum : uint32_t +{ + DIB_COMP_NONE = 0, + DIB_COMP_RLE8 = 1, + DIB_COMP_RLE4 = 2, + DIB_COMP_BITFIELDS = 3 +}; + + +// ICO file header +struct icon_dir_t +{ + uint16_t reserved; // must be 0 + uint16_t type; // 1 for icon or 2 for cursor + uint16_t count; // number of images in the file +}; + +// ICO file directory entry +struct icon_dir_entry_t +{ + constexpr unsigned get_width() const { return width ? width : 256U; } + constexpr unsigned get_height() const { return height ? height : 256U; } + + void byteswap() + { + planes = little_endianize_int16(planes); + bpp = little_endianize_int16(bpp); + size = little_endianize_int32(size); + offset = little_endianize_int32(offset); + } + + uint8_t width; // 0 means 256 + uint8_t height; // 0 means 256 + uint8_t colors; // for indexed colour, or 0 for direct colour + uint8_t reserved; // documentation says this should be 0 but .NET writes 255 + uint16_t planes; // or hotspot X for cursor + uint16_t bpp; // 0 to infer from image data, or hotspot Y for cursor + uint32_t size; // image data size in bytes + uint32_t offset; // offset to image data from start of file +}; + +// old-style DIB header +struct bitmap_core_header_t +{ + uint32_t size; // size of the header (12, 16 or 64) + int16_t width; // width of bitmap in pixels + int16_t height; // height of the image in pixels + uint16_t planes; // number of colour planes (must be 1) + uint16_t bpp; // bits per pixel +}; + +// new-style DIB header +struct bitmap_info_header_t +{ + uint32_t size; // size of the header + int32_t width; // width of bitmap in pixels + int32_t height; // height of bitmap in pixels + uint16_t planes; // number of colour planes (must be 1) + uint16_t bpp; // bits per pixel + uint32_t comp; // compression method + uint32_t rawsize; // size of bitmap data after decompression or 0 if uncompressed + int32_t hres; // horizontal resolution in pixels/metre + int32_t vres; // horizontal resolution in pixels/metre + uint32_t colors; // number of colours or 0 for 1 << bpp + uint32_t important; // number of important colours or 0 if all important + uint32_t red; // red field mask - must be contiguous + uint32_t green; // green field mask - must be contiguous + uint32_t blue; // blue field mask - must be contiguous + uint32_t alpha; // alpha field mask - must be contiguous +}; + + +bool dib_parse_mask(uint32_t mask, unsigned &shift, unsigned &bits) +{ + shift = count_leading_zeros(mask); + mask <<= shift; + bits = count_leading_ones(mask); + mask <<= shift; + shift = 32 - shift - bits; + return !mask; +} + + +void dib_truncate_channel(unsigned &shift, unsigned &bits) +{ + if (8U < bits) + { + unsigned const excess(bits - 8); + shift += excess; + bits -= excess; + } +} + + +uint8_t dib_splat_sample(uint8_t val, unsigned bits) +{ + assert(8U >= bits); + for (val <<= (8U - bits); bits && (8U > bits); bits <<= 1) + val |= val >> bits; + return val; +} + + +bool load_ico_png(util::core_file &fp, icon_dir_entry_t const &dir, bitmap_argb32 &bitmap) +{ + // skip out if the data isn't a reasonable size - PNG magic alone is eight bytes + if (9U >= dir.size) + return false; + fp.seek(dir.offset, SEEK_SET); + png_error const err(png_read_bitmap(fp, bitmap)); + switch (err) + { + case PNGERR_NONE: + // found valid PNG image + assert(bitmap.valid()); + if ((dir.get_width() == bitmap.width()) && ((dir.get_height() == bitmap.height()))) + { + LOG("Loaded %d*%d pixel PNG image from ICO file\n", bitmap.width(), bitmap.height()); + } + else + { + LOG( + "Loaded %d*%d pixel PNG image from ICO file (directory indicated %u*%u)\n", + bitmap.width(), + bitmap.height(), + dir.get_width(), + dir.get_height()); + } + return true; + + case PNGERR_BAD_SIGNATURE: + // doesn't look like PNG data - just fall back to DIB without the file header + return false; + + default: + // invalid PNG data or I/O error + LOG( + "Error %u reading PNG image data from ICO file at offset %u (directory size %u)\n", + unsigned(err), + dir.offset, + dir.size); + return false; + } +} + + +bool load_ico_dib(util::core_file &fp, icon_dir_entry_t const &dir, bitmap_argb32 &bitmap) +{ + // check that these things haven't been padded somehow + static_assert(sizeof(bitmap_core_header_t) == 12U, "compiler has applied padding to bitmap_core_header_t"); + static_assert(sizeof(bitmap_info_header_t) == 56U, "compiler has applied padding to bitmap_info_header_t"); + + // ensure the header fits in the space for the image data + union { bitmap_core_header_t core; bitmap_info_header_t info; } header; + assert(&header.core.size == &header.info.size); + if (sizeof(header.core) > dir.size) + return false; + std::memset(&header, 0, sizeof(header)); + fp.seek(dir.offset, SEEK_SET); + if (fp.read(&header.core.size, sizeof(header.core.size)) != sizeof(header.core.size)) + { + LOG( + "Error reading DIB header size from ICO file at offset %u (directory size %u)\n", + dir.offset, + dir.size); + return false; + } + header.core.size = little_endianize_int32(header.core.size); + if (dir.size < header.core.size) + { + LOG( + "ICO file image data at %u (%u bytes) is too small for DIB header (%u bytes)\n", + dir.offset, + dir.size, + header.core.size); + return false; + } + + // identify and read the header - convert OS/2 headers to Windows 3 format + unsigned palette_bytes(4U); + switch (header.core.size) + { + case 16U: + case 64U: + // extended OS/2 bitmap header with support for compression + LOG( + "ICO image data at %u (%u bytes) uses unsupported OS/2 DIB header (size %u)\n", + dir.offset, + dir.size, + header.core.size); + return false; + + case 12U: + // introduced in OS/2 and Windows 2.0 + { + palette_bytes = 3U; + uint32_t const header_read(std::min(header.core.size, sizeof(header.core)) - sizeof(header.core.size)); + if (fp.read(&header.core.width, header_read) != header_read) + { + LOG("Error reading DIB core header from ICO file image data at %u (%u bytes)\n", dir.offset, dir.size); + return false; + } + fp.seek(header.core.size - sizeof(header.core.size) - header_read, SEEK_CUR); + header.core.width = little_endianize_int16(header.core.width); + header.core.height = little_endianize_int16(header.core.height); + header.core.planes = little_endianize_int16(header.core.planes); + header.core.bpp = little_endianize_int16(header.core.bpp); + LOGMASKED( + LOG_DIB, + "Read DIB core header from ICO file image data at %u: %d*%d, %u planes, %u bpp\n", + dir.offset, + header.core.width, + header.core.height, + header.core.planes, + header.core.bpp); + + // this works because the core header only aliases the width/height of the info header + header.info.bpp = header.core.bpp; + header.info.planes = header.core.planes; + header.info.height = header.core.height; + header.info.width = header.core.width; + header.info.size = 40U; + } + break; + + default: + // the next version will be longer + if (124U >= header.core.size) + { + LOG( + "ICO image data at %u (%u bytes) uses unsupported DIB header format (size %u)\n", + dir.offset, + dir.size, + header.core.size); + return false; + } + // fall through + case 40U: + case 52U: + case 56U: + case 108U: + case 124U: + // the Windows 3 bitmap header with optional extensions + { + palette_bytes = 4U; + uint32_t const header_read(std::min(header.info.size, sizeof(header.info)) - sizeof(header.info.size)); + if (fp.read(&header.info.width, header_read) != header_read) + { + LOG("Error reading DIB info header from ICO file image data at %u (%u bytes)\n", dir.offset, dir.size); + return false; + } + fp.seek(header.info.size - sizeof(header.info.size) - header_read, SEEK_CUR); + header.info.width = little_endianize_int32(header.info.width); + header.info.height = little_endianize_int32(header.info.height); + header.info.planes = little_endianize_int16(header.info.planes); + header.info.bpp = little_endianize_int16(header.info.bpp); + header.info.comp = little_endianize_int32(header.info.comp); + header.info.rawsize = little_endianize_int32(header.info.rawsize); + header.info.hres = little_endianize_int32(header.info.hres); + header.info.vres = little_endianize_int32(header.info.vres); + header.info.colors = little_endianize_int32(header.info.colors); + header.info.important = little_endianize_int32(header.info.important); + header.info.red = little_endianize_int32(header.info.red); + header.info.green = little_endianize_int32(header.info.green); + header.info.blue = little_endianize_int32(header.info.blue); + header.info.alpha = little_endianize_int32(header.info.alpha); + LOGMASKED( + LOG_DIB, + "Read DIB info header from ICO file image data at %u: %d*%d (%d*%d ppm), %u planes, %u bpp %u/%s%u colors\n", + dir.offset, + header.info.width, + header.info.height, + header.info.hres, + header.info.vres, + header.info.planes, + header.info.bpp, + header.info.important, + header.info.colors ? "" : "2^", + header.info.colors ? header.info.colors : header.info.bpp); + } + break; + } + + // check for unsupported planes/bit depth + if ((1U != header.info.planes) || !header.info.bpp || (32U < header.info.bpp) || ((8U < header.info.bpp) ? (header.info.bpp % 8) : (8 % header.info.bpp))) + { + LOG( + "ICO file DIB image data at %u uses unsupported planes/bits per pixel %u*%u\n", + dir.offset, + header.info.planes, + header.info.bpp); + return false; + } + + // check dimensions + if ((0 >= header.info.width) || (0 == header.info.height)) + { + LOG( + "ICO file DIB image data at %u has invalid dimensions %u*%u\n", + dir.offset, + header.info.width, + header.info.height); + return false; + } + bool const top_down(0 > header.info.height); + if (top_down) + header.info.height = -header.info.height; + bool have_and_mask((2 * dir.get_height()) == header.info.height); + if (!have_and_mask && (dir.get_height() != header.info.height)) + { + osd_printf_verbose( + "ICO file DIB image data at %lu height %ld doesn't match directory height %u with or without AND mask\n", + (unsigned long)dir.offset, + (long)header.info.height, + dir.get_height()); + return false; + } + if (have_and_mask) + header.info.height >>= 1; + + // ensure compression scheme is supported + bool indexed(true), no_palette(false); + switch (header.info.comp) + { + case DIB_COMP_NONE: + // uncompressed - direct colour with implied bitfields if more than eight bits/pixel + indexed = 8U >= header.info.bpp; + if (indexed) + { + if ((1U << header.info.bpp) < header.info.colors) + { + osd_printf_verbose( + "ICO file DIB image data at %lu has oversized palette with %lu entries for %u bits per pixel\n", + (unsigned long)dir.offset, + (unsigned long)header.info.colors, + (unsigned)header.info.bpp); + } + } + if (!indexed) + { + no_palette = true; + switch(header.info.bpp) + { + case 16U: + header.info.red = 0x00007c00; + header.info.green = 0x000003e0; + header.info.blue = 0x0000001f; + header.info.alpha = 0x00000000; + break; + case 24U: + case 32U: + header.info.red = 0x00ff0000; + header.info.green = 0x0000ff00; + header.info.blue = 0x000000ff; + header.info.alpha = 0x00000000; + break; + } + } + break; + + case DIB_COMP_BITFIELDS: + // uncompressed direct colour with explicitly-specified bitfields + indexed = false; + if (offsetof(bitmap_info_header_t, alpha) > header.info.size) + { + osd_printf_verbose( + "ICO file DIB image data at %lu specifies bit masks but is too small (size %lu)\n", + (unsigned long)dir.offset, + (unsigned long)header.info.size); + return false; + } + break; + + default: + LOG("ICO file DIB image data at %u uses unsupported compression scheme %u\n", header.info.comp); + return false; + } + + // we can now calculate the size of the palette and row data + size_t const palette_entries( + indexed + ? ((1U == header.info.bpp) ? 2U : header.info.colors ? header.info.colors : (1U << header.info.bpp)) + : (no_palette ? 0U : header.info.colors)); + size_t const palette_size(palette_bytes * palette_entries); + size_t const row_bytes(((31 + (header.info.width * header.info.bpp)) >> 5) << 2); + size_t const mask_row_bytes(((31 + header.info.width) >> 5) << 2); + size_t const required_size( + header.info.size + + palette_size + + ((row_bytes + (have_and_mask ? mask_row_bytes : 0U)) * header.info.height)); + if (required_size > dir.size) + { + LOG( + "ICO file image data at %u (%u bytes) smaller than calculated DIB data size (%u bytes)\n", + dir.offset, + dir.size, + required_size); + return false; + } + + // load the palette for indexed colour formats or the shifts for direct colour formats + unsigned red_shift(0), green_shift(0), blue_shift(0), alpha_shift(0); + unsigned red_bits(0), green_bits(0), blue_bits(0), alpha_bits(0); + std::unique_ptr palette; + if (indexed) + { + // read palette and convert + std::unique_ptr palette_data(new uint8_t [palette_size]); + if (fp.read(palette_data.get(), palette_size) != palette_size) + { + LOG("Error reading palette from ICO file DIB image data at %u (%u bytes)\n", dir.offset, dir.size); + return false; + } + size_t const palette_usable(std::min(palette_entries, size_t(1) << header.info.bpp)); + palette.reset(new rgb_t [palette_usable]); + uint8_t const *ptr(palette_data.get()); + for (size_t i = 0; palette_usable > i; ++i, ptr += palette_bytes) + palette[i] = rgb_t(ptr[2], ptr[1], ptr[0]); + } + else + { + // skip over the palette if necessary + if (palette_entries) + fp.seek(palette_bytes * palette_entries, SEEK_CUR); + + // convert masks to shifts + bool const masks_contiguous( + dib_parse_mask(header.info.red, red_shift, red_bits) && + dib_parse_mask(header.info.green, green_shift, green_bits) && + dib_parse_mask(header.info.blue, blue_shift, blue_bits) && + dib_parse_mask(header.info.alpha, alpha_shift, alpha_bits)); + if (!masks_contiguous) + { + osd_printf_verbose( + "ICO file DIB image data at %lu specifies non-contiguous channel masks 0x%lx | 0x%lx | 0x%lx | 0x%lx\n", + (unsigned long)dir.offset, + (unsigned long)header.info.red, + (unsigned long)header.info.green, + (unsigned long)header.info.blue, + (unsigned long)header.info.alpha); + } + if ((32U != header.info.bpp) && ((header.info.red | header.info.green | header.info.blue | header.info.alpha) >> header.info.bpp)) + { + LOG( + "ICO file DIB image data at %lu specifies channel masks 0x%x | 0x%x | 0x%x | 0x%x that exceed %u bits per pixel\n", + dir.offset, + header.info.red, + header.info.green, + header.info.blue, + header.info.alpha, + header.info.bpp); + return false; + } + LOGMASKED( + LOG_DIB, + "DIB from ICO file image data at %1$u using channels: R((x >> %3$u) & 0x%4$0*2$x) G((x >> %5$u) & 0x%6$0*2$x) B((x >> %7$u) & 0x%8$0*2$x) A((x >> %9$u) & 0x%10$0*2$x)\n", + dir.offset, + (header.info.bpp + 3) >> 2, + red_shift, + (uint32_t(1) << red_bits) - 1, + green_shift, + (uint32_t(1) << green_bits) - 1, + blue_shift, + (uint32_t(1) << blue_bits) - 1, + alpha_shift, + (uint32_t(1) << alpha_bits) - 1); + + // the MAME bitmap only supports 8 bits/sample maximum + dib_truncate_channel(red_shift, red_bits); + dib_truncate_channel(green_shift, green_bits); + dib_truncate_channel(blue_shift, blue_bits); + dib_truncate_channel(alpha_shift, alpha_bits); + } + + // allocate the bitmap and process row data + std::unique_ptr row_data(new uint8_t [row_bytes]); + bitmap.allocate(header.info.width, header.info.height); + int const y_inc(top_down ? 1 : -1); + for (int32_t i = 0, y = top_down ? 0 : (header.info.height - 1); header.info.height > i; ++i, y += y_inc) + { + if (fp.read(row_data.get(), row_bytes) != row_bytes) + { + LOG("Error reading DIB row %d data from ICO image data at %u\n", i, dir.offset); + return false; + } + uint8_t *src(row_data.get()); + uint32_t *dest(&bitmap.pix(y)); + unsigned shift(0U); + for (int32_t x = 0; header.info.width > x; ++x, ++dest) + { + // extract or compose a pixel + uint32_t pix(0U); + if (8U >= header.info.bpp) + { + assert(8U > shift); + pix = *src >> (8U - header.info.bpp); + *src <<= header.info.bpp; + shift += header.info.bpp; + if (8U <= shift) + { + shift = 0U; + ++src; + } + } + else for (shift = 0; header.info.bpp > shift; shift += 8U, ++src) + { + pix |= uint32_t(*src) << shift; + } + + // convert to RGB + if (indexed) + { + if (palette_entries > pix) + { + *dest = palette[pix]; + } + else + { + *dest = rgb_t::transparent(); + osd_printf_verbose( + "ICO file DIB image data at %lu has out-of-range color %lu at (%ld, %ld) with %lu palette entries\n", + (unsigned long)dir.offset, + (unsigned long)pix, + (long)x, + (long)y, + (unsigned long)palette_entries); + } + } + else + { + uint8_t r(dib_splat_sample((pix >> red_shift) & ((uint32_t(1) << red_bits) - 1), red_bits)); + uint8_t g(dib_splat_sample((pix >> green_shift) & ((uint32_t(1) << green_bits) - 1), green_bits)); + uint8_t b(dib_splat_sample((pix >> blue_shift) & ((uint32_t(1) << blue_bits) - 1), blue_bits)); + uint8_t a(dib_splat_sample((pix >> alpha_shift) & ((uint32_t(1) << alpha_bits) - 1), alpha_bits)); + *dest = rgb_t(alpha_bits ? a : 255, r, g, b); + } + } + } + + // process the AND mask if present + if (have_and_mask) + { + for (int32_t i = 0, y = top_down ? 0 : (header.info.height - 1); header.info.height > i; ++i, y += y_inc) + { + if (fp.read(row_data.get(), mask_row_bytes) != mask_row_bytes) + { + LOG("Error reading DIB mask row %d data from ICO image data at %u\n", i, dir.offset); + return false; + } + uint8_t *src(row_data.get()); + uint32_t *dest(&bitmap.pix(y)); + unsigned shift(0U); + for (int32_t x = 0; header.info.width > x; ++x, ++dest) + { + assert(8U > shift); + rgb_t pix(*dest); + *dest = pix.set_a(BIT(*src, 7U - shift) ? 0U : pix.a()); + if (8U <= ++shift) + { + shift = 0U; + ++src; + } + } + } + } + + // we're done! + return true; +} + + +bool load_ico_image(util::core_file &fp, unsigned index, icon_dir_entry_t const &dir, bitmap_argb32 &bitmap) +{ + // try loading PNG image data (contains PNG file magic if used), and then fall back + if (load_ico_png(fp, dir, bitmap)) + { + LOG("Successfully loaded PNG image from ICO file entry %u\n", index); + return true; + } + else if (load_ico_dib(fp, dir, bitmap)) + { + LOG("Successfully loaded DIB image from ICO file entry %u\n", index); + return true; + } + + // no luck + return false; +} + + +bool load_ico_image(util::core_file &fp, unsigned count, unsigned index, bitmap_argb32 &bitmap) +{ + // read the directory entry + icon_dir_entry_t dir; + fp.seek(sizeof(icon_dir_t) + (sizeof(icon_dir_entry_t) * index), SEEK_SET); + if (fp.read(&dir, sizeof(dir)) != sizeof(dir)) + { + LOG("Failed to read ICO file directory entry %u\n", index); + return false; + } + dir.byteswap(); + if ((sizeof(icon_dir_t) + (sizeof(icon_dir_entry_t) * count)) > dir.offset) + { + LOG( + "ICO file image %u data starting at %u overlaps %u bytes of file header and directory\n", + index, + dir.offset, + sizeof(icon_dir_t) + (sizeof(icon_dir_entry_t) * count)); + return false; + } + else + { + return load_ico_image(fp, index, dir, bitmap); + } +} + +} // anonymous namespace + + +int images_in_ico(util::core_file &fp) +{ + // read and check the icon file header + icon_dir_t header; + fp.seek(0, SEEK_SET); + if (fp.read(&header, sizeof(header)) != sizeof(header)) + { + LOG("Failed to read ICO file header\n"); + return -1; + } + header.reserved = little_endianize_int16(header.reserved); + header.type = little_endianize_int16(header.type); + header.count = little_endianize_int16(header.count); + if (0U != header.reserved) + { + LOG("Invalid ICO file header reserved field %u (expected 0)\n", header.reserved); + return -1; + } + if ((1U != header.type) && (2U != header.type)) + { + LOG("Invalid ICO file header type field %u (expected 1 or 2)\n", header.type); + return -1; + } + return int(unsigned(little_endianize_int16(header.count))); +} + + +void render_load_ico(util::core_file &fp, unsigned index, bitmap_argb32 &bitmap) +{ + // check that these things haven't been padded somehow + static_assert(sizeof(icon_dir_t) == 6U, "compiler has applied padding to icon_dir_t"); + static_assert(sizeof(icon_dir_entry_t) == 16U, "compiler has applied padding to icon_dir_entry_t"); + + // read and check the icon file header, then try to load the specified image + int const count(images_in_ico(fp)); + if (0 > count) + { + // images_in_ico already logged an error + } + else if (index >= count) + { + osd_printf_verbose("Requested image %u from ICO file containing %d images\n", index, count); + } + else if (load_ico_image(fp, count, index, bitmap)) + { + return; + } + bitmap.reset(); +} + + +void render_load_ico_first(util::core_file &fp, bitmap_argb32 &bitmap) +{ + int const count(images_in_ico(fp)); + for (int i = 0; count > i; ++i) + { + if (load_ico_image(fp, count, i, bitmap)) + return; + } + bitmap.reset(); +} + + +void render_load_ico_highest_detail(util::core_file &fp, bitmap_argb32 &bitmap) +{ + // read and check the icon file header - logs a message on error + int const count(images_in_ico(fp)); + if (0 <= count) + { + // now load all the directory entries + size_t const dir_bytes(sizeof(icon_dir_entry_t) * count); + std::unique_ptr dir(new icon_dir_entry_t [count]); + std::unique_ptr index(new unsigned [count]); + if (count && (fp.read(dir.get(), dir_bytes) != dir_bytes)) + { + LOG("Failed to read ICO file directory entries\n"); + } + else + { + // byteswap and sort by (pixels, depth) + for (int i = 0; count > i; ++i) + { + dir[i].byteswap(); + index[i] = i; + } + std::stable_sort( + index.get(), + index.get() + count, + [&dir] (unsigned x, unsigned y) + { + unsigned const x_pixels(dir[x].get_width() * dir[x].get_height()); + unsigned const y_pixels(dir[y].get_width() * dir[y].get_height()); + if (x_pixels > y_pixels) + return true; + else if (x_pixels < y_pixels) + return false; + else + return dir[x].bpp > dir[y].bpp; + }); + + // walk down until something works + for (int i = 0; count > i; ++i) + { + LOG( + "Try loading ICO file entry %u: %u*%u, %u bits per pixel\n", + index[i], + dir[index[i]].get_width(), + dir[index[i]].get_height(), + dir[index[i]].bpp); + if (load_ico_image(fp, index[i], dir[index[i]], bitmap)) + return; + } + } + } + bitmap.reset(); +} + +} // namespace ui diff --git a/src/frontend/mame/ui/icorender.h b/src/frontend/mame/ui/icorender.h index 17bc5a1d8ea..bb3f1a083be 100644 --- a/src/frontend/mame/ui/icorender.h +++ b/src/frontend/mame/ui/icorender.h @@ -1,13 +1,13 @@ // license:BSD-3-Clause -// copyright-holders:Maurizio Petrarota, Victor Laskin +// copyright-holders:Vas Crabb /*************************************************************************** ui/icorender.h - ICOns file loader. + Windows icon file parser. - Original code by Victor Laskin (victor.laskin@gmail.com) - http://vitiy.info/Code/ico.cpp + File handles passed to these functions must support read and seek + operations. ***************************************************************************/ #ifndef MAME_FRONTEND_MAME_UI_ICORENDER_H @@ -15,219 +15,20 @@ #pragma once -// These next two structs represent how the icon information is stored -// in an ICO file. -typedef struct -{ - uint8_t bWidth; // Width of the image - uint8_t bHeight; // Height of the image (times 2) - uint8_t bColorCount; // Number of colors in image (0 if >=8bpp) - uint8_t bReserved; // Reserved - uint16_t wPlanes; // Color Planes - uint16_t wBitCount; // Bits per pixel - uint32_t dwBytesInRes; // how many bytes in this resource? - uint32_t dwImageOffset; // where in the file is this image -} ICONDIRENTRY, *LPICONDIRENTRY; +namespace ui { -typedef struct -{ - uint16_t idReserved; // Reserved - uint16_t idType; // resource type (1 for icons) - uint16_t idCount; // how many images? - //ICONDIRENTRY idEntries[1]; // the entries for each image -} ICONDIR, *LPICONDIR; +// get number of images in icon file (-1 on error) +int images_in_ico(util::core_file &fp); -// size - 40 bytes -typedef struct { - uint32_t biSize; - uint32_t biWidth; - uint32_t biHeight; // Icon Height (added height of XOR-Bitmap and AND-Bitmap) - uint16_t biPlanes; - uint16_t biBitCount; - uint32_t biCompression; - int32_t biSizeImage; - uint32_t biXPelsPerMeter; - uint32_t biYPelsPerMeter; - uint32_t biClrUsed; - uint32_t biClrImportant; -} s_BITMAPINFOHEADER, *s_PBITMAPINFOHEADER; +// load specified icon from file (zero-based) +void render_load_ico(util::core_file &fp, unsigned index, bitmap_argb32 &bitmap); -// 46 bytes -typedef struct{ - s_BITMAPINFOHEADER icHeader; // DIB header - uint32_t icColors[1]; // Color table (short 4 bytes) //RGBQUAD - uint8_t icXOR[1]; // DIB bits for XOR mask - uint8_t icAND[1]; // DIB bits for AND mask -} ICONIMAGE, *LPICONIMAGE; +// load first supported icon from file +void render_load_ico_first(util::core_file &fp, bitmap_argb32 &bitmap); -//------------------------------------------------- -// load an ICO file into a bitmap -//------------------------------------------------- +// load highest detail supported icon from file +void render_load_ico_highest_detail(util::core_file &fp, bitmap_argb32 &bitmap); -inline void render_load_ico(bitmap_argb32 &bitmap, emu_file &file, const char *dirname, const char *filename) -{ - int32_t width = 0; - int32_t height = 0; - - // deallocate previous bitmap - bitmap.reset(); - - // define file's full name - std::string fname; - - if (!dirname) - fname = filename; - else - fname.assign(dirname).append(PATH_SEPARATOR).append(filename); - - osd_file::error filerr = file.open(fname.c_str()); - - if (filerr != osd_file::error::NONE) - return; - - // allocates a buffer for the image - uint64_t size = file.size(); - uint8_t *buffer = global_alloc_array(uint8_t, size + 1); - - // read data from the file and set them in the buffer - file.read(buffer, size); - - LPICONDIR icoDir = (LPICONDIR)buffer; - int iconsCount = icoDir->idCount; - - if (icoDir->idReserved != 0 || icoDir->idType != 1 || iconsCount == 0 || iconsCount > 20) - { - file.close(); - global_free_array(buffer); - return; - } - - uint8_t* cursor = buffer; - cursor += 6; - ICONDIRENTRY* dirEntry = (ICONDIRENTRY*)(cursor); - int maxSize = 0; - int offset = 0; - int maxBitCount = 0; - for (int i = 0; i < iconsCount; i++, ++dirEntry) - { - int w = dirEntry->bWidth; - int h = dirEntry->bHeight; - int bitCount = dirEntry->wBitCount; - if (w * h > maxSize || bitCount > maxBitCount) // we choose icon with max resolution - { - width = w; - height = h; - offset = dirEntry->dwImageOffset; - maxSize = w * h; - } - } - - if (offset == 0) return; - - cursor = buffer; - cursor += offset; - ICONIMAGE* icon = (ICONIMAGE*)(cursor); - int realBitsCount = (int)icon->icHeader.biBitCount; - bool hasAndMask = (realBitsCount < 32) && (height != icon->icHeader.biHeight); - - cursor += 40; - bitmap.allocate(width, height); - - // rgba + vertical swap - if (realBitsCount >= 32) - { - for (int x = 0; x < width; ++x) - for (int y = 0; y < height; ++y) - { - int shift2 = 4 * (x + (height - y - 1) * width); - bitmap.pix32(y, x) = rgb_t(cursor[shift2 + 3], cursor[shift2 + 2], cursor[shift2 + 1], cursor[shift2]); - } - } - else if (realBitsCount == 24) - for (int x = 0; x < width; ++x) - for (int y = 0; y < height; ++y) - { - int shift2 = 3 * (x + (height - y - 1) * width); - bitmap.pix32(y, x) = rgb_t(255, cursor[shift2 + 2], cursor[shift2 + 1], cursor[shift2]); - } - else if (realBitsCount == 8) // 256 colors - { - // 256 color table - uint8_t *colors = cursor; - cursor += 256 * 4; - for (int x = 0; x < width; ++x) - for (int y = 0; y < height; ++y) - { - int shift2 = (x + (height - y - 1) * width); - int index = 4 * cursor[shift2]; - bitmap.pix32(y, x) = rgb_t(255, colors[index + 2], colors[index + 1], colors[index]); - } - } - else if (realBitsCount == 4) // 16 colors - { - // 16 color table - uint8_t *colors = cursor; - cursor += 16 * 4; - for (int x = 0; x < width; ++x) - for (int y = 0; y < height; ++y) - { - int shift2 = (x + (height - y - 1) * width); - uint8_t index = cursor[shift2 / 2]; - if (shift2 % 2 == 0) - index = (index >> 4) & 0xF; - else - index = index & 0xF; - index *= 4; - bitmap.pix32(y, x) = rgb_t(255, colors[index + 2], colors[index + 1], colors[index]); - } - } - else if (realBitsCount == 1) // 2 colors - { - // 2 color table - uint8_t *colors = cursor; - cursor += 2 * 4; - int boundary = width; // !!! 32 bit boundary (http://www.daubnet.com/en/file-format-ico) - while (boundary % 32 != 0) boundary++; - - for (int x = 0; x < width; ++x) - for (int y = 0; y < height; ++y) - { - int shift2 = (x + (height - y - 1) * boundary); - uint8_t index = cursor[shift2 / 8]; - - // select 1 bit only - uint8_t bit = 7 - (x % 8); - index = (index >> bit) & 0x01; - index *= 4; - bitmap.pix32(y, x) = rgb_t(255, colors[index + 2], colors[index + 1], colors[index]); - } - } - - // Read AND mask after base color data - 1 BIT MASK - if (hasAndMask) - { - int boundary = width * realBitsCount; // !!! 32 bit boundary (http://www.daubnet.com/en/file-format-ico) - while (boundary % 32 != 0) boundary++; - cursor += boundary * height / 8; - - boundary = width; - while (boundary % 32 != 0) boundary++; - - for (int y = 0; y < height; ++y) - for (int x = 0; x < width; ++x) - { - uint8_t bit = 7 - (x % 8); - int shift2 = (x + (height - y - 1) * boundary) / 8; - int mask = (0x01 & ((uint8_t)cursor[shift2] >> bit)); - rgb_t colors = bitmap.pix32(y, x); - uint8_t alpha = colors.a(); - alpha *= 1 - mask; - colors.set_a(alpha); - bitmap.pix32(y, x) = colors; - } - } - file.close(); - global_free_array(buffer); -} +} // namespace ui #endif // MAME_FRONTEND_MAME_UI_ICORENDER_H diff --git a/src/frontend/mame/ui/menu.cpp b/src/frontend/mame/ui/menu.cpp index 9a8fb8255e3..4bc6a13d81e 100644 --- a/src/frontend/mame/ui/menu.cpp +++ b/src/frontend/mame/ui/menu.cpp @@ -86,14 +86,14 @@ menu::global_state::global_state(running_machine &machine, ui_options const &opt , m_machine(machine) , m_cleanup_callbacks() , m_bgrnd_bitmap() + , m_bgrnd_texture(nullptr, machine.render()) , m_stack() , m_free() { render_manager &render(machine.render()); - auto const texture_free([&render](render_texture *texture) { render.texture_free(texture); }); // create a texture for main menu background - m_bgrnd_texture = texture_ptr(render.texture_alloc(render_texture::hq_scale), texture_free); + m_bgrnd_texture.reset(render.texture_alloc(render_texture::hq_scale)); if (options.use_background_image() && (&machine.system() == &GAME_NAME(___empty))) { m_bgrnd_bitmap = std::make_unique(0, 0); diff --git a/src/frontend/mame/ui/menu.h b/src/frontend/mame/ui/menu.h index f0c9600d509..ffcb0ed71e8 100644 --- a/src/frontend/mame/ui/menu.h +++ b/src/frontend/mame/ui/menu.h @@ -27,6 +27,7 @@ namespace ui { + /*************************************************************************** TYPE DEFINITIONS ***************************************************************************/ @@ -35,18 +36,17 @@ class menu { public: // flags for menu items - enum + enum : unsigned { - FLAG_LEFT_ARROW = (1 << 0), - FLAG_RIGHT_ARROW = (1 << 1), - FLAG_INVERT = (1 << 2), - FLAG_MULTILINE = (1 << 3), - FLAG_REDTEXT = (1 << 4), - FLAG_DISABLE = (1 << 5), - FLAG_UI_DATS = (1 << 6), - FLAG_UI_FAVORITE = (1 << 7), - FLAG_UI_HEADING = (1 << 8), - FLAG_COLOR_BOX = (1 << 9) + FLAG_LEFT_ARROW = 1U << 0, + FLAG_RIGHT_ARROW = 1U << 1, + FLAG_INVERT = 1U << 2, + FLAG_MULTILINE = 1U << 3, + FLAG_REDTEXT = 1U << 4, + FLAG_DISABLE = 1U << 5, + FLAG_UI_DATS = 1U << 6, + FLAG_UI_HEADING = 1U << 7, + FLAG_COLOR_BOX = 1U << 8 }; virtual ~menu(); diff --git a/src/frontend/mame/ui/selgame.cpp b/src/frontend/mame/ui/selgame.cpp index c36392e8b10..aaf73703d62 100644 --- a/src/frontend/mame/ui/selgame.cpp +++ b/src/frontend/mame/ui/selgame.cpp @@ -1,5 +1,5 @@ // license:BSD-3-Clause -// copyright-holders:Maurizio Petrarota +// copyright-holders:Maurizio Petrarota, Vas Crabb /********************************************************************* ui/selgame.cpp @@ -12,6 +12,7 @@ #include "ui/selgame.h" #include "ui/auditmenu.h" +#include "ui/icorender.h" #include "ui/inifile.h" #include "ui/miscmenu.h" #include "ui/optsmenu.h" @@ -39,8 +40,6 @@ #include #include -#include "ui/icorender.h" // this is inline code - extern const char UI_VERSION_TAG[]; @@ -220,10 +219,11 @@ menu_select_game::menu_select_game(mame_ui_manager &mui, render_container &conta : menu_select_launch(mui, container, false) , m_persistent_data(persistent_data::instance()) , m_icons(MAX_ICONS_RENDER) - , m_has_icons(false) + , m_icon_paths() , m_displaylist() , m_searchlist() , m_searched_fields(persistent_data::AVAIL_NONE) + , m_populated_favorites(false) { std::string error_string, last_filter, sub_filter; ui_options &moptions = mui.options(); @@ -231,18 +231,8 @@ menu_select_game::menu_select_game(mame_ui_manager &mui, render_container &conta // load drivers cache m_persistent_data.cache_data(); - // check if there are available icons - file_enumerator path(moptions.icons_directory()); - const osd::directory::entry *dir; - while ((dir = path.next()) != nullptr) - { - std::string src(dir->name); - if (src.find(".ico") != std::string::npos || src.find("icons") != std::string::npos) - { - m_has_icons = true; - break; - } - } + // check if there are available system icons + check_for_icons(nullptr); // build drivers list if (!load_available_machines()) @@ -334,13 +324,13 @@ void menu_select_game::handle() return; } - // if i have to reselect a software, force software list submenu + // if I have to select software, force software list submenu if (reselect_last::get()) { const game_driver *driver; const ui_software_info *software; get_selection(software, driver); - menu::stack_push(ui(), container(), driver); + menu::stack_push(ui(), container(), *driver); return; } @@ -397,7 +387,7 @@ void menu_select_game::handle() case IPT_UI_SELECT: if (get_focus() == focused_menu::MAIN) { - if (isfavorite()) + if (m_populated_favorites) inkey_select_favorite(menu_event); else inkey_select(menu_event); @@ -406,7 +396,7 @@ void menu_select_game::handle() case IPT_CUSTOM: // handle IPT_CUSTOM (mouse right click) - if (!isfavorite()) + if (!m_populated_favorites) { menu::stack_push( ui(), container(), @@ -459,7 +449,7 @@ void menu_select_game::handle() if (uintptr_t(menu_event->itemref) > skip_main_items) { favorite_manager &mfav(mame_machine_manager::instance()->favorite()); - if (!isfavorite()) + if (!m_populated_favorites) { game_driver const *const driver(reinterpret_cast(menu_event->itemref)); if (!mfav.is_favorite_system(*driver)) @@ -506,16 +496,19 @@ void menu_select_game::handle() void menu_select_game::populate(float &customtop, float &custombottom) { - set_redraw_icon(); + for (auto &icon : m_icons) // TODO: why is this here? maybe better on resize or setting change? + icon.second.texture.reset(); + set_switch_image(); int old_item_selected = -1; constexpr uint32_t flags_ui = FLAG_LEFT_ARROW | FLAG_RIGHT_ARROW; if (!isfavorite()) { - // if search is not empty, find approximate matches + m_populated_favorites = false; if (!m_search.empty()) { + // if search is not empty, find approximate matches populate_search(); } else @@ -555,11 +548,11 @@ void menu_select_game::populate(float &customtop, float &custombottom) else { // populate favorites list + m_populated_favorites = true; m_search.clear(); mame_machine_manager::instance()->favorite().apply_sorted( [this, &old_item_selected, curitem = 0] (ui_software_info const &info) mutable { - auto flags = flags_ui | FLAG_UI_FAVORITE; if (info.startempty == 1) { if (old_item_selected == -1 && info.shortname == reselect_last::driver()) @@ -573,14 +566,14 @@ void menu_select_game::populate(float &customtop, float &custombottom) cloneof = false; } - item_append(info.longname, "", (cloneof) ? (flags | FLAG_INVERT) : flags, (void *)&info); + item_append(info.longname, "", cloneof ? (flags_ui | FLAG_INVERT) : flags_ui, (void *)&info); } else { if (old_item_selected == -1 && info.shortname == reselect_last::driver()) old_item_selected = curitem; item_append(info.longname, info.devicetype, - info.parentname.empty() ? flags : (FLAG_INVERT | flags), (void *)&info); + info.parentname.empty() ? flags_ui : (FLAG_INVERT | flags_ui), (void *)&info); } curitem++; }); @@ -792,7 +785,7 @@ void menu_select_game::inkey_select(const event *menu_event) { if (!swlistdev.get_info().empty()) { - menu::stack_push(ui(), container(), driver); + menu::stack_push(ui(), container(), *driver); return; } } @@ -862,7 +855,7 @@ void menu_select_game::inkey_select_favorite(const event *menu_event) { if (!swlistdev.get_info().empty()) { - menu::stack_push(ui(), container(), ui_swinfo->driver); + menu::stack_push(ui(), container(), *ui_swinfo->driver); return; } } @@ -935,7 +928,7 @@ void menu_select_game::change_info_pane(int delta) game_driver const *drv; ui_software_info const *soft; get_selection(soft, drv); - if (!isfavorite()) + if (!m_populated_favorites) { if (uintptr_t(drv) > skip_main_items) cap_delta(ui_globals::curdats_view, ui_globals::curdats_total); @@ -1009,7 +1002,7 @@ void menu_select_game::populate_search() m_searchlist.end(), [] (auto const &lhs, auto const &rhs) { return lhs.first < rhs.first; }); uint32_t flags_ui = FLAG_LEFT_ARROW | FLAG_RIGHT_ARROW; - for (int curitem = 0; (std::min)(m_searchlist.size(), std::size_t(200)) > curitem; ++curitem) + for (int curitem = 0; (std::min)(m_searchlist.size(), MAX_VISIBLE_SEARCH) > curitem; ++curitem) { game_driver const &drv(*m_searchlist[curitem].second.get().driver); bool cloneof = strcmp(drv.parent, "0") != 0; @@ -1176,39 +1169,31 @@ void menu_select_game::general_info(const game_driver *driver, std::string &buff //------------------------------------------------- -// draw icons +// get (possibly cached) icon texture //------------------------------------------------- -float menu_select_game::draw_icon(int linenum, void *selectedref, float x0, float y0) +render_texture *menu_select_game::get_icon_texture(int linenum, void *selectedref) { - if (!m_has_icons) - return 0.0f; - - float ud_arrow_width = ui().get_line_height() * container().manager().ui_aspect(&container()); - const game_driver *driver = nullptr; - - if (item[0].flags & FLAG_UI_FAVORITE) - { - ui_software_info *soft = (ui_software_info *)selectedref; - if (soft->startempty == 1) - driver = soft->driver; - } - else - driver = (const game_driver *)selectedref; - - auto x1 = x0 + ud_arrow_width; - auto y1 = y0 + ui().get_line_height(); + game_driver const *const driver(m_populated_favorites + ? reinterpret_cast(selectedref)->driver + : reinterpret_cast(selectedref)); + assert(driver); icon_cache::iterator icon(m_icons.find(driver)); - if ((m_icons.end() == icon) || redraw_icon()) + if ((m_icons.end() == icon) || !icon->second.texture) { + if (m_icon_paths.empty()) + m_icon_paths = make_icon_paths(nullptr); + + // allocate an entry or allocate a texture on forced redraw if (m_icons.end() == icon) { - texture_ptr texture(machine().render().texture_alloc(), [&render = machine().render()] (render_texture *texture) { render.texture_free(texture); }); - icon = m_icons.emplace( - std::piecewise_construct, - std::forward_as_tuple(driver), - std::forward_as_tuple(std::piecewise_construct, std::forward_as_tuple(std::move(texture)), std::tuple<>())).first; + icon = m_icons.emplace(driver, texture_ptr(machine().render().texture_alloc(), machine().render())).first; + } + else + { + assert(!icon->second.texture); + icon->second.texture.reset(machine().render().texture_alloc()); } // set clone status @@ -1220,83 +1205,23 @@ float menu_select_game::draw_icon(int linenum, void *selectedref, float x0, floa cloneof = false; } - // get search path - path_iterator path(ui().options().icons_directory()); - std::string curpath; - std::string searchstr(ui().options().icons_directory()); - - // iterate over path and add path for zipped formats - while (path.next(curpath)) - searchstr.append(";").append(curpath.c_str()).append(PATH_SEPARATOR).append("icons"); - bitmap_argb32 tmp; - emu_file snapfile(searchstr.c_str(), OPEN_FLAG_READ); - std::string fullname = std::string(driver->name).append(".ico"); - render_load_ico(tmp, snapfile, nullptr, fullname.c_str()); - - if (!tmp.valid() && cloneof) + emu_file snapfile(std::string(m_icon_paths), OPEN_FLAG_READ); + if (snapfile.open(std::string(driver->name).append(".ico")) == osd_file::error::NONE) { - fullname.assign(driver->parent).append(".ico"); - render_load_ico(tmp, snapfile, nullptr, fullname.c_str()); - } - - bitmap_argb32 &bitmap(icon->second.second); - if (tmp.valid()) - { - float panel_width = x1 - x0; - float panel_height = y1 - y0; - auto screen_width = machine().render().ui_target().width(); - auto screen_height = machine().render().ui_target().height(); - - if (machine().render().ui_target().orientation() & ORIENTATION_SWAP_XY) - std::swap(screen_height, screen_width); - - int panel_width_pixel = panel_width * screen_width; - int panel_height_pixel = panel_height * screen_height; - - // Calculate resize ratios for resizing - auto ratioW = (float)panel_width_pixel / tmp.width(); - auto ratioH = (float)panel_height_pixel / tmp.height(); - auto dest_xPixel = tmp.width(); - auto dest_yPixel = tmp.height(); - - if (ratioW < 1 || ratioH < 1) - { - // smaller ratio will ensure that the image fits in the view - float ratio = std::min(ratioW, ratioH); - dest_xPixel = tmp.width() * ratio; - dest_yPixel = tmp.height() * ratio; - } - - bitmap_argb32 dest_bitmap; - - // resample if necessary - if (dest_xPixel != tmp.width() || dest_yPixel != tmp.height()) - { - dest_bitmap.allocate(dest_xPixel, dest_yPixel); - render_color color = { 1.0f, 1.0f, 1.0f, 1.0f }; - render_resample_argb_bitmap_hq(dest_bitmap, tmp, color, true); - } - else - dest_bitmap = std::move(tmp); - - bitmap.allocate(panel_width_pixel, panel_height_pixel); - for (int x = 0; x < dest_xPixel; x++) - for (int y = 0; y < dest_yPixel; y++) - bitmap.pix32(y, x) = dest_bitmap.pix32(y, x); - - icon->second.first->set_bitmap(bitmap, bitmap.cliprect(), TEXFORMAT_ARGB32); + render_load_ico_highest_detail(snapfile, tmp); + snapfile.close(); } - else + if (!tmp.valid() && cloneof && (snapfile.open(std::string(driver->parent).append(".ico")) == osd_file::error::NONE)) { - bitmap.reset(); + render_load_ico_highest_detail(snapfile, tmp); + snapfile.close(); } - } - if (icon->second.second.valid()) - container().add_quad(x0, y0, x1, y1, rgb_t::white(), icon->second.first.get(), PRIMFLAG_BLENDMODE(BLENDMODE_ALPHA)); + scale_icon(std::move(tmp), icon->second); + } - return ud_arrow_width * 1.5f; + return icon->second.bitmap.valid() ? icon->second.texture.get() : nullptr; } @@ -1305,12 +1230,12 @@ void menu_select_game::inkey_export() std::vector list; if (!m_search.empty()) { - for (int curitem = 0; (std::min)(m_searchlist.size(), std::size_t(200)); ++curitem) + for (int curitem = 0; (std::min)(m_searchlist.size(), MAX_VISIBLE_SEARCH); ++curitem) list.push_back(m_searchlist[curitem].second.get().driver); } else { - if (isfavorite()) + if (m_populated_favorites) { // iterate over favorites mame_machine_manager::instance()->favorite().apply( @@ -1421,7 +1346,7 @@ float menu_select_game::draw_left_panel(float x1, float y1, float x2, float y2) void menu_select_game::get_selection(ui_software_info const *&software, game_driver const *&driver) const { - if (item[0].flags & FLAG_UI_FAVORITE) // TODO: work out why this doesn't use isfavorite() + if (m_populated_favorites) { software = reinterpret_cast(get_selection_ptr()); driver = software ? software->driver : nullptr; @@ -1442,7 +1367,7 @@ void menu_select_game::make_topbox_text(std::string &line0, std::string &line1, (driver_list::total() - 1), m_persistent_data.bios_count()); - if (isfavorite()) + if (m_populated_favorites) { line1.clear(); } diff --git a/src/frontend/mame/ui/selgame.h b/src/frontend/mame/ui/selgame.h index 7d5d906a1c1..da65fee98ce 100644 --- a/src/frontend/mame/ui/selgame.h +++ b/src/frontend/mame/ui/selgame.h @@ -1,5 +1,5 @@ // license:BSD-3-Clause -// copyright-holders:Maurizio Petrarota +// copyright-holders:Maurizio Petrarota, Vas Crabb /*************************************************************************** ui/selgame.h @@ -39,25 +39,27 @@ private: CONF_PLUGINS, }; - using icon_cache = util::lru_cache_map >; + using icon_cache = texture_lru; class persistent_data; persistent_data &m_persistent_data; icon_cache m_icons; - bool m_has_icons; + std::string m_icon_paths; std::vector > m_displaylist; std::vector > > m_searchlist; unsigned m_searched_fields; + bool m_populated_favorites; static bool s_first_start; virtual void populate(float &customtop, float &custombottom) override; virtual void handle() override; - // draw left panel + // drawing virtual float draw_left_panel(float x1, float y1, float x2, float y2) override; + virtual render_texture *get_icon_texture(int linenum, void *selectedref) override; // get selected software and/or driver virtual void get_selection(ui_software_info const *&software, game_driver const *&driver) const override; @@ -89,9 +91,6 @@ private: // General info virtual void general_info(const game_driver *driver, std::string &buffer) override; - // drawing - virtual float draw_icon(int linenum, void *selectedref, float x1, float y1) override; - // handlers void inkey_select(const event *menu_event); void inkey_select_favorite(const event *menu_event); diff --git a/src/frontend/mame/ui/selmenu.cpp b/src/frontend/mame/ui/selmenu.cpp index 2702ae9260d..82c5cfee9ff 100644 --- a/src/frontend/mame/ui/selmenu.cpp +++ b/src/frontend/mame/ui/selmenu.cpp @@ -1,5 +1,5 @@ // license:BSD-3-Clause -// copyright-holders:Nicola Salmoria, Aaron Giles, Nathan Woods +// copyright-holders:Nicola Salmoria, Aaron Giles, Nathan Woods, Vas Crabb /*************************************************************************** ui/selmenu.cpp @@ -180,7 +180,7 @@ void menu_select_launch::reselect_last::set_software(game_driver const &driver, s_driver = driver.name; if (swinfo.startempty) { - // magic strings are bad... + // FIXME: magic strings are bad... s_software = "[Start empty]"; s_swlist.clear(); } @@ -373,27 +373,26 @@ void menu_select_launch::bios_selection::custom_render(void *selectedref, float menu_select_launch::cache::cache(running_machine &machine) : m_snapx_bitmap(std::make_unique(0, 0)) - , m_snapx_texture() + , m_snapx_texture(nullptr, machine.render()) , m_snapx_driver(nullptr) , m_snapx_software(nullptr) , m_no_avail_bitmap(256, 256) , m_star_bitmap(32, 32) - , m_star_texture() + , m_star_texture(nullptr, machine.render()) , m_toolbar_bitmap() , m_sw_toolbar_bitmap() , m_toolbar_texture() , m_sw_toolbar_texture() { render_manager &render(machine.render()); - auto const texture_free([&render](render_texture *texture) { render.texture_free(texture); }); // create a texture for snapshot - m_snapx_texture = texture_ptr(render.texture_alloc(render_texture::hq_scale), texture_free); + m_snapx_texture.reset(render.texture_alloc(render_texture::hq_scale)); std::memcpy(&m_no_avail_bitmap.pix32(0), no_avail_bmp, 256 * 256 * sizeof(uint32_t)); std::memcpy(&m_star_bitmap.pix32(0), favorite_star_bmp, 32 * 32 * sizeof(uint32_t)); - m_star_texture = texture_ptr(render.texture_alloc(), texture_free); + m_star_texture.reset(render.texture_alloc()); m_star_texture->set_bitmap(m_star_bitmap, m_star_bitmap.cliprect(), TEXFORMAT_ARGB32); m_toolbar_bitmap.reserve(UI_TOOLBAR_BUTTONS); @@ -405,8 +404,8 @@ menu_select_launch::cache::cache(running_machine &machine) { m_toolbar_bitmap.emplace_back(32, 32); m_sw_toolbar_bitmap.emplace_back(32, 32); - m_toolbar_texture.emplace_back(texture_ptr(render.texture_alloc(), texture_free)); - m_sw_toolbar_texture.emplace_back(texture_ptr(render.texture_alloc(), texture_free)); + m_toolbar_texture.emplace_back(render.texture_alloc(), render); + m_sw_toolbar_texture.emplace_back(render.texture_alloc(), render); std::memcpy(&m_toolbar_bitmap.back().pix32(0), toolbar_bitmap_bmp[i], 32 * 32 * sizeof(uint32_t)); if (m_toolbar_bitmap.back().valid()) @@ -458,7 +457,7 @@ menu_select_launch::menu_select_launch(mame_ui_manager &mui, render_container &c , m_pressed(false) , m_repeat(0) , m_right_visible_lines(0) - , m_redraw_icon(false) + , m_has_icons(false) , m_switch_image(false) , m_default_image(true) , m_image_view(FIRST_VIEW) @@ -990,6 +989,150 @@ float menu_select_launch::draw_left_panel( } +//------------------------------------------------- +// icon helpers +//------------------------------------------------- + +void menu_select_launch::check_for_icons(char const *listname) +{ + // only ever set the flag, never clear it + if (m_has_icons) + return; + + // iterate over configured icon paths + path_iterator paths(ui().options().icons_directory()); + std::string current; + while (paths.next(current)) + { + // if we're doing a software list, append it to the configured path + if (listname) + { + if (!current.empty() && !util::is_directory_separator(current.back())) + current.append(PATH_SEPARATOR); + current.append(listname); + } + osd_printf_verbose("Checking for icons in directory %s\n", current.c_str()); + + // open and walk the directory + osd::directory::ptr const dir(osd::directory::open(current)); + if (dir) + { + // this could be improved in many ways - it's just a rough go/no-go + osd::directory::entry const *entry; + while ((entry = dir->read()) != nullptr) + { + current = entry->name; + std::string::size_type const found(current.rfind(".ico")); + if ((std::string::npos != found) && ((current.length() - 4) == found)) + { + osd_printf_verbose("Entry %s is a candidate icon file\n", entry->name); + m_has_icons = true; + return; + } + else if (("icons" == current) || (current.find("icons.") == 0U)) + { + osd_printf_verbose("Entry %s is a candidate icon collection\n", entry->name); + m_has_icons = true; + return; + } + } + } + } + + // nothing promising + osd_printf_verbose( + "No candidate icons found for %s%s\n", + listname ? "software list " : "", + listname ? listname : "machines"); +} + +std::string menu_select_launch::make_icon_paths(char const *listname) const +{ + // iterate over configured icon paths + path_iterator paths(ui().options().icons_directory()); + std::string current, result; + while (paths.next(current)) + { + // if we're doing a software list, append it to the configured path + if (listname) + { + if (!current.empty() && !util::is_directory_separator(current.back())) + current.append(PATH_SEPARATOR); + current.append(listname); + } + + // append the configured path + if (!result.empty()) + result.append(1, ';'); // FIXME: should be a macro + result.append(current); + + // append with "icons" appended so it'll search icons.zip or icons.7z in the directory + if (!current.empty()) + { + result.append(1, ';'); // FIXME: should be a macro + result.append(current); + if (!util::is_directory_separator(result.back())) + result.append(PATH_SEPARATOR); + } + result.append("icons"); + } + + // log the result for debugging + osd_printf_verbose( + "Icon path for %s%s set to %s\n", + listname ? "software list " : "", + listname ? listname : "machines", + result.c_str()); + return result; +} + +bool menu_select_launch::scale_icon(bitmap_argb32 &&src, texture_and_bitmap &dst) const +{ + assert(dst.texture); + if (src.valid()) + { + // calculate available space for the icon in pixels + float const height(ui().get_line_height()); + float const width(height * container().manager().ui_aspect()); + render_target const &target(machine().render().ui_target()); + uint32_t const dst_height(target.height()); + uint32_t const dst_width(target.width()); + bool const rotated((target.orientation() & ORIENTATION_SWAP_XY) != 0); + int const max_height(int((rotated ? dst_width : dst_height) * height)); + int const max_width(int((rotated ? dst_height : dst_width) * width)); + + // reduce the source bitmap if it's too big + bitmap_argb32 tmp; + float const ratio((std::min)({ float(max_height) / src.height(), float(max_width) / src.width(), 1.0F })); + if (1.0F > ratio) + { + float const pix_height(src.height() * ratio); + float const pix_width(src.width() * ratio); + tmp.allocate(int32_t(pix_width), int32_t(pix_height)); + render_resample_argb_bitmap_hq(tmp, src, render_color{ 1.0F, 1.0F, 1.0F, 1.0F }, true); + } + else + { + tmp = std::move(src); + } + + // copy into the destination + dst.bitmap.allocate(max_width, max_height); + for (int y = 0; tmp.height() > y; ++y) + for (int x = 0; tmp.width() > x; ++x) + dst.bitmap.pix32(y, x) = tmp.pix32(y, x); + dst.texture->set_bitmap(dst.bitmap, dst.bitmap.cliprect(), TEXFORMAT_ARGB32); + return true; + } + else + { + // couldn't load icon + dst.bitmap.reset(); + return false; + } +} + + template bool menu_select_launch::select_bios(T const &driver, bool inlist) { s_bios biosname; @@ -1095,10 +1238,16 @@ void menu_select_launch::set_pressed() // draw icons //------------------------------------------------- -float menu_select_launch::draw_icon(int linenum, void *selectedref, float x0, float y0) +void menu_select_launch::draw_icon(int linenum, void *selectedref, float x0, float y0) { - // derived class must implement this - return 0.0f; + render_texture *const icon(get_icon_texture(linenum, selectedref)); + if (icon) + { + float const ud_arrow_width = ui().get_line_height() * container().manager().ui_aspect(&container()); + float const x1 = x0 + ud_arrow_width; + float const y1 = y0 + ui().get_line_height(); + container().add_quad(x0, y0, x1, y1, rgb_t::white(), icon, PRIMFLAG_BLENDMODE(BLENDMODE_ALPHA)); + }; } @@ -1612,8 +1761,9 @@ void menu_select_launch::draw(uint32_t flags) { bool noinput = (flags & PROCESS_NOINPUT); float line_height = ui().get_line_height(); - float ud_arrow_width = line_height * machine().render().ui_aspect(); - float gutter_width = 0.52f * ud_arrow_width; + float const ud_arrow_width = line_height * machine().render().ui_aspect(); + float const gutter_width = 0.52f * ud_arrow_width; + float const icon_offset = m_has_icons ? (1.5f * ud_arrow_width) : 0.0f; float right_panel_size = (ui_globals::panels_status == HIDE_BOTH || ui_globals::panels_status == HIDE_RIGHT_PANEL) ? 2.0f * UI_BOX_LR_BORDER : 0.3f; float visible_width = 1.0f - 4.0f * UI_BOX_LR_BORDER; float primary_left = (1.0f - visible_width) * 0.5f; @@ -1698,18 +1848,21 @@ void menu_select_launch::draw(uint32_t flags) if (mouse_in_rect(line_x0, line_y0, line_x1, line_y1) && is_selectable(pitem)) hover = itemnum; - // if we're selected, draw with a different background if (is_selected(itemnum) && m_focus == focused_menu::MAIN) { + // if we're selected, draw with a different background fgcolor = rgb_t(0xff, 0xff, 0x00); bgcolor = rgb_t(0xff, 0xff, 0xff); fgcolor3 = rgb_t(0xcc, 0xcc, 0x00); - ui().draw_textured_box(container(), line_x0 + 0.01f, line_y0, line_x1 - 0.01f, line_y1, bgcolor, rgb_t(43, 43, 43), + ui().draw_textured_box( + container(), + line_x0 + 0.01f, line_y0, line_x1 - 0.01f, line_y1, + bgcolor, rgb_t(43, 43, 43), hilight_main_texture(), PRIMFLAG_BLENDMODE(BLENDMODE_ALPHA) | PRIMFLAG_TEXWRAP(1)); } - // else if the mouse is over this item, draw with a different background else if (itemnum == hover) { + // else if the mouse is over this item, draw with a different background fgcolor = fgcolor3 = UI_MOUSEOVER_COLOR; bgcolor = UI_MOUSEOVER_BG_COLOR; highlight(line_x0, line_y0, line_x1, line_y1, bgcolor); @@ -1749,29 +1902,52 @@ void menu_select_launch::draw(uint32_t flags) else if (pitem.subtext.empty()) { // draw the item centered - int item_invert = pitem.flags & FLAG_INVERT; - auto icon = draw_icon(linenum, item[itemnum].ref, effective_left, line_y); - ui().draw_text_full(container(), itemtext, effective_left + icon, line_y, effective_width - icon, ui::text_layout::LEFT, ui::text_layout::TRUNCATE, - mame_ui_manager::NORMAL, item_invert ? fgcolor3 : fgcolor, bgcolor, nullptr, nullptr); + int const item_invert = pitem.flags & FLAG_INVERT; + if (m_has_icons) + draw_icon(linenum, item[itemnum].ref, effective_left, line_y); + ui().draw_text_full( + container(), + itemtext, + effective_left + icon_offset, line_y, effective_width - icon_offset, + ui::text_layout::LEFT, ui::text_layout::TRUNCATE, + mame_ui_manager::NORMAL, item_invert ? fgcolor3 : fgcolor, bgcolor, + nullptr, nullptr); } else { - auto item_invert = pitem.flags & FLAG_INVERT; + int const item_invert = pitem.flags & FLAG_INVERT; const char *subitem_text = pitem.subtext.c_str(); float item_width, subitem_width; // compute right space for subitem - ui().draw_text_full(container(), subitem_text, effective_left, line_y, ui().get_string_width(pitem.subtext.c_str()), - ui::text_layout::RIGHT, ui::text_layout::NEVER, mame_ui_manager::NONE, item_invert ? fgcolor3 : fgcolor, bgcolor, &subitem_width, nullptr); + ui().draw_text_full( + container(), + subitem_text, + effective_left + icon_offset, line_y, ui().get_string_width(pitem.subtext.c_str()), + ui::text_layout::RIGHT, ui::text_layout::NEVER, + mame_ui_manager::NONE, item_invert ? fgcolor3 : fgcolor, bgcolor, + &subitem_width, nullptr); subitem_width += gutter_width; // draw the item left-justified - ui().draw_text_full(container(), itemtext, effective_left, line_y, effective_width - subitem_width, - ui::text_layout::LEFT, ui::text_layout::TRUNCATE, mame_ui_manager::NORMAL, item_invert ? fgcolor3 : fgcolor, bgcolor, &item_width, nullptr); + if (m_has_icons) + draw_icon(linenum, item[itemnum].ref, effective_left, line_y); + ui().draw_text_full( + container(), + itemtext, + effective_left + icon_offset, line_y, effective_width - icon_offset - subitem_width, + ui::text_layout::LEFT, ui::text_layout::TRUNCATE, + mame_ui_manager::NORMAL, item_invert ? fgcolor3 : fgcolor, bgcolor, + &item_width, nullptr); // draw the subitem right-justified - ui().draw_text_full(container(), subitem_text, effective_left + item_width, line_y, effective_width - item_width, - ui::text_layout::RIGHT, ui::text_layout::NEVER, mame_ui_manager::NORMAL, item_invert ? fgcolor3 : fgcolor, bgcolor, nullptr, nullptr); + ui().draw_text_full( + container(), + subitem_text, + effective_left + icon_offset + item_width, line_y, effective_width - icon_offset - item_width, + ui::text_layout::RIGHT, ui::text_layout::NEVER, + mame_ui_manager::NORMAL, item_invert ? fgcolor3 : fgcolor, bgcolor, + nullptr, nullptr); } } @@ -1832,9 +2008,6 @@ void menu_select_launch::draw(uint32_t flags) // return the number of visible lines, minus 1 for top arrow and 1 for bottom arrow m_visible_items = m_visible_lines - (top_line != 0) - (top_line + m_visible_lines != visible_items); - // reset redraw icon stage - m_redraw_icon = false; - // noinput if (noinput) { @@ -2299,7 +2472,7 @@ bool menu_select_launch::has_multiple_bios(game_driver const &driver, s_bios &bi for (romload::system_bios const &bios : romload::entries(driver.rom).get_system_bioses()) { std::string name(bios.get_description()); - u32 const bios_flags(bios.get_value()); + uint32_t const bios_flags(bios.get_value()); if (default_name && !std::strcmp(bios.get_name(), default_name)) { diff --git a/src/frontend/mame/ui/selmenu.h b/src/frontend/mame/ui/selmenu.h index 2dec44ac96c..5e85cb0ce96 100644 --- a/src/frontend/mame/ui/selmenu.h +++ b/src/frontend/mame/ui/selmenu.h @@ -1,5 +1,5 @@ // license:BSD-3-Clause -// copyright-holders:Nicola Salmoria, Aaron Giles, Nathan Woods +// copyright-holders:Nicola Salmoria, Aaron Giles, Nathan Woods, Vas Crabb /*************************************************************************** ui/selmenu.h @@ -7,7 +7,6 @@ MAME system/software selection menu. ***************************************************************************/ - #ifndef MAME_FRONTEND_UI_SELMENU_H #define MAME_FRONTEND_UI_SELMENU_H @@ -35,6 +34,7 @@ public: protected: static constexpr std::size_t MAX_ICONS_RENDER = 128; + static constexpr std::size_t MAX_VISIBLE_SEARCH = 200; // tab navigation enum class focused_menu @@ -45,6 +45,20 @@ protected: RIGHTBOTTOM }; + struct texture_and_bitmap + { + template + texture_and_bitmap(T &&tex) : texture(std::forward(tex)) { } + texture_and_bitmap(texture_and_bitmap &&that) = default; + texture_and_bitmap &operator=(texture_and_bitmap &&that) = default; + + texture_ptr texture; + bitmap_argb32 bitmap; + }; + + template > + using texture_lru = util::lru_cache_map; + class system_flags { public: @@ -117,11 +131,6 @@ protected: void draw_common_arrow(float origx1, float origy1, float origx2, float origy2, int current, int dmin, int dmax, float title); void draw_info_arrow(int ub, float origx1, float origx2, float oy1, float line_height, float text_size, float ud_arrow_width); - // forcing refresh - bool redraw_icon() const { return m_redraw_icon; } - void set_redraw_icon() { m_redraw_icon = true; } - void set_switch_image() { m_switch_image = true; } - bool draw_error_text(); template @@ -130,6 +139,14 @@ protected: std::map const &filters, float x1, float y1, float x2, float y2); + // icon helpers + void check_for_icons(char const *listname); + std::string make_icon_paths(char const *listname) const; + bool scale_icon(bitmap_argb32 &&src, texture_and_bitmap &dst) const; + + // forcing refresh + void set_switch_image() { m_switch_image = true; } + template bool select_bios(T const &driver, bool inlist); bool select_part(software_info const &info, ui_software_info const &ui_info); @@ -235,7 +252,8 @@ private: void draw_toolbar(float x1, float y1, float x2, float y2); void draw_star(float x0, float y0); - virtual float draw_icon(int linenum, void *selectedref, float x1, float y1); + void draw_icon(int linenum, void *selectedref, float x1, float y1); + virtual render_texture *get_icon_texture(int linenum, void *selectedref) = 0; void get_title_search(std::string &title, std::string &search); @@ -294,7 +312,7 @@ private: int m_right_visible_lines; // right box lines - bool m_redraw_icon; + bool m_has_icons; bool m_switch_image; bool m_default_image; uint8_t m_image_view; diff --git a/src/frontend/mame/ui/selsoft.cpp b/src/frontend/mame/ui/selsoft.cpp index 5891ab73f2b..84709e49d89 100644 --- a/src/frontend/mame/ui/selsoft.cpp +++ b/src/frontend/mame/ui/selsoft.cpp @@ -1,5 +1,5 @@ // license:BSD-3-Clause -// copyright-holders:Maurizio Petrarota +// copyright-holders:Maurizio Petrarota, Vas Crabb /*************************************************************************** ui/selsoft.cpp @@ -12,6 +12,7 @@ #include "ui/selsoft.h" #include "ui/ui.h" +#include "ui/icorender.h" #include "ui/inifile.h" #include "ui/selector.h" @@ -24,69 +25,73 @@ #include "uiinput.h" #include "luaengine.h" +#include +#include +#include + namespace ui { +namespace { + //------------------------------------------------- // compares two items in the software list and // sort them by parent-clone //------------------------------------------------- -bool compare_software(ui_software_info a, ui_software_info b) +bool compare_software(ui_software_info const &a, ui_software_info const &b) { - ui_software_info *x = &a; - ui_software_info *y = &b; - - bool clonex = !x->parentname.empty(); - bool cloney = !y->parentname.empty(); - - if (!clonex && !cloney) - return (strmakelower(x->longname) < strmakelower(y->longname)); - - std::string cx(x->parentlongname), cy(y->parentlongname); - - if (cx.empty()) - clonex = false; - - if (cy.empty()) - cloney = false; + bool const clonex = !a.parentname.empty() && !a.parentlongname.empty(); + bool const cloney = !b.parentname.empty() && !b.parentlongname.empty(); if (!clonex && !cloney) - return (strmakelower(x->longname) < strmakelower(y->longname)); - else if (clonex && cloney) { - if (!core_stricmp(x->parentname.c_str(), y->parentname.c_str()) && !core_stricmp(x->instance.c_str(), y->instance.c_str())) - return (strmakelower(x->longname) < strmakelower(y->longname)); - else - return (strmakelower(cx) < strmakelower(cy)); + return 0 > core_stricmp(a.longname.c_str(), b.longname.c_str()); } else if (!clonex && cloney) { - if (!core_stricmp(x->shortname.c_str(), y->parentname.c_str()) && !core_stricmp(x->instance.c_str(), y->instance.c_str())) + if ((a.shortname == b.parentname) && (a.instance == b.instance)) return true; else - return (strmakelower(x->longname) < strmakelower(cy)); + return 0 > core_stricmp(a.longname.c_str(), b.parentlongname.c_str()); } - else + else if (clonex && !cloney) { - if (!core_stricmp(x->parentname.c_str(), y->shortname.c_str()) && !core_stricmp(x->instance.c_str(), y->instance.c_str())) + if ((a.parentname == b.shortname) && (a.instance == b.instance)) return false; else - return (strmakelower(cx) < strmakelower(y->longname)); + return 0 > core_stricmp(a.parentlongname.c_str(), b.longname.c_str()); + } + else if ((a.parentname == b.parentname) && (a.instance == b.instance)) + { + return 0 > core_stricmp(a.longname.c_str(), b.longname.c_str()); + } + else + { + return 0 > core_stricmp(a.parentlongname.c_str(), b.parentlongname.c_str()); } } +} // anonymous namespace + + //------------------------------------------------- // ctor //------------------------------------------------- -menu_select_software::menu_select_software(mame_ui_manager &mui, render_container &container, const game_driver *driver) +menu_select_software::menu_select_software(mame_ui_manager &mui, render_container &container, game_driver const &driver) : menu_select_launch(mui, container, true) + , m_icon_paths() + , m_icons(MAX_ICONS_RENDER) + , m_driver(driver) + , m_has_empty_start(false) + , m_filter_data() + , m_filters() , m_filter_type(software_filter::ALL) + , m_swinfo() { reselect_last::reselect(false); - m_driver = driver; build_software_list(); load_sw_custom_filters(); m_filter_highlight = m_filter_type; @@ -225,11 +230,14 @@ void menu_select_software::handle() void menu_select_software::populate(float &customtop, float &custombottom) { + for (auto &icon : m_icons) // TODO: why is this here? maybe better on resize or setting change? + icon.second.texture.reset(); + uint32_t flags_ui = FLAG_LEFT_ARROW | FLAG_RIGHT_ARROW; m_has_empty_start = true; int old_software = -1; - machine_config config(*m_driver, machine().options()); + machine_config config(m_driver, machine().options()); for (device_image_interface &image : image_interface_iterator(config.root_device())) if (image.filename() == nullptr && image.must_be_loaded()) { @@ -245,31 +253,28 @@ void menu_select_software::populate(float &customtop, float &custombottom) item_append("[Start empty]", "", flags_ui, (void *)&m_swinfo[0]); m_displaylist.clear(); - m_tmp.clear(); - filter_map::const_iterator const it(m_filters.find(m_filter_type)); if (m_filters.end() == it) - m_displaylist = m_sortedlist; + std::copy(std::next(m_swinfo.begin()), m_swinfo.end(), std::back_inserter(m_displaylist)); else - it->second->apply(std::begin(m_sortedlist), std::end(m_sortedlist), std::back_inserter(m_displaylist)); + it->second->apply(std::next(m_swinfo.begin()), m_swinfo.end(), std::back_inserter(m_displaylist)); // iterate over entries for (size_t curitem = 0; curitem < m_displaylist.size(); ++curitem) { if (reselect_last::software() == "[Start empty]" && !reselect_last::driver().empty()) old_software = 0; - - else if (m_displaylist[curitem]->shortname == reselect_last::software() && m_displaylist[curitem]->listname == reselect_last::swlist()) + else if (m_displaylist[curitem].get().shortname == reselect_last::software() && m_displaylist[curitem].get().listname == reselect_last::swlist()) old_software = m_has_empty_start ? curitem + 1 : curitem; - item_append(m_displaylist[curitem]->longname, m_displaylist[curitem]->devicetype, - m_displaylist[curitem]->parentname.empty() ? flags_ui : (FLAG_INVERT | flags_ui), (void *)m_displaylist[curitem]); + item_append( + m_displaylist[curitem].get().longname, m_displaylist[curitem].get().devicetype, + m_displaylist[curitem].get().parentname.empty() ? flags_ui : (FLAG_INVERT | flags_ui), (void *)&m_displaylist[curitem].get()); } } - else { - find_matches(m_search.c_str(), VISIBLE_GAMES_IN_SEARCH); + find_matches(m_search.c_str(), MAX_VISIBLE_SEARCH); for (int curitem = 0; m_searchlist[curitem] != nullptr; ++curitem) item_append(m_searchlist[curitem]->longname, m_searchlist[curitem]->devicetype, @@ -299,21 +304,51 @@ void menu_select_software::populate(float &customtop, float &custombottom) void menu_select_software::build_software_list() { // add start empty item - m_swinfo.emplace_back(*m_driver); + m_swinfo.emplace_back(m_driver); - machine_config config(*m_driver, machine().options()); + machine_config config(m_driver, machine().options()); - // iterate thru all software lists + // iterate through all software lists + std::vector orphans; + struct orphan_less + { + std::vector &swinfo; + bool operator()(std::string const &a, std::string const &b) const { return a < b; }; + bool operator()(std::string const &a, std::size_t b) const { return a < swinfo[b].parentname; }; + bool operator()(std::size_t a, std::string const &b) const { return swinfo[a].parentname < b; }; + bool operator()(std::size_t a, std::size_t b) const { return swinfo[a].parentname < swinfo[b].parentname; }; + }; + orphan_less const orphan_cmp{ m_swinfo }; for (software_list_device &swlist : software_list_device_iterator(config.root_device())) { m_filter_data.add_list(swlist.list_name(), swlist.description()); + check_for_icons(swlist.list_name().c_str()); + orphans.clear(); + std::map parentnames; + std::map::const_iterator prevparent(parentnames.end()); for (const software_info &swinfo : swlist.get_info()) { + // check for previously-encountered clones + if (swinfo.parentname().empty()) + { + if (parentnames.emplace(swinfo.shortname(), swinfo.longname()).second) + { + auto const clones(std::equal_range(orphans.begin(), orphans.end(), swinfo.shortname(), orphan_cmp)); + for (auto it = clones.first; clones.second != it; ++it) + m_swinfo[*it].parentlongname = swinfo.longname(); + orphans.erase(clones.first, clones.second); + } + else + { + assert([] (auto const x) { return x.first == x.second; } (std::equal_range(orphans.begin(), orphans.end(), swinfo.shortname(), orphan_cmp))); + } + } + const software_part &part = swinfo.parts().front(); if (swlist.is_compatible(part) == SOFTWARE_IS_COMPATIBLE) { - const char *instance_name = nullptr; - const char *type_name = nullptr; + char const *instance_name(nullptr); + char const *type_name(nullptr); for (device_image_interface &image : image_interface_iterator(config.root_device())) { char const *const interface = image.image_interface(); @@ -324,45 +359,35 @@ void menu_select_software::build_software_list() break; } } - if (!instance_name || !type_name) - continue; - - ui_software_info tmpmatches(swinfo, part, *m_driver, swlist.list_name(), instance_name, type_name); - m_filter_data.add_region(tmpmatches.longname); - m_filter_data.add_publisher(tmpmatches.publisher); - m_filter_data.add_year(tmpmatches.year); - m_filter_data.add_device_type(tmpmatches.devicetype); - m_swinfo.emplace_back(std::move(tmpmatches)); - } - } - } - m_displaylist.resize(m_swinfo.size() + 1); - - // retrieve and set the long name of software for parents - for (size_t y = 1; y < m_swinfo.size(); ++y) - { - if (!m_swinfo[y].parentname.empty()) - { - std::string lparent(m_swinfo[y].parentname); - bool found = false; - - // first scan backward - for (int x = y; x > 0; --x) - if (lparent == m_swinfo[x].shortname && m_swinfo[y].listname == m_swinfo[x].listname) + if (instance_name && type_name) { - m_swinfo[y].parentlongname = m_swinfo[x].longname; - found = true; - break; - } + // add to collection and try to resolve parent if applicable + auto const ins(m_swinfo.emplace(m_swinfo.end(), swinfo, part, m_driver, swlist.list_name(), instance_name, type_name)); + if (!swinfo.parentname().empty()) + { + if ((parentnames.end() == prevparent) || (swinfo.parentname() != prevparent->first)) + prevparent = parentnames.find(swinfo.parentname()); - // not found? then scan forward - for (size_t x = y; !found && x < m_swinfo.size(); ++x) - if (lparent == m_swinfo[x].shortname && m_swinfo[y].listname == m_swinfo[x].listname) - { - m_swinfo[y].parentlongname = m_swinfo[x].longname; - break; + if (parentnames.end() != prevparent) + { + ins->parentlongname = prevparent->second; + } + else + { + orphans.emplace( + std::upper_bound(orphans.begin(), orphans.end(), swinfo.parentname(), orphan_cmp), + std::distance(m_swinfo.begin(), ins)); + } + } + + // populate filter choices + m_filter_data.add_region(ins->longname); + m_filter_data.add_publisher(ins->publisher); + m_filter_data.add_year(ins->year); + m_filter_data.add_device_type(ins->devicetype); } + } } } @@ -401,9 +426,6 @@ void menu_select_software::build_software_list() // sort array std::stable_sort(m_swinfo.begin() + 1, m_swinfo.end(), compare_software); m_filter_data.finalise(); - - for (size_t x = 1; x < m_swinfo.size(); ++x) - m_sortedlist.push_back(&m_swinfo[x]); } @@ -467,7 +489,7 @@ void menu_select_software::load_sw_custom_filters() { // attempt to open the output file emu_file file(ui().options().ui_path(), OPEN_FLAG_READ); - if (file.open("custom_", m_driver->name, "_filter.ini") == osd_file::error::NONE) + if (file.open("custom_", m_driver.name, "_filter.ini") == osd_file::error::NONE) { software_filter::ptr flt(software_filter::create(file, m_filter_data)); if (flt) @@ -491,10 +513,10 @@ void menu_select_software::find_matches(const char *str, int count) { // pick the best match between shortname and longname // TODO: search alternate title as well - double curpenalty(util::edit_distance(search, ustr_from_utf8(normalize_unicode(m_displaylist[index]->shortname, unicode_normalization_form::D, true)))); + double curpenalty(util::edit_distance(search, ustr_from_utf8(normalize_unicode(m_displaylist[index].get().shortname, unicode_normalization_form::D, true)))); if (curpenalty) { - double const tmp(util::edit_distance(search, ustr_from_utf8(normalize_unicode(m_displaylist[index]->longname, unicode_normalization_form::D, true)))); + double const tmp(util::edit_distance(search, ustr_from_utf8(normalize_unicode(m_displaylist[index].get().longname, unicode_normalization_form::D, true)))); curpenalty = (std::min)(curpenalty, tmp); } @@ -512,7 +534,7 @@ void menu_select_software::find_matches(const char *str, int count) m_searchlist[matchnum + 1] = m_searchlist[matchnum]; } - m_searchlist[matchnum] = m_displaylist[index]; + m_searchlist[matchnum] = &m_displaylist[index].get(); penalty[matchnum] = curpenalty; } } @@ -529,6 +551,56 @@ float menu_select_software::draw_left_panel(float x1, float y1, float x2, float } +//------------------------------------------------- +// get (possibly cached) icon texture +//------------------------------------------------- + +render_texture *menu_select_software::get_icon_texture(int linenum, void *selectedref) +{ + ui_software_info const *const swinfo(reinterpret_cast(selectedref)); + assert(swinfo); + + if (swinfo->startempty) + return nullptr; + + icon_cache::iterator icon(m_icons.find(swinfo)); + if ((m_icons.end() == icon) || !icon->second.texture) + { + std::map::iterator paths(m_icon_paths.find(swinfo->listname)); + if (m_icon_paths.end() == paths) + paths = m_icon_paths.emplace(swinfo->listname, make_icon_paths(swinfo->listname.c_str())).first; + + // allocate an entry or allocate a texture on forced redraw + if (m_icons.end() == icon) + { + icon = m_icons.emplace(swinfo, texture_ptr(machine().render().texture_alloc(), machine().render())).first; + } + else + { + assert(!icon->second.texture); + icon->second.texture.reset(machine().render().texture_alloc()); + } + + bitmap_argb32 tmp; + emu_file snapfile(std::string(paths->second), OPEN_FLAG_READ); + if (snapfile.open(std::string(swinfo->shortname).append(".ico")) == osd_file::error::NONE) + { + render_load_ico_highest_detail(snapfile, tmp); + snapfile.close(); + } + if (!tmp.valid() && !swinfo->parentname.empty() && (snapfile.open(std::string(swinfo->parentname).append(".ico")) == osd_file::error::NONE)) + { + render_load_ico_highest_detail(snapfile, tmp); + snapfile.close(); + } + + scale_icon(std::move(tmp), icon->second); + } + + return icon->second.bitmap.valid() ? icon->second.texture.get() : nullptr; +} + + //------------------------------------------------- // get selected software and/or driver //------------------------------------------------- @@ -545,7 +617,7 @@ void menu_select_software::make_topbox_text(std::string &line0, std::string &lin // determine the text for the header int vis_item = !m_search.empty() ? visible_items : (m_has_empty_start ? visible_items - 1 : visible_items); line0 = string_format(_("%1$s %2$s ( %3$d / %4$d software packages )"), emulator_info::get_appname(), bare_build_version, vis_item, m_swinfo.size() - 1); - line1 = string_format(_("Driver: \"%1$s\" software list "), m_driver->type.fullname()); + line1 = string_format(_("Driver: \"%1$s\" software list "), m_driver.type.fullname()); filter_map::const_iterator const it(m_filters.find(m_filter_type)); char const *const filter((m_filters.end() != it) ? it->second->filter_text() : nullptr); @@ -581,13 +653,13 @@ void menu_select_software::filter_selected() it->second->show_ui( ui(), container(), - [this, driver = m_driver] (software_filter &filter) + [this, &driver = m_driver] (software_filter &filter) { software_filter::type const new_type(filter.get_type()); if (software_filter::CUSTOM == new_type) { emu_file file(ui().options().ui_path(), OPEN_FLAG_WRITE | OPEN_FLAG_CREATE | OPEN_FLAG_CREATE_PATHS); - if (file.open("custom_", driver->name, "_filter.ini") == osd_file::error::NONE) + if (file.open("custom_", driver.name, "_filter.ini") == osd_file::error::NONE) { filter.save_ini(file, 0); file.close(); diff --git a/src/frontend/mame/ui/selsoft.h b/src/frontend/mame/ui/selsoft.h index e65a5ddc61f..1becf695841 100644 --- a/src/frontend/mame/ui/selsoft.h +++ b/src/frontend/mame/ui/selsoft.h @@ -1,5 +1,5 @@ // license:BSD-3-Clause -// copyright-holders:Maurizio Petrarota +// copyright-holders:Maurizio Petrarota, Vas Crabb /*************************************************************************** ui/selsoft.h @@ -22,25 +22,19 @@ namespace ui { class menu_select_software : public menu_select_launch { public: - menu_select_software(mame_ui_manager &mui, render_container &container, const game_driver *driver); + menu_select_software(mame_ui_manager &mui, render_container &container, game_driver const &driver); virtual ~menu_select_software() override; private: - enum { VISIBLE_GAMES_IN_SEARCH = 200 }; - - typedef std::map filter_map; - - const game_driver *m_driver; - bool m_has_empty_start; - software_filter_data m_filter_data; - filter_map m_filters; - software_filter::type m_filter_type; + using filter_map = std::map; + using icon_cache = texture_lru; virtual void populate(float &customtop, float &custombottom) override; virtual void handle() override; - // draw left panel + // drawing virtual float draw_left_panel(float x1, float y1, float x2, float y2) override; + virtual render_texture *get_icon_texture(int linenum, void *selectedref) override; // get selected software and/or driver virtual void get_selection(ui_software_info const *&software, game_driver const *&driver) const override; @@ -56,10 +50,6 @@ private: // toolbar virtual void inkey_export() override { throw false; } - ui_software_info *m_searchlist[VISIBLE_GAMES_IN_SEARCH + 1]; - std::vector m_displaylist, m_tmp, m_sortedlist; - std::vector m_swinfo; - void build_software_list(); void find_matches(const char *str, int count); void load_sw_custom_filters(); @@ -67,9 +57,21 @@ private: // handlers void inkey_select(const event *menu_event); - virtual void general_info(const game_driver *driver, std::string &buffer) override {} + virtual void general_info(const game_driver *driver, std::string &buffer) override { } + + std::map m_icon_paths; + icon_cache m_icons; + game_driver const &m_driver; + bool m_has_empty_start; + software_filter_data m_filter_data; + filter_map m_filters; + software_filter::type m_filter_type; + + std::vector m_swinfo; + ui_software_info const *m_searchlist[MAX_VISIBLE_SEARCH + 1]; + std::vector > m_displaylist; }; } // namespace ui -#endif /* MAME_FRONTEND_UI_SELSOFT_H */ +#endif // MAME_FRONTEND_UI_SELSOFT_H diff --git a/src/frontend/mame/ui/utils.cpp b/src/frontend/mame/ui/utils.cpp index baebce20bb3..52857fc6dbf 100644 --- a/src/frontend/mame/ui/utils.cpp +++ b/src/frontend/mame/ui/utils.cpp @@ -84,6 +84,7 @@ constexpr char const *SOFTWARE_FILTER_NAMES[software_filter::COUNT] = { __("Unfiltered"), __("Available"), __("Unavailable"), + __("Favorites"), __("Parents"), __("Clones"), __("Year"), @@ -1256,6 +1257,21 @@ public: }; +class favorite_software_filter : public simple_filter_impl_base +{ +public: + favorite_software_filter(software_filter_data const &data, char const *value, emu_file *file, unsigned indent) + : m_manager(mame_machine_manager::instance()->favorite()) + { + } + + virtual bool apply(ui_software_info const &info) const override { return m_manager.is_favorite_software(info); } + +private: + favorite_manager const &m_manager; +}; + + class parents_software_filter : public simple_filter_impl_base { public: @@ -1423,6 +1439,7 @@ public: case UNSUPPORTED: return (SUPPORTED == m) || (PARTIAL_SUPPORTED == m); case ALL: + case FAVORITE: case YEAR: case PUBLISHERS: case REGION: @@ -1740,6 +1757,8 @@ software_filter::ptr software_filter::create(type n, software_filter_data const return std::make_unique(data, value, file, indent); case UNAVAILABLE: return std::make_unique(data, value, file, indent); + case FAVORITE: + return std::make_unique(data, value, file, indent); case PARENTS: return std::make_unique(data, value, file, indent); case CLONES: diff --git a/src/frontend/mame/ui/utils.h b/src/frontend/mame/ui/utils.h index b3f5350002b..e6f2acd6bc8 100644 --- a/src/frontend/mame/ui/utils.h +++ b/src/frontend/mame/ui/utils.h @@ -200,6 +200,7 @@ public: ALL = 0, AVAILABLE, UNAVAILABLE, + FAVORITE, PARENTS, CLONES, YEAR, diff --git a/src/frontend/mame/ui/widgets.cpp b/src/frontend/mame/ui/widgets.cpp index cc2c276abfc..d8907303b36 100644 --- a/src/frontend/mame/ui/widgets.cpp +++ b/src/frontend/mame/ui/widgets.cpp @@ -14,6 +14,7 @@ namespace ui { + /*************************************************************************** WIDGETS ***************************************************************************/ @@ -24,12 +25,12 @@ namespace ui { widgets_manager::widgets_manager(running_machine &machine) : m_hilight_bitmap(std::make_unique(256, 1)) - , m_hilight_texture() + , m_hilight_texture(nullptr, machine.render()) , m_hilight_main_bitmap(std::make_unique(1, 128)) - , m_hilight_main_texture() + , m_hilight_main_texture(nullptr, machine.render()) + , m_arrow_texture(nullptr, machine.render()) { render_manager &render(machine.render()); - auto const texture_free([&render](render_texture *texture) { render.texture_free(texture); }); // create a texture for hilighting items for (unsigned x = 0; x < 256; ++x) @@ -37,7 +38,7 @@ widgets_manager::widgets_manager(running_machine &machine) unsigned const alpha((x < 25) ? (0xff * x / 25) : (x >(256 - 25)) ? (0xff * (255 - x) / 25) : 0xff); m_hilight_bitmap->pix32(0, x) = rgb_t(alpha, 0xff, 0xff, 0xff); } - m_hilight_texture = texture_ptr(render.texture_alloc(), texture_free); + m_hilight_texture.reset(render.texture_alloc()); m_hilight_texture->set_bitmap(*m_hilight_bitmap, m_hilight_bitmap->cliprect(), TEXFORMAT_ARGB32); // create a texture for hilighting items in main menu @@ -50,11 +51,11 @@ widgets_manager::widgets_manager(running_machine &machine) unsigned const b = b1 + (y * (b2 - b1) / 128); m_hilight_main_bitmap->pix32(y, 0) = rgb_t(r, g, b); } - m_hilight_main_texture = texture_ptr(render.texture_alloc(), texture_free); + m_hilight_main_texture.reset(render.texture_alloc()); m_hilight_main_texture->set_bitmap(*m_hilight_main_bitmap, m_hilight_main_bitmap->cliprect(), TEXFORMAT_ARGB32); // create a texture for arrow icons - m_arrow_texture = texture_ptr(render.texture_alloc(render_triangle), texture_free); + m_arrow_texture.reset(render.texture_alloc(render_triangle)); } diff --git a/src/frontend/mame/ui/widgets.h b/src/frontend/mame/ui/widgets.h index 45f1725d7ec..567b0a4e499 100644 --- a/src/frontend/mame/ui/widgets.h +++ b/src/frontend/mame/ui/widgets.h @@ -21,6 +21,7 @@ namespace ui { + /*************************************************************************** TYPE DEFINITIONS ***************************************************************************/ @@ -34,8 +35,17 @@ public: render_texture *hilight_main_texture() { return m_hilight_main_texture.get(); } render_texture *arrow_texture() { return m_arrow_texture.get(); } + class texture_destroyer + { + public: + texture_destroyer(render_manager &manager) : m_manager(manager) { } + void operator()(render_texture *texture) const { m_manager.get().texture_free(texture); } + private: + std::reference_wrapper m_manager; + }; + using bitmap_ptr = std::unique_ptr; - using texture_ptr = std::unique_ptr >; + using texture_ptr = std::unique_ptr; private: static void render_triangle(bitmap_argb32 &dest, bitmap_argb32 &source, const rectangle &sbounds, void *param); diff --git a/src/lib/util/corefile.cpp b/src/lib/util/corefile.cpp index 264d80f4113..78a484c5511 100644 --- a/src/lib/util/corefile.cpp +++ b/src/lib/util/corefile.cpp @@ -23,9 +23,10 @@ #include - namespace util { + namespace { + /*************************************************************************** VALIDATION ***************************************************************************/ @@ -335,23 +336,6 @@ private: -/*************************************************************************** - INLINE FUNCTIONS -***************************************************************************/ - -/*------------------------------------------------- - is_directory_separator - is a given character - a directory separator? The following logic - works for most platforms --------------------------------------------------*/ - -inline int is_directory_separator(char c) -{ - return (c == '\\' || c == '/' || c == ':'); -} - - - /*************************************************************************** core_text_file ***************************************************************************/ @@ -1275,7 +1259,7 @@ core_file::core_file() std::string core_filename_extract_base(const std::string &name, bool strip_extension) { // find the start of the basename - auto const start = std::find_if(name.rbegin(), name.rend(), [](char c) { return util::is_directory_separator(c); }); + auto const start = std::find_if(name.rbegin(), name.rend(), &util::is_directory_separator); // find the end of the basename auto const chop_position = strip_extension diff --git a/src/lib/util/corefile.h b/src/lib/util/corefile.h index 2c4a5da8217..d286cb866ac 100644 --- a/src/lib/util/corefile.h +++ b/src/lib/util/corefile.h @@ -7,11 +7,11 @@ Core file I/O interface functions and definitions. ***************************************************************************/ +#ifndef MAME_LIB_UTIL_COREFILE_H +#define MAME_LIB_UTIL_COREFILE_H #pragma once -#ifndef MAME_LIB_UTIL_COREFILE_H -#define MAME_LIB_UTIL_COREFILE_H #include "corestr.h" #include "coretmpl.h" @@ -23,6 +23,7 @@ namespace util { + /*************************************************************************** ADDITIONAL OPEN FLAGS ***************************************************************************/ @@ -130,6 +131,22 @@ protected: core_file(); }; + +/*************************************************************************** + INLINE FUNCTIONS +***************************************************************************/ + +// is a given character a directory separator? + +constexpr bool is_directory_separator(char c) +{ +#if defined(WIN32) + return ('\\' == c) || ('/' == c) || (':' == c); +#else + return '/' == c; +#endif +} + } // namespace util -- cgit v1.2.3