summaryrefslogtreecommitdiff
path: root/internal/media/ffmpeg/wasm.go
blob: f395032faf1b02db76c62e44c8b1494ba1249684 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

//go:build !nowasmffmpeg && !nowasm

package ffmpeg

import (
	"context"
	"errors"
	"os"
	"runtime"
	"sync/atomic"
	"unsafe"

	"codeberg.org/gruf/go-ffmpreg/embed"
	"codeberg.org/gruf/go-ffmpreg/wasm"
	"github.com/tetratelabs/wazero"
	"golang.org/x/sys/cpu"
)

// ffmpreg is a concurrency-safe pointer
// to our necessary WebAssembly runtime
// and compiled ffmpreg module instance.
var ffmpreg atomic.Pointer[struct {
	run wazero.Runtime
	mod wazero.CompiledModule
}]

// initWASM safely prepares new WebAssembly runtime
// and compiles ffmpreg module instance, if the global
// pointer has not been already. else, is a no-op.
func initWASM(ctx context.Context) error {
	if ffmpreg.Load() != nil {
		return nil
	}

	// Check at runtime whether Wazero compiler support is available,
	// interpreter mode is too slow for a usable gotosocial experience.
	if reason, supported := isCompilerSupported(); !supported {
		return errors.New("!!! WAZERO COMPILER SUPPORT NOT AVAILABLE !!!" +
			" Reason: " + reason + "." +
			" Wazero in interpreter mode is too slow to use ffmpeg" +
			" (this will also affect SQLite if in use)." +
			" For more info and possible workarounds, please check: https://docs.gotosocial.org/en/latest/getting_started/releases/#supported-platforms")
	}

	// Allocate new runtime compiler config.
	cfg := wazero.NewRuntimeConfigCompiler()

	if dir := os.Getenv("GTS_WAZERO_COMPILATION_CACHE"); dir != "" {
		// Use on-filesystem compilation cache given by env.
		cache, err := wazero.NewCompilationCacheWithDir(dir)
		if err != nil {
			return err
		}

		// Update runtime config with cache.
		cfg = cfg.WithCompilationCache(cache)
	}

	var (
		run wazero.Runtime
		mod wazero.CompiledModule
		err error
		set bool
	)

	defer func() {
		if err == nil && set {
			// Drop binary.
			embed.Free()
			return
		}

		// Close module.
		if !isNil(mod) {
			mod.Close(ctx)
		}

		// Close runtime.
		if !isNil(run) {
			run.Close(ctx)
		}
	}()

	// Initialize new runtime from config.
	run, err = wasm.NewRuntime(ctx, cfg)
	if err != nil {
		return err
	}

	// Compile ffmpreg WebAssembly into memory.
	mod, err = run.CompileModule(ctx, embed.B())
	if err != nil {
		return err
	}

	// Try set global WASM runtime and module,
	// or if beaten to it defer will handle close.
	set = ffmpreg.CompareAndSwap(nil, &struct {
		run wazero.Runtime
		mod wazero.CompiledModule
	}{
		run: run,
		mod: mod,
	})

	return nil
}

func isCompilerSupported() (string, bool) {
	switch runtime.GOOS {
	case "linux", "android",
		"windows", "darwin",
		"freebsd", "netbsd", "dragonfly",
		"solaris", "illumos":
		break
	default:
		return "unsupported OS", false
	}
	switch runtime.GOARCH {
	case "amd64":
		// NOTE: wazero in the future may decouple the
		// requirement of simd (sse4_1+2) from requirements
		// for compiler support in the future, but even
		// still our module go-ffmpreg makes use of them.
		return "amd64 x86-64-v2 required (see: https://en.wikipedia.org/wiki/X86-64-v2)",
			cpu.Initialized && cpu.X86.HasSSE3 && cpu.X86.HasSSE41 && cpu.X86.HasSSE42
	case "arm64":
		// NOTE: this particular check may change if we
		// later update go-ffmpreg to a version that makes
		// use of threads, i.e. v7.x.x. in that case we would
		// need to check for cpu.ARM64.HasATOMICS.
		return "", true
	default:
		return "unsupported ARCH", false
	}
}

// isNil will safely check if 'v' is nil without
// dealing with weird Go interface nil bullshit.
func isNil(i interface{}) bool {
	type eface struct{ Type, Data unsafe.Pointer }
	return (*eface)(unsafe.Pointer(&i)).Data == nil
}