-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Fix colors looks darker in IINA compared to MPV, #5124 #5154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This commit will: - Change VideoView.setICCProfile to set the color space of the level to the color space of the screen - Remove setting of wantsExtendedDynamicRangeContent in VideoView.requestEdrMode - Reordered statements and methods in VideoLayer to match up with mpv - Add bufferDepth property to VideoLayer - Replaced setting of isOpaque with backgroundColor in VideoLayer - Add setting of wantsExtendedDynamicRangeContent to true in VideoLayer - Add setting of contentsFormat to RGBA16Float if bufferDepth is greater than 8 - Added setting of MPV_RENDER_PARAM_DEPTH to draw in VideoLayer - Changed createPixelFormat to support cocoa-cb-sw-renderer in VideoLayer - Added a findPixelFormat method to VideoLayer that supports cocoa-cb-10bit-context - Changed InspectorWindowController.updateInfo to handle color spaces without names - Changed parse_doc.rb to add a MPVOptionValue protocol and a CocoaCbSwRenderer enum type to MPVOption - Regenerated MPVOption - Added a getEnum method to MPVController that will return an option value as an enum constant These changes bring VideoLayer closer into alignment with the mpv reference implementation. Some differences are required. IINA uses a background thread for drawing which requires different locking compared to mpv which uses the main thread for drawing.
Lots for reviewers to criticize. See my comment in the issue for details about why this change is needed. I replaced setting of isOpaque with setting of backgroundColor because that is what mpv does. But reading the doc for The modification to |
Another change I forgot to mention. This commit adds the CGL pixel format attribute
Is mpv doing partial draws? And we need to discuss cocoa-cb-10bit-context:
Do we need to add UI for setting this? Is there a way to programmatically obtain this? Notes on the net indicate the old ways of doing so no longer work. |
On this:
Not entirely clear what Setting the layer's |
But I actually am curious if
However you seem to be correct that a lot of it is redundant now. It used to be the case that to create a transparent window you have to set Edit: I tested |
Btw is it correct to always set Out of the things you mentioned, which one was responsible for the color differences that were mentioned on SDR video? I don't think 10-bit vs 8-bit backbuffer would matter much for color shifts, so I was assuming that the main difference was in how/whether the ICC profile was set. |
if videoLayer.colorspace != VideoView.SRGB { | ||
videoLayer.colorspace = VideoView.SRGB | ||
videoLayer.wantsExtendedDynamicRangeContent = false | ||
let screenColorSpace = player.mainWindow.window?.screen?.colorSpace |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like it sets the color space of the layer to that of the screen? Why is this needed, by default caopengllayer should already match the screen's color space
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm operating under the assumption that the picture displayed by IINA must match the picture displayed by mpv (with matching options and mpv not using the new code that is not available to IINA) and therefore IINA should treat mpv as the reference implementation and thus IINA's code should as much as possible match up with how mpv uses libmpv
.
I was very undecided on some of these differences and finally decided it was better to align with mpv and if there are problems bring them to the attention of mpv. I'm still unsure about that decision, so good to question all this. I am worried about how much to match the mpv code as we already decided the init(layer:)
code was incorrect.
One difference I did not change is that IINA enables the multi-threaded OpenGL engine by setting kCGLCEMPEngine
in the OpenGL context.
On the color space of the layer…
I have added code to set the color space of the layer to the color space of the display since that is what mpv does as seen here.
If I comment out setting the color space then it ends up with a value of kCGColorSpaceExtendedLinearSRGB
and the picture looks like this:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I comment out setting the color space then it ends up with a value of kCGColorSpaceExtendedLinearSRGB
Isn't that because you set it to sRGB here:
Line 63 in 7798ac1
videoLayer.colorspace = VideoView.SRGB |
By default the colorspace is nil, and assuming CAOpenGLLayer follows the same as CAMetalLayer this indicates no colormatching is done. And an identity transformation is the same as colorspace of layer matching colorspace of display.
It's not strictly wrong to explicitly set the colorspace to that of the screen though, at worst you just have an extra noop conversion thrown in there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had commented that line out as well. I confirmed it was nil
at the end of the initializer. I failed trying to figure out how to set a watch point on Xcode to see where that is getting set. Maybe I missed something. I will debug some more to confirm I did not mess up that test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From a very quick test, that statement sets the color space to kCGColorSpaceDeviceRGB
. Something else is setting it to kCGColorSpaceExtendedLinearSRGB
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fought with Xcode and lldb quite a bit and struck out. I was able to set a breakpoint on setColorSpace
and it was not hit. It seemed like the problem was that Xcode did not find such a method on CAOpenGLLayer
. I tried to step into an assignment to colorspace
and that caused the debugger to fail. So I still have not found what is setting it to kCGColorSpaceExtendedLinearSRGB
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
br s -n "-[CAOpenGLLayer setColorspace:]
works for me in a sample application. What error were you seeing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't get an error. I was trying to set a breakpoint on all setColorSpace
methods. It worked but for some reason did not set one on the CAOpenGLLayer
method. The problem with stepping into was strange. The Xcode Debug
menu looked like it does when the app is running, with most items greyed out. Hovering over IINA showed the spinning beach ball of death.
I was able to set the breakpoint and the color space is being set when drawing if I comment out the code in IINA that set it:
(lldb) bt
* thread #1, stop reason = breakpoint 2.1
* frame #0: 0x00000001b1e1dd98 QuartzCore`-[CAOpenGLLayer setColorspace:]
frame #1: 0x00000001b1c35710 QuartzCore`CAOpenGLLayerDraw(CAOpenGLLayer*, double, CVTimeStamp const*, unsigned int) + 540
frame #2: 0x00000001b1c35324 QuartzCore`-[CAOpenGLLayer _display] + 520
frame #3: 0x000000010538f034 IINA`ViewLayer.display(self=0x000060000273dd50) at ViewLayer.swift:209:11
frame #4: 0x0000000105390130 IINA`@objc ViewLayer.display() at <compiler-generated>:0
frame #5: 0x00000001b1ba7ae8 QuartzCore`CA::Layer::display_if_needed(CA::Transaction*) + 760
frame #6: 0x00000001b1d1b4d4 QuartzCore`CA::Context::commit_transaction(CA::Transaction*, double, double*) + 456
frame #7: 0x00000001b1b8a2c0 QuartzCore`CA::Transaction::commit() + 648
frame #8: 0x000000010538f054 IINA`ViewLayer.display(self=0x000060000273dd50) at ViewLayer.swift:210:19
frame #9: 0x0000000105390130 IINA`@objc ViewLayer.display() at <compiler-generated>:0
frame #10: 0x0000000105390310 IINA`ViewLayer.draw(forced=true, self=0x000060000273dd50) at ViewLayer.swift:256:5
frame #11: 0x0000000105473400 IINA`PlayerCore.playbackRestarted(self=0x000000013e77ef90) at PlayerCore.swift:1884:37
frame #12: 0x00000001054fbba4 IINA`closure #8 in MPVController.handleEvent(self=0x0000600002226c00) at MPVController.swift:1101:16
frame #13: 0x0000000104ff3c94 IINA`thunk for @escaping @callee_guaranteed () -> () at <compiler-generated>:0
frame #14: 0x0000000109d44e30 libdispatch.dylib`_dispatch_call_block_and_release + 32
frame #15: 0x0000000109d4699c libdispatch.dylib`_dispatch_client_callout + 20
frame #16: 0x0000000109d59e24 libdispatch.dylib`_dispatch_main_queue_drain + 1220
frame #17: 0x0000000109d59950 libdispatch.dylib`_dispatch_main_queue_callback_4CF + 44
frame #18: 0x00000001aa6dfb84 CoreFoundation`__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 16
frame #19: 0x00000001aa69d304 CoreFoundation`__CFRunLoopRun + 1992
frame #20: 0x00000001aa69c3e8 CoreFoundation`CFRunLoopRunSpecific + 612
frame #21: 0x00000001b3ef2df0 HIToolbox`RunCurrentEventLoopInMode + 292
frame #22: 0x00000001b3ef2c2c HIToolbox`ReceiveNextEventCommon + 648
frame #23: 0x00000001b3ef2984 HIToolbox`_BlockUntilNextEventMatchingListInModeWithFilter + 76
frame #24: 0x00000001ad8c4134 AppKit`_DPSNextEvent + 636
frame #25: 0x00000001ad8c32d0 AppKit`-[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 716
frame #26: 0x00000001ad8b7734 AppKit`-[NSApplication run] + 464
frame #27: 0x00000001ad88eb84 AppKit`NSApplicationMain + 880
frame #28: 0x0000000105218d18 IINA`main at AppDelegate.swift:24:7
frame #29: 0x00000001aa267fd8 dyld`start + 2412
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very interesting. I looked at disassembly of quartzcore method CAOpenGLLayerDraw
and I see the line
r0 = _CAGetColorSpace(0x19);
r0 = [r20 setColorspace:r0];
That _CAGetColorSpace
private method is luckily already analyzed in https://www.fortinet.com/blog/threat-research/detailed-analysis-of-macos-ios-vulnerability-cve-2019-6231 which mentions
And the variable v3 is passed as the first argument to the function CAGetColorSpace. The function CAGetColorSpace is used to obtain the color space data from the array colorspaces. Actually, the variable v3 is the index value of this array
And based on the disassembly and their screenshots, the logic is that it first tries to lookup the cached colorspace, otherwise it creates a new colorspace and returns it. I am not skilled enough to reverse engineer any further, but based on the IDA decompilation from their screenshot which shows a switch
table of hardcoded colorspace names, the reasonable assumption is that the input constant of 0x19
results in a colorspace of kCGColorSpaceExtendedLinearSRGB
. We can verify this assumption by typing po CAGetColorSpace(0x19)
in lldb and the result is
<CGColorSpace 0x6000010dc420> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1 Linear; extended range)
which does indeed seem to be kCGColorSpaceExtendedLinearSRGB
.
Now another question is why I cannot reproduce this on a sample app that uses CAOpenGLLayer. Looking at the surrounding code in CAOpenGLLayerDraw it seems this codepath is only taken based on the output of CGLDescribePixelFormat
with 0x3a
. CGLDescribePixelFormat
allows you to query the pixel format attributes and 0x3a
is kCGLPFAColorFloat.
And indeed, as soon as I add kCGLPFAColorFloat
I see that the colorspace is now set to srgb if it started as nil.
Unfortunately there is not much info on what exactly this does or why it's even needed. Header doc says "/* color buffers store floating point pixels */" from https://registry.khronos.org/OpenGL/extensions/APPLE/APPLE_float_pixels.txt. The corresponding NS version (NSOpenGLPFAColorFloat
) has slightly more detail
A Boolean attribute. If present, this attribute indicates that only renderers that are capable using buffers storing floating point pixels are considered. This should be accompanied by a NSOpenGLPFAColorSize of 64 (for half float pixel components) or 128 (for full float pixel components). Note, not all hardware supports floating point color buffers thus the returned pixel format could be NULL.
It is not clear why this forces things into sRGB mode. . I also found https://developer.apple.com/library/archive/samplecode/DeepImageDisplayWithOpenGL/Introduction/Intro.html and https://stackoverflow.com/questions/40688440/nsopenglpfacolorfloat-broken-in-macos-10-12-sierra-for-wide-gamut-30-bit-ren which seem to imply that setting float mode here is done as a prereq for 10-bit backbuffer. Perhaps apple tries to be "helpful" and automatically switches the layer color space to wide-gamut so that ColorSync automatically transforms it as needed when displaying on a non wide-gamut display. The fact that colorspace
method was added in 10.11 at the same time wantsExtendedDynamicRangeContent
was added supports this assertion. Presumably before 10.11 the default (as with traditional OpenGL contexts) was that no conversion was done.
Either way, this behavior is not documented at all by Apple.
(Speaking of undocumented things, the fact that osx gives you opengl srgb capable framebuffers, and setting GL_FRAMEBUFFER_SRGB
extension automatically switches the ATTACHMENT_COLOR_ENCODING
to srgb is undocumented. I.e. you don't need to explicitly request an srgb capable framebuffer, it's converted on the fly. But mpv does not set this, since it handles colorspace conversion itself [my understanding is that all GL_FRAMEBUFFER_SRGB
does is add a post-process shader which encodes the image to ~2.2 gamma).
Interestingly these are the other pixel format attrs that method queries
r0 = CGLDescribePixelFormat(r28, r23, 0xc, &stack[48]); // 12 - kCGLPFADepthSize
r24 = 0xd;
r0 = CGLDescribePixelFormat(r28, r23, 0xd, &stack[44]); // 13 - kCGLPFAStencilSize
r0 = CGLDescribePixelFormat(r28, r23, 0x7, &stack[40]); // 7 - kCGLPFAAuxBuffers
r0 = CGLDescribePixelFormat(r28, r23, 0xe, &stack[36]); // 14 - kCGLPFAAccumSize
r0 = CGLDescribePixelFormat(r28, r23, 0x38, &stack[32]); // 56 - kCGLPFASamples
r0 = CGLDescribePixelFormat(r28, r23, 0x3a, &stack[28]); // 58 - kCGLPFAColorFloat
r0 = CGLDescribePixelFormat(r28, r23, 0x8, &stack[24]); // 8 - kCGLPFAColorSize
r0 = CGLDescribePixelFormat(r28, r23, 0xb, &stack[20]); // 11 - kCGLPFAAlphaSize
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw on older versions of osx where CAOpenGLLayer has no such colorspace attribute, it seems no conversion occurs (the colorspace is assumed to be same as window backbuffer = display colorspace).
Actually even on modern osx CALayer in general does not have a colorspace attribute, only CAOpenGLLayer and CAMetalLayer do. If you you inspect
NSLog(@"%@ %@ %@", [self colorspace], [self _retainColorSpace], [[self context] colorSpace]);
you see that while the CAOpenGLLayer might be assigned a custom colorspace, the CALayer root buffer ultimately ends up being the same colorspace as the window backbuffer (which starts off as the screen's colorspace unless manually re-assigned). This makes sense, because CAOpenGLLayer content has to "play nice" with other CALayer content and so things must be converted to a shared color space, and the sensible thing is to use the window's colorspace.
On this:
Confusing. The default of CALayer.isOpaque is
Seemed safer to set this to On this:
Should I remove setting On this:
The color space of the layer was the primary difference by far. But with only that change the color of the blue sky still looked to my eyes slightly different compared to mpv. I just set |
Hm interesting, by default the contentView of an NSWindow does not have a
It's probably more performant to set it to true, but there shouldn't really be any downside if leaving it as false. There is an edge-case where mpv technically supports displaying content with an alpha channel (e.g. transparent png) but upstream cocoa-cb mode doesn't support this properly either.
Up to you, from my perspective whether it is set or not has zero effect, from what I can see it's simply ignored by CAOpenGLLayer just like the double-buffering attribute. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me. I agree that isOpaque
and NSOpenGLPFABackingStore
are not important, at least they won't affect the color and won't break anything. We will ship this with the beta and see if there's any user feedback.
On this line: Line 343 in ea70976
I assume that this results in always setting CAOpenGLLayer's color space to the screen ICC profile. However we have a setting in the Video/Audio tab which lets the user to choose whether or not load ICC profile. You said that in the testing, you disabled load ICC profile to match mpv's behavior, but will enabling load ICC profile change the color on this branch? Another minor (not related to this PR) issue is that, I noticed that the entire
Could we just use this option instead? |
I dug into this quite a bit and I have thoughts. So, an ICC profile ≈ a ColorSync profile, yeah? And they aren't very useful unless you're converting from an input profile to an output profile. I stared at this function for a long time (pasted below): override func updateICCProfile() {
guard let colorSpace = window?.screen?.colorSpace else {
log.warning("Couldn't update ICC Profile, no color space available")
return
}
libmpv.setRenderICCProfile(colorSpace) // 1
layer?.colorspace = getColorSpace() // 2
} Took me a while to realize that in the last 2 lines, mpv is doing 2 separate things (although I am still super new to this, so welcome corrections):
It looks like item 1 needs to be reproduced in IINA, because the above code won't run when using libmpv. Indeed the documentation for
So to answer @uiryuu's question, this new code is essentially implementing I think, if Item 2 above seems to be unique to Mac OS, and is represented by the
From this comment this param seems to be a bit half-hearted (and also sheds some light on how much TLC is being given to cocoa-cb nowadays):
@uiryuu's other point is interesting:
I don't see a good reason for that checkbox to exist, TBH. Until now I didn't understand it, so I left it unchecked. But now it seems that the correct behavior is to always leave it checked. If anything, a much more useful feature would be to have a popup selection of ICC profiles to choose from. But if the checkbox is left as-is, how about: "if checked, use the screen's profile; if unchecked, always use generic RGB. Unless HDR is enabled for the video; in which case the checkbox setting is ignored entirely & the HDR settings are used instead." Running out of time tonight. But need to check what this does exactly, because it's not documented: player.mpv.setString(MPVOption.GPURendererOptions.iccProfile, "") |
I was worried about fully updating IINA in one huge PR. So for this PR I tried to be conservative and focused only on addressing #5124. I was going to follow up with changes related to the ICC profile. @svobs pointed out the key change I left out, setting Should I go ahead and add the ICC profile changes to this PR? |
Oh yes, for others who are reading this, I forgot to note that IINA needs to update to libmpv 0.39.0 because of this fix in client-api (among other fixes…):
Not totally clear on which part you mean. Ideally here are all the changes I can think of to fully flesh out handling of ICC profiles, along with my perceived importance of each:
In short, I suppose I'm fine with leaving this PR as-is if you want to break out the remaining ICC stuff into a separate PR, given that some of this stuff is still bleeding edge and dependencies need to be updated first. |
I see this PR has been merged. I will start working on the changes related to ICC profiles. IINA prefers to use a pure version of official mpv and FFmpeg releases, but that doesn't always work out. As can be seen here we have been applying patches to mpv 0.38.0, including the fix for If mpv had fixed the AVFoundation audio problems, we'd consider either patching or updating The issue of IINA stepping on user's mpv settings extends beyond In the case at hand, if IINA can switch to using I will start working on the ICC profile changes. |
This definitely deserves its own discussion. I've been thinking about that issue as well. Specifically, I think it would be great if the |
This commit will:
VideoView.setICCProfile
to set the color space of the level to the color space of the screenwantsExtendedDynamicRangeContent
inVideoView.requestEdrMode
VideoLayer
to match up with mpvbufferDepth
property toVideoLayer
isOpaque
withbackgroundColor
inVideoLayer
wantsExtendedDynamicRangeContent
totrue
inVideoLayer
contentsFormat
toRGBA16Float
ifbufferDepth
is greater than8
MPV_RENDER_PARAM_DEPTH
to draw inVideoLayer
createPixelFormat
to support cocoa-cb-sw-renderer inVideoLayer
findPixelForma
t method toVideoLayer
that supports cocoa-cb-10bit-contextInspectorWindowController.updateInfo
to handle color spaces without namesparse_doc.rb
to add aMPVOptionValue
protocol and aCocoaCbSwRenderer
enum type toMPVOption
MPVOption
getEnum
method toMPVController
that will return an option value as an enum constantThese changes bring
VideoLayer
closer into alignment with the mpv reference implementation. Some differences are required. IINA uses a background thread for drawing which requires different locking compared to mpv which uses the main thread for drawing.Description: