Skip to content

D3D11 gamma support#117

Open
aquadran wants to merge 1 commit into3Shain:mainfrom
WineAndAqua:gamma-suport
Open

D3D11 gamma support#117
aquadran wants to merge 1 commit into3Shain:mainfrom
WineAndAqua:gamma-suport

Conversation

@aquadran
Copy link
Contributor

@aquadran aquadran commented Jan 11, 2026

D3D11 gamma support for game Titanfall 2 and Quantum Break

@aquadran aquadran changed the title WIP dx11 gamma support WIP d3d11 gamma support Jan 11, 2026
@aquadran aquadran changed the title WIP d3d11 gamma support WIP: d3d11 gamma support Jan 11, 2026
@aquadran aquadran force-pushed the gamma-suport branch 12 times, most recently from b1acd91 to 12d626d Compare January 13, 2026 20:01
@aquadran aquadran changed the title WIP: d3d11 gamma support D3D11 gamma support Jan 13, 2026
@aquadran aquadran marked this pull request as ready for review January 13, 2026 20:07
@aquadran aquadran changed the title D3D11 gamma support WIP: D3D11 gamma support Jan 14, 2026
@aquadran aquadran changed the title WIP: D3D11 gamma support D3D11 gamma support Jan 14, 2026
Copy link
Owner

@3Shain 3Shain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to make change for every comment. Mainly it's the design of IMTLDXGIMonitor /IMTLDXGIFactory that needs refactoring.

Comment on lines +25 to +40
struct MTLDXGI_MONITOR_DATA;
namespace dxmt {
class MTLDXGIMonitor;
};

DEFINE_COM_INTERFACE("8f804acf-f020-4914-98d4-a951da684b7f", IMTLDXGIMonitor)
: public IUnknown {
virtual HRESULT STDMETHODCALLTYPE SetMonitorData(const MTLDXGI_MONITOR_DATA *pData) = 0;
virtual HRESULT STDMETHODCALLTYPE GetMonitorData(MTLDXGI_MONITOR_DATA **ppData) = 0;
};

DEFINE_COM_INTERFACE("413dae46-5707-46e1-b66d-6e58b9f15113", IMTLDXGIFactory)
: public IDXGIFactory6 {
virtual dxmt::MTLDXGIMonitor * STDMETHODCALLTYPE GetMonitor() = 0;
};

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm strongly against introducing new COM interfaces and I've been always working on removing existing one. The only valid use case is when I want to export DXMT functionality for 3rd party users, which doesn't apply here.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly the whole concept of monitor is unnecessary. How is it different from output?

