Skip to content

Commit

Permalink
all: add a new package shaderprecomp
Browse files Browse the repository at this point in the history
The current implementation is only for macOS so far.

Updates #2861
  • Loading branch information
hajimehoshi committed May 4, 2024
1 parent d7df5eb commit c46f62e
Show file tree
Hide file tree
Showing 17 changed files with 599 additions and 12 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
.vscode
go.work
go.work.sum

*.metallib
!dummy.metallib
33 changes: 33 additions & 0 deletions examples/shaderprecomp/defaultshader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2020 The Ebiten Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build ignore

//kage:unit pixels

package main

var Time float
var Cursor vec2

func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
pos := (dstPos.xy - imageDstOrigin()) / imageDstSize()
pos += Cursor / imageDstSize() / 4
clr := 0.0
clr += sin(pos.x*cos(Time/15)*80) + cos(pos.y*cos(Time/15)*10)
clr += sin(pos.y*sin(Time/10)*40) + cos(pos.x*sin(Time/25)*40)
clr += sin(pos.x*sin(Time/5)*10) + sin(pos.y*sin(Time/35)*80)
clr *= sin(Time/10) * 0.5
return vec4(clr, clr*0.5, sin(clr+Time/3)*0.75, 1)
}
73 changes: 73 additions & 0 deletions examples/shaderprecomp/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2024 The Ebitengine Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
_ "embed"
"log"

"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)

//go:embed defaultshader.go
var defaultShaderSourceBytes []byte

type Game struct {
defaultShader *ebiten.Shader
counter int
}

func (g *Game) Update() error {
g.counter++

if g.defaultShader == nil {
s, err := ebiten.NewShader(defaultShaderSourceBytes)
if err != nil {
return err
}
g.defaultShader = s
}
return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
cx, cy := ebiten.CursorPosition()
w, h := screen.Bounds().Dx(), screen.Bounds().Dy()
op := &ebiten.DrawRectShaderOptions{}
op.Uniforms = map[string]interface{}{
"Time": float32(g.counter) / float32(ebiten.TPS()),
"Cursor": []float32{float32(cx), float32(cy)},
}
screen.DrawRectShader(w, h, g.defaultShader, op)

msg := `This is a test for shader precompilation.
Precompilation works only on macOS so far.
Note that this example still works even without shader precompilation.`
ebitenutil.DebugPrint(screen, msg)
}

func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
return outsideWidth, outsideHeight
}

func main() {
if err := registerPrecompiledShaders(); err != nil {
log.Fatal(err)
}
if err := ebiten.RunGame(&Game{}); err != nil {
log.Fatal(err)
}
}
1 change: 1 addition & 0 deletions examples/shaderprecomp/metallib/dummy.metallib
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is a dummy .metallib file to trick Go's embed package.
103 changes: 103 additions & 0 deletions examples/shaderprecomp/metallib/gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright 2024 The Ebitengine Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build ignore

// This is a program to generate precompiled Metal libraries.
//
// See https://developer.apple.com/documentation/metal/shader_libraries/building_a_shader_library_by_precompiling_source_files.
package main

import (
"os"
"os/exec"
"path/filepath"

"golang.org/x/sync/errgroup"

"github.com/hajimehoshi/ebiten/v2/shaderprecomp"
)

func main() {
if err := run(); err != nil {
panic(err)
}
}

func run() error {
tmpdir, err := os.MkdirTemp("", "")
if err != nil {
return err
}
defer os.RemoveAll(tmpdir)

srcs := shaderprecomp.AppendBuildinShaderSources(nil)

defaultSrcBytes, err := os.ReadFile(filepath.Join("..", "defaultshader.go"))
if err != nil {
return err
}
defaultSrc, err := shaderprecomp.NewShaderSource(defaultSrcBytes)
if err != nil {
return err
}
srcs = append(srcs, defaultSrc)

var wg errgroup.Group
for _, src := range srcs {
source := src
wg.Go(func() error {
return compile(source, tmpdir)
})
}
if err := wg.Wait(); err != nil {
return err
}
return nil
}

