#!/usr/bin/perl
# vramgaze.pl - Visualize AMD GPU VRAM allocations from amdgpu debugfs
# Includes advanced memory analysis tools ported from memgaze.pl
#
# Usage: sudo perl vramgaze.pl
#        sudo perl vramgaze.pl --dri 1

use strict;
use warnings;
use Glib qw/TRUE FALSE/;
use Gtk2 '-init';
use Cairo;
use POSIX;
use File::Basename;
use File::Temp qw/tempfile/;
use Fcntl qw(SEEK_SET O_RDONLY);
use PDL;
use File::Spec;
use File::Path qw(make_path);
use Cwd qw(cwd);

# ---------------------------------------------------------------------------
# CLI args
# ---------------------------------------------------------------------------
my $DRI_INDEX   = 0;
my $ASIC        = 'polaris10'; # RX 470/480/570/580/590 (cosmetic for title)
my $VRAM_FILE   = undef;
my $GEM_FILE    = undef;

for (my $i = 0; $i < @ARGV; $i++) {
    if    ($ARGV[$i] eq '--dri'  && defined $ARGV[$i+1]) { $DRI_INDEX = $ARGV[++$i]; }
    elsif ($ARGV[$i] eq '--asic' && defined $ARGV[$i+1]) { $ASIC      = $ARGV[++$i]; }
    elsif ($ARGV[$i] eq '--vram' && defined $ARGV[$i+1]) { $VRAM_FILE = $ARGV[++$i]; }
    elsif ($ARGV[$i] eq '--gem'  && defined $ARGV[$i+1]) { $GEM_FILE  = $ARGV[++$i]; }
}

my $VRAM_MM_PATH     = "/sys/kernel/debug/dri/$DRI_INDEX/amdgpu_vram_mm";
my $GEM_INFO_PATH    = "/sys/kernel/debug/dri/$DRI_INDEX/amdgpu_gem_info";
my $AMDGPU_VRAM_PATH = "/sys/kernel/debug/dri/$DRI_INDEX/amdgpu_vram";

# ---------------------------------------------------------------------------
# Debug logger
# ---------------------------------------------------------------------------
sub debug {
    my ($msg) = @_;
    my @t = localtime;
    warn sprintf("[%02d:%02d:%02d] %s\n", $t[2], $t[1], $t[0], $msg);
}

# ---------------------------------------------------------------------------
# Global state
# ---------------------------------------------------------------------------
my $state = {
    regions             => [],
    total_pages         => 0,
    used_pages          => 0,
    free_pages          => 0,
    vram_size_bytes     => 0,
    map_areas           => [],
    hilbert_window_ref  => undef,
    update_timer_id     => undef,
    update_interval_sec => 2,
    statusbar           => undef,
    status_context      => undef,
    drawing_area        => undef,
    read_method         => 'none',
    gem_data            => {},
    gem_map_by_size     => {},
    range_start_idx     => undef, # Multi-region selection start (Green)
    range_end_idx       => undef, # Multi-region selection end   (Red)
};

my %C = (
    used    => [0.13, 0.67, 1.0 ],
    free    => [0.07, 0.20, 0.07],
    bg      => [0.10, 0.10, 0.10],
    grid    => [0.20, 0.20, 0.20],
    hi      => [1.0,  0.80, 0.0 ],
    text_fg => [0.90, 0.90, 0.90],
);

# ---------------------------------------------------------------------------
# VRAM reading via AMDGPU debugfs
# ---------------------------------------------------------------------------
sub read_vram {
    my ($offset, $size) = @_;

    if (! -r $AMDGPU_VRAM_PATH) {
        debug("amdgpu_vram file not readable: $AMDGPU_VRAM_PATH (run as root)");
        $state->{read_method} = 'none';
        return undef;
    }

    # Increased cap to 256MB to support multi-region selections gracefully
    $size = 256 * 1024 * 1024 if $size > 256 * 1024 * 1024;
    debug(sprintf("debugfs: reading %s from %s at +0x%x", format_bytes($size), $AMDGPU_VRAM_PATH, $offset));
    
    sysopen my $fd, $AMDGPU_VRAM_PATH, O_RDONLY or do {
        debug("open failed: $!");
        return undef;
    };
    sysseek $fd, $offset, SEEK_SET or do {
        debug("seek failed: $!");
        close $fd;
        return undef;
    };
    
    my $buf = '';
    my $total = 0;
    while ($total < $size) {
        my $chunk = ($size - $total) > 1048576 ? 1048576 : ($size - $total);
        my $n = sysread $fd, $buf, $chunk, $total;
        if (!defined $n) { debug("sysread failed: $!"); last; }
        last if $n == 0;
        $total += $n;
    }
    close $fd;
    
    if ($total > 0) {
        $state->{read_method} = 'debugfs';
        return \substr($buf, 0, $total);
    }
    return undef;
}

sub read_method_label {
    return (-r $AMDGPU_VRAM_PATH) ? "debugfs (amdgpu_vram)" : "no read access (run as root)";
}

sub build_multi_region_map_info {
    my ($start_idx, $end_idx, $base_offset, $read_size) = @_;
    my @map_info;
    for my $i ($start_idx .. $end_idx) {
        my $r = $state->{regions}[$i];
        my $r_start_byte = $r->{start} * 4096;
        my $r_end_byte   = ($r->{end} + 1) * 4096;

        my $offset_in_img = $r_start_byte - $base_offset;
        last if $offset_in_img >= $read_size; # Passed read buffer

        my $len_in_img = $r_end_byte - $r_start_byte;
        if ($offset_in_img + $len_in_img > $read_size) {
            $len_in_img = $read_size - $offset_in_img;
        }

        push @map_info, {
            offset_in_image => $offset_in_img,
            length_in_image => $len_in_img,
            path            => "Region $i",
            size            => $r->{size},
            perms           => "rw-",
            start           => $r->{start},
            end             => $r->{end},
            status          => $r->{status},
            idx             => $i
        };
    }
    return \@map_info;
}

# ---------------------------------------------------------------------------
# Image & Pixbuf Utilities
# ---------------------------------------------------------------------------
sub raw24_to_pixbuf {
    my ($raw_ref, $force_width) = @_;
    my $len = length($$raw_ref);
    return undef unless $len >= 3;

    my $pixels = int($len / 3);
    my $width  = defined $force_width ? $force_width : int(sqrt($pixels));
    $width = 4 if $width < 4;
    $width = int($width / 4) * 4;
    $width = 4 if $width < 4;
    my $height = int($pixels / $width);
    $height = 1 if $height < 1;

    my $bytes_needed = $width * $height * 3;
    my $img_data = substr($$raw_ref, 0, $bytes_needed);

    eval {
        Gtk2::Gdk::Pixbuf->new_from_data(
            $img_data, 'rgb', FALSE, 8,
            $width, $height, $width * 3);
    };
}

sub create_pixbuf_from_data {
    my ($raw_data_ref) = @_;
    my $total_bytes = length($$raw_data_ref);
    return undef if $total_bytes < 3;

    my $total_pixels = POSIX::floor($total_bytes / 3);
    my $width = POSIX::floor(sqrt($total_pixels));
    return undef if $width == 0;
    my $height = POSIX::floor($total_pixels / $width);
    return undef if $height == 0;

    my $bytes_to_use = $width * $height * 3;
    my $image_data = substr($$raw_data_ref, 0, $bytes_to_use);
    
    return Gtk2::Gdk::Pixbuf->new_from_data($image_data, 'rgb', FALSE, 8, $width, $height, $width * 3);
}

