shaderprecomp: implement for Windows

Closes #2861
This commit is contained in:
Hajime Hoshi 2024-05-05 16:52:10 +09:00
parent 5d4a68b0ea
commit 10d9660125
10 changed files with 366 additions and 7 deletions

2
.gitignore vendored
View File

@ -8,5 +8,7 @@
go.work go.work
go.work.sum go.work.sum
*.fxc
!dummy.fxc
*.metallib *.metallib
!dummy.metallib !dummy.metallib

View File

@ -0,0 +1 @@
This is a dummy .fxc file to trick Go's embed package.

View File

@ -0,0 +1,130 @@
// 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 HLSL blobs (FXC files).
//
// See https://learn.microsoft.com/en-us/windows/win32/direct3dtools/fxc.
package main
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/hajimehoshi/ebiten/v2/shaderprecomp"
)
func main() {
if err := run(); err != nil {
panic(err)
}
}
func run() error {
if _, err := exec.LookPath("fxc.exe"); err != nil {
if errors.Is(err, exec.ErrNotFound) {
fmt.Fprintln(os.Stderr, "fxc.exe not found. Please install Windows SDK.")
fmt.Fprintln(os.Stderr, "See https://learn.microsoft.com/en-us/windows/win32/direct3dtools/fxc for more details.")
fmt.Fprintln(os.Stderr, "On PowerShell, you can add a path to the PATH environment variable temporarily like:")
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, ` & (Get-Process -Id $PID).Path { $env:PATH="C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64;"+$env:PATH; go generate .\examples\shaderprecomp\fxc\ }`)
fmt.Fprintln(os.Stderr)
os.Exit(1)
}
return err
}
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)
for _, src := range srcs {
// Compiling sources in parallel causes a mixed error message on the console.
if err := compile(src, tmpdir); err != nil {
return err
}
}
return nil
}
func generateHSLSFiles(source *shaderprecomp.ShaderSource, tmpdir string) (vs, ps string, err error) {
id := source.ID().String()
vsHLSLFilePath := filepath.Join(tmpdir, id+"_vs.hlsl")
vsf, err := os.Create(vsHLSLFilePath)
if err != nil {
return "", "", err
}
defer vsf.Close()
psHLSLFilePath := filepath.Join(tmpdir, id+"_ps.hlsl")
psf, err := os.Create(psHLSLFilePath)
if err != nil {
return "", "", err
}
defer psf.Close()
if err := shaderprecomp.CompileToHLSL(vsf, psf, source); err != nil {
return "", "", err
}
return vsHLSLFilePath, psHLSLFilePath, nil
}
func compile(source *shaderprecomp.ShaderSource, tmpdir string) error {
// Generate HLSL files. Make sure this process doesn't have any handlers of the files.
// Without closing the files, fxc.exe cannot access the files.
vsHLSLFilePath, psHLSLFilePath, err := generateHSLSFiles(source, tmpdir)
if err != nil {
return err
}
id := source.ID().String()
vsFXCFilePath := id + "_vs.fxc"
cmd := exec.Command("fxc.exe", "/nologo", "/O3", "/T", shaderprecomp.HLSLVertexShaderProfile, "/E", shaderprecomp.HLSLVertexShaderEntryPoint, "/Fo", vsFXCFilePath, vsHLSLFilePath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
psFXCFilePath := id + "_ps.fxc"
cmd = exec.Command("fxc.exe", "/nologo", "/O3", "/T", shaderprecomp.HLSLPixelShaderProfile, "/E", shaderprecomp.HLSLPixelShaderEntryPoint, "/Fo", psFXCFilePath, psHLSLFilePath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
return nil
}

View File

@ -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 fxc

View File

@ -67,6 +67,7 @@ func main() {
if err := registerPrecompiledShaders(); err != nil { if err := registerPrecompiledShaders(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
ebiten.SetWindowTitle("Ebitengine Example (Shader Precompilation)")
if err := ebiten.RunGame(&Game{}); err != nil { if err := ebiten.RunGame(&Game{}); err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !darwin //go:build !darwin && !windows
package main package main

View File

@ -0,0 +1,65 @@
// 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"
)
// https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/
//go:embed fxc/*.fxc
var fxcs 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 {
vsname := src.ID().String() + "_vs.fxc"
vs, err := fxcs.ReadFile("fxc/" + vsname)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
fmt.Fprintf(os.Stderr, "precompiled HLSL library %s was not found. Run 'go generate' for 'fxc' directory to generate them.\n", vsname)
continue
}
return err
}
psname := src.ID().String() + "_ps.fxc"
ps, err := fxcs.ReadFile("fxc/" + psname)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
fmt.Fprintf(os.Stderr, "precompiled HLSL library %s was not found. Run 'go generate' for 'fxc' directory to generate them.\n", psname)
continue
}
return err
}
shaderprecomp.RegisterFXCs(src, vs, ps)
}
return nil
}

