From 4e4743afed58b899575edf8a92075196ebea269c Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Sun, 8 Mar 2026 15:33:42 -0700 Subject: [PATCH] Optimize minimap icons hot path and add Trade/Brainwasher icons Performance: - GUID->blip result cache (256-entry direct-mapped) skips classification after first match; only position lookup remains per-frame - Active NPC flag bitmask rejects most objects before the 16-entry loop - Split tracking by object type (unit vs GO) for early exit - Per-frame minimap info cache (center/radius/scale read once per cycle) - Skip subname/entry reads when no filters are active - Batch draw calls by texture (sort blips, one GxRsSet per unique texture) - Split drawMinimapTexture into getGxTex + drawMinimapBlip for batching Assets: - Add Trade.blp icon (INV_Potion_85 derived) for Trade Goods vendors - Update Brainwasher.blp with stark contrast version, downsized to 32x32 - Add png2blp.py converter (PNG to BLP2 DXT3 with mipmaps) --- src/minimapicons/addon/MinimapIcons.lua | 6 +- .../Minimap/Tracking/Brainwasher.blp | Bin 5268 -> 2564 bytes .../Interface/Minimap/Tracking/Trade.blp | Bin 0 -> 2564 bytes src/minimapicons/minimapicons.zig | 237 +++++++++++++----- src/minimapicons/png2blp.py | 188 ++++++++++++++ 5 files changed, 370 insertions(+), 61 deletions(-) create mode 100644 src/minimapicons/assets/Interface/Minimap/Tracking/Trade.blp create mode 100644 src/minimapicons/png2blp.py diff --git a/src/minimapicons/addon/MinimapIcons.lua b/src/minimapicons/addon/MinimapIcons.lua index f7848bf..2f33a38 100644 --- a/src/minimapicons/addon/MinimapIcons.lua +++ b/src/minimapicons/addon/MinimapIcons.lua @@ -95,17 +95,17 @@ local NPC_CATEGORIES = { getFilter = function() return getLocaleFilter(REAGENT_FILTERS) end }, { name = "Poison Vendor", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Poison", getFilter = function() return getLocaleFilter(POISON_FILTERS) end }, - { name = "Trade Goods", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Profession", + { name = "Trade Goods", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Trade", getFilter = function() return getLocaleFilter(TRADE_FILTERS) end }, { name = "Ammunition", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Ammunition", getFilter = function() return getLocaleFilter(AMMO_FILTERS) end }, { name = "General Vendor", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Food" }, + { name = "Stable Master", trackingType = "stablemaster", icon = "Interface\\Minimap\\Tracking\\StableMaster" }, + { name = "Battle Master", trackingType = "battlemaster", icon = "Interface\\Minimap\\Tracking\\BattleMaster" }, { name = "Class Trainer", trackingType = "trainer", icon = "Interface\\Minimap\\Tracking\\Class", getFilter = function() return (UnitClass("player")) end }, { name = "Profession Trainer", trackingType = "trainer", icon = "Interface\\Minimap\\Tracking\\Profession", getExclude = function() return UnitClass("player") end }, - { name = "Battle Master", trackingType = "battlemaster", icon = "Interface\\Minimap\\Tracking\\BattleMaster" }, - { name = "Stable Master", trackingType = "stablemaster", icon = "Interface\\Minimap\\Tracking\\StableMaster" }, } -- ============================================================================= diff --git a/src/minimapicons/assets/Interface/Minimap/Tracking/Brainwasher.blp b/src/minimapicons/assets/Interface/Minimap/Tracking/Brainwasher.blp index 6d4b0dd935e8528d037231fd26dece5f179eaec9..4a1caa710c1de748bc9c98d01acf609b0305a56e 100644 GIT binary patch literal 2564 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 MAX_BLIPS) return; + const obj = getObjectByGUID(guid_lo, guid_hi); + if (obj == 0 or !isValidPtr(obj)) return; + const pos = getObjectPosition(obj) orelse return; - const cur = C3Vector{ - .x = hook.readMem(f32, info + ADDR.MI_POS), - .y = hook.readMem(f32, info + ADDR.MI_POS + 4), - .z = hook.readMem(f32, info + ADDR.MI_POS + 8), - }; - const radius = hook.readMem(f32, info + ADDR.MI_RADIUS); - const layout_scale = hook.readMem(f32, info + ADDR.MI_LAYOUT_SCALE); - const unk_scale = getFrameUnkScale(info); + // Cache minimap info per frame — same for all objects in one enumeration cycle + if (!g_minimap_info.valid) { + g_minimap_info = .{ + .cur = .{ + .x = hook.readMem(f32, info + ADDR.MI_POS), + .y = hook.readMem(f32, info + ADDR.MI_POS + 4), + .z = hook.readMem(f32, info + ADDR.MI_POS + 8), + }, + .radius = hook.readMem(f32, info + ADDR.MI_RADIUS), + .layout_scale = hook.readMem(f32, info + ADDR.MI_LAYOUT_SCALE), + .unk_scale = getFrameUnkScale(info), + .valid = true, + }; + } var minimap_pos: C2Vector = undefined; - worldPosToMinimapCoords(&minimap_pos, cur, radius, pos.x, pos.y, layout_scale, unk_scale); + worldPosToMinimapCoords(&minimap_pos, g_minimap_info.cur, g_minimap_info.radius, pos.x, pos.y, g_minimap_info.layout_scale, g_minimap_info.unk_scale); g_blips[g_blip_count] = .{ .pos = minimap_pos, @@ -719,25 +805,31 @@ fn trackObject(info: u32, obj: u32, blip: Blip) void { } fn refreshActiveTrackingCache() void { + var has_unit = false; + var has_go = false; + var has_filters = false; + var flag_mask: u32 = 0; for (&g_flag_tracking) |*entry| { if (entry.active) { - g_has_active_tracking = true; - return; + has_unit = true; + flag_mask |= entry.flag; + if (entry.hasFilter()) has_filters = true; } } for (&g_go_tracking) |*entry| { - if (entry.active) { - g_has_active_tracking = true; - return; - } + if (entry.active) has_go = true; } for (&g_go_id_tracking) |*entry| { - if (entry.active) { - g_has_active_tracking = true; - return; - } + if (entry.active) has_go = true; } - g_has_active_tracking = false; + g_has_active_unit_tracking = has_unit; + g_has_active_go_tracking = has_go; + g_has_active_tracking = has_unit or has_go; + g_has_any_filters = has_filters; + g_active_flag_mask = flag_mask; + + // Config changed — cached match results may be stale + guidCacheClear(); } // ============================================================================= @@ -767,13 +859,41 @@ var render_blips_hook: hook.Detour(RenderBlipsFn) = .{}; fn renderObjectBlipsDetour(thisptr: u32, _edx: u32, dn_info: u32) callconv(fc) void { render_blips_hook.callOriginal(.{ thisptr, _edx, dn_info }); - // Draw our custom blips after the original ones - for (0..g_blip_count) |i| { - drawMinimapTexture( - g_blips[i].blip.texture, - g_blips[i].pos, - g_blips[i].blip.scale, - ); + const count = g_blip_count; + if (count == 0) return; + + // Sort blips by texture handle to batch draw calls + const blips = g_blips[0..count]; + std.mem.sort(TrackedBlip, blips, {}, struct { + fn lessThan(_: void, a: TrackedBlip, b: TrackedBlip) bool { + return a.blip.texture < b.blip.texture; + } + }.lessThan); + + // Draw batched by texture — one GxRsSet per unique texture + var i: usize = 0; + while (i < count) { + const tex = blips[i].blip.texture; + if (tex == 0) { + i += 1; + continue; + } + + // Bind texture once for this group + const gx_tex = getGxTex(tex); + if (gx_tex == 0) { + // Skip all blips with this texture + while (i < count and blips[i].blip.texture == tex) : (i += 1) {} + continue; + } + + const gxRsSet: *const fn (u32, u32) callconv(fc) void = @ptrFromInt(ADDR.GxRsSet); + gxRsSet(23, gx_tex); + + // Draw all blips sharing this texture + while (i < count and blips[i].blip.texture == tex) : (i += 1) { + drawMinimapBlip(blips[i].pos, blips[i].blip.scale); + } } } @@ -785,6 +905,7 @@ fn enumVisibleObjectsDetour(callback: u32, context: u32) callconv(fc) i32 { // Clear tracked blips when the minimap's own callback is about to enumerate if (callback == ADDR.ObjectEnumProc) { g_blip_count = 0; + g_minimap_info.valid = false; } return enum_vis_hook.callOriginal(.{ callback, context }); } diff --git a/src/minimapicons/png2blp.py b/src/minimapicons/png2blp.py new file mode 100644 index 0000000..abb991c --- /dev/null +++ b/src/minimapicons/png2blp.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""Convert PNG to BLP2 (DXT3 compressed, with mipmaps).""" + +import struct +import sys +from PIL import Image + + +def rgb_to_565(r, g, b): + return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3) + + +def color_565_to_rgb(c): + r = ((c >> 11) & 0x1F) << 3 + g = ((c >> 5) & 0x3F) << 2 + b = (c & 0x1F) << 3 + return r, g, b + + +def color_distance(r0, g0, b0, r1, g1, b1): + return (r0 - r1) ** 2 + (g0 - g1) ** 2 + (b0 - b1) ** 2 + + +def encode_dxt1_block(pixels): + """Encode a 4x4 block of (r,g,b) tuples into 8-byte DXT1 color block. + pixels: list of 16 (r,g,b) tuples in row-major order. + """ + # Find min/max colors by luminance + min_c = min(pixels, key=lambda c: c[0] * 299 + c[1] * 587 + c[2] * 114) + max_c = max(pixels, key=lambda c: c[0] * 299 + c[1] * 587 + c[2] * 114) + + color0 = rgb_to_565(*max_c) + color1 = rgb_to_565(*min_c) + + # Ensure color0 > color1 for 4-color mode + if color0 == color1: + # All same color, indices all 0 + return struct.pack('> 4) << (i * 4) + + alpha_data = struct.pack(' 1 or h > 1: + w = max(1, w // 2) + h = max(1, h // 2) + levels.append(img.resize((w, h), Image.LANCZOS)) + return [(l.size[0], l.size[1], l.tobytes()) for l in levels] + + +def png_to_blp2(png_path, blp_path): + img = Image.open(png_path).convert('RGBA') + width, height = img.size + + mipmaps = generate_mipmaps(img) + num_mipmaps = min(len(mipmaps), 16) + + # Encode each mipmap level as DXT3 + encoded = [] + for w, h, data in mipmaps[:num_mipmaps]: + encoded.append(encode_dxt3_image(data, w, h)) + + # BLP2 header: 1172 bytes + # 4 (magic) + 4 (type) + 1 (compression) + 1 (alpha_depth) + 1 (alpha_type) + # + 1 (has_mips) + 4 (width) + 4 (height) + 64 (offsets) + 64 (lengths) + # + 1024 (palette) = 1172 + header_size = 4 + 4 + 1 + 1 + 1 + 1 + 4 + 4 + 64 + 64 + 1024 + + # Calculate offsets + offsets = [0] * 16 + lengths = [0] * 16 + offset = header_size + for i, data in enumerate(encoded): + offsets[i] = offset + lengths[i] = len(data) + offset += len(data) + + # Build header + header = bytearray() + header.extend(b'BLP2') + header.extend(struct.pack(' {blp_path}') + print(f' {width}x{height}, {num_mipmaps} mipmaps, DXT3') + print(f' {offset} bytes total') + + +if __name__ == '__main__': + if len(sys.argv) != 3: + print(f'Usage: {sys.argv[0]} input.png output.blp') + sys.exit(1) + png_to_blp2(sys.argv[1], sys.argv[2])