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:
@@ -241,8 +241,8 @@ func (m *ManagerClient) DownloadContext(contextPath, jobToken string) (io.ReadCl
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("context download failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
@@ -435,8 +435,8 @@ func (m *ManagerClient) DownloadBlender(version string) (io.ReadCloser, error) {
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("failed to download blender: status %d, body: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -46,23 +46,22 @@ type Runner struct {
|
||||
gpuLockedOut bool
|
||||
gpuLockedOutMu sync.RWMutex
|
||||
|
||||
// hasHIP/hasNVIDIA are set at startup by running latest Blender to detect GPU backends.
|
||||
// Used to force CPU only for Blender < 4.x when HIP is present (no official HIP support pre-4).
|
||||
// gpuDetectionFailed is true when detection could not run; we then force CPU for all versions (we could not determine HIP vs NVIDIA).
|
||||
// hasAMD/hasNVIDIA/hasIntel are set at startup by hardware/Blender GPU backend detection.
|
||||
// Used to force CPU only for Blender < 4.x when AMD is present (no official HIP support pre-4).
|
||||
// gpuDetectionFailed is true when detection could not run; we then force CPU for all versions.
|
||||
gpuBackendMu sync.RWMutex
|
||||
hasHIP bool
|
||||
hasAMD bool
|
||||
hasNVIDIA bool
|
||||
hasIntel bool
|
||||
gpuBackendProbed bool
|
||||
gpuDetectionFailed bool
|
||||
|
||||
// forceCPURendering forces CPU rendering for all jobs regardless of metadata/backend detection.
|
||||
forceCPURendering bool
|
||||
// disableHIPRT disables HIPRT acceleration when configuring Cycles HIP devices.
|
||||
disableHIPRT bool
|
||||
}
|
||||
|
||||
// New creates a new runner.
|
||||
func New(managerURL, name, hostname string, forceCPURendering, disableHIPRT bool) *Runner {
|
||||
func New(managerURL, name, hostname string, forceCPURendering bool) *Runner {
|
||||
manager := api.NewManagerClient(managerURL)
|
||||
|
||||
r := &Runner{
|
||||
@@ -74,7 +73,6 @@ func New(managerURL, name, hostname string, forceCPURendering, disableHIPRT bool
|
||||
processors: make(map[string]tasks.Processor),
|
||||
|
||||
forceCPURendering: forceCPURendering,
|
||||
disableHIPRT: disableHIPRT,
|
||||
}
|
||||
|
||||
// Generate fingerprint
|
||||
@@ -93,25 +91,25 @@ func (r *Runner) CheckRequiredTools() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var cachedCapabilities map[string]interface{} = nil
|
||||
var (
|
||||
cachedCapabilities map[string]interface{}
|
||||
capabilitiesOnce sync.Once
|
||||
)
|
||||
|
||||
// ProbeCapabilities detects hardware capabilities.
|
||||
func (r *Runner) ProbeCapabilities() map[string]interface{} {
|
||||
if cachedCapabilities != nil {
|
||||
return cachedCapabilities
|
||||
}
|
||||
capabilitiesOnce.Do(func() {
|
||||
caps := make(map[string]interface{})
|
||||
|
||||
caps := make(map[string]interface{})
|
||||
if err := exec.Command("ffmpeg", "-version").Run(); err == nil {
|
||||
caps["ffmpeg"] = true
|
||||
} else {
|
||||
caps["ffmpeg"] = false
|
||||
}
|
||||
|
||||
// Check for ffmpeg and probe encoding capabilities
|
||||
if err := exec.Command("ffmpeg", "-version").Run(); err == nil {
|
||||
caps["ffmpeg"] = true
|
||||
} else {
|
||||
caps["ffmpeg"] = false
|
||||
}
|
||||
|
||||
cachedCapabilities = caps
|
||||
return caps
|
||||
cachedCapabilities = caps
|
||||
})
|
||||
return cachedCapabilities
|
||||
}
|
||||
|
||||
// Register registers the runner with the manager.
|
||||
@@ -141,52 +139,66 @@ func (r *Runner) Register(apiKey string) (int64, error) {
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// DetectAndStoreGPUBackends downloads the latest Blender from the manager (if needed),
|
||||
// runs a detection script to see if HIP (AMD) and/or NVIDIA devices are available,
|
||||
// and stores the result. Call after Register. Used so we only force CPU for Blender < 4.x
|
||||
// when the runner has HIP (no official HIP support pre-4); NVIDIA is allowed.
|
||||
// DetectAndStoreGPUBackends runs host-level backend detection and stores AMD/NVIDIA/Intel results.
|
||||
// Call after Register. Used so we only force CPU for Blender < 4.x when AMD is present.
|
||||
func (r *Runner) DetectAndStoreGPUBackends() {
|
||||
r.gpuBackendMu.Lock()
|
||||
defer r.gpuBackendMu.Unlock()
|
||||
if r.gpuBackendProbed {
|
||||
return
|
||||
}
|
||||
latestVer, err := r.manager.GetLatestBlenderVersion()
|
||||
if err != nil {
|
||||
log.Printf("GPU backend detection failed (could not get latest Blender version: %v). All jobs will use CPU because we could not determine HIP vs NVIDIA.", err)
|
||||
hasAMD, hasNVIDIA, hasIntel, ok := blender.DetectGPUBackends()
|
||||
if !ok {
|
||||
log.Printf("GPU backend detection failed (host probe unavailable). All jobs will use CPU because backend availability is unknown.")
|
||||
r.gpuBackendProbed = true
|
||||
r.gpuDetectionFailed = true
|
||||
return
|
||||
}
|
||||
binaryPath, err := r.blender.GetBinaryPath(latestVer)
|
||||
if err != nil {
|
||||
log.Printf("GPU backend detection failed (could not get Blender binary: %v). All jobs will use CPU because we could not determine HIP vs NVIDIA.", err)
|
||||
r.gpuBackendProbed = true
|
||||
r.gpuDetectionFailed = true
|
||||
return
|
||||
|
||||
detectedTypes := 0
|
||||
if hasAMD {
|
||||
detectedTypes++
|
||||
}
|
||||
hasHIP, hasNVIDIA, err := blender.DetectGPUBackends(binaryPath, r.workspace.BaseDir())
|
||||
if err != nil {
|
||||
log.Printf("GPU backend detection failed (script error: %v). All jobs will use CPU because we could not determine HIP vs NVIDIA.", err)
|
||||
r.gpuBackendProbed = true
|
||||
r.gpuDetectionFailed = true
|
||||
return
|
||||
if hasNVIDIA {
|
||||
detectedTypes++
|
||||
}
|
||||
r.hasHIP = hasHIP
|
||||
if hasIntel {
|
||||
detectedTypes++
|
||||
}
|
||||
if detectedTypes > 1 {
|
||||
log.Printf("mixed GPU vendors detected (AMD=%v NVIDIA=%v INTEL=%v): multi-vendor setups may not work reliably, but runner will continue with GPU enabled", hasAMD, hasNVIDIA, hasIntel)
|
||||
}
|
||||
|
||||
r.hasAMD = hasAMD
|
||||
r.hasNVIDIA = hasNVIDIA
|
||||
r.hasIntel = hasIntel
|
||||
r.gpuBackendProbed = true
|
||||
r.gpuDetectionFailed = false
|
||||
log.Printf("GPU backend detection: HIP=%v NVIDIA=%v (Blender < 4.x will force CPU only when HIP is present)", hasHIP, hasNVIDIA)
|
||||
log.Printf("GPU backend detection: AMD=%v NVIDIA=%v INTEL=%v (Blender < 4.x will force CPU only when AMD is present)", hasAMD, hasNVIDIA, hasIntel)
|
||||
}
|
||||
|
||||
// HasHIP returns whether the runner detected HIP (AMD) devices. Used to force CPU for Blender < 4.x only when HIP is present.
|
||||
func (r *Runner) HasHIP() bool {
|
||||
// HasAMD returns whether the runner detected AMD devices. Used to force CPU for Blender < 4.x only when AMD is present.
|
||||
func (r *Runner) HasAMD() bool {
|
||||
r.gpuBackendMu.RLock()
|
||||
defer r.gpuBackendMu.RUnlock()
|
||||
return r.hasHIP
|
||||
return r.hasAMD
|
||||
}
|
||||
|
||||
// GPUDetectionFailed returns true when startup GPU backend detection could not run or failed. When true, all jobs use CPU because we could not determine HIP vs NVIDIA.
|
||||
// HasNVIDIA returns whether the runner detected NVIDIA GPUs.
|
||||
func (r *Runner) HasNVIDIA() bool {
|
||||
r.gpuBackendMu.RLock()
|
||||
defer r.gpuBackendMu.RUnlock()
|
||||
return r.hasNVIDIA
|
||||
}
|
||||
|
||||
// HasIntel returns whether the runner detected Intel GPUs (e.g. Arc).
|
||||
func (r *Runner) HasIntel() bool {
|
||||
r.gpuBackendMu.RLock()
|
||||
defer r.gpuBackendMu.RUnlock()
|
||||
return r.hasIntel
|
||||
}
|
||||
|
||||
// GPUDetectionFailed returns true when startup GPU backend detection could not run or failed. When true, all jobs use CPU because backend availability is unknown.
|
||||
func (r *Runner) GPUDetectionFailed() bool {
|
||||
r.gpuBackendMu.RLock()
|
||||
defer r.gpuBackendMu.RUnlock()
|
||||
@@ -313,10 +325,11 @@ func (r *Runner) executeJob(job *api.NextJobResponse) (err error) {
|
||||
r.encoder,
|
||||
r.processes,
|
||||
r.IsGPULockedOut(),
|
||||
r.HasHIP(),
|
||||
r.HasAMD(),
|
||||
r.HasNVIDIA(),
|
||||
r.HasIntel(),
|
||||
r.GPUDetectionFailed(),
|
||||
r.forceCPURendering,
|
||||
r.disableHIPRT,
|
||||
func() { r.SetGPULockedOut(true) },
|
||||
)
|
||||
|
||||
|
||||
@@ -298,6 +298,9 @@ func (p *EncodeProcessor) Process(ctx *Context) error {
|
||||
ctx.Info(line)
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Printf("Error reading encode stdout: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Stream stderr
|
||||
@@ -311,6 +314,9 @@ func (p *EncodeProcessor) Process(ctx *Context) error {
|
||||
ctx.Warn(line)
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Printf("Error reading encode stderr: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = cmd.Wait()
|
||||
|
||||
@@ -11,8 +11,6 @@ import (
|
||||
"jiggablend/pkg/executils"
|
||||
"jiggablend/pkg/types"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -43,23 +41,25 @@ type Context struct {
|
||||
|
||||
// GPULockedOut is set when the runner has detected a GPU error (e.g. HIP) and disables GPU for all jobs.
|
||||
GPULockedOut bool
|
||||
// HasHIP is true when the runner detected HIP (AMD) devices at startup. Used to force CPU for Blender < 4.x only when HIP is present.
|
||||
HasHIP bool
|
||||
// GPUDetectionFailed is true when startup GPU backend detection could not run; we force CPU for all versions (could not determine HIP vs NVIDIA).
|
||||
// HasAMD is true when the runner detected AMD devices at startup.
|
||||
HasAMD bool
|
||||
// HasNVIDIA is true when the runner detected NVIDIA GPUs at startup.
|
||||
HasNVIDIA bool
|
||||
// HasIntel is true when the runner detected Intel GPUs (e.g. Arc) at startup.
|
||||
HasIntel bool
|
||||
// GPUDetectionFailed is true when startup GPU backend detection could not run; we force CPU for all versions (backend availability unknown).
|
||||
GPUDetectionFailed bool
|
||||
// OnGPUError is called when a GPU error line is seen in render logs; typically sets runner GPU lockout.
|
||||
OnGPUError func()
|
||||
// ForceCPURendering is a runner-level override that forces CPU rendering for all jobs.
|
||||
ForceCPURendering bool
|
||||
// DisableHIPRT is a runner-level override that disables HIPRT acceleration in Blender.
|
||||
DisableHIPRT bool
|
||||
}
|
||||
|
||||
// ErrJobCancelled indicates the manager-side job was cancelled during execution.
|
||||
var ErrJobCancelled = errors.New("job cancelled")
|
||||
|
||||
// NewContext creates a new task context. frameEnd should be >= frame; if 0 or less than frame, it is treated as single-frame (frameEnd = frame).
|
||||
// gpuLockedOut is the runner's current GPU lockout state; hasHIP means the runner has HIP (AMD) devices (force CPU for Blender < 4.x only when true); gpuDetectionFailed means detection failed at startup (force CPU for all versions—could not determine HIP vs NVIDIA); onGPUError is called when a GPU error is detected in logs (may be nil).
|
||||
// gpuLockedOut is the runner's current GPU lockout state; gpuDetectionFailed means detection failed at startup (force CPU for all versions); onGPUError is called when a GPU error is detected in logs (may be nil).
|
||||
func NewContext(
|
||||
taskID, jobID int64,
|
||||
jobName string,
|
||||
@@ -75,10 +75,11 @@ func NewContext(
|
||||
encoder *encoding.Selector,
|
||||
processes *executils.ProcessTracker,
|
||||
gpuLockedOut bool,
|
||||
hasHIP bool,
|
||||
hasAMD bool,
|
||||
hasNVIDIA bool,
|
||||
hasIntel bool,
|
||||
gpuDetectionFailed bool,
|
||||
forceCPURendering bool,
|
||||
disableHIPRT bool,
|
||||
onGPUError func(),
|
||||
) *Context {
|
||||
if frameEnd < frameStart {
|
||||
@@ -101,10 +102,11 @@ func NewContext(
|
||||
Encoder: encoder,
|
||||
Processes: processes,
|
||||
GPULockedOut: gpuLockedOut,
|
||||
HasHIP: hasHIP,
|
||||
HasAMD: hasAMD,
|
||||
HasNVIDIA: hasNVIDIA,
|
||||
HasIntel: hasIntel,
|
||||
GPUDetectionFailed: gpuDetectionFailed,
|
||||
ForceCPURendering: forceCPURendering,
|
||||
DisableHIPRT: disableHIPRT,
|
||||
OnGPUError: onGPUError,
|
||||
}
|
||||
}
|
||||
@@ -187,8 +189,7 @@ func (c *Context) ShouldEnableExecution() bool {
|
||||
}
|
||||
|
||||
// ShouldForceCPU returns true if GPU should be disabled and CPU rendering forced
|
||||
// (runner GPU lockout, GPU detection failed at startup for any version, metadata force_cpu,
|
||||
// or Blender < 4.x when the runner has HIP).
|
||||
// (runner GPU lockout, GPU detection failed at startup, or metadata force_cpu).
|
||||
func (c *Context) ShouldForceCPU() bool {
|
||||
if c.ForceCPURendering {
|
||||
return true
|
||||
@@ -196,17 +197,10 @@ func (c *Context) ShouldForceCPU() bool {
|
||||
if c.GPULockedOut {
|
||||
return true
|
||||
}
|
||||
// Detection failed at startup: we could not determine HIP vs NVIDIA, so force CPU for all versions.
|
||||
// Detection failed at startup: backend availability unknown, so force CPU for all versions.
|
||||
if c.GPUDetectionFailed {
|
||||
return true
|
||||
}
|
||||
v := c.GetBlenderVersion()
|
||||
major := parseBlenderMajor(v)
|
||||
isPre4 := v != "" && major >= 0 && major < 4
|
||||
// Blender < 4.x: force CPU when runner has HIP (no official HIP support).
|
||||
if isPre4 && c.HasHIP {
|
||||
return true
|
||||
}
|
||||
if c.Metadata != nil && c.Metadata.RenderSettings.EngineSettings != nil {
|
||||
if v, ok := c.Metadata.RenderSettings.EngineSettings["force_cpu"]; ok {
|
||||
if b, ok := v.(bool); ok && b {
|
||||
@@ -217,21 +211,6 @@ func (c *Context) ShouldForceCPU() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// parseBlenderMajor returns the major version number from a string like "4.2.3" or "3.6".
|
||||
// Returns -1 if the version cannot be parsed.
|
||||
func parseBlenderMajor(version string) int {
|
||||
version = strings.TrimSpace(version)
|
||||
if version == "" {
|
||||
return -1
|
||||
}
|
||||
parts := strings.SplitN(version, ".", 2)
|
||||
major, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return major
|
||||
}
|
||||
|
||||
// IsJobCancelled checks whether the manager marked this job as cancelled.
|
||||
func (c *Context) IsJobCancelled() (bool, error) {
|
||||
if c.Manager == nil {
|
||||
|
||||
@@ -104,15 +104,10 @@ func (p *RenderProcessor) Process(ctx *Context) error {
|
||||
renderFormat := "EXR"
|
||||
|
||||
if ctx.ShouldForceCPU() {
|
||||
v := ctx.GetBlenderVersion()
|
||||
major := parseBlenderMajor(v)
|
||||
isPre4 := v != "" && major >= 0 && major < 4
|
||||
if ctx.ForceCPURendering {
|
||||
ctx.Info("Runner compatibility flag is enabled: forcing CPU rendering for this job")
|
||||
} else if ctx.GPUDetectionFailed {
|
||||
ctx.Info("GPU backend detection failed at startup—we could not determine whether this machine has HIP (AMD) or NVIDIA GPUs, so rendering will use CPU to avoid compatibility issues")
|
||||
} else if isPre4 && ctx.HasHIP {
|
||||
ctx.Info("Blender < 4.x has no official HIP support: using CPU rendering only")
|
||||
ctx.Info("GPU backend detection failed at startup—we could not determine available GPU backends, so rendering will use CPU to avoid compatibility issues")
|
||||
} else {
|
||||
ctx.Info("GPU lockout active: using CPU rendering only")
|
||||
}
|
||||
@@ -195,7 +190,6 @@ func (p *RenderProcessor) createRenderScript(ctx *Context, renderFormat string)
|
||||
settingsMap = make(map[string]interface{})
|
||||
}
|
||||
settingsMap["force_cpu"] = ctx.ShouldForceCPU()
|
||||
settingsMap["disable_hiprt"] = ctx.DisableHIPRT
|
||||
settingsJSON, err := json.Marshal(settingsMap)
|
||||
if err == nil {
|
||||
if err := os.WriteFile(renderSettingsFilePath, settingsJSON, 0644); err != nil {
|
||||
@@ -277,6 +271,9 @@ func (p *RenderProcessor) runBlender(ctx *Context, blenderBinary, blendFile, out
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Printf("Error reading stdout: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Stream stderr and watch for GPU error lines
|
||||
@@ -297,6 +294,9 @@ func (p *RenderProcessor) runBlender(ctx *Context, blenderBinary, blendFile, out
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Printf("Error reading stderr: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for completion
|
||||
|
||||
@@ -99,6 +99,11 @@ func ExtractTarStripPrefix(reader io.Reader, destDir string) error {
|
||||
|
||||
targetPath := filepath.Join(destDir, name)
|
||||
|
||||
// Sanitize path to prevent directory traversal
|
||||
if !strings.HasPrefix(filepath.Clean(targetPath), filepath.Clean(destDir)+string(os.PathSeparator)) {
|
||||
return fmt.Errorf("invalid file path in tar: %s", header.Name)
|
||||
}
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
|
||||
|
||||
101
internal/runner/workspace/archive_test.go
Normal file
101
internal/runner/workspace/archive_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package workspace
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func createTarBuffer(files map[string]string) *bytes.Buffer {
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
for name, content := range files {
|
||||
hdr := &tar.Header{
|
||||
Name: name,
|
||||
Mode: 0644,
|
||||
Size: int64(len(content)),
|
||||
}
|
||||
tw.WriteHeader(hdr)
|
||||
tw.Write([]byte(content))
|
||||
}
|
||||
tw.Close()
|
||||
return &buf
|
||||
}
|
||||
|
||||
func TestExtractTar(t *testing.T) {
|
||||
destDir := t.TempDir()
|
||||
|
||||
buf := createTarBuffer(map[string]string{
|
||||
"hello.txt": "world",
|
||||
"sub/a.txt": "nested",
|
||||
})
|
||||
|
||||
if err := ExtractTar(buf, destDir); err != nil {
|
||||
t.Fatalf("ExtractTar: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(destDir, "hello.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("read hello.txt: %v", err)
|
||||
}
|
||||
if string(data) != "world" {
|
||||
t.Errorf("hello.txt = %q, want %q", data, "world")
|
||||
}
|
||||
|
||||
data, err = os.ReadFile(filepath.Join(destDir, "sub", "a.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("read sub/a.txt: %v", err)
|
||||
}
|
||||
if string(data) != "nested" {
|
||||
t.Errorf("sub/a.txt = %q, want %q", data, "nested")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractTarStripPrefix(t *testing.T) {
|
||||
destDir := t.TempDir()
|
||||
|
||||
buf := createTarBuffer(map[string]string{
|
||||
"toplevel/": "",
|
||||
"toplevel/foo.txt": "bar",
|
||||
})
|
||||
|
||||
if err := ExtractTarStripPrefix(buf, destDir); err != nil {
|
||||
t.Fatalf("ExtractTarStripPrefix: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(destDir, "foo.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("read foo.txt: %v", err)
|
||||
}
|
||||
if string(data) != "bar" {
|
||||
t.Errorf("foo.txt = %q, want %q", data, "bar")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractTarStripPrefix_PathTraversal(t *testing.T) {
|
||||
destDir := t.TempDir()
|
||||
|
||||
buf := createTarBuffer(map[string]string{
|
||||
"prefix/../../../etc/passwd": "pwned",
|
||||
})
|
||||
|
||||
err := ExtractTarStripPrefix(buf, destDir)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for path traversal, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractTar_PathTraversal(t *testing.T) {
|
||||
destDir := t.TempDir()
|
||||
|
||||
buf := createTarBuffer(map[string]string{
|
||||
"../../../etc/passwd": "pwned",
|
||||
})
|
||||
|
||||
err := ExtractTar(buf, destDir)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for path traversal, got nil")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user