# ---------------------------------------------------------------------------
# VRAM Image Popup (Advanced Analysis Window)
# ---------------------------------------------------------------------------
sub show_vram_image_popup {
    my ($parent, $raw_ref, $title_info, $map_info) = @_;

    my $popup = Gtk2::Window->new('toplevel');
    $popup->set_title(sprintf("VRAM Image - %s  [via %s]", $title_info->{title}, $state->{read_method}));
    $popup->set_transient_for($parent);
    $popup->set_default_size(860, 680);
    $popup->set_position('center-on-parent');

    my $vbox = Gtk2::VBox->new(FALSE, 5);
    $vbox->set_border_width(5);
    $popup->add($vbox);

    my $popup_state = {
        raw_data_ref              => $raw_ref,
        loaded_raw_data_ref       => undef,
        pixbuf                    => undef,
        current_width             => 0,
        image_map_info            => $map_info,
        timer_id                  => undef,
        diff_points               => undef,
        diff_toggle_button        => undef,
        overlay_map_areas         => [],
        drawing_area              => undef,
        update_toggle_button      => undef,
        overlay_toggle_button     => undef,
        star_info                 => undef,
        star_timer_id             => undef,
        sound_player_pid          => undef,
        sound_child_watch_id      => undef,
        sound_temp_file           => undef,
        _is_changing_sound_button => FALSE,
        update_baseline_ref       => undef,
        selection_start_offset    => undef,
        selection_end_offset      => undef,
        selection_highlight_rects => [],
        selection_toggle_button   => undef,
        start_offset_entry        => undef,
        end_offset_entry          => undef,
        extra_selection_widgets   => [],
        thumbnail_window_ref      => undef,
        infobar_labels            => {},
        pname                     => $title_info->{title},
        pid                       => $title_info->{pid}
    };
    $popup_state->{pname} =~ s/ /_/g;

    # --- Toolbars ---
    my $toolbar1 = Gtk2::HBox->new(FALSE, 5);
    my $toolbar2 = Gtk2::HBox->new(FALSE, 5);
    my $toolbar3 = Gtk2::HBox->new(FALSE, 5);
    
    # Toolbar 1: Core IO & Sound
    my $btn_save_png = Gtk2::Button->new("Save PNG");
    my $btn_save_raw = Gtk2::Button->new("Save RAM Raw");
    my $btn_load_raw = Gtk2::Button->new("Load RAM Raw");
    my $status_box = Gtk2::DrawingArea->new;
    $status_box->set_size_request(20, -1);
    $status_box->modify_bg('normal', Gtk2::Gdk::Color->parse('red'));
    my $diff_button = Gtk2::Button->new("Diff");
    my $diff_toggle = Gtk2::CheckButton->new("Show Diffs");
    $popup_state->{diff_toggle_button} = $diff_toggle;
    my $strings_button = Gtk2::Button->new("Strings");
    my $sound_button = Gtk2::ToggleButton->new("Sound");
    my $save_wav_button = Gtk2::Button->new("Save wav");
    
    $toolbar1->pack_start($btn_save_png, FALSE, FALSE, 0);
    $toolbar1->pack_start($btn_save_raw, FALSE, FALSE, 5);
    $toolbar1->pack_start($btn_load_raw, FALSE, FALSE, 5);
    $toolbar1->pack_start($status_box, FALSE, FALSE, 5);
    $toolbar1->pack_start($diff_button, FALSE, FALSE, 0);
    $toolbar1->pack_start($diff_toggle, FALSE, FALSE, 5);
    $toolbar1->pack_start($strings_button, FALSE, FALSE, 5);
    $toolbar1->pack_start($sound_button, FALSE, FALSE, 0);
    $toolbar1->pack_start($save_wav_button, FALSE, FALSE, 5);

    # Toolbar 2: View Options & Updating
    my $width_label = Gtk2::Label->new("Width:");
    my $width_adj   = Gtk2::Adjustment->new(0, 4, 8192, 1, 64, 0);
    my $width_spin  = Gtk2::SpinButton->new($width_adj, 0, 0);
    $width_spin->set_width_chars(5);
    my $btn_autowidth = Gtk2::Button->new("Auto Width");

    my $overlay_toggle = Gtk2::CheckButton->new("Overlays");
    $overlay_toggle->set_active(TRUE);
    $popup_state->{overlay_toggle_button} = $overlay_toggle;
    my $thumbnail_check = Gtk2::CheckButton->new("Thumbnail");
    my $infobar_toggle = Gtk2::CheckButton->new("Infobar");

    my $update_label = Gtk2::Label->new("  Update(s):");
    my $update_entry = Gtk2::Entry->new();
    $update_entry->set_text("1.0");
    $update_entry->set_width_chars(4);
    my $update_toggle = Gtk2::ToggleButton->new("Start Updating");
    $popup_state->{update_toggle_button} = $update_toggle;

    $toolbar2->pack_start($width_label, FALSE, FALSE, 0);
    $toolbar2->pack_start($width_spin, FALSE, FALSE, 0);
    $toolbar2->pack_start($btn_autowidth, FALSE, FALSE, 5);
    $toolbar2->pack_start(Gtk2::Label->new(" | Show:"), FALSE, FALSE, 5);
    $toolbar2->pack_start($overlay_toggle, FALSE, FALSE, 0);
    $toolbar2->pack_start($thumbnail_check, FALSE, FALSE, 5);
    $toolbar2->pack_start($infobar_toggle, FALSE, FALSE, 5);
    $toolbar2->pack_start($update_label, FALSE, FALSE, 0);
    $toolbar2->pack_start($update_entry, FALSE, FALSE, 0);
    $toolbar2->pack_start($update_toggle, FALSE, FALSE, 5);

    # Toolbar 3: Analysis & Selections
    my $decode_button = Gtk2::Button->new("Bitmap Decode");
    my $digraph_button = Gtk2::Button->new("Digraph");
    my $photorec_button = Gtk2::Button->new("photorec");
    my $start_label = Gtk2::Label->new("  Selected Offset start:");
    my $start_entry = Gtk2::Entry->new();
    $start_entry->set_width_chars(10);
    $popup_state->{start_offset_entry} = $start_entry;
    my $stop_label = Gtk2::Label->new("stop:");
    my $end_entry = Gtk2::Entry->new();
    $end_entry->set_width_chars(10);
    $popup_state->{end_offset_entry} = $end_entry;
    my $selection_toggle = Gtk2::CheckButton->new();
    $selection_toggle->set_active(TRUE);
    $popup_state->{selection_toggle_button} = $selection_toggle;
    my $add_more_button = Gtk2::Button->new("Add more");

    $toolbar3->pack_start($decode_button, FALSE, FALSE, 0);
    $toolbar3->pack_start($digraph_button, FALSE, FALSE, 5);
    $toolbar3->pack_start($photorec_button, FALSE, FALSE, 5);
    $toolbar3->pack_start($start_label, FALSE, FALSE, 0);
    $toolbar3->pack_start($start_entry, FALSE, FALSE, 5);
    $toolbar3->pack_start($stop_label, FALSE, FALSE, 5);
    $toolbar3->pack_start($end_entry, FALSE, FALSE, 5);
    $toolbar3->pack_start($selection_toggle, FALSE, FALSE, 5);
    $toolbar3->pack_start($add_more_button, FALSE, FALSE, 10);
    
    my $extra_selections_hbox = Gtk2::HBox->new(FALSE, 5);
    $toolbar3->pack_start($extra_selections_hbox, TRUE, TRUE, 0);

    $vbox->pack_start($toolbar1, FALSE, FALSE, 0);
    $vbox->pack_start($toolbar2, FALSE, FALSE, 0);
    $vbox->pack_start($toolbar3, FALSE, FALSE, 0);

    # --- Info Label & Infobar ---
    my $info_label = Gtk2::Label->new;
    $info_label->set_alignment(0, 0.5);
    $vbox->pack_start($info_label, FALSE, FALSE, 0);

    my $infobar_hbox = Gtk2::HBox->new(FALSE, 5);
    my $path_info_label = Gtk2::Label->new;
    my $size_info_label = Gtk2::Label->new;
    my $perms_info_label = Gtk2::Label->new;
    my $range_info_label = Gtk2::Label->new;
    
    for my $item (["Path:", $path_info_label], ["Size:", $size_info_label], ["Perms:", $perms_info_label], ["Range:", $range_info_label]) {
        my $lbl = Gtk2::Label->new; $lbl->set_markup("<b>$item->[0]</b> ");
        $infobar_hbox->pack_start($lbl, FALSE, FALSE, 5);
        $infobar_hbox->pack_start($item->[1], FALSE, FALSE, 5);
    }
    $vbox->pack_start($infobar_hbox, FALSE, FALSE, 5);
    $popup_state->{infobar_labels} = { path => $path_info_label, size => $size_info_label, perms => $perms_info_label, range => $range_info_label };

    # --- Image Drawing Area ---
    $popup_state->{drawing_area} = Gtk2::DrawingArea->new;
    $popup_state->{drawing_area}->set_events(['button-press-mask', 'pointer-motion-mask']);
    $popup_state->{drawing_area}->signal_connect(button_press_event => \&on_image_button_press, $popup_state);
    $popup_state->{drawing_area}->signal_connect(expose_event => \&draw_image_with_overlays, $popup_state);
    $popup_state->{drawing_area}->signal_connect(motion_notify_event => \&on_image_motion, $popup_state);
    $popup_state->{drawing_area}->set_has_tooltip(TRUE);
    $popup_state->{drawing_area}->signal_connect(query_tooltip => \&on_query_image_tooltip, $popup_state);

    my $sw = Gtk2::ScrolledWindow->new(undef, undef);
    $sw->set_policy('automatic', 'automatic');
    $sw->add_with_viewport($popup_state->{drawing_area});
    $vbox->pack_start($sw, TRUE, TRUE, 0);

    my $hadjustment = $sw->get_hadjustment;
    my $vadjustment = $sw->get_vadjustment;
    my $update_thumb_cb = sub { if (my $thumb_da = $popup_state->{thumbnail_window_ref}) { $thumb_da->queue_draw(); } };
    $hadjustment->signal_connect(value_changed => $update_thumb_cb);
    $vadjustment->signal_connect(value_changed => $update_thumb_cb);

    # --- Setup & Update logic ---
    my $update_image = sub {
        my $w = $width_adj->get_value;
        $w = int($w / 4) * 4; $w = 4 if $w < 4;
        my $pixbuf = raw24_to_pixbuf($popup_state->{raw_data_ref}, $w);
        if ($pixbuf) {
            $popup_state->{pixbuf} = $pixbuf;
            $popup_state->{current_width} = $w;
            $popup_state->{drawing_area}->set_size_request($pixbuf->get_width, $pixbuf->get_height);
            $popup_state->{drawing_area}->queue_draw();
            $info_label->set_markup(sprintf(
                "<tt>%s  |  %s read  |  %dx%d px  |  method: %s</tt>",
                $title_info->{title},
                format_bytes(length(${$popup_state->{raw_data_ref}})),
                $pixbuf->get_width, $pixbuf->get_height, $state->{read_method}));
            if (my $thumb = $popup_state->{thumbnail_window_ref}) {
                $thumb->queue_draw();
            }
        }
    };

    my $init_pixels = int(length($$raw_ref) / 3);
    my $init_width  = int(sqrt($init_pixels));
    $init_width = int($init_width / 4) * 4; $init_width = 4 if $init_width < 4;
    $width_adj->set_value($init_width);
    $update_image->();

    # --- Event Binding ---
    $width_adj->signal_connect(value_changed => $update_image);
    $btn_autowidth->signal_connect(clicked => sub {
        my $w = int(sqrt(int(length(${$popup_state->{raw_data_ref}}) / 3)));
        $w = int($w / 4) * 4; $w = 4 if $w < 4;
        $width_adj->set_value($w);
    });

    $btn_save_png->signal_connect(clicked => sub {
        my ($data_ref_for_action, $filename_offsets, $range_map, $path_info) = get_data_for_action($popup_state);
        return unless (defined $data_ref_for_action and length ${$data_ref_for_action} > 0);
        
        my $pixbuf_to_save = create_pixbuf_from_data($data_ref_for_action);
        return unless $pixbuf_to_save;

        my $chooser = Gtk2::FileChooserDialog->new("Save Memory Image", $popup, 'save', 'gtk-cancel' => 'cancel', 'gtk-save' => 'accept');
        $chooser->set_current_name(_generate_filename($popup_state, $filename_offsets, $path_info) . ".png");
        if ($chooser->run eq 'accept') {
            my $fn = $chooser->get_filename; $fn .= ".png" unless $fn =~ /\.png$/i;
            eval { $pixbuf_to_save->save($fn, 'png'); };
        }
        $chooser->destroy;
    });

    $btn_save_raw->signal_connect(clicked => sub {
        my ($data_ref_for_action, $filename_offsets, $range_map, $path_info) = get_data_for_action($popup_state);
        return unless (defined $data_ref_for_action and length ${$data_ref_for_action} > 0);
        my $chooser = Gtk2::FileChooserDialog->new("Save Raw Dump", $popup, 'save', 'gtk-cancel' => 'cancel', 'gtk-save' => 'accept');
        $chooser->set_current_name(_generate_filename($popup_state, $filename_offsets, $path_info) . ".raw");
        if ($chooser->run eq 'accept') {
            my $fn = $chooser->get_filename;
            if (open(my $fh, '>', $fn)) { binmode $fh; print $fh ${$data_ref_for_action}; close $fh; }
        }
        $chooser->destroy;
    });

    $btn_load_raw->signal_connect(clicked => sub {
        my $chooser = Gtk2::FileChooserDialog->new("Load Raw Dump", $popup, 'open', 'gtk-cancel' => 'cancel', 'gtk-open' => 'accept');
        if ($chooser->run eq 'accept') {
            my $fn = $chooser->get_filename;
            if (open(my $fh, '<', $fn)) {
                binmode $fh; my $d; read $fh, $d, -s $fh; close $fh;
                $popup_state->{loaded_raw_data_ref} = \$d;
                $status_box->modify_bg('normal', Gtk2::Gdk::Color->parse('green'));
                $diff_button->show; $diff_toggle->show;
            }
        }
        $chooser->destroy;
    });

    $diff_button->signal_connect(clicked => sub { perform_ram_diff($popup_state); $diff_toggle->set_active(TRUE); });
    $strings_button->signal_connect(clicked => \&show_strings_view, $popup_state);
    
    my $aplay_path = _find_executable('aplay');
    unless ($aplay_path) {
        $sound_button->set_sensitive(FALSE); $save_wav_button->set_sensitive(FALSE);
    }
    $sound_button->signal_connect(toggled => \&on_sound_toggle, $popup_state);
    
    $save_wav_button->signal_connect(clicked => sub {
        my ($data_ref_for_action, $filename_offsets, $range_map, $path_info) = get_data_for_action($popup_state);
        return unless (defined $data_ref_for_action and length ${$data_ref_for_action} > 0);
        my $chooser = Gtk2::FileChooserDialog->new("Save as WAV", $popup, 'save', 'gtk-cancel' => 'cancel', 'gtk-save' => 'accept');
        $chooser->set_current_name(_generate_filename($popup_state, $filename_offsets, $path_info) . ".wav");
        if ($chooser->run eq 'accept') {
            my $fn = $chooser->get_filename; $fn .= ".wav" unless $fn =~ /\.wav$/i;
            if (open(my $fh, '>', $fn)) { binmode $fh; print $fh _create_wav_header(length ${$data_ref_for_action}); print $fh ${$data_ref_for_action}; close $fh; }
        }
        $chooser->destroy;
    });

    $overlay_toggle->signal_connect(toggled => sub { $popup_state->{drawing_area}->queue_draw(); });
    $diff_toggle->signal_connect(toggled => sub { $popup_state->{drawing_area}->queue_draw(); });
    $selection_toggle->signal_connect(toggled => sub { _update_selection_visuals($popup_state); });
    $start_entry->signal_connect(activate => \&on_offset_entry_activate, $popup_state);
    $end_entry->signal_connect(activate => \&on_offset_entry_activate, $popup_state);
    
    $add_more_button->signal_connect(clicked => sub { _add_extra_selection_ui($extra_selections_hbox, $popup_state); });

    $infobar_toggle->signal_connect(toggled => sub {
        my $btn = shift;
        if ($btn->get_active) { $infobar_hbox->show(); } else { $infobar_hbox->hide(); }
    });

    $thumbnail_check->signal_connect(toggled => sub {
        my $btn = shift;
        if ($btn->get_active) {
            show_thumbnail_view($popup, $popup_state, $hadjustment, $vadjustment);
        } else {
            if (my $thumb_da = $popup_state->{thumbnail_window_ref}) {
                if (my $win = $thumb_da->get_toplevel) { $win->destroy; }
            }
        }
    });

    $decode_button->signal_connect(clicked => sub { on_bitmap_decode_click($popup, $popup_state); });
    $digraph_button->signal_connect(clicked => sub { show_digraph_view($popup, $popup_state); });
    $photorec_button->signal_connect(clicked => sub { on_photorec_click($popup, $popup_state); });

    $update_toggle->signal_connect(toggled => sub {
        my $button = shift;
        if ($button->get_active) {
            my $sec = $update_entry->get_text;
            if ($sec =~ /^\d*\.?\d+$/ && $sec > 0) {
                $button->set_label("Stop Updating");
                my $baseline = ${$popup_state->{raw_data_ref}};
                $popup_state->{update_baseline_ref} = \$baseline;
                $popup_state->{loaded_raw_data_ref} = undef;
                $popup_state->{diff_points} = undef;
                $diff_button->hide; $diff_toggle->hide;
                $status_box->modify_bg('normal', Gtk2::Gdk::Color->parse('red'));
                $popup_state->{drawing_area}->queue_draw();
                
                $popup_state->{timer_id} = Glib::Timeout->add($sec * 1000, sub {
                    my $start_idx = $popup_state->{image_map_info}[0]{idx};
                    my $end_idx   = $popup_state->{image_map_info}[-1]{idx};
                    my $offset    = $state->{regions}[$start_idx]{start} * 4096;
                    my $size      = length(${$popup_state->{raw_data_ref}});
                    my $new_raw   = read_vram($offset, $size);

                    if ($new_raw) {
                        if (defined $popup_state->{sound_player_pid}) { kill 'TERM', -($popup_state->{sound_player_pid}); }
                        $popup_state->{raw_data_ref} = $new_raw;
                        $update_image->();
                        if ($popup_state->{diff_toggle_button} && $popup_state->{diff_toggle_button}->get_active) {
                            perform_ram_diff($popup_state);
                        }
                        return TRUE;
                    }
                    $button->set_label("Read Failed");
                    $button->set_sensitive(FALSE);
                    return FALSE;
                });
            } else { $button->set_active(FALSE); }
        } else {
            if (defined $popup_state->{timer_id}) {
                Glib::Source->remove($popup_state->{timer_id});
                $popup_state->{timer_id} = undef;
            }
            $button->set_label("Start Updating");
            if (defined $popup_state->{update_baseline_ref}) {
                $popup_state->{loaded_raw_data_ref} = $popup_state->{update_baseline_ref};
                $popup_state->{update_baseline_ref} = undef;
                $status_box->modify_bg('normal', Gtk2::Gdk::Color->parse('green'));
                $diff_button->show; $diff_toggle->show;
            }
        }
    });

    $popup->signal_connect(destroy => sub {
        if (defined $popup_state->{timer_id}) { Glib::Source->remove($popup_state->{timer_id}); }
        if (defined $popup_state->{star_timer_id}) { Glib::Source->remove($popup_state->{star_timer_id}); }
        if (defined $popup_state->{sound_player_pid}) {
            kill 'TERM', -($popup_state->{sound_player_pid});
            unlink $popup_state->{sound_temp_file} if defined $popup_state->{sound_temp_file};
        }
        if (my $thumb_da = $popup_state->{thumbnail_window_ref}) {
            my $win = $thumb_da->get_toplevel; $win->destroy if $win;
        }
        $popup_state->{raw_data_ref}        = undef;
        $popup_state->{loaded_raw_data_ref} = undef;
        $popup_state->{pixbuf}              = undef;
        $popup_state->{update_baseline_ref} = undef;
        $popup_state->{image_map_info}      = [];
        $popup_state->{diff_points}         = undef;
    });

    $diff_button->hide(); $diff_toggle->hide(); $infobar_hbox->hide();
    $popup->show_all;
    $diff_button->hide(); $diff_toggle->hide(); $infobar_hbox->hide();
}


