diff --git a/examples/windowsize/main.go b/examples/windowsize/main.go index c2bbdb7dc..8722a6e7e 100644 --- a/examples/windowsize/main.go +++ b/examples/windowsize/main.go @@ -50,6 +50,7 @@ var ( flagMaxWindowSize = flag.String("maxwindowsize", "", "maximum window size (e.g., 1920x1080)") flagGraphicsLibrary = flag.String("graphicslibrary", "", "graphics library (e.g. opengl)") 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() { @@ -473,6 +474,14 @@ func main() { default: 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.ScreenTransparent = *flagTransparent op.X11ClassName = "Window-Size" diff --git a/graphics.go b/graphics.go index de18ca376..3104309b1 100644 --- a/graphics.go +++ b/graphics.go @@ -72,3 +72,17 @@ type DebugInfo struct { func ReadDebugInfo(d *DebugInfo) { 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 +) diff --git a/internal/graphicsdriver/graphics.go b/internal/graphicsdriver/graphics.go index 1c40b16ea..f1960a80f 100644 --- a/internal/graphicsdriver/graphics.go +++ b/internal/graphicsdriver/graphics.go @@ -95,3 +95,11 @@ type Shader interface { } type ShaderID int + +type ColorSpace int + +const ( + ColorSpaceDefault ColorSpace = iota + ColorSpaceSRGB + ColorSpaceDisplayP3 +) diff --git a/internal/graphicsdriver/metal/ca/ca_darwin.go b/internal/graphicsdriver/metal/ca/ca_darwin.go index 8c5bce768..2825368e2 100644 --- a/internal/graphicsdriver/metal/ca/ca_darwin.go +++ b/internal/graphicsdriver/metal/ca/ca_darwin.go @@ -29,6 +29,7 @@ import ( "github.com/ebitengine/purego/objc" "github.com/hajimehoshi/ebiten/v2/internal/cocoa" + "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver" "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/metal/mtl" ) @@ -51,7 +52,7 @@ type MetalLayer struct { // NewMetalLayer creates a new Core Animation Metal layer. // // 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) if err != nil { return MetalLayer{}, err @@ -67,14 +68,31 @@ func NewMetalLayer() (MetalLayer, error) { return MetalLayer{}, err } - kCGColorSpaceDisplayP3, err := purego.Dlsym(coreGraphics, "kCGColorSpaceDisplayP3") - if err != nil { - 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") + if err != nil { + return MetalLayer{}, err + } + colorSpaceSym = kCGColorSpaceDisplayP3 } 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" { - 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) purego.SyscallN(cgColorSpaceRelease, colorspace) } diff --git a/internal/graphicsdriver/metal/graphics_darwin.go b/internal/graphicsdriver/metal/graphics_darwin.go index 685fbe483..0e4660e82 100644 --- a/internal/graphicsdriver/metal/graphics_darwin.go +++ b/internal/graphicsdriver/metal/graphics_darwin.go @@ -35,6 +35,8 @@ import ( type Graphics struct { view view + colorSpace graphicsdriver.ColorSpace + cq mtl.CommandQueue cb mtl.CommandBuffer rce mtl.RenderCommandEncoder @@ -90,7 +92,7 @@ func init() { // NewGraphics creates an implementation of graphicsdriver.Graphics for Metal. // 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). // 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. @@ -98,12 +100,14 @@ func NewGraphics() (graphicsdriver.Graphics, error) { return nil, fmt.Errorf("metal: mtl.CreateSystemDefaultDevice failed: %w", systemDefaultDeviceErr) } - g := &Graphics{} + g := &Graphics{ + colorSpace: colorSpace, + } if runtime.GOOS != "ios" { // 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. - if err := g.view.initialize(systemDefaultDevice); err != nil { + if err := g.view.initialize(systemDefaultDevice, colorSpace); err != nil { return nil, err } } @@ -388,7 +392,7 @@ func (g *Graphics) Initialize() error { if runtime.GOOS == "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 } } diff --git a/internal/graphicsdriver/metal/view_darwin.go b/internal/graphicsdriver/metal/view_darwin.go index d45690b81..2a8607beb 100644 --- a/internal/graphicsdriver/metal/view_darwin.go +++ b/internal/graphicsdriver/metal/view_darwin.go @@ -17,6 +17,7 @@ package metal import ( "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/mtl" ) @@ -58,10 +59,10 @@ func (v *view) colorPixelFormat() mtl.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 - ml, err := ca.NewMetalLayer() + ml, err := ca.NewMetalLayer(colorSpace) if err != nil { return err } diff --git a/internal/graphicsdriver/opengl/graphics_js.go b/internal/graphicsdriver/opengl/graphics_js.go index 1d98d6119..001f6dfd4 100644 --- a/internal/graphicsdriver/opengl/graphics_js.go +++ b/internal/graphicsdriver/opengl/graphics_js.go @@ -27,7 +27,7 @@ type graphicsPlatform struct { // NewGraphics creates an implementation of graphicsdriver.Graphics for OpenGL. // 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 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") } + switch colorSpace { + case graphicsdriver.ColorSpaceSRGB: + glContext.Set("drawingBufferColorSpace", "srgb") + case graphicsdriver.ColorSpaceDisplayP3: + glContext.Set("drawingBufferColorSpace", "display-p3") + } + ctx, err := gl.NewDefaultContext(glContext) if err != nil { return nil, err diff --git a/internal/ui/ui.go b/internal/ui/ui.go index ed7efbee9..7d1436a7a 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -23,6 +23,7 @@ import ( _ "github.com/ebitengine/hideconsole" "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/thread" ) @@ -177,6 +178,7 @@ type RunOptions struct { SkipTaskbar bool SingleThread bool DisableHiDPI bool + ColorSpace graphicsdriver.ColorSpace X11ClassName string X11InstanceName string } diff --git a/internal/ui/ui_android.go b/internal/ui/ui_android.go index 3e9326f48..d688d85fc 100644 --- a/internal/ui/ui_android.go +++ b/internal/ui/ui_android.go @@ -95,6 +95,7 @@ import ( ) type graphicsDriverCreatorImpl struct { + colorSpace graphicsdriver.ColorSpace } func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) { diff --git a/internal/ui/ui_darwin.go b/internal/ui/ui_darwin.go index 6c73bcfc4..c2736e0a1 100644 --- a/internal/ui/ui_darwin.go +++ b/internal/ui/ui_darwin.go @@ -167,6 +167,7 @@ func (u *UserInterface) initializePlatform() error { type graphicsDriverCreatorImpl struct { transparent bool + colorSpace graphicsdriver.ColorSpace } 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") } -func (*graphicsDriverCreatorImpl) newMetal() (graphicsdriver.Graphics, error) { - return metal.NewGraphics() +func (g *graphicsDriverCreatorImpl) newMetal() (graphicsdriver.Graphics, error) { + return metal.NewGraphics(g.colorSpace) } func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, error) { diff --git a/internal/ui/ui_glfw.go b/internal/ui/ui_glfw.go index 9c3ccf342..1b253618f 100644 --- a/internal/ui/ui_glfw.go +++ b/internal/ui/ui_glfw.go @@ -1089,6 +1089,7 @@ func (u *UserInterface) initOnMainThread(options *RunOptions) error { g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{ transparent: options.ScreenTransparent, + colorSpace: options.ColorSpace, }, options.GraphicsLibrary) if err != nil { return err diff --git a/internal/ui/ui_ios.go b/internal/ui/ui_ios.go index 9da63fecb..190140df7 100644 --- a/internal/ui/ui_ios.go +++ b/internal/ui/ui_ios.go @@ -34,6 +34,7 @@ import ( ) type graphicsDriverCreatorImpl struct { + colorSpace graphicsdriver.ColorSpace } 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) { - return metal.NewGraphics() + return metal.NewGraphics(g.colorSpace) } func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, error) { diff --git a/internal/ui/ui_js.go b/internal/ui/ui_js.go index a8d58fa8f..3fa2dea3f 100644 --- a/internal/ui/ui_js.go +++ b/internal/ui/ui_js.go @@ -29,7 +29,8 @@ import ( ) type graphicsDriverCreatorImpl struct { - canvas js.Value + canvas js.Value + colorSpace graphicsdriver.ColorSpace } 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) { - return opengl.NewGraphics(g.canvas) + return opengl.NewGraphics(g.canvas, g.colorSpace) } func (*graphicsDriverCreatorImpl) newDirectX() (graphicsdriver.Graphics, error) { @@ -771,7 +772,8 @@ func (u *UserInterface) initOnMainThread(options *RunOptions) error { } g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{ - canvas: canvas, + canvas: canvas, + colorSpace: options.ColorSpace, }, options.GraphicsLibrary) if err != nil { return err diff --git a/internal/ui/ui_linbsd.go b/internal/ui/ui_linbsd.go index 4d8d79149..c0d77b2b0 100644 --- a/internal/ui/ui_linbsd.go +++ b/internal/ui/ui_linbsd.go @@ -36,6 +36,7 @@ func (u *UserInterface) initializePlatform() error { type graphicsDriverCreatorImpl struct { transparent bool + colorSpace graphicsdriver.ColorSpace } func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) { diff --git a/internal/ui/ui_mobile.go b/internal/ui/ui_mobile.go index b86e9a755..c1dd426b8 100644 --- a/internal/ui/ui_mobile.go +++ b/internal/ui/ui_mobile.go @@ -143,7 +143,9 @@ func (u *UserInterface) runMobile(game Game, options *RunOptions) (err error) { u.context = newContext(game) - g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{}, options.GraphicsLibrary) + g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{ + colorSpace: options.ColorSpace, + }, options.GraphicsLibrary) if err != nil { return err } diff --git a/internal/ui/ui_windows.go b/internal/ui/ui_windows.go index 77882e3a8..adadac79e 100644 --- a/internal/ui/ui_windows.go +++ b/internal/ui/ui_windows.go @@ -36,6 +36,7 @@ func (u *UserInterface) initializePlatform() error { type graphicsDriverCreatorImpl struct { transparent bool + colorSpace graphicsdriver.ColorSpace } func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) { diff --git a/run.go b/run.go index 4b75970e8..d1f5e3f1a 100644 --- a/run.go +++ b/run.go @@ -22,6 +22,7 @@ import ( "sync/atomic" "github.com/hajimehoshi/ebiten/v2/internal/clock" + "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver" "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. 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. X11ClassName string @@ -713,6 +722,7 @@ func toUIRunOptions(options *RunGameOptions) *ui.RunOptions { SkipTaskbar: options.SkipTaskbar, SingleThread: options.SingleThread, DisableHiDPI: options.DisableHiDPI, + ColorSpace: graphicsdriver.ColorSpace(options.ColorSpace), X11ClassName: options.X11ClassName, X11InstanceName: options.X11InstanceName, }