ebiten: add ColorSpace and RunGameOptions.ColorSpace

This works only for macOS Metal and WebGL so far.

Closes #2871
This commit is contained in:
Hajime Hoshi 2024-08-27 23:55:33 +09:00
parent 42209606b1
commit f98003bcd5
17 changed files with 102 additions and 19 deletions

View File

@ -50,6 +50,7 @@ var (
flagMaxWindowSize = flag.String("maxwindowsize", "", "maximum window size (e.g., 1920x1080)") flagMaxWindowSize = flag.String("maxwindowsize", "", "maximum window size (e.g., 1920x1080)")
flagGraphicsLibrary = flag.String("graphicslibrary", "", "graphics library (e.g. opengl)") flagGraphicsLibrary = flag.String("graphicslibrary", "", "graphics library (e.g. opengl)")
flagRunnableOnUnfocused = flag.Bool("runnableonunfocused", true, "whether the app is runnable even on unfocused") flagRunnableOnUnfocused = flag.Bool("runnableonunfocused", true, "whether the app is runnable even on unfocused")
flagColorSpace = flag.String("colorspace", "", "color space ('', 'srgb', or 'display-p3')")
) )
func init() { func init() {
@ -473,6 +474,14 @@ func main() {
default: default:
log.Fatalf("unexpected graphics library: %s", *flagGraphicsLibrary) log.Fatalf("unexpected graphics library: %s", *flagGraphicsLibrary)
} }
switch *flagColorSpace {
case "":
op.ColorSpace = ebiten.ColorSpaceDefault
case "srgb":
op.ColorSpace = ebiten.ColorSpaceSRGB
case "display-p3":
op.ColorSpace = ebiten.ColorSpaceDisplayP3
}
op.InitUnfocused = !*flagInitFocused op.InitUnfocused = !*flagInitFocused
op.ScreenTransparent = *flagTransparent op.ScreenTransparent = *flagTransparent
op.X11ClassName = "Window-Size" op.X11ClassName = "Window-Size"

View File

@ -72,3 +72,17 @@ type DebugInfo struct {
func ReadDebugInfo(d *DebugInfo) { func ReadDebugInfo(d *DebugInfo) {
d.GraphicsLibrary = GraphicsLibrary(ui.Get().GraphicsLibrary()) d.GraphicsLibrary = GraphicsLibrary(ui.Get().GraphicsLibrary())
} }
// ColorSpace represents the color space of the screen.
type ColorSpace int
const (
// ColorSpaceDefault represents the default color space.
ColorSpaceDefault ColorSpace = iota
// ColorSpaceSRGB represents the sRGB color space (https://en.wikipedia.org/wiki/SRGB).
ColorSpaceSRGB
// ColorSpaceDisplayP3 represents the Display P3 color space (https://en.wikipedia.org/wiki/DCI-P3).
ColorSpaceDisplayP3
)

View File

@ -95,3 +95,11 @@ type Shader interface {
} }
type ShaderID int type ShaderID int
type ColorSpace int
const (
ColorSpaceDefault ColorSpace = iota
ColorSpaceSRGB
ColorSpaceDisplayP3
)

View File

@ -29,6 +29,7 @@ import (
"github.com/ebitengine/purego/objc" "github.com/ebitengine/purego/objc"
"github.com/hajimehoshi/ebiten/v2/internal/cocoa" "github.com/hajimehoshi/ebiten/v2/internal/cocoa"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/metal/mtl" "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/metal/mtl"
) )
@ -51,7 +52,7 @@ type MetalLayer struct {
// NewMetalLayer creates a new Core Animation Metal layer. // NewMetalLayer creates a new Core Animation Metal layer.
// //
// Reference: https://developer.apple.com/documentation/quartzcore/cametallayer?language=objc. // Reference: https://developer.apple.com/documentation/quartzcore/cametallayer?language=objc.
func NewMetalLayer() (MetalLayer, error) { func NewMetalLayer(colorSpace graphicsdriver.ColorSpace) (MetalLayer, error) {
coreGraphics, err := purego.Dlopen("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics", purego.RTLD_LAZY|purego.RTLD_GLOBAL) coreGraphics, err := purego.Dlopen("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics", purego.RTLD_LAZY|purego.RTLD_GLOBAL)
if err != nil { if err != nil {
return MetalLayer{}, err return MetalLayer{}, err
@ -67,14 +68,31 @@ func NewMetalLayer() (MetalLayer, error) {
return MetalLayer{}, err return MetalLayer{}, err
} }
var colorSpaceSym uintptr
switch colorSpace {
case graphicsdriver.ColorSpaceSRGB:
kCGColorSpaceSRGB, err := purego.Dlsym(coreGraphics, "kCGColorSpaceSRGB")
if err != nil {
return MetalLayer{}, err
}
colorSpaceSym = kCGColorSpaceSRGB
default:
fallthrough
case graphicsdriver.ColorSpaceDisplayP3:
kCGColorSpaceDisplayP3, err := purego.Dlsym(coreGraphics, "kCGColorSpaceDisplayP3") kCGColorSpaceDisplayP3, err := purego.Dlsym(coreGraphics, "kCGColorSpaceDisplayP3")
if err != nil { if err != nil {
return MetalLayer{}, err return MetalLayer{}, err
} }
colorSpaceSym = kCGColorSpaceDisplayP3
}
layer := objc.ID(objc.GetClass("CAMetalLayer")).Send(objc.RegisterName("new")) layer := objc.ID(objc.GetClass("CAMetalLayer")).Send(objc.RegisterName("new"))
// setColorspace: is available from iOS 13.0?
// https://github.com/hajimehoshi/ebiten/commit/3af351a2aa31e30affd433429c42130015b302f3
// TODO: Enable this on iOS as well.
if runtime.GOOS != "ios" { if runtime.GOOS != "ios" {
colorspace, _, _ := purego.SyscallN(cgColorSpaceCreateWithName, **(**uintptr)(unsafe.Pointer(&kCGColorSpaceDisplayP3))) // Dlsym returns pointer to symbol so dereference it // Dlsym returns pointer to symbol so dereference it.
colorspace, _, _ := purego.SyscallN(cgColorSpaceCreateWithName, **(**uintptr)(unsafe.Pointer(&colorSpaceSym)))
layer.Send(objc.RegisterName("setColorspace:"), colorspace) layer.Send(objc.RegisterName("setColorspace:"), colorspace)
purego.SyscallN(cgColorSpaceRelease, colorspace) purego.SyscallN(cgColorSpaceRelease, colorspace)
} }

View File

@ -35,6 +35,8 @@ import (
type Graphics struct { type Graphics struct {
view view view view
colorSpace graphicsdriver.ColorSpace
cq mtl.CommandQueue cq mtl.CommandQueue
cb mtl.CommandBuffer cb mtl.CommandBuffer
rce mtl.RenderCommandEncoder rce mtl.RenderCommandEncoder
@ -90,7 +92,7 @@ func init() {
// NewGraphics creates an implementation of graphicsdriver.Graphics for Metal. // NewGraphics creates an implementation of graphicsdriver.Graphics for Metal.
// The returned graphics value is nil iff the error is not nil. // The returned graphics value is nil iff the error is not nil.
func NewGraphics() (graphicsdriver.Graphics, error) { func NewGraphics(colorSpace graphicsdriver.ColorSpace) (graphicsdriver.Graphics, error) {
// On old mac devices like iMac 2011, Metal is not supported (#779). // On old mac devices like iMac 2011, Metal is not supported (#779).
// TODO: Is there a better way to check whether Metal is available or not? // TODO: Is there a better way to check whether Metal is available or not?
// It seems OK to call MTLCreateSystemDefaultDevice multiple times, so this should be fine. // It seems OK to call MTLCreateSystemDefaultDevice multiple times, so this should be fine.
@ -98,12 +100,14 @@ func NewGraphics() (graphicsdriver.Graphics, error) {
return nil, fmt.Errorf("metal: mtl.CreateSystemDefaultDevice failed: %w", systemDefaultDeviceErr) return nil, fmt.Errorf("metal: mtl.CreateSystemDefaultDevice failed: %w", systemDefaultDeviceErr)
} }
g := &Graphics{} g := &Graphics{
colorSpace: colorSpace,
}
if runtime.GOOS != "ios" { if runtime.GOOS != "ios" {
// Initializing a Metal device and a layer must be done in the main thread on macOS. // Initializing a Metal device and a layer must be done in the main thread on macOS.
// Note that this assumes NewGraphics is called on the main thread on desktops. // Note that this assumes NewGraphics is called on the main thread on desktops.
if err := g.view.initialize(systemDefaultDevice); err != nil { if err := g.view.initialize(systemDefaultDevice, colorSpace); err != nil {
return nil, err return nil, err
} }
} }
@ -388,7 +392,7 @@ func (g *Graphics) Initialize() error {
if runtime.GOOS == "ios" { if runtime.GOOS == "ios" {
// Initializing a Metal device and a layer must be done in the render thread on iOS. // Initializing a Metal device and a layer must be done in the render thread on iOS.
if err := g.view.initialize(systemDefaultDevice); err != nil { if err := g.view.initialize(systemDefaultDevice, g.colorSpace); err != nil {
return err return err
} }
} }

View File

@ -17,6 +17,7 @@ package metal
import ( import (
"sync" "sync"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/metal/ca" "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/metal/ca"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/metal/mtl" "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/metal/mtl"
) )
@ -58,10 +59,10 @@ func (v *view) colorPixelFormat() mtl.PixelFormat {
return v.ml.PixelFormat() return v.ml.PixelFormat()
} }
func (v *view) initialize(device mtl.Device) error { func (v *view) initialize(device mtl.Device, colorSpace graphicsdriver.ColorSpace) error {
v.device = device v.device = device
ml, err := ca.NewMetalLayer() ml, err := ca.NewMetalLayer(colorSpace)
if err != nil { if err != nil {
return err return err
} }

View File

@ -27,7 +27,7 @@ type graphicsPlatform struct {
// NewGraphics creates an implementation of graphicsdriver.Graphics for OpenGL. // NewGraphics creates an implementation of graphicsdriver.Graphics for OpenGL.
// The returned graphics value is nil iff the error is not nil. // The returned graphics value is nil iff the error is not nil.
func NewGraphics(canvas js.Value) (graphicsdriver.Graphics, error) { func NewGraphics(canvas js.Value, colorSpace graphicsdriver.ColorSpace) (graphicsdriver.Graphics, error) {
var glContext js.Value var glContext js.Value
attr := js.Global().Get("Object").New() attr := js.Global().Get("Object").New()
@ -41,6 +41,13 @@ func NewGraphics(canvas js.Value) (graphicsdriver.Graphics, error) {
return nil, fmt.Errorf("opengl: getContext for webgl2 failed") return nil, fmt.Errorf("opengl: getContext for webgl2 failed")
} }
switch colorSpace {
case graphicsdriver.ColorSpaceSRGB:
glContext.Set("drawingBufferColorSpace", "srgb")
case graphicsdriver.ColorSpaceDisplayP3:
glContext.Set("drawingBufferColorSpace", "display-p3")
}
ctx, err := gl.NewDefaultContext(glContext) ctx, err := gl.NewDefaultContext(glContext)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -23,6 +23,7 @@ import (
_ "github.com/ebitengine/hideconsole" _ "github.com/ebitengine/hideconsole"
"github.com/hajimehoshi/ebiten/v2/internal/atlas" "github.com/hajimehoshi/ebiten/v2/internal/atlas"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/mipmap" "github.com/hajimehoshi/ebiten/v2/internal/mipmap"
"github.com/hajimehoshi/ebiten/v2/internal/thread" "github.com/hajimehoshi/ebiten/v2/internal/thread"
) )
@ -177,6 +178,7 @@ type RunOptions struct {
SkipTaskbar bool SkipTaskbar bool
SingleThread bool SingleThread bool
DisableHiDPI bool DisableHiDPI bool
ColorSpace graphicsdriver.ColorSpace
X11ClassName string X11ClassName string
X11InstanceName string X11InstanceName string
} }

View File

@ -95,6 +95,7 @@ import (
) )
type graphicsDriverCreatorImpl struct { type graphicsDriverCreatorImpl struct {
colorSpace graphicsdriver.ColorSpace
} }
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) { func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {

View File

@ -167,6 +167,7 @@ func (u *UserInterface) initializePlatform() error {
type graphicsDriverCreatorImpl struct { type graphicsDriverCreatorImpl struct {
transparent bool transparent bool
colorSpace graphicsdriver.ColorSpace
} }
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) { func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {
@ -189,8 +190,8 @@ func (*graphicsDriverCreatorImpl) newDirectX() (graphicsdriver.Graphics, error)
return nil, errors.New("ui: DirectX is not supported in this environment") return nil, errors.New("ui: DirectX is not supported in this environment")
} }
func (*graphicsDriverCreatorImpl) newMetal() (graphicsdriver.Graphics, error) { func (g *graphicsDriverCreatorImpl) newMetal() (graphicsdriver.Graphics, error) {
return metal.NewGraphics() return metal.NewGraphics(g.colorSpace)
} }
func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, error) { func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, error) {

View File

@ -1089,6 +1089,7 @@ func (u *UserInterface) initOnMainThread(options *RunOptions) error {
g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{ g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{
transparent: options.ScreenTransparent, transparent: options.ScreenTransparent,
colorSpace: options.ColorSpace,
}, options.GraphicsLibrary) }, options.GraphicsLibrary)
if err != nil { if err != nil {
return err return err

View File

@ -34,6 +34,7 @@ import (
) )
type graphicsDriverCreatorImpl struct { type graphicsDriverCreatorImpl struct {
colorSpace graphicsdriver.ColorSpace
} }
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) { func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {
@ -57,7 +58,7 @@ func (*graphicsDriverCreatorImpl) newDirectX() (graphicsdriver.Graphics, error)
} }
func (g *graphicsDriverCreatorImpl) newMetal() (graphicsdriver.Graphics, error) { func (g *graphicsDriverCreatorImpl) newMetal() (graphicsdriver.Graphics, error) {
return metal.NewGraphics() return metal.NewGraphics(g.colorSpace)
} }
func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, error) { func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, error) {

View File

@ -30,6 +30,7 @@ import (
type graphicsDriverCreatorImpl struct { type graphicsDriverCreatorImpl struct {
canvas js.Value canvas js.Value
colorSpace graphicsdriver.ColorSpace
} }
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) { func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {
@ -38,7 +39,7 @@ func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, Graphics
} }
func (g *graphicsDriverCreatorImpl) newOpenGL() (graphicsdriver.Graphics, error) { func (g *graphicsDriverCreatorImpl) newOpenGL() (graphicsdriver.Graphics, error) {
return opengl.NewGraphics(g.canvas) return opengl.NewGraphics(g.canvas, g.colorSpace)
} }
func (*graphicsDriverCreatorImpl) newDirectX() (graphicsdriver.Graphics, error) { func (*graphicsDriverCreatorImpl) newDirectX() (graphicsdriver.Graphics, error) {
@ -772,6 +773,7 @@ func (u *UserInterface) initOnMainThread(options *RunOptions) error {
g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{ g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{
canvas: canvas, canvas: canvas,
colorSpace: options.ColorSpace,
}, options.GraphicsLibrary) }, options.GraphicsLibrary)
if err != nil { if err != nil {
return err return err

View File

@ -36,6 +36,7 @@ func (u *UserInterface) initializePlatform() error {
type graphicsDriverCreatorImpl struct { type graphicsDriverCreatorImpl struct {
transparent bool transparent bool
colorSpace graphicsdriver.ColorSpace
} }
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) { func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {

View File

@ -143,7 +143,9 @@ func (u *UserInterface) runMobile(game Game, options *RunOptions) (err error) {
u.context = newContext(game) u.context = newContext(game)
g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{}, options.GraphicsLibrary) g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{
colorSpace: options.ColorSpace,
}, options.GraphicsLibrary)
if err != nil { if err != nil {
return err return err
} }

View File

@ -36,6 +36,7 @@ func (u *UserInterface) initializePlatform() error {
type graphicsDriverCreatorImpl struct { type graphicsDriverCreatorImpl struct {
transparent bool transparent bool
colorSpace graphicsdriver.ColorSpace
} }
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) { func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {

10
run.go
View File

@ -22,6 +22,7 @@ import (
"sync/atomic" "sync/atomic"
"github.com/hajimehoshi/ebiten/v2/internal/clock" "github.com/hajimehoshi/ebiten/v2/internal/clock"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/ui" "github.com/hajimehoshi/ebiten/v2/internal/ui"
) )
@ -283,6 +284,14 @@ type RunGameOptions struct {
// The default (zero) value is false, which means that HiDPI is enabled. // The default (zero) value is false, which means that HiDPI is enabled.
DisableHiDPI bool DisableHiDPI bool
// ColorSpace indicates the color space of the screen.
//
// ColorSpace is available only with some graphics libraries (macOS Metal and WebGL so far).
// Otherwise, ColorSpace is ignored.
//
// The default (zero) value is ColorSpaceDefault, which means that color space depends on the environment.
ColorSpace ColorSpace
// X11DisplayName is a class name in the ICCCM WM_CLASS window property. // X11DisplayName is a class name in the ICCCM WM_CLASS window property.
X11ClassName string X11ClassName string
@ -713,6 +722,7 @@ func toUIRunOptions(options *RunGameOptions) *ui.RunOptions {
SkipTaskbar: options.SkipTaskbar, SkipTaskbar: options.SkipTaskbar,
SingleThread: options.SingleThread, SingleThread: options.SingleThread,
DisableHiDPI: options.DisableHiDPI, DisableHiDPI: options.DisableHiDPI,
ColorSpace: graphicsdriver.ColorSpace(options.ColorSpace),
X11ClassName: options.X11ClassName, X11ClassName: options.X11ClassName,
X11InstanceName: options.X11InstanceName, X11InstanceName: options.X11InstanceName,
} }