Refactor runner and installation scripts for improved functionality
- Removed the `--disable-hiprt` flag from the runner command, simplifying the rendering options for users. - Updated the `jiggablend-runner` script and README to reflect the removal of the HIPRT control flag, enhancing clarity in usage instructions. - Enhanced the installation script to provide clearer examples for running the jiggablend manager and runner, improving user experience during setup. - Implemented a more robust GPU backend detection mechanism, allowing for better compatibility with various hardware configurations.
This commit is contained in:
@@ -1,45 +1,116 @@
|
||||
// Package blender: GPU backend detection for HIP vs NVIDIA.
|
||||
// Package blender: host GPU backend detection for AMD/NVIDIA/Intel.
|
||||
package blender
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"jiggablend/pkg/scripts"
|
||||
)
|
||||
|
||||
// DetectGPUBackends runs a minimal Blender script to detect whether HIP (AMD) and/or
|
||||
// NVIDIA (CUDA/OptiX) devices are available. Use this to decide whether to force CPU
|
||||
// for Blender < 4.x (only force when HIP is present, since HIP has no official support pre-4).
|
||||
func DetectGPUBackends(blenderBinary, scriptDir string) (hasHIP, hasNVIDIA bool, err error) {
|
||||
scriptPath := filepath.Join(scriptDir, "detect_gpu_backends.py")
|
||||
if err := os.WriteFile(scriptPath, []byte(scripts.DetectGPUBackends), 0644); err != nil {
|
||||
return false, false, fmt.Errorf("write detection script: %w", err)
|
||||
}
|
||||
defer os.Remove(scriptPath)
|
||||
// DetectGPUBackends detects whether AMD, NVIDIA, and/or Intel GPUs are available
|
||||
// using host-level hardware probing only.
|
||||
func DetectGPUBackends() (hasAMD, hasNVIDIA, hasIntel bool, ok bool) {
|
||||
return detectGPUBackendsFromHost()
|
||||
}
|
||||
|
||||
env := TarballEnv(blenderBinary, os.Environ())
|
||||
cmd := exec.Command(blenderBinary, "-b", "--python", scriptPath)
|
||||
cmd.Env = env
|
||||
cmd.Dir = scriptDir
|
||||
out, err := cmd.CombinedOutput()
|
||||
func detectGPUBackendsFromHost() (hasAMD, hasNVIDIA, hasIntel bool, ok bool) {
|
||||
if amd, nvidia, intel, found := detectGPUBackendsFromDRM(); found {
|
||||
return amd, nvidia, intel, true
|
||||
}
|
||||
if amd, nvidia, intel, found := detectGPUBackendsFromLSPCI(); found {
|
||||
return amd, nvidia, intel, true
|
||||
}
|
||||
return false, false, false, false
|
||||
}
|
||||
|
||||
func detectGPUBackendsFromDRM() (hasAMD, hasNVIDIA, hasIntel bool, ok bool) {
|
||||
entries, err := os.ReadDir("/sys/class/drm")
|
||||
if err != nil {
|
||||
return false, false, fmt.Errorf("run blender detection: %w (output: %s)", err, string(out))
|
||||
return false, false, false, false
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if !isDRMCardNode(name) {
|
||||
continue
|
||||
}
|
||||
|
||||
vendorPath := filepath.Join("/sys/class/drm", name, "device", "vendor")
|
||||
vendorRaw, err := os.ReadFile(vendorPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
vendor := strings.TrimSpace(strings.ToLower(string(vendorRaw)))
|
||||
switch vendor {
|
||||
case "0x1002":
|
||||
hasAMD = true
|
||||
ok = true
|
||||
case "0x10de":
|
||||
hasNVIDIA = true
|
||||
ok = true
|
||||
case "0x8086":
|
||||
hasIntel = true
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
|
||||
return hasAMD, hasNVIDIA, hasIntel, ok
|
||||
}
|
||||
|
||||
func isDRMCardNode(name string) bool {
|
||||
if !strings.HasPrefix(name, "card") {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(name, "-") {
|
||||
// Connector entries like card0-DP-1 are not GPU device nodes.
|
||||
return false
|
||||
}
|
||||
if len(name) <= len("card") {
|
||||
return false
|
||||
}
|
||||
_, err := strconv.Atoi(strings.TrimPrefix(name, "card"))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func detectGPUBackendsFromLSPCI() (hasAMD, hasNVIDIA, hasIntel bool, ok bool) {
|
||||
if _, err := exec.LookPath("lspci"); err != nil {
|
||||
return false, false, false, false
|
||||
}
|
||||
|
||||
out, err := exec.Command("lspci").CombinedOutput()
|
||||
if err != nil {
|
||||
return false, false, false, false
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(string(out)))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
switch line {
|
||||
case "HAS_HIP":
|
||||
hasHIP = true
|
||||
case "HAS_NVIDIA":
|
||||
line := strings.ToLower(strings.TrimSpace(scanner.Text()))
|
||||
if !isGPUControllerLine(line) {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(line, "nvidia") {
|
||||
hasNVIDIA = true
|
||||
ok = true
|
||||
}
|
||||
if strings.Contains(line, "amd") || strings.Contains(line, "ati") || strings.Contains(line, "radeon") {
|
||||
hasAMD = true
|
||||
ok = true
|
||||
}
|
||||
if strings.Contains(line, "intel") {
|
||||
hasIntel = true
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
return hasHIP, hasNVIDIA, scanner.Err()
|
||||
|
||||
return hasAMD, hasNVIDIA, hasIntel, ok
|
||||
}
|
||||
|
||||
func isGPUControllerLine(line string) bool {
|
||||
return strings.Contains(line, "vga compatible controller") ||
|
||||
strings.Contains(line, "3d controller") ||
|
||||
strings.Contains(line, "display controller")
|
||||
}
|
||||
|
||||
@@ -1,143 +1,19 @@
|
||||
package blender
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"jiggablend/pkg/blendfile"
|
||||
)
|
||||
|
||||
// ParseVersionFromFile parses the Blender version that a .blend file was saved with.
|
||||
// Returns major and minor version numbers.
|
||||
// Delegates to the shared pkg/blendfile implementation.
|
||||
func ParseVersionFromFile(blendPath string) (major, minor int, err error) {
|
||||
file, err := os.Open(blendPath)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to open blend file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Read the first 12 bytes of the blend file header
|
||||
// Format: BLENDER-v<major><minor><patch> or BLENDER_v<major><minor><patch>
|
||||
// The header is: "BLENDER" (7 bytes) + pointer size (1 byte: '-' for 64-bit, '_' for 32-bit)
|
||||
// + endianness (1 byte: 'v' for little-endian, 'V' for big-endian)
|
||||
// + version (3 bytes: e.g., "402" for 4.02)
|
||||
header := make([]byte, 12)
|
||||
n, err := file.Read(header)
|
||||
if err != nil || n < 12 {
|
||||
return 0, 0, fmt.Errorf("failed to read blend file header: %w", err)
|
||||
}
|
||||
|
||||
// Check for BLENDER magic
|
||||
if string(header[:7]) != "BLENDER" {
|
||||
// Might be compressed - try to decompress
|
||||
file.Seek(0, 0)
|
||||
return parseCompressedVersion(file)
|
||||
}
|
||||
|
||||
// Parse version from bytes 9-11 (3 digits)
|
||||
versionStr := string(header[9:12])
|
||||
|
||||
// Version format changed in Blender 3.0
|
||||
// Pre-3.0: "279" = 2.79, "280" = 2.80
|
||||
// 3.0+: "300" = 3.0, "402" = 4.02, "410" = 4.10
|
||||
if len(versionStr) == 3 {
|
||||
// First digit is major version
|
||||
fmt.Sscanf(string(versionStr[0]), "%d", &major)
|
||||
// Next two digits are minor version
|
||||
fmt.Sscanf(versionStr[1:3], "%d", &minor)
|
||||
}
|
||||
|
||||
return major, minor, nil
|
||||
}
|
||||
|
||||
// parseCompressedVersion handles gzip and zstd compressed blend files.
|
||||
func parseCompressedVersion(file *os.File) (major, minor int, err error) {
|
||||
magic := make([]byte, 4)
|
||||
if _, err := file.Read(magic); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
file.Seek(0, 0)
|
||||
|
||||
if magic[0] == 0x1f && magic[1] == 0x8b {
|
||||
// gzip compressed
|
||||
gzReader, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to create gzip reader: %w", err)
|
||||
}
|
||||
defer gzReader.Close()
|
||||
|
||||
header := make([]byte, 12)
|
||||
n, err := gzReader.Read(header)
|
||||
if err != nil || n < 12 {
|
||||
return 0, 0, fmt.Errorf("failed to read compressed blend header: %w", err)
|
||||
}
|
||||
|
||||
if string(header[:7]) != "BLENDER" {
|
||||
return 0, 0, fmt.Errorf("invalid blend file format")
|
||||
}
|
||||
|
||||
versionStr := string(header[9:12])
|
||||
if len(versionStr) == 3 {
|
||||
fmt.Sscanf(string(versionStr[0]), "%d", &major)
|
||||
fmt.Sscanf(versionStr[1:3], "%d", &minor)
|
||||
}
|
||||
|
||||
return major, minor, nil
|
||||
}
|
||||
|
||||
// Check for zstd magic (Blender 3.0+): 0x28 0xB5 0x2F 0xFD
|
||||
if magic[0] == 0x28 && magic[1] == 0xb5 && magic[2] == 0x2f && magic[3] == 0xfd {
|
||||
return parseZstdVersion(file)
|
||||
}
|
||||
|
||||
return 0, 0, fmt.Errorf("unknown blend file format")
|
||||
}
|
||||
|
||||
// parseZstdVersion handles zstd-compressed blend files (Blender 3.0+).
|
||||
// Uses zstd command line tool since Go doesn't have native zstd support.
|
||||
func parseZstdVersion(file *os.File) (major, minor int, err error) {
|
||||
file.Seek(0, 0)
|
||||
|
||||
cmd := exec.Command("zstd", "-d", "-c")
|
||||
cmd.Stdin = file
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to create zstd stdout pipe: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to start zstd decompression: %w", err)
|
||||
}
|
||||
|
||||
// Read just the header (12 bytes)
|
||||
header := make([]byte, 12)
|
||||
n, readErr := io.ReadFull(stdout, header)
|
||||
|
||||
// Kill the process early - we only need the header
|
||||
cmd.Process.Kill()
|
||||
cmd.Wait()
|
||||
|
||||
if readErr != nil || n < 12 {
|
||||
return 0, 0, fmt.Errorf("failed to read zstd compressed blend header: %v", readErr)
|
||||
}
|
||||
|
||||
if string(header[:7]) != "BLENDER" {
|
||||
return 0, 0, fmt.Errorf("invalid blend file format in zstd archive")
|
||||
}
|
||||
|
||||
versionStr := string(header[9:12])
|
||||
if len(versionStr) == 3 {
|
||||
fmt.Sscanf(string(versionStr[0]), "%d", &major)
|
||||
fmt.Sscanf(versionStr[1:3], "%d", &minor)
|
||||
}
|
||||
|
||||
return major, minor, nil
|
||||
return blendfile.ParseVersionFromFile(blendPath)
|
||||
}
|
||||
|
||||
// VersionString returns a formatted version string like "4.2".
|
||||
func VersionString(major, minor int) string {
|
||||
return fmt.Sprintf("%d.%d", major, minor)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user