# ---------------------------------------------------------------------------
# Interaction & Sub-Views for VRAM Image Popup
# ---------------------------------------------------------------------------
sub on_image_motion {
    my ($widget, $event, $popup_state) = @_;
    my ($x, $y) = ($event->x, $event->y);
    my $labels = $popup_state->{infobar_labels};
    my $found = FALSE;

    for my $area (@{$popup_state->{overlay_map_areas}}) {
        for my $rect (@{$area->{rects}}) {
            my ($rx, $ry, $rw, $rh) = @$rect;
            if ($x >= $rx && $x < ($rx + $rw) && $y >= $ry && $y < ($ry + $rh)) {
                my $seg = $area->{data};
                $labels->{path}->set_text($seg->{path} || "");
                $labels->{size}->set_text(format_bytes($seg->{size}));
                $labels->{perms}->set_text($seg->{perms});
                $labels->{range}->set_text(sprintf("0x%x - 0x%x", $seg->{start}, $seg->{end}));
                $found = TRUE; last; 
            }
        }
        last if $found;
    }
    if (!$found) {
        $labels->{path}->set_text(""); $labels->{size}->set_text("");
        $labels->{perms}->set_text(""); $labels->{range}->set_text("");
    }
    return TRUE;
}

sub on_image_button_press {
    my ($widget, $event, $popup_state) = @_;
    if ($event->button == 1) {
        my ($x, $y) = ($event->x, $event->y);
        for my $area (@{$popup_state->{overlay_map_areas}}) {
            for my $rect (@{$area->{rects}}) {
                my ($rx, $ry, $rw, $rh) = @$rect;
                if ($x >= $rx && $x < ($rx + $rw) && $y >= $ry && $y < ($ry + $rh)) {
                    my $seg = $area->{data};
                    my $c_start = $seg->{offset_in_image};
                    my $c_end   = $seg->{offset_in_image} + $seg->{length_in_image} - 1;
                    $popup_state->{selection_start_offset} = $c_start;
                    $popup_state->{selection_end_offset}   = $c_end;
                    _update_selection_visuals($popup_state);
                    return TRUE;
                }
            }
        }
    }
    elsif ($event->button == 3) {
        my $w = $popup_state->{pixbuf}->get_width;
        my $byte_offset = POSIX::floor($event->y * $w + $event->x) * 3;
        if (!defined $popup_state->{selection_start_offset}) {
            $popup_state->{selection_start_offset} = $byte_offset;
            $popup_state->{selection_end_offset} = undef;
        } elsif (!defined $popup_state->{selection_end_offset}) {
            $popup_state->{selection_end_offset} = $byte_offset;
        } else {
            $popup_state->{selection_start_offset} = $byte_offset;
            $popup_state->{selection_end_offset} = undef;
        }
        _update_selection_visuals($popup_state);
        return TRUE;
    }
    return FALSE;
}

sub on_query_image_tooltip {
    my ($widget, $x, $y, $keyboard_mode, $tooltip, $popup_state) = @_;
    for my $area (@{$popup_state->{overlay_map_areas}}) {
        for my $rect (@{$area->{rects}}) {
            my ($rx, $ry, $rw, $rh) = @$rect;
            if ($x >= $rx && $x <= ($rx + $rw) && $y >= $ry && $y <= ($ry + $rh)) {
                my $seg = $area->{data};
                my $tooltip_text = sprintf("<b>Path:</b> %s\n<b>Size:</b> %s\n<b>Range:</b> 0x%x - 0x%x",
                    $seg->{path}, format_bytes($seg->{size}), $seg->{start}, $seg->{end});
                $tooltip->set_markup($tooltip_text);
                return TRUE;
            }
        }
    }
    return FALSE;
}

sub draw_image_with_overlays {
    my ($widget, $event, $popup_state) = @_;
    my $cr = Gtk2::Gdk::Cairo::Context->create($widget->window);
    unless ($popup_state->{pixbuf}) { $cr->set_source_rgb(0.1, 0.1, 0.1); $cr->paint; return TRUE; }
    $cr->set_source_pixbuf($popup_state->{pixbuf}, 0, 0); $cr->paint;

    my $image_width = $popup_state->{pixbuf}->get_width;
    $popup_state->{overlay_map_areas} = [];
    for my $seg (@{$popup_state->{image_map_info}}) {
        my $start_pixel = POSIX::floor($seg->{offset_in_image} / 3);
        my $end_pixel   = POSIX::floor(($seg->{offset_in_image} + $seg->{length_in_image}) / 3);
        my $start_y = POSIX::floor($start_pixel / $image_width); my $start_x = $start_pixel % $image_width;
        my $end_y = POSIX::floor($end_pixel / $image_width);     my $end_x = $end_pixel % $image_width;

        my @rects;
        if ($start_y == $end_y) { push @rects, [$start_x, $start_y, $end_x - $start_x + 1, 1]; }
        else {
            push @rects, [$start_x, $start_y, $image_width - $start_x, 1];
            if ($end_y > $start_y + 1) { push @rects, [0, $start_y + 1, $image_width, $end_y - ($start_y + 1)]; }
            push @rects, [0, $end_y, $end_x + 1, 1];
        }
        push @{$popup_state->{overlay_map_areas}}, { rects => \@rects, data => $seg };
    }
    
    if ($popup_state->{overlay_toggle_button} && $popup_state->{overlay_toggle_button}->get_active) {
        $cr->set_source_rgba(1.0, 1.0, 0.0, 0.15); # default yellow tint for VRAM overlays
        for my $area (@{$popup_state->{overlay_map_areas}}) {
            for my $rect (@{$area->{rects}}) { $cr->rectangle(@$rect); $cr->fill; }
        }
    }
    
    if (defined $popup_state->{diff_points} && $popup_state->{diff_toggle_button} && $popup_state->{diff_toggle_button}->get_active) {
        $cr->set_source_rgba(1, 0, 1, 0.6);
        for my $offset (@{$popup_state->{diff_points}}) {
            my $pixel_index = POSIX::floor($offset / 3);
            my $x = $pixel_index % $image_width;
            my $y = POSIX::floor($pixel_index / $image_width);
            $cr->rectangle($x - 1, $y - 1, 3, 3);
            $cr->fill;
        }
    }

    if (@{$popup_state->{selection_highlight_rects}}) {
        $cr->set_source_rgba(0, 1, 1, 0.3);
        for my $rect (@{$popup_state->{selection_highlight_rects}}) {
            $cr->rectangle(@$rect); $cr->fill;
        }
    }
    
    if (defined $popup_state->{selection_start_offset}) {
        my $p = POSIX::floor($popup_state->{selection_start_offset} / 3);
        _draw_x_marker($cr, $p % $image_width, POSIX::floor($p / $image_width));
    }
    if (defined $popup_state->{selection_end_offset}) {
        my $p = POSIX::floor($popup_state->{selection_end_offset} / 3);
        _draw_x_marker($cr, $p % $image_width, POSIX::floor($p / $image_width));
    }
    if (defined $popup_state->{star_info}) {
        _draw_star($cr, $popup_state->{star_info}->{x}, $popup_state->{star_info}->{y});
    }
    return TRUE;
}

# --- Action Extractors ---
sub get_data_for_action {
    my ($popup_state) = @_;
    my $concatenated_data = "";
    my @filename_parts;
    my @range_map; 
    my $first_offset;

    if ($popup_state->{selection_toggle_button}->get_active &&
        defined $popup_state->{selection_start_offset} && defined $popup_state->{selection_end_offset}) {
        my $start = $popup_state->{selection_start_offset};
        my $end = $popup_state->{selection_end_offset};
        ($start, $end) = ($end, $start) if $start > $end;
        my $length = $end - $start + 1;

        if ($length > 0 && $start + $length <= length(${$popup_state->{raw_data_ref}})) {
            $first_offset = $start unless defined $first_offset;
            push @range_map, [$start, $length, length($concatenated_data)];
            $concatenated_data .= substr(${$popup_state->{raw_data_ref}}, $start, $length);
            push @filename_parts, sprintf("0x%X-0x%X", $start, $end);
        }
    }

    for my $wset (@{$popup_state->{extra_selection_widgets}}) {
        if ($wset->{toggle}->get_active) {
            my $s = ($wset->{start_entry}->get_text =~ /^0x([0-9a-f]+)$/i) ? hex($1) : undef;
            my $e = ($wset->{end_entry}->get_text =~ /^0x([0-9a-f]+)$/i) ? hex($1) : undef;
            if (defined $s && defined $e) {
                ($s, $e) = ($e, $s) if $s > $e;
                my $len = $e - $s + 1;
                if ($len > 0 && $s + $len <= length(${$popup_state->{raw_data_ref}})) {
                    $first_offset = $s unless defined $first_offset;
                    push @range_map, [$s, $len, length($concatenated_data)];
                    $concatenated_data .= substr(${$popup_state->{raw_data_ref}}, $s, $len);
                    push @filename_parts, sprintf("0x%X-0x%X", $s, $e);
                }
            }
        }
    }
    
    my $path_info = (defined $first_offset) ? _get_segment_info_for_offset($popup_state, $first_offset) : "";

    if (length $concatenated_data > 0) {
        return (\$concatenated_data, join('_', @filename_parts), \@range_map, $path_info);
    } else {
        return ($popup_state->{raw_data_ref}, undef, undef, undef);
    }
}

sub _generate_filename {
    my ($popup_state, $offsets_str, $path_info) = @_;
    my $pname = $popup_state->{pname};
    $pname =~ s/[^A-Za-z0-9\._-]+/_/g;
    my @parts = ($pname, 'pid', $popup_state->{pid}, 'memory');
    push @parts, $offsets_str if defined $offsets_str and $offsets_str ne '';
    push @parts, $path_info if defined $path_info and $path_info ne '';
    return join('_', @parts);
}

sub _get_segment_info_for_offset {
    my ($popup_state, $target_offset) = @_;
    return "" unless defined $target_offset;
    for my $seg (@{$popup_state->{image_map_info}}) {
        my $start = $seg->{offset_in_image};
        my $end   = $start + $seg->{length_in_image};
        if ($target_offset >= $start && $target_offset < $end) {
            my $path = $seg->{path} || "anonymous";
            $path =~ s/\[heap\]/HEAP/i;
            $path =~ s/^.+?([^\/]+)$/$1/ if $path =~ m|/|; 
            $path =~ s/[^A-Za-z0-9\._-]+/_/g; 
            return $path;
        }
    }
    return "unknown-segment";
}

sub perform_ram_diff {
    my ($popup_state) = @_;
    return unless (defined $popup_state->{raw_data_ref} && defined $popup_state->{loaded_raw_data_ref});
    my $live_data = ${$popup_state->{raw_data_ref}};
    my $loaded_data = ${$popup_state->{loaded_raw_data_ref}};
    my @diffs;
    my $len = length($live_data) < length($loaded_data) ? length($live_data) : length($loaded_data);
    for my $i (0 .. $len - 1) {
        push @diffs, $i if substr($live_data, $i, 1) ne substr($loaded_data, $i, 1);
    }
    $popup_state->{diff_points} = \@diffs;
    $popup_state->{drawing_area}->queue_draw();
}

# --- Drawing Utilities ---
sub _draw_x_marker {
    my ($cr, $cx, $cy) = @_;
    my $size = 8;
    $cr->save; $cr->translate($cx, $cy);
    $cr->move_to(-$size, -$size); $cr->line_to($size, $size);
    $cr->move_to(-$size, $size);  $cr->line_to($size, -$size);
    $cr->set_line_width(5); $cr->set_source_rgb(1, 1, 1); $cr->stroke_preserve;
    $cr->set_line_width(2.5); $cr->set_source_rgb(0, 0, 0); $cr->stroke_preserve;
    $cr->set_line_width(1.5); $cr->set_source_rgba(1, 1, 0, 0.8); $cr->stroke;
    $cr->restore;
}