View File

@ -72,6 +72,7 @@ const (
var ( var (
procD3DCompile *windows.LazyProc procD3DCompile *windows.LazyProc
procD3DCreateBlob *windows.LazyProc
) )
func init() { func init() {
@ -93,6 +94,7 @@ func init() {
} }
procD3DCompile = d3dcompiler.NewProc("D3DCompile") procD3DCompile = d3dcompiler.NewProc("D3DCompile")
procD3DCreateBlob = d3dcompiler.NewProc("D3DCreateBlob")
} }
func isD3DCompilerDLLAvailable() bool { func isD3DCompilerDLLAvailable() bool {
@ -135,6 +137,19 @@ func _D3DCompile(srcData []byte, sourceName string, pDefines []_D3D_SHADER_MACRO
return code, nil return code, nil
} }
func _D3DCreateBlob(size uint) (*_ID3DBlob, error) {
if !isD3DCompilerDLLAvailable() {
return nil, fmt.Errorf("directx: d3dcompiler_*.dll is missing in this environment")
}
var blob *_ID3DBlob
r, _, _ := procD3DCreateBlob.Call(uintptr(size), uintptr(unsafe.Pointer(&blob)))
if uint32(r) != uint32(windows.S_OK) {
return nil, fmt.Errorf("directx: D3DCreateBlob failed: %w", handleError(windows.Handle(uint32(r))))
}
return blob, nil
}
type _D3D_SHADER_MACRO struct { type _D3D_SHADER_MACRO struct {
Name *byte Name *byte
Definition *byte Definition *byte

View File

@ -16,6 +16,8 @@ package directx
import ( import (
"fmt" "fmt"
"sync"
"unsafe"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
@ -24,12 +26,57 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/shaderir/hlsl" "github.com/hajimehoshi/ebiten/v2/internal/shaderir/hlsl"
) )
const (
VertexShaderProfile = "vs_4_0"
PixelShaderProfile = "ps_4_0"
VertexShaderEntryPoint = "VSMain"
PixelShaderEntryPoint = "PSMain"
)
type fxcPair struct {
vertex []byte
pixel []byte
}
type precompiledFXCs struct {
binaries map[shaderir.SourceHash]fxcPair
m sync.Mutex
}
func (c *precompiledFXCs) put(hash shaderir.SourceHash, vertex, pixel []byte) {
c.m.Lock()
defer c.m.Unlock()
if c.binaries == nil {
c.binaries = map[shaderir.SourceHash]fxcPair{}
}
if _, ok := c.binaries[hash]; ok {
panic(fmt.Sprintf("directx: the precompiled library for the hash %s is already registered", hash.String()))
}
c.binaries[hash] = fxcPair{
vertex: vertex,
pixel: pixel,
}
}
func (c *precompiledFXCs) get(hash shaderir.SourceHash) ([]byte, []byte) {
c.m.Lock()
defer c.m.Unlock()
f := c.binaries[hash]
return f.vertex, f.pixel
}
var thePrecompiledFXCs precompiledFXCs
func RegisterPrecompiledFXCs(hash shaderir.SourceHash, vertex, pixel []byte) {
thePrecompiledFXCs.put(hash, vertex, pixel)
}
var vertexShaderCache = map[string]*_ID3DBlob{} var vertexShaderCache = map[string]*_ID3DBlob{}
func compileShader(program *shaderir.Program) (vsh, psh *_ID3DBlob, ferr error) { func compileShader(program *shaderir.Program) (vsh, psh *_ID3DBlob, ferr error) {
vs, ps := hlsl.Compile(program)
var flag uint32 = uint32(_D3DCOMPILE_OPTIMIZATION_LEVEL3)
defer func() { defer func() {
if ferr == nil { if ferr == nil {
return return
@ -42,6 +89,22 @@ func compileShader(program *shaderir.Program) (vsh, psh *_ID3DBlob, ferr error)
} }
}() }()
if vshBin, pshBin := thePrecompiledFXCs.get(program.SourceHash); vshBin != nil && pshBin != nil {
var err error
if vsh, err = _D3DCreateBlob(uint(len(vshBin))); err != nil {
return nil, nil, err
}
if psh, err = _D3DCreateBlob(uint(len(pshBin))); err != nil {
return nil, nil, err
}
copy(unsafe.Slice((*byte)(vsh.GetBufferPointer()), vsh.GetBufferSize()), vshBin)
copy(unsafe.Slice((*byte)(psh.GetBufferPointer()), psh.GetBufferSize()), pshBin)
return vsh, psh, nil
}
vs, ps := hlsl.Compile(program)
var flag uint32 = uint32(_D3DCOMPILE_OPTIMIZATION_LEVEL3)
var wg errgroup.Group var wg errgroup.Group
// Vertex shaders are likely the same. If so, reuse the same _ID3DBlob. // Vertex shaders are likely the same. If so, reuse the same _ID3DBlob.
@ -58,7 +121,7 @@ func compileShader(program *shaderir.Program) (vsh, psh *_ID3DBlob, ferr error)
} }
}() }()
wg.Go(func() error { wg.Go(func() error {
v, err := _D3DCompile([]byte(vs), "shader", nil, nil, "VSMain", "vs_4_0", flag, 0) v, err := _D3DCompile([]byte(vs), "shader", nil, nil, VertexShaderEntryPoint, VertexShaderProfile, flag, 0)
if err != nil { if err != nil {
return fmt.Errorf("directx: D3DCompile for VSMain failed, original source: %s, %w", vs, err) return fmt.Errorf("directx: D3DCompile for VSMain failed, original source: %s, %w", vs, err)
} }
@ -67,7 +130,7 @@ func compileShader(program *shaderir.Program) (vsh, psh *_ID3DBlob, ferr error)
}) })
} }
wg.Go(func() error { wg.Go(func() error {
p, err := _D3DCompile([]byte(ps), "shader", nil, nil, "PSMain", "ps_4_0", flag, 0) p, err := _D3DCompile([]byte(ps), "shader", nil, nil, PixelShaderEntryPoint, PixelShaderProfile, flag, 0)
if err != nil { if err != nil {
return fmt.Errorf("directx: D3DCompile for PSMain failed, original source: %s, %w", ps, err) return fmt.Errorf("directx: D3DCompile for PSMain failed, original source: %s, %w", ps, err)
} }

View File

@ -0,0 +1,65 @@
// 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 shaderprecomp
import (
"io"
"github.com/hajimehoshi/ebiten/v2/internal/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/directx"
"github.com/hajimehoshi/ebiten/v2/internal/shaderir"
"github.com/hajimehoshi/ebiten/v2/internal/shaderir/hlsl"
)
const (
// HLSLVertexShaderProfile is the target profile for vertex shaders.
HLSLVertexShaderProfile = directx.VertexShaderProfile
// HLSLPixelShaderProfile is the target profile for pixel shaders.
HLSLPixelShaderProfile = directx.PixelShaderProfile
// HLSLVertexShaderEntryPoint is the entry point name for vertex shaders.
HLSLVertexShaderEntryPoint = directx.VertexShaderEntryPoint
// HLSLPixelShaderEntryPoint is the entry point name for pixel shaders.
HLSLPixelShaderEntryPoint = directx.PixelShaderEntryPoint
)
// CompileToHLSL compiles the shader source to High-Level Shader Language to writers.
//
// CompileToHLSL is concurrent-safe.
func CompileToHLSL(vertexWriter, pixelWriter io.Writer, source *ShaderSource) error {
ir, err := graphics.CompileShader(source.source)
if err != nil {
return err
}
vs, ps := hlsl.Compile(ir)
if _, err = vertexWriter.Write([]byte(vs)); err != nil {
return err
}
if _, err = pixelWriter.Write([]byte(ps)); err != nil {
return err
}
return nil
}
// RegisterFXCs registers a precompiled HLSL (FXC) for a shader source.
// vertexFXC and pixelFXC must be the content of .fxc files generated by `fxc` command.
// For more details, see https://learn.microsoft.com/en-us/windows/win32/direct3dtools/dx-graphics-tools-fxc-using.
//
// RegisterFXCs is concurrent-safe.
func RegisterFXCs(source *ShaderSource, vertexFXC, pixelFXC []byte) {
directx.RegisterPrecompiledFXCs(shaderir.SourceHash(source.ID()), vertexFXC, pixelFXC)
}