// Copyright 2022 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. package directx import ( "errors" "fmt" "math" "os" "runtime" "strconv" "strings" "time" "unsafe" "golang.org/x/sys/windows" "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver" "github.com/hajimehoshi/ebiten/v2/internal/microsoftgdk" ) type stencilMode int const ( prepareStencil stencilMode = iota drawWithStencil noStencil ) const frameCount = 2 func pow2(x uint32) uint32 { if x > (math.MaxUint32+1)/2 { return math.MaxUint32 } var p2 uint32 = 1 for p2 < x { p2 *= 2 } return p2 } func parseFeatureLevel(str string) (_D3D_FEATURE_LEVEL, bool) { switch str { case "11_0": return _D3D_FEATURE_LEVEL_11_0, true case "11_1": return _D3D_FEATURE_LEVEL_11_1, true case "12_0": return _D3D_FEATURE_LEVEL_12_0, true case "12_1": return _D3D_FEATURE_LEVEL_12_1, true case "12_2": return _D3D_FEATURE_LEVEL_12_2, true default: return 0, false } } // NewGraphics creates an implementation of graphicsdriver.Graphics for DirectX. // The returned graphics value is nil iff the error is not nil. func NewGraphics() (graphicsdriver.Graphics, error) { if !isD3DCompilerDLLAvailable() { return nil, fmt.Errorf("directx: d3dcompiler_*.dll is missing in this environment") } var useWARP bool var useDebugLayer bool version := 11 // Specify the feature level 11 by default. // Some old cards don't work well with the default feature level (#2447, #2486). featureLevel := _D3D_FEATURE_LEVEL_11_0 // Parse a special environment variable for backward compatibility. if env := os.Getenv("EBITENGINE_DIRECTX_FEATURE_LEVEL"); env != "" { if fl, ok := parseFeatureLevel(env); ok { featureLevel = fl } } env := os.Getenv("EBITENGINE_DIRECTX") if env == "" { // For backward compatibility, read the EBITEN_ version. env = os.Getenv("EBITEN_DIRECTX") } for _, t := range strings.Split(env, ",") { t := strings.TrimSpace(t) switch { case t == "warp": // TODO: Is WARP available on Xbox? useWARP = true case t == "debug": useDebugLayer = true case strings.HasPrefix(t, "version="): v, err := strconv.Atoi(t[len("version="):]) if err != nil { continue } version = v case strings.HasPrefix(t, "featurelevel="): fl, ok := parseFeatureLevel(t[len("featurelevel="):]) if !ok { continue } featureLevel = fl } } // On Xbox, only DirectX 12 is available. if microsoftgdk.IsXbox() { version = 12 } switch version { case 11: g, err := newGraphics11(useWARP, useDebugLayer) if err != nil { return nil, err } return g, nil case 12: g, err := newGraphics12(useWARP, useDebugLayer, featureLevel) if err != nil { return nil, err } return g, nil default: panic(fmt.Sprintf("directx: unexpected DirectX version: %d", version)) } } type graphicsInfra struct { factory *_IDXGIFactory swapChain *_IDXGISwapChain swapChain4 *_IDXGISwapChain4 allowTearing bool // occluded reports whether the screen is invisible or not. occluded bool // lastTime is the last time for rendering. lastTime time.Time } func newGraphicsInfra() (*graphicsInfra, error) { f, err := _CreateDXGIFactory() if err != nil { return nil, err } g := &graphicsInfra{ factory: f, } runtime.SetFinalizer(g, (*graphicsInfra).release) if f, err := g.factory.QueryInterface(&_IID_IDXGIFactory5); err == nil && f != nil { factory := (*_IDXGIFactory5)(f) defer factory.Release() var allowTearing int32 if err := factory.CheckFeatureSupport(_DXGI_FEATURE_PRESENT_ALLOW_TEARING, unsafe.Pointer(&allowTearing), uint32(unsafe.Sizeof(allowTearing))); err == nil && allowTearing != 0 { g.allowTearing = true } } return g, nil } func (g *graphicsInfra) release() { if g.factory != nil { g.factory.Release() g.factory = nil } if g.swapChain != nil { g.swapChain.Release() g.swapChain = nil } if g.swapChain4 != nil { g.swapChain4.Release() g.swapChain4 = nil } } // appendAdapters appends found adapters to the given adapters. // Releasing them is the caller's responsibility. // // warpForDX12 is valid only for DirectX 12. func (g *graphicsInfra) appendAdapters(adapters []*_IDXGIAdapter1, warpForDX12 bool) ([]*_IDXGIAdapter1, error) { f, err := g.factory.QueryInterface(&_IID_IDXGIFactory4) if err != nil { return nil, err } if f == nil { return nil, fmt.Errorf("directx: IID_IDXGIFactory4 was not available") } factory4 := (*_IDXGIFactory4)(f) defer factory4.Release() if warpForDX12 { a, err := factory4.EnumWarpAdapter() if err != nil { return nil, err } adapters = append(adapters, a) return adapters, nil } for i := uint32(0); ; i++ { a, err := factory4.EnumAdapters1(i) if errors.Is(err, _DXGI_ERROR_NOT_FOUND) { break } if err != nil { return nil, err } adapters = append(adapters, a) } return adapters, nil } func (g *graphicsInfra) isSwapChainInited() bool { return g.swapChain != nil } func (g *graphicsInfra) initSwapChain(width, height int, device unsafe.Pointer, window windows.HWND) (ferr error) { if g.swapChain != nil { return fmt.Errorf("directx: swap chain must not be initialized at initSwapChain, but is already done") } // Create a swap chain. // // DXGI_ALPHA_MODE_PREMULTIPLIED doesn't work with a HWND well. // // IDXGIFactory::CreateSwapChain: Alpha blended swapchains must be created with CreateSwapChainForComposition, // or CreateSwapChainForCoreWindow with the DXGI_SWAP_CHAIN_FLAG_FOREGROUND_LAYER flag // // Use *_SEQUENTIAL swap effects to follow the Mozilla way: // https://github.com/mozilla/gecko-dev/blob/0907529ff72c456ddb47839f5f7ba16291f28dce/gfx/layers/d3d11/CompositorD3D11.cpp#L167-L254 desc := &_DXGI_SWAP_CHAIN_DESC{ BufferDesc: _DXGI_MODE_DESC{ Width: uint32(width), Height: uint32(height), Format: _DXGI_FORMAT_B8G8R8A8_UNORM, }, SampleDesc: _DXGI_SAMPLE_DESC{ Count: 1, Quality: 0, }, BufferUsage: _DXGI_USAGE_RENDER_TARGET_OUTPUT, BufferCount: frameCount, OutputWindow: window, Windowed: 1, SwapEffect: _DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL, } // DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL/DISCARD are not supported for older Windows than 10 or DirectX 12. // https://learn.microsoft.com/en-us/windows/win32/api/dxgi/ne-dxgi-dxgi_swap_effect if !isWindows10OrGreaterWin32() { desc.SwapEffect = _DXGI_SWAP_EFFECT_SEQUENTIAL } if g.allowTearing { desc.Flags |= uint32(_DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING) } s, err := g.factory.CreateSwapChain(device, desc) if err != nil { return err } g.swapChain = s defer func() { if ferr != nil { g.release() } }() if s4, err := g.swapChain.QueryInterface(&_IID_IDXGISwapChain4); err == nil && s4 != nil { g.swapChain4 = (*_IDXGISwapChain4)(s4) } // MakeWindowAssociation should be called after swap chain creation. // https://docs.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-idxgifactory-makewindowassociation if err := g.factory.MakeWindowAssociation(window, _DXGI_MWA_NO_WINDOW_CHANGES|_DXGI_MWA_NO_ALT_ENTER); err != nil { return err } return nil } func (g *graphicsInfra) resizeSwapChain(width, height int) error { if g.swapChain == nil { return fmt.Errorf("directx: swap chain must be initialized at resizeSwapChain, but is not") } var flag uint32 if g.allowTearing { flag |= uint32(_DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING) } if err := g.swapChain.ResizeBuffers(frameCount, uint32(width), uint32(height), _DXGI_FORMAT_B8G8R8A8_UNORM, flag); err != nil { return err } return nil } func (g *graphicsInfra) currentBackBufferIndex() (int, error) { if g.swapChain4 == nil { return 0, fmt.Errorf("directx: IDXGISwapChain4 is not available") } return int(g.swapChain4.GetCurrentBackBufferIndex()), nil } func (g *graphicsInfra) present(vsyncEnabled bool) error { if g.swapChain == nil { return fmt.Errorf("directx: swap chain must be initialized at present, but is not") } var syncInterval uint32 var flags _DXGI_PRESENT if g.occluded { // The screen is not visible. Test whether we can resume. flags |= _DXGI_PRESENT_TEST } else { // Do actual rendering only when the screen is visible. if vsyncEnabled { syncInterval = 1 } else if g.allowTearing { flags |= _DXGI_PRESENT_ALLOW_TEARING } } occluded, err := g.swapChain.Present(syncInterval, uint32(flags)) if err != nil { return err } g.occluded = occluded // Reduce FPS when the screen is invisible. now := time.Now() if g.occluded { if delta := 100*time.Millisecond - now.Sub(g.lastTime); delta > 0 { time.Sleep(delta) } } g.lastTime = now return nil } func (g *graphicsInfra) getBuffer(buffer uint32, riid *windows.GUID) (unsafe.Pointer, error) { return g.swapChain.GetBuffer(buffer, riid) }