void
Presenter::updateGammaLUT() {
if (gamma_curve_ && gamma_curve_->updated) {
gamma_curve_->updated = false;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't here a pso invalidation happen? pso_valid.clear();

and I think the whole function should be a part of Presenter::synchronizeLayerProperties() because it also effectively queries information for presenting the next frame and rebuild pso when something changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at later changes I was thinking about putting into synchronizeLayerProperties too, but wasn't sure if it's right thing. I'll look into

Comment on lines +150 to +158
monitor_info_ = factory_->GetMonitor();
MTLDXGI_MONITOR_DATA monitorData;
for (uint32_t i = 0; i < DXMT_DXGI_GAMMA_CP_COUNT; i++) {
float pos = GetGammaControlPointPosition(i);
monitorData.gammaCurve.Red[i] = monitorData.gammaCurve.Green[i] = monitorData.gammaCurve.Blue[i] = pos;
}
monitorData.gammaCurve.gammaIsIdentity = true;
monitorData.gammaCurve.updated = false;
monitor_info_->SetMonitorData(&monitorData);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the "source of truth" for gamma curve is from IDXGIFactory/Monitor instead of IDXGIOutput? Shouldn't the IDXGISwapChain pull gamma curve from IDXGIOutput for every Present()? That means gamma curve should not be passed to constructor of Presenter but as a parameter (const reference/pointer) of Presenter::synchronizeLayerProperties() and Presenter holds its own copy of MTLDXGI_MONITOR_DATA for change detection.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Com<IDXGIOutput1> target_; to get corresponding IDXGIOutput of swapchain in fullscreen state (where gamma correction makes sense). And it can be NULL in windowed state => no gamma correction. Actually I found another oversight (not in this PR), that in SetFullscreenState() the second parameter of target output can be NULL: SetFullscreenState(TRUE, NULL) is valid, but DXGI will choose the output based on the swap-chain's device and the output window's placement. per MSDN.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized you may need to get access to some DXMT-internal methods from IDXGIOutput (i.e. get instance of MTLDXGIOutput), then you can check MTLD3D11Fence/MTLD3D11FenceImpl for reference and make appropriate modifications.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the "source of truth" for gamma curve is from IDXGIFactory/Monitor instead of IDXGIOutput? Shouldn't the IDXGISwapChain pull gamma curve from IDXGIOutput for every Present()? That means gamma curve should not be passed to constructor of Presenter but as a parameter (const reference/pointer) of Presenter::synchronizeLayerProperties() and Presenter holds its own copy of MTLDXGI_MONITOR_DATA for change detection.

concept was a bit influenced from DXVK, due connection between MTLDXGIOutput and IDXGISwapChain. there are multiple instances MTLDXGIOutput, like from MTLDXGIAdatper::EnumOutputs. so its not reliable place to store gamma curve and fetch from IDXGISwapChain I didn't saw how I can pass gamma curve to IDXGISwapChain.

Copy link
Contributor Author

@aquadran aquadran Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the "source of truth" for gamma curve is from IDXGIFactory/Monitor instead of IDXGIOutput? Shouldn't the IDXGISwapChain pull gamma curve from IDXGIOutput for every Present()? That means gamma curve should not be passed to constructor of Presenter but as a parameter (const reference/pointer) of Presenter::synchronizeLayerProperties() and Presenter holds its own copy of MTLDXGI_MONITOR_DATA for change detection.

I cannot fetch from IDXGIOutput, I need to pass through one instance like IDXGIFactory.

? WMTGetPrimaryDisplayId()
: WMTGetSecondaryDisplayId(),
&native_desc_);
monitor_info_ = factory_->GetMonitor();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why GetMonitor() here but QueryInterface() in other place?

@3Shain
Copy link
Owner

3Shain commented Feb 2, 2026

Also please split your commit into several smaller chunks.

@aquadran
Copy link
Contributor Author

aquadran commented Feb 2, 2026

Also please split your commit into several smaller chunks.

yes, that was a plan, I wanted to see feedback from you first.

@aquadran
Copy link
Contributor Author

aquadran commented Feb 2, 2026

No need to make change for every comment. Mainly it's the design of IMTLDXGIMonitor /IMTLDXGIFactory that needs refactoring.

I'm not sure what to do. I need to pass gamma curve from MTLDXGIOutput instance which game is using to MTLD3D11SwapChain

@3Shain
Copy link
Owner

3Shain commented Feb 3, 2026

No need to make change for every comment. Mainly it's the design of IMTLDXGIMonitor /IMTLDXGIFactory that needs refactoring.

I'm not sure what to do. I need to pass gamma curve from MTLDXGIOutput instance which game is using to MTLD3D11SwapChain

What's the problem to access the target output in swapchain in fullscreen state? Do you mean it's possible to get multiple instances of MTLDXGIOutput from one logical monitor HMONITOR? Need to check if on Windows it is always the same instance (not only for IDXGIAdapter::EnumOutputs() but also IDXGIFactory::EnumAdapters()). If so, then we need some sort of cache. If not, then we need to hoist the gamma curve state, maybe to adapter in favor of factory (But if factory is more convenient, that's ok, just don't introduce new COM interface => expose public MTLDXGIFactory interface and keep private MTLDXGIFactoryImpl).

@aquadran
Copy link
Contributor Author

aquadran commented Feb 3, 2026

No need to make change for every comment. Mainly it's the design of IMTLDXGIMonitor /IMTLDXGIFactory that needs refactoring.

I'm not sure what to do. I need to pass gamma curve from MTLDXGIOutput instance which game is using to MTLD3D11SwapChain

What's the problem to access the target output in swapchain in fullscreen state? Do you mean it's possible to get multiple instances of MTLDXGIOutput from one logical monitor HMONITOR? Need to check if on Windows it is always the same instance (not only for IDXGIAdapter::EnumOutputs() but also IDXGIFactory::EnumAdapters()). If so, then we need some sort of cache. If not, then we need to hoist the gamma curve state, maybe to adapter in favor of factory (But if factory is more convenient, that's ok, just don't introduce new COM interface => expose public MTLDXGIFactory interface and keep private MTLDXGIFactoryImpl).

what I mean IDXGIAdapter::EnumOutputs() is called few times by engine. So there are few instances. We don't have connection to which one actually is called to update gamma curve. I was wondering of IDXGIAdapter::EnumOutputs(), should it create a new instance every time, but instead keep reference in device and re-use it at next time.

The factory was convenient as it's created along with device. I'll check if using adapter would be fine.

For reference:

CreateDXGIFactory1()
CreateDXGIFactory2()
MTLDXGIFactory::EnumAdapters1()
MTLDXGIAdatper::CreateAdapter()
D3D11CoreCreateDevice()
DeviceImpl::CreateDXMTDevice()
CreateD3D11Device()
MTLDXGIFactory::EnumAdapters1()
MTLDXGIAdatper::CreateAdapter()
MTLDXGIOutput::CreateOutput()
MTLDXGIOutput::MTLDXGIOutput()
MTLDXGIFactory::EnumAdapters1()
MTLDXGIOutput::CreateOutput()
MTLDXGIOutput::MTLDXGIOutput()
DXGIFactory::CreateSwapChain()
DXGIFactory::CreateSwapChainForHwnd()
MTLD3D11DXGIDevice::CreateSwapChain()
MTLDXGIOutput::CreateOutput()
MTLDXGIOutput::MTLDXGIOutput()

3 x Output
2 x Adapter
1 x Factory

@3Shain
Copy link
Owner

3Shain commented Feb 3, 2026

what I mean IDXGIAdapter::EnumOutputs() is called few times by engine. So there are few instances. We don't have connection to which one actually is called to update gamma curve. I was wondering of IDXGIAdapter::EnumOutputs(), should it create a new instance every time, but instead keep reference in device and re-use it at next time.

The factory was convenient as it's created along with device. I'll check if using adapter would be fine.

For reference:

CreateDXGIFactory1()
CreateDXGIFactory2()
MTLDXGIFactory::EnumAdapters1()
MTLDXGIAdatper::CreateAdapter()
D3D11CoreCreateDevice()
DeviceImpl::CreateDXMTDevice()
CreateD3D11Device()
MTLDXGIFactory::EnumAdapters1()
MTLDXGIAdatper::CreateAdapter()
MTLDXGIOutput::CreateOutput()
MTLDXGIOutput::MTLDXGIOutput()
MTLDXGIFactory::EnumAdapters1()
MTLDXGIOutput::CreateOutput()
MTLDXGIOutput::MTLDXGIOutput()
DXGIFactory::CreateSwapChain()
DXGIFactory::CreateSwapChainForHwnd()
MTLD3D11DXGIDevice::CreateSwapChain()
MTLDXGIOutput::CreateOutput()
MTLDXGIOutput::MTLDXGIOutput()

3 x Output 2 x Adapter 1 x Factory

Let's check the Windows behavior. I think there is at least one misalignment because I often get page fault when replying DXMT trace in RenderDoc on Windows 🤔. That worths a separated PR.

@aquadran
Copy link
Contributor Author

aquadran commented Feb 3, 2026

Let's check the Windows behavior. I think there is at least one misalignment because I often get page fault when -replying DXMT trace in RenderDoc on Windows 🤔. That worths a separated PR.

I did api trace under windows, it shows 2 adapters and 1 factory.

When I run api trace with DXMT:
1 adapter and 1 output
plus additional from GetContainingOutput() after switching to fullscreen earlier.

So, last output could store gamma curve. But how to trigger update in swap chain from this output🤔

@3Shain
Copy link
Owner

3Shain commented Feb 4, 2026

Per MSDN:

DXGI’s gamma controls apply only as long as the app is full screen. Windows restores the previous state of the display when the app exits or returns to windowed mode. But Windows doesn’t restore your app’s gamma state if the app re-enters full-screen mode. Your app must explicitly restore its gamma state when it re-enters full-screen mode.

I understand it as SetGammaControl() has no effect if the output is not the fullscreen target. So there is another thing to verify on Windows: is it valid to get arbitrary instance of IDXGIOutput, set gamma curve, use another different instance of IDXGIOutput (of same HMONITOR) for entering fullscreen, and previous gamma curve setting is still applied.

@aquadran
Copy link
Contributor Author

aquadran commented Feb 4, 2026

another instance from api trace log return some error, and get dropped.

Per MSDN:

DXGI’s gamma controls apply only as long as the app is full screen. Windows restores the previous state of the display when the app exits or returns to windowed mode. But Windows doesn’t restore your app’s gamma state if the app re-enters full-screen mode. Your app must explicitly restore its gamma state when it re-enters full-screen mode.

I understand it as SetGammaControl() has no effect if the output is not the fullscreen target. So there is another thing to verify on Windows: is it valid to get arbitrary instance of IDXGIOutput, set gamma curve, use another different instance of IDXGIOutput (of same HMONITOR) for entering fullscreen, and previous gamma curve setting is still applied.

I did check how entering and leaving fullscreen involve Output with DXMT.
Before each enter fullscreen, the Output is enumerated (new instance) from Adapter and based on new instance a gamma ramp is set. There is nothing going on while leaving fullscreen.
On Windows it looks similar.

I also checked what happened with instances on Windows.
Game engine enumerate adapters first. There are 2 adapters - HW GPU and SW renderer.
It enumerate by index until reach not found. For each adapter it enumerate Outputs connected to adapter until not found. For SW renderer it does not found any Output.

It's hard to figure out what is going on from api traces. Like to check if IDXGIOutput instances are connected, or old ones are invalided (my guess is yes).
About old gamma curve set on other instances. The engine set gamma each time switching to fullscreen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants