#!/usr/bin/env bash # BC-250 live CU/WGP manager. # # This is a self-contained runtime manager. It uses UMR to read/write the # BC-250 gfx1013 registers that control CU enumeration and WGP dispatch. # # Hardware granularity is WGP pairs: # WGP0 = CU0,CU1 WGP1 = CU2,CU3 WGP2 = CU4,CU5 # WGP3 = CU6,CU7 WGP4 = CU8,CU9 set -euo pipefail SCRIPT_NAME="$(basename "$0")" BC250_PCI_ID="13fe" ASIC="${UMR_ASIC:-cyan_skillfish.gfx1013}" REG_CC="mmCC_GC_SHADER_ARRAY_CONFIG" REG_SPI="mmSPI_PG_ENABLE_STATIC_WGP_MASK" REG_RLC="mmRLC_PG_ALWAYS_ON_WGP_MASK" SERVICE_NAME="bc250-cu-live-manager.service" SERVICE_PATH="/etc/systemd/system/$SERVICE_NAME" SERVICE_BIN="/usr/local/bin/bc250-cu-live-manager" SERVICE_CONF="/etc/bc250-cu-live-manager.conf" OLD_UDEV_RULE="/etc/udev/rules.d/99-bc250-cu-live-manager.rules" LAST_REG_PATH="" WGP_FULL_MASK=0x1f UMR="${UMR:-}" UMR_INSTANCE="${UMR_INSTANCE:-}" UMR_INSTANCE_SOURCE="${UMR_INSTANCE:+env}" YES=0 DRY_RUN=0 FORCE=0 SERVICE_TABLE_PENDING=0 DISCLAIMER_ACCEPTED=0 DISCLAIMER_NONINTERACTIVE_SHOWN=0 UMR_INSTALL_OFFERED=0 UMR_INSTANCE_ARGS=() if [ -t 1 ]; then BOLD=$'\033[1m'; DIM=$'\033[2m'; RESET=$'\033[0m' RED=$'\033[31m'; GREEN=$'\033[32m'; YELLOW=$'\033[33m' CYAN=$'\033[36m'; REV=$'\033[7m' else BOLD=""; DIM=""; RESET=""; RED=""; GREEN=""; YELLOW="" CYAN=""; REV="" fi info() { printf '%s[ OK ]%s %s\n' "$GREEN" "$RESET" "$*"; } warn() { printf '%s[WARN]%s %s\n' "$YELLOW" "$RESET" "$*"; } err() { printf '%s[ERR ]%s %s\n' "$RED" "$RESET" "$*" >&2; } die() { err "$@"; exit 1; } hr() { printf '%s+------------------------------------------------------------------------------+%s\n' "$DIM" "$RESET" } panel_title() { local title="$1" hr printf '%s|%s %-76s %s|%s\n' "$DIM" "$RESET$BOLD" "$title" "$DIM" "$RESET" hr } prompt_line() { printf '%s>%s %s' "$CYAN" "$RESET" "$1" } usage() { cat </dev/null; then printf '%s\n' "$inst" return 0 fi done done < <(lspci -Dnn 2>/dev/null | grep -i '\[1002:13fe\]' || true) for dir in "$debug_root"/[0-9]*; do [ -e "$dir/name" ] || continue inst="${dir##*/}" [[ "$inst" =~ ^[0-9]+$ ]] || continue [ "$inst" -lt 128 ] || continue seen+=("$inst") done [ "${#seen[@]}" -eq 1 ] || return 1 printf '%s\n' "${seen[0]}" return 0 } select_umr_instance() { local mode="${1:-default}" detected configured_instance="" configured_source="" if [ -n "$UMR_INSTANCE" ]; then validate_umr_instance "$UMR_INSTANCE" || die "invalid --umr-instance '$UMR_INSTANCE' (expected non-negative integer)" UMR_INSTANCE_SOURCE="${UMR_INSTANCE_SOURCE:-env}" configured_instance="$UMR_INSTANCE" configured_source="$UMR_INSTANCE_SOURCE" if [ "$mode" != "apply-service" ] || [ "$UMR_INSTANCE_SOURCE" = "cli" ]; then init_umr_instance_args return 0 fi fi detected="$(detect_umr_instance || true)" if [ -n "$detected" ]; then UMR_INSTANCE="$detected" UMR_INSTANCE_SOURCE="auto" elif [ -n "$configured_instance" ]; then UMR_INSTANCE="$configured_instance" UMR_INSTANCE_SOURCE="$configured_source" else UMR_INSTANCE="" UMR_INSTANCE_SOURCE="default" fi init_umr_instance_args } need_root() { [ "$(id -u)" = "0" ] || die "register writes require root" } need_umr_root() { [ "$(id -u)" = "0" ] || die "umr register access requires root. Run: sudo ./$SCRIPT_NAME ${1:-status}" } needs_root_command() { local cmd="$1" root_reason_for_command "$cmd" >/dev/null } root_reason_for_command() { local cmd="$1" case "$cmd" in ""|menu|status|table|apply-service|stock-dispatch|enable|disable|enable-wgp|disable-wgp) printf '%s' "it needs live UMR register access" return 0 ;; install-service|write-service-table|uninstall-service) printf '%s' "it needs root to write systemd/service files under /etc" return 0 ;; install-umr) printf '%s' "it needs root to install host packages" return 0 ;; *) return 1 ;; esac } reexec_with_sudo_if_needed() { local cmd="$1" shift local reason script_path [ "$(id -u)" = "0" ] && return 0 reason="$(root_reason_for_command "$cmd" || true)" [ -n "$reason" ] || return 0 command -v sudo >/dev/null 2>&1 || die "this command requires root, and sudo was not found" script_path="$(readlink -f "$0" 2>/dev/null || printf '%s' "$0")" info "re-running with sudo: $reason" exec sudo --preserve-env=UMR,UMR_ASIC,UMR_INSTANCE "$script_path" "$@" } print_disclaimer() { panel_title "Safety Disclaimer" printf '| %-76s |\n' "This tool writes low-level AMDGPU registers on BC-250 hardware." printf '| %-76s |\n' "Incorrect values can freeze the GPU, crash the system, or force a reboot." printf '| %-76s |\n' "You may lose unsaved work and can increase power draw and thermals." printf '| %-76s |\n' "No warranty is provided by the authors or contributors of this script." printf '| %-76s |\n' "You are fully responsible for validation, monitoring, and any outcomes." printf '| %-76s |\n' "Recommended: stable PSU, active cooling, and a remote shell fallback." hr } confirm_disclaimer() { local ans [ "$DISCLAIMER_ACCEPTED" -eq 1 ] && return 0 if [ "$YES" -eq 1 ]; then if [ "$DISCLAIMER_NONINTERACTIVE_SHOWN" -eq 0 ]; then printf '\n' print_disclaimer warn "--yes is set; continuing without interactive acknowledgment" DISCLAIMER_NONINTERACTIVE_SHOWN=1 fi DISCLAIMER_ACCEPTED=1 return 0 fi while true; do printf '\n' print_disclaimer prompt_line "Type 'accept' to continue or 'no' to cancel: " read -r ans case "$ans" in accept|ACCEPT|Accept) DISCLAIMER_ACCEPTED=1 return 0 ;; n|N|no|NO|No|cancel|CANCEL|Cancel) warn "cancelled" return 1 ;; *) warn "type accept or no" ;; esac done } confirm_service_install() { confirm_disclaimer || return 1 [ "$YES" -eq 1 ] && return 0 local ans while true; do printf '\n' panel_title "Confirm Service Install" printf '| %-76s |\n' "This will install and enable the boot service." printf '| %-76s |\n' "Use write-service-table when you want to change the saved WGP table." hr prompt_line "Install/update service? [y/n]: " read -r ans case "$ans" in y|Y|yes|YES) return 0 ;; n|N|no|NO) warn "cancelled"; return 1 ;; *) warn "type y or n" ;; esac done } confirm_write_service_table() { [ "$YES" -eq 1 ] && return 0 local ans while true; do printf '\n' panel_title "Confirm Boot Table Save" printf '| %-76s |\n' "This will save the current live WGP table as the boot profile." printf '| %-76s |\n' "The installed service will use this table on the next start/boot." hr prompt_line "Write current table to service config? [y/n]: " read -r ans case "$ans" in y|Y|yes|YES) return 0 ;; n|N|no|NO) warn "cancelled"; return 1 ;; *) warn "type y or n" ;; esac done } mask_tokens() { local mask="$1" driver_mask="${2:-0}" wgp bit token out="" for wgp in 0 1 2 3 4; do bit=$((1 << wgp)) if [ $((driver_mask & bit)) -ne 0 ] && [ $((mask & bit)) -ne 0 ]; then token="D+" elif [ $((driver_mask & bit)) -ne 0 ]; then token="D!" elif [ $((mask & bit)) -ne 0 ]; then token="S+" else token="--" fi out="${out}${out:+ }$token" done printf '%s\n' "$out" } mask_change_label() { local old="$1" new="$2" wgp bit out="" for wgp in 0 1 2 3 4; do bit=$((1 << wgp)) if [ $((old & bit)) -eq 0 ] && [ $((new & bit)) -ne 0 ]; then out="${out}${out:+,}W${wgp}+" elif [ $((old & bit)) -ne 0 ] && [ $((new & bit)) -eq 0 ]; then out="${out}${out:+,}W${wgp}-" fi done printf '%s\n' "${out:-none}" } dispatch_total() { local idx total=0 for idx in 0 1 2 3; do total=$((total + $(wgp_mask_cu_count "${target_masks[$idx]}"))) done printf '%s\n' "$total" } mask_csv() { local -n ref="$1" printf '%s,%s,%s,%s\n' \ "$(hex_mask "${ref[0]}")" \ "$(hex_mask "${ref[1]}")" \ "$(hex_mask "${ref[2]}")" \ "$(hex_mask "${ref[3]}")" } mask_summary() { local -n ref="$1" printf '%s=%s %s=%s %s=%s %s=%s\n' \ "$(row_label 0)" "$(hex_mask "${ref[0]}")" \ "$(row_label 1)" "$(hex_mask "${ref[1]}")" \ "$(row_label 2)" "$(hex_mask "${ref[2]}")" \ "$(row_label 3)" "$(hex_mask "${ref[3]}")" } load_service_masks() { local line csv item idx value local -a _service_items service_masks=() [ -f "$SERVICE_CONF" ] || return 1 while IFS= read -r line; do case "$line" in BC250_WGP_MASKS=*) csv="${line#BC250_WGP_MASKS=}" break ;; esac done <"$SERVICE_CONF" [ -n "${csv:-}" ] || return 1 IFS=',' read -ra _service_items <<<"$csv" [ "${#_service_items[@]}" -eq 4 ] || return 1 for idx in 0 1 2 3; do item="${_service_items[$idx]}" [[ "$item" =~ ^(0x[0-9a-fA-F]+|[0-9]+)$ ]] || return 1 value=$((item)) [ "$value" -ge 0 ] && [ "$value" -le 31 ] || return 1 service_masks[$idx]="$value" done return 0 } service_masks_match_current() { local idx [ "${#service_masks[@]}" -eq 4 ] || return 1 [ "${#current_masks[@]}" -eq 4 ] || return 1 for idx in 0 1 2 3; do [ "$((service_masks[idx] & 31))" -eq "$((current_masks[idx] & 31))" ] || return 1 done return 0 } confirm_dispatch_plan() { local title="$1" idx ans current target driver confirm_disclaimer || return 1 [ "$YES" -eq 1 ] && return 0 while true; do printf '\n' panel_title "$title" printf ' Legend: D+=driver+routed, S+=SPI+routed, D!=driver+off, --=off\n\n' printf ' +---------+----------------+----------------+-----------------------+\n' printf ' | Row | Current | Target | Change |\n' printf ' +---------+----------------+----------------+-----------------------+\n' for idx in 0 1 2 3; do current="${current_masks[$idx]}" target="${target_masks[$idx]}" driver="${driver_masks[$idx]:-0}" printf ' | %-7s | %-14s | %-14s | %-21s |\n' \ "$(row_label "$idx")" \ "$(mask_tokens "$current" "$driver")" \ "$(mask_tokens "$target" "$driver")" \ "$(mask_change_label "$current" "$target")" done printf ' +---------+----------------+----------------+-----------------------+\n' printf '\n Target total: %s%s/40 CUs%s\n' "$BOLD" "$(dispatch_total)" "$RESET" prompt_line "Apply changes? [y/n]: " read -r ans case "$ans" in y|Y|yes|YES) return 0 ;; n|N|no|NO) warn "cancelled"; return 1 ;; *) warn "type y or n" ;; esac done } check_bc250() { if command -v lspci >/dev/null 2>&1 && lspci -nn 2>/dev/null | grep -qi "$BC250_PCI_ID"; then return 0 fi warn "BC-250 PCI ID 13fe was not detected by lspci." return 1 } require_bc250_for_write() { if check_bc250; then return 0 fi [ "$FORCE" -eq 1 ] || die "refusing register writes on unknown hardware. Use --force only if this is a BC-250 and lspci detection is wrong." warn "forcing register writes despite failed BC-250 PCI detection" } install_umr() { need_root if command -v dpkg >/dev/null 2>&1 && dpkg -s umr >/dev/null 2>&1; then info "umr is already installed." return 0 fi if command -v pacman >/dev/null 2>&1 && pacman -Qi umr >/dev/null 2>&1; then info "umr is already installed." return 0 fi if command -v apt-get >/dev/null 2>&1; then info "Installing umr build dependencies with apt-get..." apt-get update -qq || true DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential cmake \ libncurses-dev libpciaccess-dev libdrm-dev llvm-dev libnanomsg-dev \ libgl-dev libegl-dev libgles-dev libopengl-dev libgbm-dev \ libedit-dev libz3-dev libzstd-dev libcurl4-gnutls-dev libsdl2-dev python3-sphinx info "Cloning and building umr from source..." ( build_tmp="$(mktemp -d)" cd "$build_tmp" git clone https://gitlab.freedesktop.org/tomstdenis/umr.git cd umr cmake -DUMR_GUI=OFF -B build-dir -S . cmake --build build-dir info "Packaging and installing umr..." cd build-dir sed -i 's/set(CPACK_DEBIAN_PACKAGE_DEPENDS ".*")/set(CPACK_DEBIAN_PACKAGE_DEPENDS "")/' CPackConfig.cmake sed -i 's/set(CPACK_GENERATOR "RPM;DEB")/set(CPACK_GENERATOR "DEB")/' CPackConfig.cmake cpack dpkg -i umr-*-Linux.deb cd / rm -rf "$build_tmp" ) && return 0 die "Failed to build and install umr from source." fi if command -v pacman >/dev/null 2>&1; then if pacman -Si umr >/dev/null 2>&1; then info "Installing umr with pacman..." pacman -S --needed umr return 0 fi fi if command -v paru >/dev/null 2>&1; then local user_name="${SUDO_USER:-}" [ -n "$user_name" ] || die "paru install needs SUDO_USER set; run through sudo from your normal user" info "Installing umr with paru as $user_name..." sudo -u "$user_name" paru -S --needed umr return 0 fi if command -v rpm-ostree >/dev/null 2>&1; then warn "rpm-ostree layering is host-level and may affect upgrade workflows on immutable systems." info "Installing umr with rpm-ostree (reboot required)..." if rpm-ostree install umr; then info "umr was staged successfully. Reboot, then run this script again." return 0 fi die "rpm-ostree could not install umr; try layering it manually and check rpm-ostree output." fi if command -v dnf >/dev/null 2>&1; then info "Installing umr with dnf..." dnf install -y umr && return 0 die "dnf could not install umr; check repository availability for package 'umr'." fi die "could not install umr automatically; install it with apt/pacman/paru/rpm-ostree/dnf first" } install_service() { need_root need_umr confirm_service_install || return 0 local source_path source_path="$(readlink -f "$0")" if ! install -m 0755 "$source_path" "$SERVICE_BIN"; then if [ -d /var/usrlocal/bin ]; then SERVICE_BIN="/var/usrlocal/bin/bc250-cu-live-manager" install -m 0755 "$source_path" "$SERVICE_BIN" else die "failed to install service binary at $SERVICE_BIN" fi fi cat > "$SERVICE_PATH" </dev/null && exit 0; sleep 1; done; exit 1' ExecStart=$SERVICE_BIN --yes apply-service RemainAfterExit=yes [Install] WantedBy=multi-user.target EOF rm -f "$OLD_UDEV_RULE" systemctl daemon-reload systemctl enable "$SERVICE_NAME" if [ -f "$SERVICE_CONF" ]; then info "installed and enabled $SERVICE_NAME" info "saved boot table will be applied on next boot; use apply-service to apply it now" else info "installed and enabled $SERVICE_NAME" warn "no boot table is saved yet; use write-service-table before rebooting" fi } write_service_table() { need_root need_umr select_asic require_bc250_for_write local -a current_masks read_current_masks confirm_write_service_table || return 0 cat > "$SERVICE_CONF" </dev/null 2>&1 || true rm -f "$SERVICE_PATH" "$SERVICE_BIN" "/var/usrlocal/bin/bc250-cu-live-manager" "$SERVICE_CONF" "$OLD_UDEV_RULE" systemctl daemon-reload info "removed $SERVICE_NAME" } select_asic() { local out value out="$("$UMR" "${UMR_INSTANCE_ARGS[@]}" -r "$ASIC.$REG_SPI" 2>&1 || true)" value="$(printf '%s\n' "$out" | parse_hex)" [ -n "$value" ] && return 0 die "failed to read $ASIC.$REG_SPI with umr. Set UMR_ASIC to the exact selector if your board differs." } reg_candidates() { printf '%s\n' "$1" } parse_hex() { awk ' { for (i = NF; i >= 1; i--) { if ($i ~ /^0x[0-9a-fA-F]+$/) { print $i exit } } } ' } umr_output_failed() { local out="$1" printf '%s\n' "$out" | grep -Eqi '(\[ERROR\]|error|failed|invalid|unknown|cannot|no such)' } read_reg_bank() { local reg="$1" se="$2" sh="$3" candidate out value if value="$(try_read_reg_bank "$reg" "$se" "$sh")"; then printf '%s\n' "$value" return 0 fi die "failed to read $reg for SE$se SH$sh with umr" } try_read_reg_bank() { local reg="$1" se="$2" sh="$3" candidate out value LAST_REG_PATH="" while IFS= read -r candidate; do out="$("$UMR" "${UMR_INSTANCE_ARGS[@]}" -r "$ASIC.$candidate" -b "$se" "$sh" 0xffffffff 2>&1 || true)" value="$(printf '%s\n' "$out" | parse_hex)" if [ -z "$value" ]; then out="$("$UMR" "${UMR_INSTANCE_ARGS[@]}" -r "$ASIC.$candidate" -b "$se" "$sh" 2>&1 || true)" value="$(printf '%s\n' "$out" | parse_hex)" fi if [ -n "$value" ]; then LAST_REG_PATH="$ASIC.$candidate" printf '%s\n' "$value" return 0 fi done < <(reg_candidates "$reg") return 1 } try_write_reg_global() { local reg="$1" value="$2" candidate out LAST_REG_PATH="" while IFS= read -r candidate; do LAST_REG_PATH="$ASIC.$candidate" if [ "$DRY_RUN" -eq 1 ]; then printf 'dry-run: %s -w %s %s\n' "$(umr_cmd_string)" "$LAST_REG_PATH" "$value" return 0 fi out="$("$UMR" "${UMR_INSTANCE_ARGS[@]}" -w "$LAST_REG_PATH" "$value" 2>&1 || true)" if ! umr_output_failed "$out"; then return 0 fi done < <(reg_candidates "$reg") return 1 } write_reg_bank() { local reg="$1" value="$2" se="$3" sh="$4" candidate out LAST_REG_PATH="" while IFS= read -r candidate; do LAST_REG_PATH="$ASIC.$candidate" if [ "$DRY_RUN" -eq 1 ]; then printf 'dry-run: %s -w %s %s -b %s %s 0xffffffff\n' \ "$(umr_cmd_string)" "$LAST_REG_PATH" "$value" "$se" "$sh" return 0 fi out="$("$UMR" "${UMR_INSTANCE_ARGS[@]}" -w "$LAST_REG_PATH" "$value" -b "$se" "$sh" 0xffffffff 2>&1 || true)" if ! umr_output_failed "$out"; then return 0 fi done < <(reg_candidates "$reg") die "failed to write $reg=$value for SE$se SH$sh with umr" } hex_to_dec() { printf '%d' "$(( $1 ))" } hex_mask() { printf '0x%02x' "$(( $1 & 31 ))" } wgp_mask_cu_count() { local mask="$1" wgp count=0 for wgp in 0 1 2 3 4; do if [ $((mask & (1 << wgp))) -ne 0 ]; then count=$((count + 2)) fi done printf '%s\n' "$count" } module_status() { local mode enum mode="$(cat /sys/module/amdgpu/parameters/bc250_cc_write_mode 2>/dev/null || true)" enum="$(dmesg 2>/dev/null | grep -o 'active_cu_number [0-9]*' | tail -1 | awk '{print $2}' || true)" printf ' amdgpu : bc250_cc_write_mode=%s, active_cu_number=%s\n' \ "${mode:-not exposed}" "${enum:-unknown}" } live_table_rule() { printf ' +---------+------+------+------+------+------+------+------------+--------+\n' } live_table_header() { live_table_rule printf ' | Row | WGP0 | WGP1 | WGP2 | WGP3 | WGP4 | SPI | CC | CUs |\n' printf ' | | 0-1 | 2-3 | 4-5 | 6-7 | 8-9 | | | |\n' live_table_rule } live_wgp_dispatch_cell() { local spi_on="$1" driver_on="${2:-0}" if [ "$driver_on" -ne 0 ] && [ "$spi_on" -ne 0 ]; then printf ' %sD+%s |' "$GREEN$BOLD" "$RESET" elif [ "$driver_on" -ne 0 ]; then printf ' %sD!%s |' "$RED$BOLD" "$RESET" elif [ "$spi_on" -ne 0 ]; then printf ' %sS+%s |' "$CYAN" "$RESET" else printf ' %s--%s |' "$DIM" "$RESET" fi } read_driver_wgp_masks() { local line idx mask local -a out=() while IFS= read -r line; do out+=("$line") done < <(python3 <<'PYEOF' import ctypes import os import struct import sys def open_render_node(): candidates = ["/dev/dri/renderD128"] dri = "/dev/dri" if os.path.isdir(dri): for name in sorted(os.listdir(dri)): if name.startswith("renderD"): path = os.path.join(dri, name) if path not in candidates: candidates.append(path) last = None for path in candidates: try: return os.open(path, os.O_RDWR) except OSError as exc: last = exc raise RuntimeError(f"no DRM render node could be opened: {last}") try: libdrm = ctypes.CDLL("libdrm_amdgpu.so.1") fd = open_render_node() dev = ctypes.c_void_p() maj, min_ = ctypes.c_uint32(), ctypes.c_uint32() rc = libdrm.amdgpu_device_initialize(fd, ctypes.byref(maj), ctypes.byref(min_), ctypes.byref(dev)) if rc != 0: raise RuntimeError(f"amdgpu_device_initialize failed: {rc}") buf = (ctypes.c_uint8 * 1024)() rc = libdrm.amdgpu_query_info(dev, 0x16, 1024, ctypes.byref(buf)) if rc != 0: raise RuntimeError(f"amdgpu_query_info(CU_INFO) failed: {rc}") raw = bytes(buf) num_se = struct.unpack_from("= 0: os.close(fd) except Exception: pass PYEOF ) [ "${#out[@]}" -gt 0 ] || return 1 for idx in 0 1 2 3; do driver_masks[$idx]=0 done for line in "${out[@]}"; do read -r idx mask <<<"$line" [[ "$idx" =~ ^[0-3]$ ]] || continue driver_masks[$idx]="$mask" done return 0 } register_status() { need_umr_root status need_umr select_asic check_bc250 || true local total=0 driver_total=0 blocked_total=0 se sh spi cc_hex count idx wgp bit driver_mask spi_on driver_on local -a driver_masks current_masks service_masks local driver_ok=0 service_has_config=0 if read_driver_wgp_masks; then driver_ok=1 fi read_current_masks SERVICE_TABLE_PENDING=0 if load_service_masks; then service_has_config=1 if ! service_masks_match_current; then SERVICE_TABLE_PENDING=1 fi elif [ -f "$SERVICE_PATH" ]; then SERVICE_TABLE_PENDING=1 fi panel_title "BC-250 CU Dashboard / Live Dispatch" printf ' UMR : %s\n' "$UMR" if [ -n "$UMR_INSTANCE" ]; then printf ' UMR inst : %s (%s)\n' "$UMR_INSTANCE" "$UMR_INSTANCE_SOURCE" else printf ' UMR inst : default (0)\n' fi printf ' ASIC : %s\n' "$ASIC" module_status if command -v systemctl >/dev/null 2>&1 && [ -f "$SERVICE_PATH" ]; then printf ' Service : %s\n' "$(systemctl is-enabled "$SERVICE_NAME" 2>/dev/null || printf 'installed')" if [ "$SERVICE_TABLE_PENDING" -eq 1 ]; then printf ' %sBoot sync : pending changes; press [w] Write table%s\n' "$YELLOW$BOLD" "$RESET" elif [ "$service_has_config" -eq 1 ]; then printf ' Boot sync : current table saved\n' else printf ' %sBoot sync : no saved table; press [w] Write table%s\n' "$YELLOW$BOLD" "$RESET" fi fi printf ' Source : SPI dispatch masks' if [ "$driver_ok" -eq 1 ]; then printf ' + amdgpu boot CU map\n' printf ' Legend : %sD+%s driver+routed, %sS+%s SPI+routed, %sD!%s driver+off, %s--%s off\n\n' \ "$GREEN$BOLD" "$RESET" "$CYAN" "$RESET" "$RED$BOLD" "$RESET" "$DIM" "$RESET" else printf '\n' printf ' Legend : %sS+%s routed, %s--%s off. Driver topology data unavailable.\n\n' \ "$CYAN" "$RESET" "$DIM" "$RESET" fi live_table_header for se in 0 1; do for sh in 0 1; do idx=$((se * 2 + sh)) cc_hex="$(read_reg_bank "$REG_CC" "$se" "$sh")" spi="${current_masks[$idx]}" driver_mask="${driver_masks[$idx]:-0}" count=0 printf ' | SE%s.SH%s |' "$se" "$sh" for wgp in 0 1 2 3 4; do bit=$((1 << wgp)) spi_on=0 driver_on=0 if [ $((spi & bit)) -ne 0 ]; then spi_on=1 count=$((count + 2)) fi if [ "$driver_ok" -eq 1 ] && [ $((driver_mask & bit)) -ne 0 ]; then driver_on=1 driver_total=$((driver_total + 2)) fi if [ "$driver_on" -ne 0 ] && [ "$spi_on" -eq 0 ]; then blocked_total=$((blocked_total + 2)) fi live_wgp_dispatch_cell "$spi_on" "$driver_on" done total=$((total + count)) printf ' %s%s%s | %s | %3s/10 |\n' "$DIM" "$(hex_mask "$spi")" "$RESET" "$cc_hex" "$count" done done live_table_rule printf '\n CUs active & routed : %s%s/40%s\n' "$BOLD" "$total" "$RESET" } status() { if [ "$(id -u)" != "0" ]; then panel_title "BC-250 CU Dashboard" warn "UMR register view skipped; run with sudo for register access." return 0 fi if ! find_umr; then panel_title "BC-250 CU Dashboard" warn "UMR register view skipped; umr was not found." return 0 fi register_status } clear_screen() { if [ -t 1 ]; then printf '\033[2J\033[H' fi } pause_screen() { printf '\nPress Enter to continue... ' read -r _ } print_menu() { clear_screen status printf '\n' hr printf '%s|%s %s%-76s%s %s|%s\n' "$DIM" "$RESET" "$BOLD" "Actions" "$RESET" "$DIM" "$RESET" hr printf '%s|%s %-22s %-22s %-27s %s|%s\n' "$DIM" "$RESET" "[e] Edit WGP table" "[f] Enable all CUs" "[t] Enable default CUs" "$DIM" "$RESET" printf '%s|%s %-22s ' "$DIM" "$RESET" "[i] Install service" if [ "$SERVICE_TABLE_PENDING" -eq 1 ]; then printf '%s%-22s%s ' "$YELLOW$BOLD" "[w] Write table *" "$RESET" else printf '%-22s ' "[w] Write table" fi printf '%-27s %s|%s\n' "[u] Uninstall service" "$DIM" "$RESET" printf '%s|%s %-75s %s|%s\n' "$DIM" "$RESET" "[q] Quit" "$DIM" "$RESET" hr printf '\n' } offer_umr_install_from_menu() { local ans find_umr && return 0 [ "$UMR_INSTALL_OFFERED" -eq 1 ] && return 0 UMR_INSTALL_OFFERED=1 printf '\n' prompt_line "UMR is not installed. Install it now? [y/n]: " read -r ans case "$ans" in y|Y|yes|YES) clear_screen if [ "$(id -u)" != "0" ]; then warn "install-umr requires root. Re-run with sudo." else install_umr fi pause_screen return 1 ;; *) return 0 ;; esac } row_label() { case "$1" in 0) printf 'SE0.SH0' ;; 1) printf 'SE0.SH1' ;; 2) printf 'SE1.SH0' ;; 3) printf 'SE1.SH1' ;; esac } row_coords() { case "$1" in 0) printf '0 0' ;; 1) printf '0 1' ;; 2) printf '1 0' ;; 3) printf '1 1' ;; esac } read_spi_masks() { local idx se sh spi_hex for idx in 0 1 2 3; do read -r se sh <<<"$(row_coords "$idx")" spi_hex="$(read_reg_bank "$REG_SPI" "$se" "$sh")" masks[$idx]=$(( $(hex_to_dec "$spi_hex") & 31 )) done } read_current_masks() { local -a masks read_spi_masks current_masks=("${masks[@]}") } draw_wgp_table() { local cursor_row="$1" cursor_wgp="$2" idx wgp bit cell style endstyle driver_on blocked_total=0 clear_screen panel_title "BC-250 WGP Routing" printf ' %sArrows/hjkl%s move %sSpace%s toggle selected %sEnter/a%s apply %sq%s cancel\n' \ "$CYAN" "$RESET" "$CYAN" "$RESET" "$CYAN" "$RESET" "$CYAN" "$RESET" printf ' Legend: %sD+%s driver+routed, %sS+%s SPI+routed, %sD!%s driver+off, %s--%s off\n\n' \ "$GREEN$BOLD" "$RESET" "$CYAN" "$RESET" "$RED$BOLD" "$RESET" "$DIM" "$RESET" printf ' +---------+------+------+------+------+------+\n' printf ' | Row | WGP0 | WGP1 | WGP2 | WGP3 | WGP4 |\n' printf ' | | 0-1 | 2-3 | 4-5 | 6-7 | 8-9 |\n' printf ' +---------+------+------+------+------+------+\n' for idx in 0 1 2 3; do printf ' | %-7s |' "$(row_label "$idx")" for wgp in 0 1 2 3 4; do bit=$((1 << wgp)) driver_on=0 if [ "${driver_lock_ok:-0}" -eq 1 ] && [ $((driver_masks[idx] & bit)) -ne 0 ]; then driver_on=1 fi if [ $((masks[idx] & bit)) -ne 0 ]; then if [ "$driver_on" -eq 1 ]; then cell=" D+ " style="$GREEN$BOLD" else cell=" S+ " style="$CYAN" fi else if [ "$driver_on" -eq 1 ]; then cell=" D! " style="$RED$BOLD" blocked_total=$((blocked_total + 2)) else cell=" -- " style="$DIM" fi fi if [ "$idx" -eq "$cursor_row" ] && [ "$wgp" -eq "$cursor_wgp" ]; then style="${REV}${style}" fi endstyle="$RESET" printf '%s%s%s|' "$style" "$cell" "$endstyle" done printf '\n' done printf ' +---------+------+------+------+------+------+\n\n' if [ "${driver_lock_ok:-0}" -ne 1 ]; then printf ' Note: boot map unavailable.\n' fi } apply_spi_masks() { require_bc250_for_write local -a current_masks target_masks read_current_masks target_masks=("${masks[@]}") confirm_dispatch_plan "Apply WGP Routing" || return 0 apply_target_masks } apply_target_masks() { local idx se sh union=0 if ! try_write_reg_global "$REG_CC" 0x0; then warn "could not write global $REG_CC; trying per-row CC clears" fi for idx in 0 1 2 3; do read -r se sh <<<"$(row_coords "$idx")" write_reg_bank "$REG_CC" 0x0 "$se" "$sh" write_reg_bank "$REG_SPI" "$(hex_mask "${target_masks[$idx]}")" "$se" "$sh" union=$((union | target_masks[idx])) done if ! try_write_reg_global "$REG_RLC" "$(hex_mask "$union")"; then warn "could not write $REG_RLC; continuing with SPI masks applied" fi info "dispatch registers updated ($(dispatch_total)/40 CUs target)" } table_editor() { need_umr_root table need_umr select_asic local -a masks driver_masks local driver_lock_ok=0 local row=0 wgp=0 key rest bit read_spi_masks if read_driver_wgp_masks; then driver_lock_ok=1 fi while true; do draw_wgp_table "$row" "$wgp" IFS= read -rsn1 key || return 0 case "$key" in $'\x1b') IFS= read -rsn2 -t 0.1 rest || rest="" case "$rest" in '[A') row=$((row > 0 ? row - 1 : 3)) ;; '[B') row=$((row < 3 ? row + 1 : 0)) ;; '[C') wgp=$((wgp < 4 ? wgp + 1 : 0)) ;; '[D') wgp=$((wgp > 0 ? wgp - 1 : 4)) ;; esac ;; h|H) wgp=$((wgp > 0 ? wgp - 1 : 4)) ;; l|L) wgp=$((wgp < 4 ? wgp + 1 : 0)) ;; k|K) row=$((row > 0 ? row - 1 : 3)) ;; j|J) row=$((row < 3 ? row + 1 : 0)) ;; ' ') bit=$((1 << wgp)) masks[$row]=$((masks[row] ^ bit)) ;; ''|$'\n'|$'\r'|a|A) apply_spi_masks pause_screen return 0 ;; q|Q) return 0 ;; esac done } parse_wgp_item() { local item="$1" se sh wgp extra IFS='.' read -r se sh wgp extra <<<"$item" [ -z "${extra:-}" ] || die "invalid WGP entry '$item' (expected SE.SH.WGP)" [[ "$se" =~ ^[0-1]$ ]] || die "invalid SE in '$item' (expected 0 or 1)" [[ "$sh" =~ ^[0-1]$ ]] || die "invalid SH in '$item' (expected 0 or 1)" [[ "$wgp" =~ ^[0-4]$ ]] || die "invalid WGP in '$item' (expected 0..4)" printf '%s %s %s\n' "$se" "$sh" "$wgp" } modify_wgps() { local op="$1" shift need_root need_umr select_asic require_bc250_for_write local -a current_masks target_masks driver_masks read_current_masks target_masks=("${current_masks[@]}") read_driver_wgp_masks || true local item se sh wgp parsed idx=0 local -a target_se target_sh target_wgp for item in "$@"; do parsed="$(parse_wgp_item "$item")" read -r se sh wgp <<<"$parsed" if [ "$op" = "enable" ]; then target_masks[$((se * 2 + sh))]=$((target_masks[se * 2 + sh] | (1 << wgp))) else target_masks[$((se * 2 + sh))]=$((target_masks[se * 2 + sh] & ~(1 << wgp))) fi target_se[$idx]="$se" target_sh[$idx]="$sh" target_wgp[$idx]="$wgp" idx=$((idx + 1)) done confirm_dispatch_plan "Apply WGP Changes" || return 0 for idx in "${!target_wgp[@]}"; do se="${target_se[$idx]}" sh="${target_sh[$idx]}" wgp="${target_wgp[$idx]}" info "${op}d SE$se SH$sh WGP$wgp (CU$((wgp * 2))-CU$((wgp * 2 + 1)))" done apply_target_masks } enable_all() { need_root need_umr select_asic require_bc250_for_write local -a current_masks target_masks driver_masks read_current_masks read_driver_wgp_masks || true target_masks=("$WGP_FULL_MASK" "$WGP_FULL_MASK" "$WGP_FULL_MASK" "$WGP_FULL_MASK") confirm_dispatch_plan "Enable Full Dispatch" || return 0 apply_target_masks } disable_all() { need_root need_umr select_asic require_bc250_for_write local -a current_masks target_masks driver_masks read_current_masks read_driver_wgp_masks || true target_masks=(0 0 0 0) confirm_dispatch_plan "Disable Dispatch" || return 0 apply_target_masks } stock_dispatch() { need_root need_umr select_asic require_bc250_for_write local -a current_masks target_masks driver_masks read_driver_wgp_masks || die "driver topology unavailable; cannot restore boot dispatch mask" read_current_masks target_masks=("${driver_masks[@]}") confirm_dispatch_plan "Restore Driver Dispatch" || return 0 apply_target_masks } apply_service() { need_root need_umr apply-service select_asic require_bc250_for_write local -a current_masks target_masks service_masks load_service_masks || die "service profile not found or invalid at $SERVICE_CONF; run write-service-table to save the current table" read_current_masks target_masks=("${service_masks[@]}") confirm_dispatch_plan "Apply Saved Service Table" || return 0 apply_target_masks } interactive_menu() { while true; do print_menu if ! offer_umr_install_from_menu; then continue fi prompt_line "Select action: " read -r opt case "$opt" in e|E) table_editor ;; f|F) clear_screen; enable_all; pause_screen ;; t|T) clear_screen; stock_dispatch; pause_screen ;; i|I) clear_screen; install_service; pause_screen ;; w|W) clear_screen; write_service_table; pause_screen ;; u|U) clear_screen; uninstall_service; pause_screen ;; q|Q) exit 0 ;; *) warn "unknown option: $opt" ;; esac done } main() { local -a original_args=("$@") local args=() while [ "$#" -gt 0 ]; do case "$1" in -y|--yes) YES=1; shift ;; -n|--dry-run) DRY_RUN=1; YES=1; shift ;; -i|--umr-instance) [ "${2:-}" != "" ] || die "$1 requires an argument" validate_umr_instance "$2" || die "invalid --umr-instance '$2' (expected non-negative integer)" UMR_INSTANCE="$2" UMR_INSTANCE_SOURCE="cli" shift 2 ;; --umr-instance=*) UMR_INSTANCE="${1#*=}" validate_umr_instance "$UMR_INSTANCE" || die "invalid --umr-instance '$UMR_INSTANCE' (expected non-negative integer)" UMR_INSTANCE_SOURCE="cli" shift ;; --force) FORCE=1; shift ;; -h|--help) usage; exit 0 ;; *) args+=("$1"); shift ;; esac done set -- "${args[@]}" local cmd="${1:-menu}" reexec_with_sudo_if_needed "$cmd" "${original_args[@]}" shift || true case "$cmd" in status) status ;; table) table_editor ;; install-umr) install_umr ;; install-service) install_service ;; write-service-table) write_service_table ;; apply-service) apply_service ;; uninstall-service) uninstall_service ;; menu) interactive_menu ;; enable) case "${1:-}" in all) enable_all ;; "") die "enable requires 'all' or WGP entries; see --help" ;; *) modify_wgps enable "$@" ;; esac ;; disable) case "${1:-}" in all) disable_all ;; "") die "disable requires 'all' or WGP entries; see --help" ;; *) modify_wgps disable "$@" ;; esac ;; enable-wgp) [ "$#" -gt 0 ] || die "enable-wgp requires SE.SH.WGP entries"; modify_wgps enable "$@" ;; disable-wgp) [ "$#" -gt 0 ] || die "disable-wgp requires SE.SH.WGP entries"; modify_wgps disable "$@" ;; stock-dispatch) stock_dispatch ;; *) usage; die "unknown command: $cmd" ;; esac } main "$@"