sub _draw_star {
    my ($cr, $cx, $cy) = @_;
    my ($radius1, $radius2, $points) = (12, 6, 5);
    $cr->save; $cr->translate($cx, $cy);
    $cr->move_to($radius1, 0);
    for my $i (1 .. $points * 2) {
        my $angle = $i * 3.14159265 / $points;
        my $r = ($i % 2) == 0 ? $radius1 : $radius2;
        $cr->line_to($r * cos($angle), $r * sin($angle));
    }
    $cr->close_path;
    $cr->set_line_width(5); $cr->set_source_rgb(1, 1, 1); $cr->stroke_preserve;
    $cr->set_line_width(2.5); $cr->set_source_rgb(0, 0, 0); $cr->stroke_preserve;
    $cr->set_source_rgb(1.0, 0.84, 0); $cr->fill;
    $cr->restore;
}

# --- Selection Extras ---
sub on_offset_entry_activate {
    my ($entry, $popup_state) = @_;
    $popup_state->{selection_start_offset} = ($popup_state->{start_offset_entry}->get_text =~ /^0x([0-9a-f]+)$/i) ? hex($1) : undef;
    $popup_state->{selection_end_offset}   = ($popup_state->{end_offset_entry}->get_text =~ /^0x([0-9a-f]+)$/i) ? hex($1) : undef;
    _update_selection_visuals($popup_state);
}

sub _update_selection_visuals {
    my ($popup_state) = @_;
    my ($s, $e) = ($popup_state->{selection_start_offset}, $popup_state->{selection_end_offset});
    $popup_state->{start_offset_entry}->set_text(defined $s ? sprintf("0x%X", $s) : '');
    $popup_state->{end_offset_entry}->set_text(defined $e ? sprintf("0x%X", $e) : '');

    my @all_rects;
    my $iw = $popup_state->{pixbuf}->get_width;
    my $calc_rects = sub {
        my ($s, $e) = @_; return () unless (defined $s && defined $e);
        ($s, $e) = ($e, $s) if $s > $e;
        my $sp = POSIX::floor($s/3); my $ep = POSIX::floor($e/3);
        my $sy = POSIX::floor($sp/$iw); my $sx = $sp % $iw;
        my $ey = POSIX::floor($ep/$iw); my $ex = $ep % $iw;
        my @rects;
        if ($sy == $ey) { push @rects, [$sx, $sy, $ex - $sx + 1, 1]; }
        else {
            push @rects, [$sx, $sy, $iw - $sx, 1];
            if ($ey > $sy + 1) { push @rects, [0, $sy + 1, $iw, $ey - ($sy + 1)]; }
            push @rects, [0, $ey, $ex + 1, 1];
        }
        return @rects;
    };

    push @all_rects, $calc_rects->($s, $e) if $popup_state->{selection_toggle_button}->get_active;
    for my $wset (@{$popup_state->{extra_selection_widgets}}) {
        if ($wset->{toggle}->get_active) {
            my $s = ($wset->{start_entry}->get_text =~ /^0x([0-9a-f]+)$/i) ? hex($1) : undef;
            my $e = ($wset->{end_entry}->get_text =~ /^0x([0-9a-f]+)$/i) ? hex($1) : undef;
            push @all_rects, $calc_rects->($s, $e);
        }
    }
    $popup_state->{selection_highlight_rects} = \@all_rects;
    $popup_state->{drawing_area}->queue_draw();
}

sub _add_extra_selection_ui {
    my ($container, $popup_state) = @_;
    my $hbox = Gtk2::HBox->new(FALSE, 5);
    my $s_entry = Gtk2::Entry->new(); $s_entry->set_width_chars(10);
    my $e_entry = Gtk2::Entry->new(); $e_entry->set_width_chars(10);
    my $toggle = Gtk2::CheckButton->new(); $toggle->set_active(TRUE);
    $hbox->pack_start(Gtk2::Label->new("Start:"), FALSE, FALSE, 5);
    $hbox->pack_start($s_entry, FALSE, FALSE, 0);
    $hbox->pack_start(Gtk2::Label->new("Stop:"), FALSE, FALSE, 5);
    $hbox->pack_start($e_entry, FALSE, FALSE, 0);
    $hbox->pack_start($toggle, FALSE, FALSE, 5);

    push @{$popup_state->{extra_selection_widgets}}, { start_entry => $s_entry, end_entry => $e_entry, toggle => $toggle };
    my $cb = sub { _update_selection_visuals($popup_state); };
    $s_entry->signal_connect(activate => $cb); $e_entry->signal_connect(activate => $cb); $toggle->signal_connect(toggled => $cb);
    $container->pack_start($hbox, FALSE, FALSE, 0);
    $container->show_all;
}

# --- Thumbnail Navigation ---
sub show_thumbnail_view {
    my ($parent, $popup_state, $hadj, $vadj) = @_;
    if (defined $popup_state->{thumbnail_window_ref}) {
        my $win = $popup_state->{thumbnail_window_ref}->get_toplevel; $win->present if $win; return;
    }
    my $full = $popup_state->{pixbuf}; return unless $full;
    my $tw = 500; my $ow = $full->get_width; my $oh = $full->get_height; return if $ow == 0;
    my $scale = $tw / $ow; my $th = int($oh * $scale);
    my $thumb_pixbuf = $full->scale_simple($tw, $th, 'bilinear');

    my $thumb_state = { pixbuf => $thumb_pixbuf, scale_x => $scale, scale_y => $th / $oh, hadj => $hadj, vadj => $vadj };
    my $win = Gtk2::Window->new('toplevel');
    $win->set_title("Thumbnail View"); $win->set_transient_for($parent); $win->set_position('center-on-parent');
    my $da = Gtk2::DrawingArea->new; $da->set_size_request($tw, $th); $da->set_events(['button-press-mask']);
    $da->signal_connect(expose_event => \&on_expose_thumbnail, $thumb_state);
    $da->signal_connect(button_press_event => \&on_thumbnail_click, $thumb_state);
    $win->add($da);
    $popup_state->{thumbnail_window_ref} = $da;
    $win->signal_connect(destroy => sub { $popup_state->{thumbnail_window_ref} = undef; });
    $win->show_all;
}

sub on_expose_thumbnail {
    my ($widget, $event, $ts) = @_;
    my $cr = Gtk2::Gdk::Cairo::Context->create($widget->window);
    $cr->set_source_pixbuf($ts->{pixbuf}, 0, 0); $cr->paint;

    my ($h_val, $v_val) = ($ts->{hadj}->get_value, $ts->{vadj}->get_value);
    my ($h_page, $v_page) = ($ts->{hadj}->get('page-size'), $ts->{vadj}->get('page-size'));
    my $rx = $h_val * $ts->{scale_x}; my $ry = $v_val * $ts->{scale_y};
    my $rw = $h_page * $ts->{scale_x}; my $rh = $v_page * $ts->{scale_y};

    $cr->set_source_rgba(1, 0, 1, 0.4); $cr->rectangle($rx, $ry, $rw, $rh); $cr->fill;
    $cr->set_source_rgba(1, 0, 1, 0.9); $cr->set_line_width(1.5); $cr->rectangle($rx, $ry, $rw, $rh); $cr->stroke;
    return TRUE;
}

sub on_thumbnail_click {
    my ($widget, $event, $ts) = @_;
    my $fx = $event->x / $ts->{scale_x}; my $fy = $event->y / $ts->{scale_y};
    my ($h_page, $v_page) = ($ts->{hadj}->get('page-size'), $ts->{vadj}->get('page-size'));
    my $nx = $fx - ($h_page / 2); my $ny = $fy - ($v_page / 2);
    $nx = 0 if $nx < 0; $ny = 0 if $ny < 0;
    my $mx = $ts->{hadj}->get('upper') - $h_page; my $my = $ts->{vadj}->get('upper') - $v_page;
    $nx = $mx if $nx > $mx; $ny = $my if $ny > $my;
    $ts->{hadj}->set_value($nx); $ts->{vadj}->set_value($ny);
    return TRUE;
}

# --- Sound (aplay) ---
sub _create_wav_header {
    my ($data_length) = @_;
    my $header;
    $header .= pack('A4', 'RIFF'); $header .= pack('V', 36 + $data_length);
    $header .= pack('A4', 'WAVE'); $header .= pack('A4', 'fmt ');
    $header .= pack('V', 16); $header .= pack('v', 1); $header .= pack('v', 1);
    $header .= pack('V', 8000); $header .= pack('V', 8000);
    $header .= pack('v', 1); $header .= pack('v', 8);
    $header .= pack('A4', 'data'); $header .= pack('V', $data_length);
    return $header;
}

sub on_sound_finished {
    my ($pid, $status, $data) = @_;
    my ($button, $popup_state) = @$data;
    return unless (defined $popup_state->{sound_player_pid} && $popup_state->{sound_player_pid} == $pid);
    $popup_state->{_is_changing_sound_button} = TRUE;
    $button->set_active(FALSE);
    $popup_state->{_is_changing_sound_button} = FALSE;
    Glib::Source->remove($popup_state->{sound_child_watch_id}) if defined $popup_state->{sound_child_watch_id};
    unlink $popup_state->{sound_temp_file} if defined $popup_state->{sound_temp_file};
    $popup_state->{sound_player_pid} = undef; $popup_state->{sound_child_watch_id} = undef; $popup_state->{sound_temp_file} = undef;
}

sub on_sound_toggle {
    my ($button, $popup_state, $data_getter) = @_;
    $data_getter ||= \&get_data_for_action;
    return if $popup_state->{_is_changing_sound_button};

    if ($button->get_active) {
        return if defined $popup_state->{sound_player_pid};
        my ($data_ref) = $data_getter->($popup_state);
        unless (length ${$data_ref} > 0) {
            $popup_state->{_is_changing_sound_button} = TRUE; $button->set_active(FALSE); $popup_state->{_is_changing_sound_button} = FALSE;
            return;
        }
        my ($fh, $fn) = tempfile(UNLINK => 0); binmode $fh; print $fh ${$data_ref}; close $fh;
        $popup_state->{sound_temp_file} = $fn; chmod 0644, $fn;
        
        my $user = qx(loginctl list-sessions --no-legend | grep "seat0" | awk '{print \$3}'); chomp $user;
        my $uid = $user ? getpwnam($user) : undef;
        my $pid = fork(); die "Cannot fork: $!" unless defined $pid;

        if ($pid == 0) {
            setpgrp(0, 0);
            if ($user && defined $uid) {
                exec("sudo", "-u", $user, "env", "XDG_RUNTIME_DIR=/run/user/$uid", "aplay", "-q", "-f", "U8", "-r", "8000", "-c", "1", $fn);
            } else {
                exec('aplay', '-q', '-f', 'U8', '-r', '8000', '-c', '1', $fn);
            }
            die "Failed to exec aplay: $!";
        } else {
            $popup_state->{sound_player_pid} = $pid;
            $popup_state->{sound_child_watch_id} = Glib::Child->watch_add($pid, \&on_sound_finished, [$button, $popup_state]);
        }
    } else {
        kill 'TERM', -($popup_state->{sound_player_pid}) if defined $popup_state->{sound_player_pid};
    }
}

# --- Strings & Search ---
sub show_strings_view {
    my ($button, $popup_state) = @_;
    my $parent_window = $button->get_toplevel;
    my ($data_ref, undef, $range_map_ref, undef) = get_data_for_action($popup_state);
    return unless defined $data_ref && ${$data_ref};

    my ($fh, $fn) = tempfile(UNLINK => 1); binmode $fh; print $fh ${$data_ref}; close $fh;
    my $out = `strings -n 8 "$fn"`;

    my $win = Gtk2::Window->new('toplevel');
    $win->set_title("Strings"); $win->set_transient_for($parent_window);
    $win->set_default_size(700, 500); $win->set_position('center-on-parent');
    my $vbox = Gtk2::VBox->new(FALSE, 5); $win->add($vbox);

    my $search_hbox = Gtk2::HBox->new(FALSE, 5);
    my $entry = Gtk2::Entry->new(); my $btn = Gtk2::Button->new_from_stock('gtk-find');
    $search_hbox->pack_start(Gtk2::Label->new("Find:"), FALSE, FALSE, 0);
    $search_hbox->pack_start($entry, TRUE, TRUE, 0); $search_hbox->pack_start($btn, FALSE, FALSE, 0);
    $vbox->pack_start($search_hbox, FALSE, FALSE, 0);

    my $sw = Gtk2::ScrolledWindow->new(undef, undef); $sw->set_policy('automatic', 'automatic');
    $vbox->pack_start($sw, TRUE, TRUE, 0);
    my $tv = Gtk2::TextView->new(); $tv->set_editable(FALSE); $tv->set_cursor_visible(TRUE);
    my $buffer = $tv->get_buffer(); $buffer->set_text($out || "No strings found.");
    $sw->add($tv);

    my $search_state = { original_text => $out, last_search_term => undef, byte_offsets => [], current_match_index => -1 };
    my $cb_data = [$entry, $tv, $search_state, $popup_state, $data_ref, $range_map_ref];
    $btn->signal_connect(clicked => \&search_and_highlight, $cb_data);
    $entry->signal_connect(activate => \&search_and_highlight, $cb_data);
    $win->show_all;
}

