Skip to content

Commit

Permalink
ebiten: add ColorSpace and RunGameOptions.ColorSpace
Browse files Browse the repository at this point in the history
This works only for macOS Metal and WebGL so far.

Closes #2871
  • Loading branch information
hajimehoshi committed Aug 27, 2024
1 parent 4220960 commit f98003b
Show file tree
Hide file tree
Showing 17 changed files with 102 additions and 19 deletions.
9 changes: 9 additions & 0 deletions examples/windowsize/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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"
Expand Down
14 changes: 14 additions & 0 deletions graphics.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
8 changes: 8 additions & 0 deletions internal/graphicsdriver/graphics.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,11 @@ type Shader interface {
}

type ShaderID int

type ColorSpace int

const (
ColorSpaceDefault ColorSpace = iota
ColorSpaceSRGB
ColorSpaceDisplayP3
)
28 changes: 23 additions & 5 deletions internal/graphicsdriver/metal/ca/ca_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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
Expand All @@ -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)
}
Expand Down
12 changes: 8 additions & 4 deletions internal/graphicsdriver/metal/graphics_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import (
type Graphics struct {
view view

colorSpace graphicsdriver.ColorSpace

cq mtl.CommandQueue
cb mtl.CommandBuffer
rce mtl.RenderCommandEncoder
Expand Down Expand Up @@ -90,20 +92,22 @@ 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.
if systemDefaultDeviceErr != nil {
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
}
}
Expand Down Expand Up @@ -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
}
}
Expand Down
5 changes: 3 additions & 2 deletions internal/graphicsdriver/metal/view_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
}
Expand Down
9 changes: 8 additions & 1 deletion internal/graphicsdriver/opengl/graphics_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions internal/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -177,6 +178,7 @@ type RunOptions struct {
SkipTaskbar bool
SingleThread bool
DisableHiDPI bool
ColorSpace graphicsdriver.ColorSpace
X11ClassName string
X11InstanceName string
}
Expand Down
1 change: 1 addition & 0 deletions internal/ui/ui_android.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import (
)

type graphicsDriverCreatorImpl struct {
colorSpace graphicsdriver.ColorSpace
}

func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {
Expand Down
5 changes: 3 additions & 2 deletions internal/ui/ui_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions internal/ui/ui_glfw.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion internal/ui/ui_ios.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
)

type graphicsDriverCreatorImpl struct {
colorSpace graphicsdriver.ColorSpace
}

func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {
Expand All @@ -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) {
Expand Down
8 changes: 5 additions & 3 deletions internal/ui/ui_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions internal/ui/ui_linbsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 3 additions & 1 deletion internal/ui/ui_mobile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions internal/ui/ui_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
10 changes: 10 additions & 0 deletions run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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,
}
Expand Down

0 comments on commit f98003b

Please sign in to comment.