func compile(source *shaderprecomp.ShaderSource, tmpdir string) error {
id := source.ID().String()

metalFilePath := filepath.Join(tmpdir, id+".metal")

f, err := os.Create(metalFilePath)
if err != nil {
return err
}
defer f.Close()

if err := shaderprecomp.CompileToMSL(f, source); err != nil {
return err
}
if err := f.Sync(); err != nil {
return err
}

irFilePath := filepath.Join(tmpdir, id+".ir")
cmd := exec.Command("xcrun", "-sdk", "macosx", "metal", "-o", irFilePath, "-c", metalFilePath)
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}

metallibFilePath := id + ".metallib"
cmd = exec.Command("xcrun", "-sdk", "macosx", "metallib", "-o", metallibFilePath, irFilePath)
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}

return nil
}
17 changes: 17 additions & 0 deletions examples/shaderprecomp/metallib/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2024 The Ebitengine Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:generate go run gen.go

package metallib
52 changes: 52 additions & 0 deletions examples/shaderprecomp/register_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2024 The Ebitengine Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"embed"
"errors"
"fmt"
"io/fs"
"os"

"github.com/hajimehoshi/ebiten/v2/shaderprecomp"
)

//go:embed metallib/*.metallib
var metallibs embed.FS

func registerPrecompiledShaders() error {
srcs := shaderprecomp.AppendBuildinShaderSources(nil)
defaultShaderSource, err := shaderprecomp.NewShaderSource(defaultShaderSourceBytes)
if err != nil {
return err
}
srcs = append(srcs, defaultShaderSource)

for _, src := range srcs {
name := src.ID().String() + ".metallib"
lib, err := metallibs.ReadFile("metallib/" + name)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
fmt.Fprintf(os.Stderr, "precompiled Metal library %s was not found. Run 'go generate' for 'metallib' directory to generate them\n", name)
continue
}
return err
}
shaderprecomp.RegisterMetalLibrary(src, lib)
}

return nil
}
27 changes: 27 additions & 0 deletions examples/shaderprecomp/register_others.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2024 The Ebitengine Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build !darwin

package main

import (
"fmt"
"os"
)

func registerPrecompiledShaders() error {
fmt.Fprintf(os.Stderr, "precompiled shaders are not available in this environment.\n")
return nil
}
10 changes: 10 additions & 0 deletions internal/builtinshader/shader.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,13 @@ func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
return vec4(0)
}
`)

func AppendShaderSources(sources [][]byte) [][]byte {
for filter := Filter(0); filter < FilterCount; filter++ {
for address := Address(0); address < AddressCount; address++ {
sources = append(sources, ShaderSource(filter, address, false), ShaderSource(filter, address, true))
}
}
sources = append(sources, ScreenShaderSource, ClearShaderSource)
return sources
}
25 changes: 21 additions & 4 deletions internal/graphics/shader.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ func __vertex(dstPos vec2, srcPos vec2, color vec4) (vec4, vec2, vec4) {
return shaderSuffix, nil
}

func CompileShader(src []byte) (*shaderir.Program, error) {
unit, err := shader.ParseCompilerDirectives(src)
func completeShaderSource(fragmentSrc []byte) ([]byte, error) {
unit, err := shader.ParseCompilerDirectives(fragmentSrc)
if err != nil {
return nil, err
}
Expand All @@ -172,14 +172,23 @@ func CompileShader(src []byte) (*shaderir.Program, error) {
}

var buf bytes.Buffer
buf.Write(src)
buf.Write(fragmentSrc)
buf.WriteString(suffix)

return buf.Bytes(), nil
}

func CompileShader(fragmentSrc []byte) (*shaderir.Program, error) {
src, err := completeShaderSource(fragmentSrc)
if err != nil {
return nil, err
}

const (
vert = "__vertex"
frag = "Fragment"
)
ir, err := shader.Compile(buf.Bytes(), vert, frag, ShaderImageCount)
ir, err := shader.Compile(src, vert, frag, ShaderImageCount)
if err != nil {
return nil, err
}
Expand All @@ -193,3 +202,11 @@ func CompileShader(src []byte) (*shaderir.Program, error) {

return ir, nil
}

func CalcSourceHash(fragmentSrc []byte) (shaderir.SourceHash, error) {
src, err := completeShaderSource(fragmentSrc)
if err != nil {
return shaderir.SourceHash{}, err
}
return shaderir.CalcSourceHash(src), nil
}

0 comments on commit c46f62e

Please sign in to comment.