sub _translate_concatenated_offset {
    my ($rel, $range_map_ref) = @_;
    return $rel unless (defined $range_map_ref && @$range_map_ref);
    for my $r (reverse @$range_map_ref) {
        my ($abs_s, $len, $concat_s) = @$r;
        if ($rel >= $concat_s) { return $abs_s + ($rel - $concat_s); }
    }
    return $rel;
}

sub search_and_highlight {
    my ($widget, $data) = @_;
    my ($entry, $tv, $s_state, $p_state, $data_ref, $range_map_ref) = @$data;
    my $buffer = $tv->get_buffer(); my $term = $entry->get_text();

    unless (length $term) {
        $buffer->set_text($s_state->{original_text} || "No strings found.");
        $s_state->{last_search_term} = undef; $s_state->{byte_offsets} = []; $s_state->{current_match_index} = -1;
        return;
    }
    
    if (!defined $s_state->{last_search_term} || $term ne $s_state->{last_search_term}) {
        $s_state->{byte_offsets} = []; $s_state->{current_match_index} = -1; $s_state->{last_search_term} = $term;
        my $concat = ${$data_ref}; my $rel = -1; my $res = "";
        while (($rel = CORE::index($concat, $term, $rel + 1)) != -1) {
            my $abs = _translate_concatenated_offset($rel, $range_map_ref);
            push @{$s_state->{byte_offsets}}, $abs;
            my $c_start = $abs - 40; $c_start = 0 if $c_start < 0;
            my $ctx = substr(${$p_state->{raw_data_ref}}, $c_start, length($term) + 80);
            $ctx =~ s/[^\x20-\x7E]/./g;
            $res .= sprintf("0x%X: %s\n", $abs, $ctx);
        }
        $buffer->set_text($res || "String not found.");
    }
    return unless @{$s_state->{byte_offsets}};

    $s_state->{current_match_index} = ($s_state->{current_match_index} + 1) % @{$s_state->{byte_offsets}};
    my $idx = $s_state->{current_match_index};

    my ($si, $ei) = $buffer->get_bounds();
    my $tt = $buffer->get_tag_table(); my $tag = $tt->lookup('hl');
    unless ($tag) { $tag = Gtk2::TextTag->new('hl'); $tag->set_property('background', 'yellow'); $tt->add($tag); }
    $buffer->remove_tag_by_name('hl', $si, $ei);
    my $lsi = $buffer->get_iter_at_line($idx); my $lei = $lsi->copy; $lei->forward_to_line_end();
    $buffer->apply_tag_by_name('hl', $lsi, $lei);
    $tv->scroll_to_iter($lsi, 0.0, TRUE, 0.0, 0.5);

    my $byte = $s_state->{byte_offsets}->[$idx];
    if (defined $byte && defined $p_state->{pixbuf}) {
        my $w = $p_state->{pixbuf}->get_width; my $px = POSIX::floor($byte / 3);
        $p_state->{star_info} = { x => $px % $w, y => POSIX::floor($px / $w) };
        if (defined $p_state->{star_timer_id}) { Glib::Source->remove($p_state->{star_timer_id}); }
        $p_state->{star_timer_id} = Glib::Timeout->add(15000, sub { $p_state->{star_info} = undef; $p_state->{drawing_area}->queue_draw(); return FALSE; });
        $p_state->{drawing_area}->queue_draw();
        
        # Scroll image to star
        if (my $da = $p_state->{drawing_area}) {
            if (my $vp = $da->get_parent) {
                if (my $sw = $vp->get_parent) {
                    my $vadj = $sw->get_vadjustment; my $hadj = $sw->get_hadjustment;
                    my $ny = $p_state->{star_info}->{y} - ($vadj->get('page-size') / 2); $ny = 0 if $ny < 0; $vadj->set_value($ny);
                    my $nx = $p_state->{star_info}->{x} - ($hadj->get('page-size') / 2); $nx = 0 if $nx < 0; $hadj->set_value($nx);
                }
            }
        }
    }
}

# --- Digraph Generator ---
sub _generate_digraph_pixbuf {
    my ($digraph_state, $is_weighted) = @_;
    my $counts = $digraph_state->{counts};
    my $max_count = $digraph_state->{max_count};
    my $pixels = "\0" x (256 * 256 * 3);
    my $log_max = ($max_count > 0) ? log(1 + $max_count) : 1;

    for my $y (0 .. 255) {
        for my $x (0 .. 255) {
            my $c = $counts->[$y][$x]; next if $c == 0;
            my $g = $is_weighted ? int((log(1 + $c) / $log_max) * 255) : 255;
            $g = 255 if $g > 255;
            substr($pixels, $y * 256 * 3 + $x * 3 + 1, 1) = pack('C', $g);
        }
    }
    return Gtk2::Gdk::Pixbuf->new_from_data($pixels, 'rgb', 0, 8, 256, 256, 256 * 3);
}

sub show_digraph_view {
    my ($parent, $popup_state) = @_;
    my ($data_ref, $offs_str, undef, $path_info) = get_data_for_action($popup_state);
    return unless defined $data_ref && length($$data_ref) > 1;

    my $info_text = _generate_filename($popup_state, $offs_str, $path_info);
    my @counts = map { [ (0) x 256 ] } (0..255);
    my $max = 0; my @bytes = unpack 'C*', $$data_ref;
    for my $i (0 .. $#bytes - 1) {
        my $x = $bytes[$i]; my $y = $bytes[$i+1];
        $counts[$y][$x]++; $max = $counts[$y][$x] if $counts[$y][$x] > $max;
    }

    my $ds = { pixbuf => undef, counts => \@counts, max_count => $max };
    $ds->{pixbuf} = _generate_digraph_pixbuf($ds, FALSE);

    my $win = Gtk2::Window->new('toplevel');
    $win->set_title("Digraph View"); $win->set_transient_for($parent); $win->set_position('center-on-parent');
    my $vbox = Gtk2::VBox->new(FALSE, 5); $win->add($vbox);
    
    my $hbox = Gtk2::HBox->new(FALSE, 5);
    my $w_tog = Gtk2::ToggleButton->new("Weighted");
    my $btn_png = Gtk2::Button->new_from_stock('gtk-save');
    $hbox->pack_start($w_tog, FALSE, FALSE, 0);
    $hbox->pack_start($btn_png, FALSE, FALSE, 5);
    $vbox->pack_start($hbox, FALSE, FALSE, 0);
    
    my $img = Gtk2::Image->new_from_pixbuf($ds->{pixbuf});
    $vbox->pack_start($img, TRUE, TRUE, 5);

    $w_tog->signal_connect(toggled => sub {
        my $new_pb = _generate_digraph_pixbuf($ds, shift->get_active);
        $ds->{pixbuf} = $new_pb; $img->set_from_pixbuf($new_pb);
    });

    $btn_png->signal_connect(clicked => sub {
        my $chooser = Gtk2::FileChooserDialog->new("Save Digraph", $win, 'save', 'gtk-cancel' => 'cancel', 'gtk-save' => 'accept');
        $chooser->set_current_name($info_text . "_digraph.png");
        if ($chooser->run eq 'accept') { eval { $ds->{pixbuf}->save($chooser->get_filename, 'png'); }; }
        $chooser->destroy;
    });

    $win->show_all;
}

# --- Bitmap Decoder ---
sub on_bitmap_decode_click {
    my ($parent, $popup_state) = @_;
    my ($data_ref) = get_data_for_action($popup_state);
    return unless defined $data_ref && $$data_ref;
    show_bitmap_decode_result($parent, $popup_state, $data_ref, 200, 200, 800);
}

sub run_bitmap_decode {
    my ($raw_ref, $min_w, $max_w, $prog_cb, $cancel_ref) = @_;
    my $raw = $$raw_ref; my $sz = length($raw);
    return -1 if $sz < ($min_w * 3 * 2);

    my $pdl = PDL->pdl( unpack 'C*', $raw );
    my $best_w = -1; my $low_score = -1;
    $prog_cb->("\n--- Starting Bitmap Analysis ---\n") if $prog_cb;

    for my $w ($min_w .. $max_w) {
        if ($cancel_ref && $$cancel_ref) { $prog_cb->("\nCancelled.\n") if $prog_cb; return undef; }
        my $num_pix = $sz / 3; next if $w > $num_pix;
        my $h = int($num_pix / $w); next if $h < 2;

        my $trimmed = $w * $h * 3;
        my $img_pdl = $pdl->slice("0:" . ($trimmed - 1));
        my $reshaped = $img_pdl->reshape(3 * $w, $h);
        my $score = sum(abs($reshaped->slice(":,0:-2") - $reshaped->slice(":,1:-1")));
        my $norm = $score / $h;

        $prog_cb->(sprintf("Width: %4d | Height: %4d | Score: %.2f\n", $w, $h, $norm)) if $prog_cb;
        if ($low_score == -1 || $norm < $low_score) { $low_score = $norm; $best_w = $w; }
    }
    $prog_cb->(sprintf("\nComplete. Best Width: %d (Score: %.2f)\n", $best_w, $low_score)) if $prog_cb;
    return $best_w;
}

sub show_bitmap_decode_result {
    my ($parent, $main_state, $raw_ref, $init_w, $min_w, $max_w) = @_;
    my $win = Gtk2::Window->new('toplevel'); $win->set_transient_for($parent); $win->set_position('center-on-parent');
    my $vbox = Gtk2::VBox->new(FALSE, 2); $win->add($vbox);
    my $raw = $$raw_ref; my $sz = length($raw); my $curr_pb; my $img = Gtk2::Image->new;
    
    my $tb1 = Gtk2::HBox->new(FALSE, 5); $tb1->set_border_width(5);
    my $btn_save = Gtk2::Button->new("Save PNG"); my $btn_find = Gtk2::Button->new("Find Width");
    my $w_adj = Gtk2::Adjustment->new($init_w, $min_w, $max_w + 1, 1, 10, 0);
    my $min_spin = Gtk2::SpinButton->new(Gtk2::Adjustment->new($min_w, 8, 8192, 1, 10, 0), 0, 0); $min_spin->set_width_chars(5);
    my $max_spin = Gtk2::SpinButton->new(Gtk2::Adjustment->new($max_w, 8, 8192, 1, 10, 0), 0, 0); $max_spin->set_width_chars(5);
    my $lbl_w = Gtk2::Label->new($init_w); $lbl_w->set_width_chars(5);
    $tb1->pack_start($btn_save, FALSE, FALSE, 0); $tb1->pack_start($min_spin, FALSE, FALSE, 5);
    $tb1->pack_start(Gtk2::HScrollbar->new($w_adj), TRUE, TRUE, 5);
    $tb1->pack_start($max_spin, FALSE, FALSE, 5); $tb1->pack_start($lbl_w, FALSE, FALSE, 5);
    $tb1->pack_end($btn_find, FALSE, FALSE, 10); $vbox->pack_start($tb1, FALSE, FALSE, 0);

    my $tb2 = Gtk2::HBox->new(FALSE, 5); $tb2->set_border_width(5);
    my $off_adj = Gtk2::Adjustment->new(0, 0, 2048, 1, 10, 0);
    $tb2->pack_start(Gtk2::Label->new("Pixel Byte Offset: "), FALSE, FALSE, 0);
    $tb2->pack_start(Gtk2::SpinButton->new($off_adj, 0, 0), TRUE, TRUE, 5);
    
    my $fmt_store = Gtk2::ListStore->new('Glib::String', 'Glib::String', 'Glib::Int');
    my $fmt_combo = Gtk2::ComboBox->new_with_model($fmt_store);
    my $rend = Gtk2::CellRendererText->new; $fmt_combo->pack_start($rend, TRUE); $fmt_combo->add_attribute($rend, 'text', 0);
    for my $f (["24-bit RGB",'rgb24',3], ["32-bit RGBA",'rgba32',4], ["8-bit Grayscale",'gray8',1], ["16-bit Grayscale (LE)",'gray16le',2]) {
        $fmt_store->set($fmt_store->append, 0, $f->[0], 1, $f->[1], 2, $f->[2]);
    }
    $fmt_combo->set_active(0);
    $tb2->pack_start(Gtk2::Label->new("Format:"), FALSE, FALSE, 10); $tb2->pack_start($fmt_combo, FALSE, FALSE, 0);
    $vbox->pack_start($tb2, FALSE, FALSE, 0);

    my $sw = Gtk2::ScrolledWindow->new(undef, undef); $sw->set_policy('automatic', 'automatic');
    $sw->add_with_viewport($img); $vbox->pack_start($sw, TRUE, TRUE, 0);

    my $update = sub {
        my $nw = int($w_adj->get_value); my $bo = int($off_adj->get_value);
        my $iter = $fmt_combo->get_active_iter; return unless $iter;
        my ($fmt, $bpp) = $fmt_combo->get_model->get($iter, 1, 2); return if $nw <= 0 || $bpp <= 0;
        
        if ($bo >= $sz - ($nw * $bpp)) { $bo = $sz - ($nw * $bpp); $bo = 0 if $bo < 0; $off_adj->set_value($bo); }
        my $sliced = substr($raw, $bo); my $nh = int((length($sliced)/$bpp)/$nw); return if $nh <= 0;
        
        my $b_proc = $nw * $nh * $bpp; my $pd; my $alpha = FALSE;
        if ($fmt eq 'rgb24') { $pd = substr($sliced, 0, $b_proc); }
        elsif ($fmt eq 'rgba32') { $pd = substr($sliced, 0, $b_proc); $alpha = TRUE; }
        elsif ($fmt eq 'gray8') { $pd = pack 'C*', map {($_,$_,$_)} unpack 'C*', substr($sliced, 0, $b_proc); }
        elsif ($fmt eq 'gray16le') { $pd = pack 'C*', map {my $v=int($_/257);($v,$v,$v)} unpack 'v*', substr($sliced, 0, $b_proc); }
        
        return unless defined $pd && length $pd > 0;
        $curr_pb = eval { Gtk2::Gdk::Pixbuf->new_from_data($pd, 'rgb', $alpha, 8, $nw, $nh, $nw * ($alpha ? 4 : 3)); };
        if ($@ || !$curr_pb) { $img->clear; return; }
        $img->set_from_pixbuf($curr_pb); $lbl_w->set_text($nw); $win->set_title("Decoded Bitmap: $nw x $nh (Offset: $bo)");
    };

    $w_adj->signal_connect(value_changed => $update); $off_adj->signal_connect(value_changed => $update); $fmt_combo->signal_connect(changed => $update);
    $update->(); $win->set_default_size(800, 600); $win->show_all;
}

# --- Photorec ---
sub _find_executable {
    my ($cmd) = @_;
    return $cmd if ($cmd =~ m#/# && -x $cmd && !-d $cmd);
    for my $p (split(/:/, $ENV{PATH} || ''), '/usr/sbin', '/sbin') {
        my $f = File::Spec->catfile($p, $cmd); return $f if -x $f && !-d $f;
    }
    return;
}

sub on_photorec_click {
    my ($parent, $p_state) = @_;
    my $pr = _find_executable('photorec');
    unless ($pr) { Gtk2::MessageDialog->new($parent,'destroy-with-parent','error','close',"Required command not found: 'photorec'")->run->destroy; return; }
    
    my $user = $ENV{SUDO_USER};
    unless ($user) { Gtk2::MessageDialog->new($parent,'destroy-with-parent','error','close',"Cannot determine original user via SUDO_USER.")->run->destroy; return; }
    my ($uid, $gid) = (getpwnam $user)[2,3];

    my ($data_ref, $offs, undef, $path) = get_data_for_action($p_state);
    return unless defined $data_ref && $$data_ref;

    my $out_dir = File::Spec->catfile(cwd(), 'captures', _generate_filename($p_state, $offs, $path));
    eval { make_path($out_dir); chown $uid, $gid, $out_dir; };
    if ($@) { Gtk2::MessageDialog->new($parent,'destroy-with-parent','error','close',"Cannot create output dir: $@")->run->destroy; return; }

    my ($fh, $tmp) = tempfile(UNLINK => 0); binmode $fh; print $fh $$data_ref; close $fh; chmod 0644, $tmp;

    my $win = Gtk2::Window->new('toplevel'); $win->set_title("photorec results"); $win->set_transient_for($parent); $win->set_default_size(600, 400);
    my $vbox = Gtk2::VBox->new(FALSE, 5); $win->add($vbox);
    my $sw = Gtk2::ScrolledWindow->new(undef,undef); $sw->set_policy('automatic', 'automatic'); $vbox->pack_start($sw, TRUE, TRUE, 0);
    my $tv = Gtk2::TextView->new(); $tv->set_editable(FALSE); my $buf = $tv->get_buffer; $buf->set_text("Starting photorec...\n"); $sw->add($tv);
    $win->show_all;

    my $args = 'partition_none,options,paranoid_no,keep_corrupted_file,wholespace,fileopt,everything,enable,gsm,disable,dovecot,disable,search';
    pipe(my $rd, my $wr); my $pid = fork(); die "Cannot fork: $!" unless defined $pid;

    if ($pid == 0) {
        close $rd; open STDOUT, '>&', $wr; open STDERR, '>&', $wr;
        exec('sudo', '-u', $user, $pr, '/d', "$out_dir/", '/log', '/cmd', $tmp, $args);
        die "Exec failed";
    }
    close $wr;
    my $io_id = Glib::IO->add_watch(fileno($rd), ['in', 'hup'], sub {
        my $l = <$rd>; return FALSE unless defined $l; $buf->insert($buf->get_end_iter, $l); return TRUE;
    });
    my $ch_id = Glib::Child->watch_add($pid, sub {
        Glib::Source->remove($io_id) if defined $io_id; close($rd); unlink($tmp);
        $buf->insert($buf->get_end_iter, "\n--- Done ---\nCheck directory: $out_dir\n");
    });
}

# ---------------------------------------------------------------------------
# Data Parsing & Status
# ---------------------------------------------------------------------------
sub parse_vram_mm {
    my ($source) = @_;
    my @lines = defined $source ? do { open my $fh, '<', $source; <$fh> } : `cat "$VRAM_MM_PATH" 2>/dev/null`;
    if (!@lines && !defined $source) { open my $fh, '<', $VRAM_MM_PATH or return 0; @lines = <$fh>; close $fh; }
    
    @{$state->{regions}} = (); $state->{total_pages} = $state->{used_pages} = $state->{free_pages} = 0;
    for my $line (@lines) {
        chomp $line;
        if ($line =~ /^(0x[0-9a-f]+)-(0x[0-9a-f]+):\s*(\d+):\s*(used|free)/i) {
            my ($s, $e, $sz, $st) = (hex($1), hex($2), $3+0, $4);
            push @{$state->{regions}}, { start=>$s, end=>$e, size=>$sz, status=>$st };
            $state->{total_pages} += $sz;
            if ($st eq 'used') { $state->{used_pages} += $sz; } else { $state->{free_pages} += $sz; }
        }
    }
    $state->{vram_size_bytes} = $state->{regions}[-1]{end} * 4096 if @{$state->{regions}};
    return scalar @{$state->{regions}};
}

sub parse_gem_info {
    my ($source) = @_;
    my @lines = defined $source ? do { open my $fh, '<', $source; <$fh> } : `cat "$GEM_INFO_PATH" 2>/dev/null`;
    my (%gem, $c_pid, $c_cmd);
    for my $line (@lines) {
        chomp $line;
        if ($line =~ /^pid\s+(\d+)\s+command\s+(\S+):/) { ($c_pid, $c_cmd) = ($1, $2); $gem{$c_pid} //= { cmd=>$c_cmd, bos=>[], total_vram=>0 }; }
        elsif (defined $c_pid && $line =~ /^\s+(0x[0-9a-f]+):\s+(\d+)\s+byte\s+(VRAM|GTT|CPU)/i) {
            my ($h, $b, $t) = ($1, $2+0, $3); my $f = ($line =~ /byte\s+\S+\s+(.+)/) ? $1 : '';
            push @{$gem{$c_pid}{bos}}, { handle=>$h, bytes=>$b, type=>$t, flags=>$f };
            $gem{$c_pid}{total_vram} += $b if $t eq 'VRAM';
        }
    }
    return \%gem;
}

sub push_status {
    my ($msg) = @_; debug("STATUS: $msg");
    if ($state->{statusbar}) { $state->{statusbar}->pop($state->{status_context}); $state->{statusbar}->push($state->{status_context}, $msg); }
}

sub make_status_text {
    return "No data" unless @{$state->{regions}};
    sprintf("VRAM %.1f GB | Used %.1f MB (%.0f%%) | %d regions | %s",
        $state->{vram_size_bytes}/(1024**3), $state->{used_pages}*4096/(1024**2),
        100.0*$state->{used_pages}/($state->{total_pages}||1), scalar @{$state->{regions}}, read_method_label());
}

sub format_bytes {
    my ($b) = @_; return "0 B" unless $b;
    my @u = qw(B KB MB GB TB); my $i = 0; while ($b >= 1024 && $i < $#u) { $b /= 1024; $i++; }
    sprintf "%.2f %s", $b, $u[$i];
}

sub build_region_tooltip {
    my ($r, $idx) = @_;
    my $bytes = $r->{size} * 4096;
    my $tt = sprintf("Region %d\n0x%x-0x%x\n%s\n%s", $idx, $r->{start}, $r->{end}, uc($r->{status}), format_bytes($bytes));
    if ($r->{status} eq 'used') {
        my $owners = $state->{gem_map_by_size}{$bytes};
        if ($owners && @$owners) {
            my %seen; $seen{"$_->{cmd} (PID $_->{pid})"}++ for @$owners;
            my @sorted = sort { $seen{$b} <=> $seen{$a} } keys %seen;
            $tt .= "\n\nPotential Owners (Heuristic match by size):";
            my $limit = 6;
            for my $i (0 .. $#sorted) { last if $i >= $limit; $tt .= sprintf("\n  %s  [%d BOs]", $sorted[$i], $seen{$sorted[$i]}); }
            $tt .= sprintf("\n  ...and %d more processes", scalar(@sorted) - $limit) if @sorted > $limit;
        } else { $tt .= "\n\n(No matching VRAM BOs found in GEM info)"; }
    }
    return $tt;
}

# ---------------------------------------------------------------------------
# Hilbert Map Generation & View
# ---------------------------------------------------------------------------
sub _hilbert_rot {
    my ($n, $x, $y, $rx, $ry) = @_;
    if ($ry == 0) { if ($rx == 1) { $x=$n-1-$x; $y=$n-1-$y; } return ($y,$x); }
    return ($x, $y);
}
sub d2xy {
    my ($n, $d) = @_; my ($x, $y) = (0, 0); my $s = 1;
    while ($s < $n) { my $rx = 1 & ($d >> 1); my $ry = 1 & ($d ^ $rx); ($x, $y) = _hilbert_rot($s, $x, $y, $rx, $ry); $x += $s*$rx; $y += $s*$ry; $d >>= 2; $s <<= 1; }
    return ($x, $y);
}
sub xy2d {
    my ($n, $x, $y) = @_; my $d = 0; my $s = $n >> 1;
    while ($s > 0) { my $rx = ($x & $s) ? 1 : 0; my $ry = ($y & $s) ? 1 : 0; $d += $s*$s*((3*$rx)^$ry); ($x, $y) = _hilbert_rot($s, $x, $y, $rx, $ry); $s >>= 1; }
    return $d;
}
sub generate_hilbert_path { my ($order) = @_; my $N = 2**$order; my @path; push @path, [d2xy($N, $_)] for 0 .. $N*$N-1; return \@path; }

sub _render_hilbert {
    my ($cr, $h_state) = @_;
    $cr->set_source_rgb(@{$C{bg}}); $cr->paint;
    return unless @{$state->{regions}} && $state->{total_pages};
    my ($order, $scale, $padding, $path) = @{$h_state}{qw/order scale padding path/};
    my $side = 2**$order; my $total_cells = $side * $side;
    my $ppc = $state->{total_pages} / $total_cells; $ppc = 1 if $ppc < 1;
    my (@cells, $reg_i, $reg_rem); $reg_i = 0; $reg_rem = @{$state->{regions}} ? $state->{regions}[0]{size} : 0;
    
    for my $c (0 .. $total_cells-1) {
        my $need = $ppc; my ($up, $fp, $first) = (0, 0, $reg_i);
        while ($need > 0 && $reg_i < @{$state->{regions}}) {
            my $take = ($reg_rem < $need) ? $reg_rem : $need;
            if ($state->{regions}[$reg_i]{status} eq 'used') { $up += $take; } else { $fp += $take; }
            $need -= $take; $reg_rem -= $take;
            if ($reg_rem <= 0) { $reg_i++; $reg_rem = ($reg_i < @{$state->{regions}}) ? $state->{regions}[$reg_i]{size} : 0; }
        }
        push @cells, { used=>$up, free=>$fp, reg=>$first };
    }
    $h_state->{cells} = \@cells;
    for my $d (0 .. $total_cells-1) {
        my ($gx, $gy) = @{$path->[$d]}; my $c = $cells[$d]; my $tot = $c->{used} + $c->{free}; next if $tot == 0;
        my $frac = $c->{used} / $tot; my @col = map { $C{free}[$_] + $frac*($C{used}[$_]-$C{free}[$_]) } (0,1,2);
        $cr->set_source_rgb(@col); $cr->rectangle($padding+$gx*$scale, $padding+$gy*$scale, ($scale>1?$scale:1), ($scale>1?$scale:1)); $cr->fill;
    }
}

sub show_hilbert_view {
    my ($parent) = @_;
    if (defined $state->{hilbert_window_ref}) { $state->{hilbert_window_ref}->get_toplevel->present; return; }
    my ($order, $scale, $padding) = (8, 2, 10);
    my $side = 2**$order; my $canvas = $side * $scale + $padding * 2;

    my $popup = Gtk2::Window->new('toplevel'); $popup->set_title("VRAM Hilbert Map");
    $popup->set_transient_for($parent); $popup->set_default_size($canvas, $canvas + 60);

    my $vbox = Gtk2::VBox->new(FALSE, 2); my $toolbar = Gtk2::HBox->new(FALSE, 4);
    my $btn_ref = Gtk2::Button->new("Refresh"); my $lbl_method = Gtk2::Label->new("  " . read_method_label());
    $toolbar->pack_start($btn_ref, FALSE, FALSE, 2); $toolbar->pack_start($lbl_method, FALSE, FALSE, 4);
    $vbox->pack_start($toolbar, FALSE, FALSE, 2); $popup->add($vbox);

    my $h_state = { order => $order, scale => $scale, padding => $padding, path => generate_hilbert_path($order), backing_pixmap => undef, cells => [], hover_region => undef };

    my $da = Gtk2::DrawingArea->new; $da->set_size_request($canvas, $canvas); $da->set_has_tooltip(TRUE);
    $da->set_events(['pointer-motion-mask', 'button-press-mask']); $da->{'hilbert_state'} = $h_state;

    $da->signal_connect('configure-event' => sub { $h_state->{backing_pixmap} = undef; return FALSE; });
    $da->signal_connect('expose-event' => sub {
        my ($widget, $event) = @_;
        unless (defined $h_state->{backing_pixmap}) {
            my ($w, $h) = $widget->window->get_size; $h_state->{backing_pixmap} = Gtk2::Gdk::Pixmap->new($widget->window, $w, $h, -1);
            my $cr = Gtk2::Gdk::Cairo::Context->create($h_state->{backing_pixmap}); _render_hilbert($cr, $h_state);
        }
        my $cr = Gtk2::Gdk::Cairo::Context->create($widget->window);
        $cr->set_source_pixmap($h_state->{backing_pixmap}, 0, 0); $cr->paint;
        
        my $min = undef; my $max = undef;
        if (defined $state->{range_start_idx}) {
            if (defined $state->{range_end_idx}) {
                $min = $state->{range_start_idx} < $state->{range_end_idx} ? $state->{range_start_idx} : $state->{range_end_idx};
                $max = $state->{range_start_idx} > $state->{range_end_idx} ? $state->{range_start_idx} : $state->{range_end_idx};
            } else {
                $min = $max = $state->{range_start_idx};
            }
        }

        for my $d (0 .. $side*$side-1) {
            my $reg = $h_state->{cells}[$d]{reg};
            my ($gx,$gy) = @{$h_state->{path}[$d]};
            my $draw = 0;

            if (defined $min && defined $max && $reg >= $min && $reg <= $max) {
                if (defined $state->{range_end_idx}) {
                    $cr->set_source_rgba(0.9, 0.2, 0.2, 0.8); # Red Range
                } else {
                    $cr->set_source_rgba(0.2, 0.9, 0.2, 0.8); # Green Start
                }
                $draw = 1;
            } elsif (defined $h_state->{hover_region} && $reg == $h_state->{hover_region}) {
                $cr->set_source_rgba(@{$C{hi}}, 0.8); # Hover region
                $draw = 1;
            }

            if ($draw) {
                $cr->rectangle($padding+$gx*$scale, $padding+$gy*$scale, ($scale>1?$scale:1), ($scale>1?$scale:1));
                $cr->fill;
            }
        }
        return FALSE;
    });

    $da->signal_connect('motion-notify-event' => sub {
        my ($widget, $event) = @_;
        my $gx = POSIX::floor(($event->x - $padding) / $scale); my $gy = POSIX::floor(($event->y - $padding) / $scale);
        if ($gx >= 0 && $gx < $side && $gy >= 0 && $gy < $side) {
            my $d = xy2d($side, $gx, $gy);
            if (defined $h_state->{cells} && $d < @{$h_state->{cells}}) {
                my $ri = $h_state->{cells}[$d]{reg};
                if (!defined $h_state->{hover_region} || $h_state->{hover_region} != $ri) { 
                    $h_state->{hover_region} = $ri; 
                    $widget->queue_draw; 
                }
            }
        } else {
            if (defined $h_state->{hover_region}) {
                $h_state->{hover_region} = undef;
                $widget->queue_draw;
            }
        }
        return FALSE;
    });

    $da->signal_connect('query-tooltip' => sub {
        my ($widget, $x, $y, $kb, $tooltip) = @_;
        my $gx = POSIX::floor(($x-$padding)/$scale); my $gy = POSIX::floor(($y-$padding)/$scale);
        return FALSE if $gx<0||$gx>=$side||$gy<0||$gy>=$side;
        my $d = xy2d($side,$gx,$gy); return FALSE unless defined $h_state->{cells} && $d < @{$h_state->{cells}};
        my $c = $h_state->{cells}[$d]; my $ri = $c->{reg}; return FALSE unless $ri < @{$state->{regions}};
        $tooltip->set_text(build_region_tooltip($state->{regions}[$ri], $ri));
        return TRUE;
    });

    $da->signal_connect('button-press-event' => sub {
        my ($widget, $event) = @_;
        return FALSE unless $event->button == 1 || $event->button == 3;
        
        my $gx = POSIX::floor(($event->x - $padding) / $scale); my $gy = POSIX::floor(($event->y - $padding) / $scale);
        return FALSE unless $gx>=0 && $gx<$side && $gy>=0 && $gy<$side;
        my $d = xy2d($side, $gx, $gy); return FALSE unless defined $h_state->{cells} && $d < @{$h_state->{cells}};
        my $c = $h_state->{cells}[$d]; my $ri = $c->{reg}; return FALSE unless $ri < @{$state->{regions}};
        
        if ($event->button == 1) {
            $state->{range_start_idx} = $ri;
            $state->{range_end_idx}   = undef;
            $widget->queue_draw;
            if (defined $state->{drawing_area}) { $state->{drawing_area}->queue_draw; } 
            push_status("Selection start set to Region $ri");
            return TRUE;
        }
        if ($event->button == 3) {
            unless (-r $AMDGPU_VRAM_PATH) { push_status("No read method available -- run as root"); return TRUE; }
            
            my $start_idx = defined $state->{range_start_idx} ? $state->{range_start_idx} : $ri;
            my $end_idx = $ri;
            
            $state->{range_start_idx} = $start_idx;
            $state->{range_end_idx}   = $end_idx;
            
            my ($s, $e_idx) = ($start_idx, $end_idx);
            if ($s > $e_idx) { ($s, $e_idx) = ($e_idx, $s); }

            my $offset     = $state->{regions}[$s]{start} * 4096;
            my $end_offset = ($state->{regions}[$e_idx]{end} + 1) * 4096;
            my $read_size  = $end_offset - $offset;

            my $max_read = 256 * 1024 * 1024;
            if ($read_size > $max_read) {
                $read_size = $max_read;
                push_status("Selection too large, truncating to 256MB");
            } else {
                push_status(sprintf("Reading %s at offset 0x%x via debugfs...", format_bytes($read_size), $offset));
            }

            Glib::Idle->add(sub {
                my $raw_ref = read_vram($offset, $read_size);
                if (defined $raw_ref && length($$raw_ref) > 0) {
                    my $map_info = build_multi_region_map_info($s, $e_idx, $offset, length($$raw_ref));
                    my $title_str = ($s == $e_idx) ? "Region $s" : "Regions $s - $e_idx";
                    my $title_info = { title => $title_str, pid => "GPU" };
                    show_vram_image_popup($parent, $raw_ref, $title_info, $map_info);
                    push_status(sprintf("Loaded %s [%s]", format_bytes(length($$raw_ref)), $state->{read_method}));
                } else { push_status("Read failed - check you are running as root"); }
                return FALSE;
            });
            
            $widget->queue_draw;
            if (defined $state->{drawing_area}) { $state->{drawing_area}->queue_draw; } 
            return TRUE;
        }
        return FALSE;
    });

    my $sw = Gtk2::ScrolledWindow->new(undef, undef); $sw->set_policy('automatic', 'automatic'); $sw->add_with_viewport($da);
    $vbox->pack_start($sw, TRUE, TRUE, 0);

    my $hbar = Gtk2::Statusbar->new; my $hctx = $hbar->get_context_id("hilbert");
    $vbox->pack_start($hbar, FALSE, FALSE, 0);

    $btn_ref->signal_connect(clicked => sub {
        parse_vram_mm($VRAM_FILE);
        $h_state->{backing_pixmap} = undef; $da->queue_draw;
        $hbar->pop($hctx); $hbar->push($hctx, make_status_text());
        $lbl_method->set_text("  " . read_method_label());
    });

    $hbar->push($hctx, make_status_text()); $state->{hilbert_window_ref} = $da;
    $popup->signal_connect(destroy => sub { $state->{hilbert_window_ref} = undef; });
    $popup->show_all;
}

# ---------------------------------------------------------------------------
# Extra Windows (GEM Info, Sensors)
# ---------------------------------------------------------------------------
sub show_gem_window {
    my ($parent) = @_; my $gem = parse_gem_info($GEM_FILE);
    my $win = Gtk2::Window->new('toplevel'); $win->set_title("GEM Buffer Objects"); $win->set_transient_for($parent); $win->set_default_size(700, 500);
    my $vbox = Gtk2::VBox->new(FALSE, 4); $vbox->set_border_width(5); $win->add($vbox);
    my $store = Gtk2::ListStore->new('Glib::Int','Glib::String','Glib::String','Glib::Int');
    my $tv = Gtk2::TreeView->new($store);
    for (['PID',0],['Command',1],['VRAM',2],['BOs',3]) {
        my $c = Gtk2::TreeViewColumn->new_with_attributes($_->[0], Gtk2::CellRendererText->new, text => $_->[1]);
        $c->set_sort_column_id($_->[1]); $tv->append_column($c);
    }
    my $total = 0;
    for my $pid (sort { $a <=> $b } keys %$gem) {
        my $g = $gem->{$pid}; $total += $g->{total_vram};
        $store->set($store->append, 0,$pid, 1,$g->{cmd}, 2,format_bytes($g->{total_vram}), 3,scalar @{$g->{bos}});
    }
    my $detail = Gtk2::TextView->new; $detail->set_editable(FALSE); $detail->modify_font(Gtk2::Pango::FontDescription->from_string('monospace 9'));
    $tv->get_selection->signal_connect(changed => sub {
        my ($sel) = @_; my ($m, $i) = $sel->get_selected; return unless $i;
        my $pid = $m->get($i, 0); my $g = $gem->{$pid} or return;
        my $txt = sprintf("PID %d  (%s)  VRAM: %s\n%s\n", $pid, $g->{cmd}, format_bytes($g->{total_vram}), "-"x60);
        $txt .= sprintf(" %-12s %10s  %-4s  %s\n", $_->{handle}, format_bytes($_->{bytes}), $_->{type}, $_->{flags}//'') for @{$g->{bos}};
        $detail->get_buffer->set_text($txt);
    });
    my $paned = Gtk2::VPaned->new; my $sw1 = Gtk2::ScrolledWindow->new(undef,undef); $sw1->add($tv);
    my $sw2 = Gtk2::ScrolledWindow->new(undef,undef); $sw2->add($detail);
    $paned->add1($sw1); $paned->add2($sw2); $paned->set_position(240);
    $vbox->pack_start($paned, TRUE, TRUE, 0); $vbox->pack_start(Gtk2::Label->new("Total: ".format_bytes($total)), FALSE, FALSE, 4);
    $win->show_all;
}

sub show_sensors_window {
    my ($parent) = @_; my $win = Gtk2::Window->new('toplevel'); $win->set_title("amdgpu Sensors"); $win->set_transient_for($parent); $win->set_default_size(420, 380);
    my $vbox = Gtk2::VBox->new(FALSE, 4); $vbox->set_border_width(5); $win->add($vbox);
    my $btn = Gtk2::Button->new("Refresh"); $vbox->pack_start($btn, FALSE, FALSE, 2);
    my $tv = Gtk2::TextView->new; $tv->set_editable(FALSE); $tv->modify_font(Gtk2::Pango::FontDescription->from_string('monospace 9'));
    my $sw = Gtk2::ScrolledWindow->new(undef,undef); $sw->add($tv); $vbox->pack_start($sw, TRUE, TRUE, 0);
    my $update = sub {
        my $t = `cat /sys/kernel/debug/dri/$DRI_INDEX/amdgpu_sensors 2>/dev/null` || `cat /sys/kernel/debug/dri/$DRI_INDEX/amdgpu_pm_info 2>/dev/null` || "N/A";
        $tv->get_buffer->set_text($t);
    };
    $btn->signal_connect(clicked => $update); $update->(); $win->show_all;
}

# ---------------------------------------------------------------------------
# Main UI Expose / Drawing
# ---------------------------------------------------------------------------
my $hover_idx = undef;

sub on_expose_main {
    my ($widget, $event) = @_;
    my $cr = Gtk2::Gdk::Cairo::Context->create($widget->window);
    my ($W, $H) = $widget->window->get_size;
    $cr->set_source_rgb(@{$C{bg}}); $cr->paint;

    unless (@{$state->{regions}}) {
        $cr->set_source_rgb(@{$C{text_fg}}); $cr->select_font_face('Sans','normal','normal'); $cr->set_font_size(13);
        my $msg = "No data -- click Refresh"; my $ex = $cr->text_extents($msg); $cr->move_to(($W-$ex->{width})/2, $H/2); $cr->show_text($msg); return FALSE;
    }

    my ($pad, $strip_h, $strip_y) = (10, 60, 22);
    $cr->set_source_rgb(@{$C{text_fg}}); $cr->select_font_face('monospace','normal','normal'); $cr->set_font_size(9);
    $cr->move_to($pad, $strip_y - 4); $cr->show_text(sprintf("VRAM %.1f GB  |  Left-click=Start, Right-click=End/Open  |  Read: %s", $state->{vram_size_bytes}/(1024**3), read_method_label()));

    $state->{map_areas} = [];
    for my $i (0 .. $#{$state->{regions}}) {
        my $r  = $state->{regions}[$i];
        my $x0 = $pad + $r->{start} / $state->{total_pages} * ($W-$pad*2);
        my $x1 = $pad + $r->{end}   / $state->{total_pages} * ($W-$pad*2);
        $x1 = $x0 + 1 if $x1-$x0 < 1;
        
        my @col = (defined $hover_idx && $i==$hover_idx) ? @{$C{hi}} : ($r->{status} eq 'used') ? @{$C{used}} : @{$C{free}};
        
        if (defined $state->{range_start_idx} && defined $state->{range_end_idx}) {
            my $min = $state->{range_start_idx} < $state->{range_end_idx} ? $state->{range_start_idx} : $state->{range_end_idx};
            my $max = $state->{range_start_idx} > $state->{range_end_idx} ? $state->{range_start_idx} : $state->{range_end_idx};
            if ($i >= $min && $i <= $max) {
                @col = (0.9, 0.2, 0.2); # Red for range
            }
        } elsif (defined $state->{range_start_idx} && $i == $state->{range_start_idx}) {
            @col = (0.2, 0.9, 0.2); # Bright green for start selection
        }
        
        $cr->set_source_rgb(@col); $cr->rectangle($x0, $strip_y, $x1-$x0, $strip_h); $cr->fill;
        push @{$state->{map_areas}}, { x=>$x0, w=>$x1-$x0, y=>$strip_y, h=>$strip_h, idx=>$i, data=>$r };
    }
    $cr->set_source_rgb(@{$C{grid}}); $cr->set_line_width(1); $cr->rectangle($pad, $strip_y, $W-$pad*2, $strip_h); $cr->stroke;

    if (defined $hover_idx && $hover_idx < @{$state->{regions}}) {
        my $r = $state->{regions}[$hover_idx];
        $cr->set_source_rgb(@{$C{hi}}); $cr->set_font_size(9); $cr->move_to($pad, $strip_y + $strip_h + 13);
        $cr->show_text(sprintf("Region %d  0x%x-0x%x  [%s]  %s", $hover_idx, $r->{start}, $r->{end}, uc($r->{status}), format_bytes($r->{size}*4096)));
    }

    my ($key_x, $key_y, $bsz, $lh) = ($pad, $strip_y+$strip_h+28, 13, 19);
    $cr->select_font_face('Sans','normal','normal'); $cr->set_font_size(11);
    for my $item ([$C{used},"Used VRAM"],[$C{free},"Free VRAM"],[[0.2,0.9,0.2],"Start"],[[0.9,0.2,0.2],"Range"],[$C{hi},"Hover"]) {
        $cr->set_source_rgb(@{$item->[0]}); $cr->rectangle($key_x, $key_y, $bsz, $bsz); $cr->fill;
        $cr->set_source_rgb(@{$C{text_fg}}); $cr->move_to($key_x+$bsz+5, $key_y+$bsz-1); $cr->show_text($item->[1]); $key_y += $lh;
    }

    my ($sx, $sy) = ($W/2, $strip_y+$strip_h+30);
    $cr->set_source_rgb(@{$C{text_fg}}); $cr->select_font_face('Sans','normal','bold'); $cr->set_font_size(11);
    $cr->move_to($sx,$sy); $cr->show_text("VRAM: ".format_bytes($state->{vram_size_bytes})); $sy+=20;
    $cr->select_font_face('Sans','normal','normal');
    for my $row (
        sprintf("Used:  %s (%.1f%%)", format_bytes($state->{used_pages}*4096), 100.0*$state->{used_pages}/($state->{total_pages}||1)),
        sprintf("Free:  %s", format_bytes($state->{free_pages}*4096)),
        sprintf("Regions: %d  (%d used / %d free)", scalar @{$state->{regions}}, scalar(grep { $_->{status} eq 'used' } @{$state->{regions}}), scalar(grep { $_->{status} eq 'free' } @{$state->{regions}})),
        sprintf("Frag: %.1f%%  |  Read: %s", @{$state->{regions}} ? 100.0*scalar(grep{$_->{status}eq'free'}@{$state->{regions}})/scalar @{$state->{regions}} : 0, read_method_label())
    ) { $cr->move_to($sx,$sy); $cr->show_text($row); $sy+=17; }
    return FALSE;
}

sub on_query_tooltip_main {
    my ($widget, $x, $y, $kb, $tooltip) = @_;
    for my $a (@{$state->{map_areas}}) {
        if ($x>=$a->{x}&&$x<=$a->{x}+$a->{w}&&$y>=$a->{y}&&$y<=$a->{y}+$a->{h}) {
            $tooltip->set_text(build_region_tooltip($a->{data}, $a->{idx})); return TRUE;
        }
    }
    return FALSE;
}

# ---------------------------------------------------------------------------
# main()
# ---------------------------------------------------------------------------
sub main {
    debug("=== vramgaze starting  asic=$ASIC  dri=$DRI_INDEX ===");

    my $window = Gtk2::Window->new('toplevel');
    $window->set_title("vramgaze  --  AMD VRAM Visualizer  [$ASIC]");
    $window->set_default_size(900, 320); $window->set_border_width(5);
    $window->signal_connect(destroy => sub { Gtk2->main_quit; });

    my $vbox = Gtk2::VBox->new(FALSE, 4); $window->add($vbox);

    my $hb = Gtk2::HBox->new(FALSE, 5);
    my $btn_ref  = Gtk2::Button->new("Refresh");
    my $btn_gem  = Gtk2::Button->new("GEM Info");
    my $btn_hil  = Gtk2::Button->new("Hilbert Map");
    my $btn_sens = Gtk2::Button->new("Sensors");
    my $chk_auto = Gtk2::CheckButton->new("Auto");
    my $upd_entry = Gtk2::Entry->new; $upd_entry->set_text($state->{update_interval_sec}); $upd_entry->set_width_chars(3);
    my $lbl_method = Gtk2::Label->new("  " . read_method_label());

    for my $btn ($btn_gem, $btn_hil) {
        my $orange = Gtk2::Gdk::Color->parse('darkorange'); my $black = Gtk2::Gdk::Color->parse('black');
        for my $st (qw/normal prelight active/) { $btn->modify_bg($st, $orange); $btn->modify_fg($st, $black); }
    }
    if (-r $AMDGPU_VRAM_PATH) {
        $btn_ref->modify_bg('normal', Gtk2::Gdk::Color->parse('#004400')); $btn_ref->modify_fg('normal', Gtk2::Gdk::Color->parse('#aaffaa'));
    }

    $hb->pack_start($btn_ref, FALSE, FALSE, 2); $hb->pack_start($btn_gem, FALSE, FALSE, 2); $hb->pack_start($btn_hil, FALSE, FALSE, 2);
    $hb->pack_start($btn_sens, FALSE, FALSE, 2); $hb->pack_start($chk_auto, FALSE, FALSE, 8); $hb->pack_start($upd_entry, FALSE, FALSE, 2);
    $hb->pack_start(Gtk2::Label->new("s"), FALSE, FALSE, 2); $hb->pack_end($lbl_method, FALSE, FALSE, 4);
    $vbox->pack_start($hb, FALSE, FALSE, 2);

    my $da = Gtk2::DrawingArea->new; $da->set_size_request(860, 240); $da->set_has_tooltip(TRUE);
    $da->set_events(['pointer-motion-mask', 'button-press-mask']); $state->{drawing_area} = $da;

    $da->signal_connect('expose-event' => \&on_expose_main); $da->signal_connect('query-tooltip' => \&on_query_tooltip_main);
    $da->signal_connect('motion-notify-event' => sub {
        my ($w, $e) = @_; my $new = undef;
        for my $a (@{$state->{map_areas}}) { if ($e->x>=$a->{x}&&$e->x<=$a->{x}+$a->{w}&&$e->y>=$a->{y}&&$e->y<=$a->{y}+$a->{h}) { $new=$a->{idx}; last; } }
        if (($hover_idx//-1) != ($new//-1)) { $hover_idx=$new; $w->queue_draw; } return FALSE;
    });

    $da->signal_connect('button-press-event' => sub {
        my ($w, $e) = @_; 
        return FALSE unless $e->button == 1 || $e->button == 3;
        
        my $hit = undef;
        for my $a (@{$state->{map_areas}}) { if ($e->x>=$a->{x}&&$e->x<=$a->{x}+$a->{w}&&$e->y>=$a->{y}&&$e->y<=$a->{y}+$a->{h}) { $hit=$a->{idx}; last; } }
        return FALSE unless defined $hit;

        if ($e->button == 1) {
            $state->{range_start_idx} = $hit;
            $state->{range_end_idx}   = undef;
            $w->queue_draw;
            if (defined $state->{hilbert_window_ref}) { $state->{hilbert_window_ref}->queue_draw; }
            push_status("Selection start set to Region $hit"); 
            return TRUE;
        }
        if ($e->button == 3) {
            unless (-r $AMDGPU_VRAM_PATH) { push_status("No read method available -- run as root"); return TRUE; }
            
            my $start_idx = defined $state->{range_start_idx} ? $state->{range_start_idx} : $hit;
            my $end_idx = $hit;
            
            $state->{range_start_idx} = $start_idx;
            $state->{range_end_idx}   = $end_idx;
            
            my ($s, $e_idx) = ($start_idx, $end_idx);
            if ($s > $e_idx) { ($s, $e_idx) = ($e_idx, $s); }

            my $offset     = $state->{regions}[$s]{start} * 4096;
            my $end_offset = ($state->{regions}[$e_idx]{end} + 1) * 4096;
            my $read_size  = $end_offset - $offset;

            # Hard cap for Multi-region selection so we don't OOM or freeze UI (Max 256MB)
            my $max_read = 256 * 1024 * 1024;
            if ($read_size > $max_read) {
                $read_size = $max_read;
                push_status("Selection too large, truncating to 256MB");
            } else {
                push_status(sprintf("Reading %s at offset 0x%x via debugfs...", format_bytes($read_size), $offset));
            }

            Glib::Idle->add(sub {
                my $raw_ref = read_vram($offset, $read_size);
                if (defined $raw_ref && length($$raw_ref) > 0) {
                    my $map_info = build_multi_region_map_info($s, $e_idx, $offset, length($$raw_ref));
                    my $title_str = ($s == $e_idx) ? "Region $s" : "Regions $s - $e_idx";
                    my $title_info = { title => $title_str, pid => "GPU" };
                    show_vram_image_popup($window, $raw_ref, $title_info, $map_info);
                    push_status(sprintf("Loaded %s [%s]", format_bytes(length($$raw_ref)), $state->{read_method}));
                } else { push_status("Read failed -- check root permissions"); }
                return FALSE;
            });
            
            $w->queue_draw;
            if (defined $state->{hilbert_window_ref}) { $state->{hilbert_window_ref}->queue_draw; }
            return TRUE;
        }
        return FALSE;
    });

    $vbox->pack_start($da, TRUE, TRUE, 0);

    $state->{statusbar} = Gtk2::Statusbar->new; $state->{status_context} = $state->{statusbar}->get_context_id("main");
    $vbox->pack_start($state->{statusbar}, FALSE, FALSE, 0);

    my $do_refresh = sub {
        my $n = parse_vram_mm($VRAM_FILE);
        $state->{gem_data} = parse_gem_info($GEM_FILE);
        my %by_size;
        for my $pid (keys %{$state->{gem_data}}) {
            my $g = $state->{gem_data}{$pid};
            for my $bo (@{$g->{bos}}) { next unless uc($bo->{type}) eq 'VRAM'; push @{$by_size{ $bo->{bytes} }}, { pid => $pid, cmd => $g->{cmd}, handle => $bo->{handle} }; }
        }
        $state->{gem_map_by_size} = \%by_size;

        push_status($n ? make_status_text() : "No data");
        $lbl_method->set_text("  " . read_method_label()); $da->queue_draw;
        if (defined $state->{hilbert_window_ref}) {
            my $hd = $state->{hilbert_window_ref}; $hd->{'hilbert_state'}{backing_pixmap} = undef if $hd->{'hilbert_state'}; $hd->queue_draw;
        }
    };

    $btn_ref->signal_connect( clicked => $do_refresh);
    $btn_gem->signal_connect( clicked => sub { show_gem_window($window) });
    $btn_hil->signal_connect( clicked => sub { show_hilbert_view($window) });
    $btn_sens->signal_connect(clicked => sub { show_sensors_window($window) });

    $chk_auto->signal_connect(toggled => sub {
        if ($chk_auto->get_active) {
            my $secs = $upd_entry->get_text; $secs = $state->{update_interval_sec} unless $secs =~ /^\d+\.?\d*$/ && $secs > 0;
            $state->{update_timer_id} = Glib::Timeout->add(int($secs*1000), sub { return FALSE unless $chk_auto->get_active; $do_refresh->(); return TRUE; });
        } else {
            if (defined $state->{update_timer_id}) { Glib::Source->remove($state->{update_timer_id}); $state->{update_timer_id} = undef; }
        }
    });

    $upd_entry->signal_connect(activate => sub { my $v = $upd_entry->get_text; $state->{update_interval_sec} = ($v =~ /^\d+\.?\d*$/ && $v > 0) ? $v : 2; });

    $window->show_all; $do_refresh->(); Gtk2->main;
}

main();
