// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include "DxRenderer.hpp" #include "CustomTextLayout.h" #include "../../interactivity/win32/CustomWindowMessages.h" #include "../../types/inc/Viewport.hpp" #include "../../inc/unicode.hpp" #include "../../inc/DefaultSettings.h" #include #include "ScreenPixelShader.h" #include "ScreenVertexShader.h" #include #include #include using namespace DirectX; std::atomic Microsoft::Console::Render::DxEngine::_tracelogCount{ 0 }; #pragma warning(suppress : 26477) // We don't control tracelogging macros TRACELOGGING_DEFINE_PROVIDER(g_hDxRenderProvider, "Microsoft.Windows.Terminal.Renderer.DirectX", // {c93e739e-ae50-5a14-78e7-f171e947535d} (0xc93e739e, 0xae50, 0x5a14, 0x78, 0xe7, 0xf1, 0x71, 0xe9, 0x47, 0x53, 0x5d), ); // Quad where we draw the terminal. // pos is world space coordinates where origin is at the center of screen. // tex is texel coordinates where origin is top left. // Layout the quad as a triangle strip where the _screenQuadVertices are place like so. // 2 0 // 3 1 struct ShaderInput { XMFLOAT3 pos; XMFLOAT2 tex; } const _screenQuadVertices[] = { { XMFLOAT3(1.f, 1.f, 0.f), XMFLOAT2(1.f, 0.f) }, { XMFLOAT3(1.f, -1.f, 0.f), XMFLOAT2(1.f, 1.f) }, { XMFLOAT3(-1.f, 1.f, 0.f), XMFLOAT2(0.f, 0.f) }, { XMFLOAT3(-1.f, -1.f, 0.f), XMFLOAT2(0.f, 1.f) }, }; D3D11_INPUT_ELEMENT_DESC _shaderInputLayout[] = { { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 } }; #pragma hdrstop using namespace Microsoft::Console::Render; using namespace Microsoft::Console::Types; // Routine Description: // - Constructs a DirectX-based renderer for console text // which primarily uses DirectWrite on a Direct2D surface #pragma warning(suppress : 26455) // TODO GH 2683: The default constructor should not throw. DxEngine::DxEngine() : RenderEngineBase(), _pool{ til::pmr::get_default_resource() }, _invalidMap{ &_pool }, _invalidScroll{}, _allInvalid{ false }, _firstFrame{ true }, _presentParams{ 0 }, _presentReady{ false }, _presentScroll{ 0 }, _presentDirty{ 0 }, _presentOffset{ 0 }, _isEnabled{ false }, _isPainting{ false }, _displaySizePixels{}, _foregroundColor{ 0 }, _backgroundColor{ 0 }, _selectionBackground{}, _haveDeviceResources{ false }, _swapChainHandle{ INVALID_HANDLE_VALUE }, _swapChainDesc{ 0 }, _swapChainFrameLatencyWaitableObject{ INVALID_HANDLE_VALUE }, _recreateDeviceRequested{ false }, _terminalEffectsEnabled{ false }, _retroTerminalEffect{ false }, _pixelShaderPath{}, _forceFullRepaintRendering{ false }, _softwareRendering{ false }, _antialiasingMode{ D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE }, _defaultBackgroundIsTransparent{ true }, _hwndTarget{ static_cast(INVALID_HANDLE_VALUE) }, _sizeTarget{}, _dpi{ USER_DEFAULT_SCREEN_DPI }, _scale{ 1.0f }, _prevScale{ 1.0f }, _intenseIsBold{ true }, _chainMode{ SwapChainMode::ForComposition }, _customLayout{}, _customRenderer{ ::Microsoft::WRL::Make() }, _drawingContext{} { const auto was = _tracelogCount.fetch_add(1); if (0 == was) { TraceLoggingRegister(g_hDxRenderProvider); } THROW_IF_FAILED(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, IID_PPV_ARGS(&_d2dFactory))); THROW_IF_FAILED(DWriteCreateFactory( DWRITE_FACTORY_TYPE_SHARED, __uuidof(_dwriteFactory), reinterpret_cast(_dwriteFactory.GetAddressOf()))); // Initialize our default selection color to DEFAULT_FOREGROUND, but make // sure to set to to a D2D1::ColorF SetSelectionBackground(DEFAULT_FOREGROUND); _fontRenderData = std::make_unique(_dwriteFactory); } // Routine Description: // - Destroys an instance of the DirectX rendering engine DxEngine::~DxEngine() { _ReleaseDeviceResources(); const auto was = _tracelogCount.fetch_sub(1); if (1 == was) { TraceLoggingUnregister(g_hDxRenderProvider); } } // Routine Description: // - Sets this engine to enabled allowing painting and presentation to occur // Arguments: // - // Return Value: // - Generally S_OK, but might return a DirectX or memory error if // resources need to be created or adjusted when enabling to prepare for draw // Can give invalid state if you enable an enabled class. [[nodiscard]] HRESULT DxEngine::Enable() noexcept { return _EnableDisplayAccess(true); } // Routine Description: // - Sets this engine to disabled to prevent painting and presentation from occurring // Arguments: // - // Return Value: // - Should be OK. We might close/free resources, but that shouldn't error. // Can give invalid state if you disable a disabled class. [[nodiscard]] HRESULT DxEngine::Disable() noexcept { return _EnableDisplayAccess(false); } // Routine Description: // - Helper to enable/disable painting/display access/presentation in a unified // manner between enable/disable functions. // Arguments: // - outputEnabled - true to enable, false to disable // Return Value: // - Generally OK. Can return invalid state if you set to the state that is already // active (enabling enabled, disabling disabled). [[nodiscard]] HRESULT DxEngine::_EnableDisplayAccess(const bool outputEnabled) noexcept { // Invalid state if we're setting it to the same as what we already have. RETURN_HR_IF(E_NOT_VALID_STATE, outputEnabled == _isEnabled); _isEnabled = outputEnabled; if (!_isEnabled) { _ReleaseDeviceResources(); } return S_OK; } // Routine Description: // - Compiles a shader source into binary blob. // Arguments: // - source - Shader source // - target - What kind of shader this is // - entry - Entry function of shader // Return Value: // - Compiled binary. Errors are thrown and logged. inline Microsoft::WRL::ComPtr _CompileShader( std::string source, std::string target, std::string entry = "main") { #if !TIL_FEATURE_DXENGINESHADERSUPPORT_ENABLED THROW_HR(E_UNEXPECTED); return 0; #else Microsoft::WRL::ComPtr code{}; Microsoft::WRL::ComPtr error{}; const HRESULT hr = D3DCompile( source.c_str(), source.size(), nullptr, nullptr, nullptr, entry.c_str(), target.c_str(), 0, 0, &code, &error); if (FAILED(hr)) { LOG_HR_MSG(hr, "D3DCompile failed with %x.", static_cast(hr)); if (error) { LOG_HR_MSG(hr, "D3DCompile error\n%*S", static_cast(error->GetBufferSize()), static_cast(error->GetBufferPointer())); } THROW_HR(hr); } return code; #endif } // Routine Description: // - Checks if terminal effects are enabled. // Arguments: // Return Value: // - True if terminal effects are enabled bool DxEngine::_HasTerminalEffects() const noexcept { return _terminalEffectsEnabled && (_retroTerminalEffect || !_pixelShaderPath.empty()); } // Routine Description: // - Toggles terminal effects off and on. If no terminal effect is configured has no effect // Arguments: // Return Value: // - Void void DxEngine::ToggleShaderEffects() noexcept { _terminalEffectsEnabled = !_terminalEffectsEnabled; _recreateDeviceRequested = true; #pragma warning(suppress : 26447) // The function is declared 'noexcept' but calls function 'Log_IfFailed()' which may throw exceptions (f.6). LOG_IF_FAILED(InvalidateAll()); } // Routine Description: // - Loads pixel shader source depending on _retroTerminalEffect and _pixelShaderPath // Arguments: // Return Value: // - Pixel shader source code std::string DxEngine::_LoadPixelShaderFile() const { // If the user specified the new pixel shader, it has precedence if (!_pixelShaderPath.empty()) { try { wil::unique_hfile hFile{ CreateFileW(_pixelShaderPath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr) }; THROW_LAST_ERROR_IF(!hFile); // This will be caught below. // fileSize is in bytes const auto fileSize = GetFileSize(hFile.get(), nullptr); THROW_LAST_ERROR_IF(fileSize == INVALID_FILE_SIZE); std::vector utf8buffer; utf8buffer.reserve(fileSize); DWORD bytesRead = 0; THROW_LAST_ERROR_IF(!ReadFile(hFile.get(), utf8buffer.data(), fileSize, &bytesRead, nullptr)); // convert buffer to UTF-8 string std::string utf8string(utf8buffer.data(), fileSize); return utf8string; } catch (...) { // If we ran into any problems during loading pixel shader, call to // the warning callback to surface the file not found error const auto exceptionHr = LOG_CAUGHT_EXCEPTION(); if (_pfnWarningCallback) { _pfnWarningCallback(exceptionHr); } return std::string{}; } } else if (_retroTerminalEffect) { return std::string{ retroPixelShaderString }; } return std::string{}; } // Routine Description: // - Setup D3D objects for doing shader things for terminal effects. // Arguments: // Return Value: // - HRESULT status. HRESULT DxEngine::_SetupTerminalEffects() { _pixelShaderLoaded = false; const auto pixelShaderSource = _LoadPixelShaderFile(); if (pixelShaderSource.empty()) { // There's no shader to compile. This might be due to failing to load, // or because there's just no shader enabled at all. // Turn the effects off for now. _terminalEffectsEnabled = false; return S_FALSE; } ::Microsoft::WRL::ComPtr swapBuffer; RETURN_IF_FAILED(_dxgiSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&swapBuffer)); // Setup render target. RETURN_IF_FAILED(_d3dDevice->CreateRenderTargetView(swapBuffer.Get(), nullptr, &_renderTargetView)); // Setup _framebufferCapture, to where we'll copy current frame when rendering effects. D3D11_TEXTURE2D_DESC framebufferCaptureDesc{}; swapBuffer->GetDesc(&framebufferCaptureDesc); WI_SetFlag(framebufferCaptureDesc.BindFlags, D3D11_BIND_SHADER_RESOURCE); RETURN_IF_FAILED(_d3dDevice->CreateTexture2D(&framebufferCaptureDesc, nullptr, &_framebufferCapture)); // Setup the viewport. D3D11_VIEWPORT vp; vp.Width = _displaySizePixels.width(); vp.Height = _displaySizePixels.height(); vp.MinDepth = 0.0f; vp.MaxDepth = 1.0f; vp.TopLeftX = 0; vp.TopLeftY = 0; _d3dDeviceContext->RSSetViewports(1, &vp); // Prepare shaders. auto vertexBlob = _CompileShader(screenVertexShaderString, "vs_5_0"); Microsoft::WRL::ComPtr pixelBlob; // As the pixel shader source is user provided it's possible there's a problem with it // so load it inside a try catch, on any error log and fallback on the error pixel shader // If even the error pixel shader fails to load rely on standard exception handling try { pixelBlob = _CompileShader(pixelShaderSource, "ps_5_0"); } catch (...) { // Call to the warning callback to surface the shader compile error const auto exceptionHr = LOG_CAUGHT_EXCEPTION(); if (_pfnWarningCallback) { // If this fails, it'll return E_FAIL, which is terribly // uninformative. Instead, raise something more useful. _pfnWarningCallback(D2DERR_SHADER_COMPILE_FAILED); } return exceptionHr; } RETURN_IF_FAILED(_d3dDevice->CreateVertexShader( vertexBlob->GetBufferPointer(), vertexBlob->GetBufferSize(), nullptr, &_vertexShader)); RETURN_IF_FAILED(_d3dDevice->CreatePixelShader( pixelBlob->GetBufferPointer(), pixelBlob->GetBufferSize(), nullptr, &_pixelShader)); RETURN_IF_FAILED(_d3dDevice->CreateInputLayout( static_cast(_shaderInputLayout), ARRAYSIZE(_shaderInputLayout), vertexBlob->GetBufferPointer(), vertexBlob->GetBufferSize(), &_vertexLayout)); // Create vertex buffer for screen quad. D3D11_BUFFER_DESC bd{}; bd.Usage = D3D11_USAGE_DEFAULT; bd.ByteWidth = sizeof(ShaderInput) * ARRAYSIZE(_screenQuadVertices); bd.BindFlags = D3D11_BIND_VERTEX_BUFFER; bd.CPUAccessFlags = 0; D3D11_SUBRESOURCE_DATA InitData{}; InitData.pSysMem = static_cast(_screenQuadVertices); RETURN_IF_FAILED(_d3dDevice->CreateBuffer(&bd, &InitData, &_screenQuadVertexBuffer)); D3D11_BUFFER_DESC pixelShaderSettingsBufferDesc{}; pixelShaderSettingsBufferDesc.Usage = D3D11_USAGE_DEFAULT; pixelShaderSettingsBufferDesc.ByteWidth = sizeof(_pixelShaderSettings); pixelShaderSettingsBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; _shaderStartTime = std::chrono::steady_clock::now(); _ComputePixelShaderSettings(); D3D11_SUBRESOURCE_DATA pixelShaderSettingsInitData{}; pixelShaderSettingsInitData.pSysMem = &_pixelShaderSettings; RETURN_IF_FAILED(_d3dDevice->CreateBuffer(&pixelShaderSettingsBufferDesc, &pixelShaderSettingsInitData, &_pixelShaderSettingsBuffer)); // Sampler state is needed to use texture as input to shader. D3D11_SAMPLER_DESC samplerDesc{}; samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; samplerDesc.MipLODBias = 0.0f; samplerDesc.MaxAnisotropy = 1; samplerDesc.ComparisonFunc = D3D11_COMPARISON_ALWAYS; samplerDesc.BorderColor[0] = 0; samplerDesc.BorderColor[1] = 0; samplerDesc.BorderColor[2] = 0; samplerDesc.BorderColor[3] = 0; samplerDesc.MinLOD = 0; samplerDesc.MaxLOD = D3D11_FLOAT32_MAX; // Create the texture sampler state. RETURN_IF_FAILED(_d3dDevice->CreateSamplerState(&samplerDesc, &_samplerState)); _pixelShaderLoaded = true; return S_OK; } // Routine Description: // - Puts the correct values in _pixelShaderSettings, so the struct can be // passed the GPU and updates the GPU resource. // Arguments: // - // Return Value: // - void DxEngine::_ComputePixelShaderSettings() noexcept { if (_HasTerminalEffects() && _d3dDeviceContext && _pixelShaderSettingsBuffer) { try { // Set the time (seconds since the shader was loaded) _pixelShaderSettings.Time = std::chrono::duration_cast>(std::chrono::steady_clock::now() - _shaderStartTime).count(); // Set the UI Scale _pixelShaderSettings.Scale = _scale; // Set the display resolution const float w = 1.0f * _displaySizePixels.width(); const float h = 1.0f * _displaySizePixels.height(); _pixelShaderSettings.Resolution = XMFLOAT2{ w, h }; // Set the background DirectX::XMFLOAT4 background{}; background.x = _backgroundColor.r; background.y = _backgroundColor.g; background.z = _backgroundColor.b; background.w = _backgroundColor.a; _pixelShaderSettings.Background = background; _d3dDeviceContext->UpdateSubresource(_pixelShaderSettingsBuffer.Get(), 0, nullptr, &_pixelShaderSettings, 0, 0); } CATCH_LOG(); } } // Method Description: // - Use DCompositionCreateSurfaceHandle to create a swapchain handle. This API // is only present in Windows 8.1+, so we need to delay-load it to make sure // we can still load on Windows 7. // - We can't actually hit this on Windows 7, because only the WPF control uses // us on Windows 7, and they're using the ForHwnd path, which doesn't hit this // at all. // Arguments: // - // Return Value: // - An HRESULT for failing to load dcomp.dll, or failing to find the API, or an // actual failure from the API itself. [[nodiscard]] HRESULT DxEngine::_CreateSurfaceHandle() noexcept { #pragma warning(suppress : 26447) wil::unique_hmodule hDComp{ LoadLibraryEx(L"Dcomp.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32) }; RETURN_LAST_ERROR_IF(hDComp.get() == nullptr); auto fn = GetProcAddressByFunctionDeclaration(hDComp.get(), DCompositionCreateSurfaceHandle); RETURN_LAST_ERROR_IF(fn == nullptr); return fn(GENERIC_ALL, nullptr, &_swapChainHandle); } // Routine Description; // - Creates device-specific resources required for drawing // which generally means those that are represented on the GPU and can // vary based on the monitor, display adapter, etc. // - These may need to be recreated during the course of painting a frame // should something about that hardware pipeline change. // - Will free device resources that already existed as first operation. // Arguments: // - createSwapChain - If true, we create the entire rendering pipeline // - If false, we just set up the adapter. // Return Value: // - Could be any DirectX/D3D/D2D/DXGI/DWrite error or memory issue. [[nodiscard]] HRESULT DxEngine::_CreateDeviceResources(const bool createSwapChain) noexcept try { if (_haveDeviceResources) { _ReleaseDeviceResources(); } auto freeOnFail = wil::scope_exit([&]() noexcept { _ReleaseDeviceResources(); }); RETURN_IF_FAILED(CreateDXGIFactory1(IID_PPV_ARGS(&_dxgiFactory2))); const DWORD DeviceFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT | // clang-format off // This causes problems for folks who do not have the whole DirectX SDK installed // when they try to run the rest of the project in debug mode. // As such, I'm leaving this flag here for people doing DX-specific work to toggle it // only when they need it and shutting it off otherwise. // Find out more about the debug layer here: // https://docs.microsoft.com/en-us/windows/desktop/direct3d11/overviews-direct3d-11-devices-layers // You can find out how to install it here: // https://docs.microsoft.com/en-us/windows/uwp/gaming/use-the-directx-runtime-and-visual-studio-graphics-diagnostic-features // clang-format on // D3D11_CREATE_DEVICE_DEBUG | D3D11_CREATE_DEVICE_SINGLETHREADED; const std::array FeatureLevels{ D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0, D3D_FEATURE_LEVEL_9_1 }; // Trying hardware first for maximum performance, then trying WARP (software) renderer second // in case we're running inside a downlevel VM where hardware passthrough isn't enabled like // for Windows 7 in a VM. HRESULT hardwareResult = E_NOT_SET; // If we're not forcing software rendering, try hardware first. // Otherwise, let the error state fall down and create with the software renderer directly. if (!_softwareRendering) { hardwareResult = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, DeviceFlags, FeatureLevels.data(), gsl::narrow_cast(FeatureLevels.size()), D3D11_SDK_VERSION, &_d3dDevice, nullptr, &_d3dDeviceContext); } if (FAILED(hardwareResult)) { RETURN_IF_FAILED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, DeviceFlags, FeatureLevels.data(), gsl::narrow_cast(FeatureLevels.size()), D3D11_SDK_VERSION, &_d3dDevice, nullptr, &_d3dDeviceContext)); } _displaySizePixels = _GetClientSize(); // Get the other device types so we have deeper access to more functionality // in our pipeline than by just walking straight from the D3D device. RETURN_IF_FAILED(_d3dDevice.As(&_dxgiDevice)); RETURN_IF_FAILED(_d2dFactory->CreateDevice(_dxgiDevice.Get(), _d2dDevice.ReleaseAndGetAddressOf())); // Create a device context out of it (supercedes render targets) RETURN_IF_FAILED(_d2dDevice->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, &_d2dDeviceContext)); if (createSwapChain) { _swapChainDesc = { 0 }; _swapChainDesc.Flags = 0; // requires DXGI 1.3 which was introduced in Windows 8.1 WI_SetFlagIf(_swapChainDesc.Flags, DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT, IsWindows8Point1OrGreater()); _swapChainDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; _swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; _swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; _swapChainDesc.BufferCount = 2; _swapChainDesc.SampleDesc.Count = 1; _swapChainDesc.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED; _swapChainDesc.Scaling = DXGI_SCALING_NONE; switch (_chainMode) { case SwapChainMode::ForHwnd: { // use the HWND's dimensions for the swap chain dimensions. RECT rect = { 0 }; RETURN_IF_WIN32_BOOL_FALSE(GetClientRect(_hwndTarget, &rect)); _swapChainDesc.Width = rect.right - rect.left; _swapChainDesc.Height = rect.bottom - rect.top; // We can't do alpha for HWNDs. Set to ignore. It will fail otherwise. _swapChainDesc.AlphaMode = DXGI_ALPHA_MODE_IGNORE; const auto createSwapChainResult = _dxgiFactory2->CreateSwapChainForHwnd(_d3dDevice.Get(), _hwndTarget, &_swapChainDesc, nullptr, nullptr, &_dxgiSwapChain); if (FAILED(createSwapChainResult)) { _swapChainDesc.Scaling = DXGI_SCALING_STRETCH; RETURN_IF_FAILED(_dxgiFactory2->CreateSwapChainForHwnd(_d3dDevice.Get(), _hwndTarget, &_swapChainDesc, nullptr, nullptr, &_dxgiSwapChain)); } break; } case SwapChainMode::ForComposition: { if (!_swapChainHandle) { RETURN_IF_FAILED(_CreateSurfaceHandle()); } RETURN_IF_FAILED(_dxgiFactory2.As(&_dxgiFactoryMedia)); // Use the given target size for compositions. _swapChainDesc.Width = _displaySizePixels.width(); _swapChainDesc.Height = _displaySizePixels.height(); // We're doing advanced composition pretty much for the purpose of pretty alpha, so turn it on. _swapChainDesc.AlphaMode = DXGI_ALPHA_MODE_PREMULTIPLIED; // It's 100% required to use scaling mode stretch for composition. There is no other choice. _swapChainDesc.Scaling = DXGI_SCALING_STRETCH; RETURN_IF_FAILED(_dxgiFactoryMedia->CreateSwapChainForCompositionSurfaceHandle(_d3dDevice.Get(), _swapChainHandle.get(), &_swapChainDesc, nullptr, &_dxgiSwapChain)); break; } default: THROW_HR(E_NOTIMPL); } if (IsWindows8Point1OrGreater()) { ::Microsoft::WRL::ComPtr swapChain2; const HRESULT asResult = _dxgiSwapChain.As(&swapChain2); if (SUCCEEDED(asResult)) { _swapChainFrameLatencyWaitableObject = wil::unique_handle{ swapChain2->GetFrameLatencyWaitableObject() }; } else { LOG_HR_MSG(asResult, "Failed to obtain IDXGISwapChain2 from swap chain"); } } if (_HasTerminalEffects()) { const HRESULT hr = _SetupTerminalEffects(); if (FAILED(hr)) { LOG_HR_MSG(hr, "Failed to setup terminal effects. Disabling."); _terminalEffectsEnabled = false; } } // With a new swap chain, mark the entire thing as invalid. RETURN_IF_FAILED(InvalidateAll()); // This is our first frame on this new target. _firstFrame = true; RETURN_IF_FAILED(_PrepareRenderTarget()); } _haveDeviceResources = true; if (_isPainting) { // TODO: MSFT: 21169176 - remove this or restore the "try a few times to render" code... I think _d2dDeviceContext->BeginDraw(); } freeOnFail.release(); // don't need to release if we made it to the bottom and everything was good. // Notify that swap chain changed. if (_pfn) { try { _pfn(); } CATCH_LOG(); // A failure in the notification function isn't a failure to prepare, so just log it and go on. } _recreateDeviceRequested = false; return S_OK; } CATCH_RETURN(); static constexpr D2D1_ALPHA_MODE _dxgiAlphaToD2d1Alpha(DXGI_ALPHA_MODE mode) noexcept { switch (mode) { case DXGI_ALPHA_MODE_PREMULTIPLIED: return D2D1_ALPHA_MODE_PREMULTIPLIED; case DXGI_ALPHA_MODE_STRAIGHT: return D2D1_ALPHA_MODE_STRAIGHT; case DXGI_ALPHA_MODE_IGNORE: return D2D1_ALPHA_MODE_IGNORE; case DXGI_ALPHA_MODE_FORCE_DWORD: return D2D1_ALPHA_MODE_FORCE_DWORD; default: case DXGI_ALPHA_MODE_UNSPECIFIED: return D2D1_ALPHA_MODE_UNKNOWN; } } [[nodiscard]] HRESULT DxEngine::_PrepareRenderTarget() noexcept { try { // Pull surface out of swap chain. RETURN_IF_FAILED(_dxgiSwapChain->GetBuffer(0, IID_PPV_ARGS(&_dxgiSurface))); // Make a bitmap and bind it to the swap chain surface const auto bitmapProperties = D2D1::BitmapProperties1( D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW, D2D1::PixelFormat(_swapChainDesc.Format, _dxgiAlphaToD2d1Alpha(_swapChainDesc.AlphaMode))); RETURN_IF_FAILED(_d2dDeviceContext->CreateBitmapFromDxgiSurface(_dxgiSurface.Get(), bitmapProperties, &_d2dBitmap)); // Assign that bitmap as the target of the D2D device context. Draw commands hit the context // and are backed by the bitmap which is bound to the swap chain which goes on to be presented. // (The foot bone connected to the leg bone, // The leg bone connected to the knee bone, // The knee bone connected to the thigh bone // ... and so on) _d2dDeviceContext->SetTarget(_d2dBitmap.Get()); // We need the AntialiasMode for non-text object to be Aliased to ensure // that background boxes line up with each other and don't leave behind // stray colors. // See GH#3626 for more details. _d2dDeviceContext->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); _d2dDeviceContext->SetTextAntialiasMode(_antialiasingMode); RETURN_IF_FAILED(_d2dDeviceContext->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::DarkRed), &_d2dBrushBackground)); RETURN_IF_FAILED(_d2dDeviceContext->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &_d2dBrushForeground)); _strokeStyleProperties = D2D1_STROKE_STYLE_PROPERTIES{ D2D1_CAP_STYLE_SQUARE, // startCap D2D1_CAP_STYLE_SQUARE, // endCap D2D1_CAP_STYLE_SQUARE, // dashCap D2D1_LINE_JOIN_MITER, // lineJoin 0.f, // miterLimit D2D1_DASH_STYLE_SOLID, // dashStyle 0.f, // dashOffset }; RETURN_IF_FAILED(_d2dFactory->CreateStrokeStyle(&_strokeStyleProperties, nullptr, 0, &_strokeStyle)); _dashStrokeStyleProperties = D2D1_STROKE_STYLE_PROPERTIES{ D2D1_CAP_STYLE_SQUARE, // startCap D2D1_CAP_STYLE_SQUARE, // endCap D2D1_CAP_STYLE_FLAT, // dashCap D2D1_LINE_JOIN_MITER, // lineJoin 0.f, // miterLimit D2D1_DASH_STYLE_CUSTOM, // dashStyle 0.f, // dashOffset }; // Custom dashes: // # # # # // 1234123412341234 static constexpr std::array hyperlinkDashes{ 1.f, 3.f }; RETURN_IF_FAILED(_d2dFactory->CreateStrokeStyle(&_dashStrokeStyleProperties, hyperlinkDashes.data(), gsl::narrow_cast(hyperlinkDashes.size()), &_dashStrokeStyle)); _hyperlinkStrokeStyle = _dashStrokeStyle; // If in composition mode, apply scaling factor matrix if (_chainMode == SwapChainMode::ForComposition) { DXGI_MATRIX_3X2_F inverseScale = { 0 }; inverseScale._11 = 1.0f / _scale; inverseScale._22 = inverseScale._11; ::Microsoft::WRL::ComPtr sc2; RETURN_IF_FAILED(_dxgiSwapChain.As(&sc2)); RETURN_IF_FAILED(sc2->SetMatrixTransform(&inverseScale)); } _prevScale = _scale; return S_OK; } CATCH_RETURN(); } // Routine Description: // - Releases device-specific resources (typically held on the GPU) // Arguments: // - // Return Value: // - void DxEngine::_ReleaseDeviceResources() noexcept { try { _haveDeviceResources = false; // Destroy Terminal Effect resources _renderTargetView.Reset(); _vertexShader.Reset(); _pixelShader.Reset(); _vertexLayout.Reset(); _screenQuadVertexBuffer.Reset(); _pixelShaderSettingsBuffer.Reset(); _samplerState.Reset(); _framebufferCapture.Reset(); _d2dBrushForeground.Reset(); _d2dBrushBackground.Reset(); _d2dBitmap.Reset(); if (nullptr != _d2dDeviceContext.Get() && _isPainting) { _d2dDeviceContext->EndDraw(); } _d2dDeviceContext.Reset(); _dxgiSurface.Reset(); _dxgiSwapChain.Reset(); _swapChainFrameLatencyWaitableObject.reset(); _d2dDevice.Reset(); _dxgiDevice.Reset(); if (nullptr != _d3dDeviceContext.Get()) { // To ensure the swap chain goes away we must unbind any views from the // D3D pipeline _d3dDeviceContext->OMSetRenderTargets(0, nullptr, nullptr); } _d3dDeviceContext.Reset(); _d3dDevice.Reset(); _dxgiFactory2.Reset(); } CATCH_LOG(); } // Routine Description: // - Calculates whether or not we should force grayscale AA based on the // current renderer state. // Arguments: // - - Uses internal state of _antialiasingMode, _defaultTextBackgroundOpacity, // _backgroundColor, and _defaultBackgroundColor. // Return Value: // - True if we must render this text in grayscale AA as cleartype simply won't work. False otherwise. [[nodiscard]] bool DxEngine::_ShouldForceGrayscaleAA() noexcept { // GH#5098: If we're rendering with cleartype text, we need to always // render onto an opaque background. If our background's opacity is // 1.0f, that's great, we can use that. Otherwise, we need to force the // text renderer to render this text in grayscale. In // UpdateDrawingBrushes, we'll set the backgroundColor's a channel to // 1.0 if we're in cleartype mode and the background's opacity is 1.0. // Otherwise, at this point, the _backgroundColor's alpha is <1.0. // // Currently, only text with the default background color uses an alpha // of 0, every other background uses 1.0 // // DANGER: Layers slow us down. Only do this in the specific case where // someone has chosen the slower ClearType antialiasing (versus the faster // grayscale antialiasing) const bool usingCleartype = _antialiasingMode == D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE; const bool usingTransparency = _defaultBackgroundIsTransparent; // Another way of naming "bgIsDefault" is "bgHasTransparency" const auto bgIsDefault = (_backgroundColor.a == _defaultBackgroundColor.a) && (_backgroundColor.r == _defaultBackgroundColor.r) && (_backgroundColor.g == _defaultBackgroundColor.g) && (_backgroundColor.b == _defaultBackgroundColor.b); const bool forceGrayscaleAA = usingCleartype && usingTransparency && bgIsDefault; return forceGrayscaleAA; } // Routine Description: // - Helper to create a DirectWrite text layout object // out of a string. // Arguments: // - string - The text to attempt to layout // - stringLength - Length of string above in characters // - ppTextLayout - Location to receive new layout object // Return Value: // - S_OK if layout created successfully, otherwise a DirectWrite error [[nodiscard]] HRESULT DxEngine::_CreateTextLayout( _In_reads_(stringLength) PCWCHAR string, _In_ size_t stringLength, _Out_ IDWriteTextLayout** ppTextLayout) noexcept try { return _dwriteFactory->CreateTextLayout(string, gsl::narrow(stringLength), _fontRenderData->DefaultTextFormat().Get(), _displaySizePixels.width(), _fontRenderData->GlyphCell().height() != 0 ? _fontRenderData->GlyphCell().height() : _displaySizePixels.height(), ppTextLayout); } CATCH_RETURN() // Routine Description: // - Sets the target window handle for our display pipeline // - We will take over the surface of this window for drawing // Arguments: // - hwnd - Window handle // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::SetHwnd(const HWND hwnd) noexcept { _hwndTarget = hwnd; _chainMode = SwapChainMode::ForHwnd; return S_OK; } [[nodiscard]] HRESULT DxEngine::SetWindowSize(const SIZE Pixels) noexcept try { _sizeTarget = Pixels; return S_OK; } CATCH_RETURN(); void DxEngine::SetCallback(std::function pfn) noexcept { _pfn = std::move(pfn); } void DxEngine::SetWarningCallback(std::function pfn) noexcept { _pfnWarningCallback = std::move(pfn); } bool DxEngine::GetRetroTerminalEffect() const noexcept { return _retroTerminalEffect; } void DxEngine::SetRetroTerminalEffect(bool enable) noexcept try { if (_retroTerminalEffect != enable) { // Enable shader effects if the path isn't empty. Otherwise leave it untouched. _terminalEffectsEnabled = enable ? true : _terminalEffectsEnabled; _retroTerminalEffect = enable; _recreateDeviceRequested = true; LOG_IF_FAILED(InvalidateAll()); } } CATCH_LOG() void DxEngine::SetPixelShaderPath(std::wstring_view value) noexcept try { if (_pixelShaderPath != value) { // Enable shader effects if the path isn't empty. Otherwise leave it untouched. _terminalEffectsEnabled = value.empty() ? _terminalEffectsEnabled : true; _pixelShaderPath = { value }; _recreateDeviceRequested = true; LOG_IF_FAILED(InvalidateAll()); } } CATCH_LOG() void DxEngine::SetForceFullRepaintRendering(bool enable) noexcept try { if (_forceFullRepaintRendering != enable) { _forceFullRepaintRendering = enable; LOG_IF_FAILED(InvalidateAll()); } } CATCH_LOG() void DxEngine::SetSoftwareRendering(bool enable) noexcept try { if (_softwareRendering != enable) { _softwareRendering = enable; _recreateDeviceRequested = true; LOG_IF_FAILED(InvalidateAll()); } } CATCH_LOG() void DxEngine::SetIntenseIsBold(bool enable) noexcept try { if (_intenseIsBold != enable) { _intenseIsBold = enable; LOG_IF_FAILED(InvalidateAll()); } } CATCH_LOG() HANDLE DxEngine::GetSwapChainHandle() noexcept { if (!_swapChainHandle) { #pragma warning(suppress : 26447) // The function is declared 'noexcept' but calls function 'Log_IfFailed()' which may throw exceptions (f.6). LOG_IF_FAILED(_CreateDeviceResources(true)); } return _swapChainHandle.get(); } void DxEngine::_InvalidateRectangle(const til::rectangle& rc) { const auto size = _invalidMap.size(); const auto topLeft = til::point{ 0, std::min(size.height(), rc.top()) }; const auto bottomRight = til::point{ size.width(), std::min(size.height(), rc.bottom()) }; _invalidMap.set({ topLeft, bottomRight }); } bool DxEngine::_IsAllInvalid() const noexcept { return std::llabs(_invalidScroll.y()) >= _invalidMap.size().height(); } // Routine Description: // - Invalidates a rectangle described in characters // Arguments: // - psrRegion - Character rectangle // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::Invalidate(const SMALL_RECT* const psrRegion) noexcept try { RETURN_HR_IF_NULL(E_INVALIDARG, psrRegion); if (!_allInvalid) { _InvalidateRectangle(Viewport::FromExclusive(*psrRegion).ToInclusive()); } return S_OK; } CATCH_RETURN() // Routine Description: // - Invalidates the cells of the cursor // Arguments: // - psrRegion - the region covered by the cursor // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept { return Invalidate(psrRegion); } // Routine Description: // - Invalidates a rectangle describing a pixel area on the display // Arguments: // - prcDirtyClient - pixel rectangle // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::InvalidateSystem(const RECT* const prcDirtyClient) noexcept try { RETURN_HR_IF_NULL(E_INVALIDARG, prcDirtyClient); if (!_allInvalid) { // Dirty client is in pixels. Use divide specialization against glyph factor to make conversion // to cells. _InvalidateRectangle(til::rectangle{ *prcDirtyClient }.scale_down(_fontRenderData->GlyphCell())); } return S_OK; } CATCH_RETURN(); // Routine Description: // - Invalidates a series of character rectangles // Arguments: // - rectangles - One or more rectangles describing character positions on the grid // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::InvalidateSelection(const std::vector& rectangles) noexcept { if (!_allInvalid) { for (const auto& rect : rectangles) { RETURN_IF_FAILED(Invalidate(&rect)); } } return S_OK; } // Routine Description: // - Scrolls the existing dirty region (if it exists) and // invalidates the area that is uncovered in the window. // Arguments: // - pcoordDelta - The number of characters to move and uncover. // - -Y is up, Y is down, -X is left, X is right. // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::InvalidateScroll(const COORD* const pcoordDelta) noexcept try { RETURN_HR_IF(E_INVALIDARG, !pcoordDelta); const til::point deltaCells{ *pcoordDelta }; if (!_allInvalid) { if (deltaCells != til::point{ 0, 0 }) { // Shift the contents of the map and fill in revealed area. _invalidMap.translate(deltaCells, true); _invalidScroll += deltaCells; _allInvalid = _IsAllInvalid(); } } return S_OK; } CATCH_RETURN(); // Routine Description: // - Invalidates the entire window area // Arguments: // - // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::InvalidateAll() noexcept try { _invalidMap.set_all(); _allInvalid = true; // Since everything is invalidated here, mark this as a "first frame", so // that we won't use incremental drawing on it. The caller of this intended // for _everything_ to get redrawn, so setting _firstFrame will force us to // redraw the entire frame. This will make sure that things like the gutters // get cleared correctly. // // Invalidating everything is supposed to happen with resizes of the // entire canvas, changes of the font, and other such adjustments. _firstFrame = true; return S_OK; } CATCH_RETURN(); // Routine Description: // - This currently has no effect in this renderer. // Arguments: // - pForcePaint - Always filled with false // Return Value: // - S_FALSE because we don't use this. [[nodiscard]] HRESULT DxEngine::InvalidateCircling(_Out_ bool* const pForcePaint) noexcept { RETURN_HR_IF_NULL(E_INVALIDARG, pForcePaint); *pForcePaint = false; return S_FALSE; } // Routine Description: // - Gets the area in pixels of the surface we are targeting // Arguments: // - // Return Value: // - X by Y area in pixels of the surface [[nodiscard]] til::size DxEngine::_GetClientSize() const { switch (_chainMode) { case SwapChainMode::ForHwnd: { RECT clientRect = { 0 }; LOG_IF_WIN32_BOOL_FALSE(GetClientRect(_hwndTarget, &clientRect)); return til::rectangle{ clientRect }.size(); } case SwapChainMode::ForComposition: { return _sizeTarget; } default: FAIL_FAST_HR(E_NOTIMPL); } } // Routine Description: // - Helper to multiply all parameters of a rectangle by the font size // to convert from characters to pixels. // Arguments: // - cellsToPixels - rectangle to update // - fontSize - scaling factors // Return Value: // - - Updates reference void _ScaleByFont(RECT& cellsToPixels, SIZE fontSize) noexcept { cellsToPixels.left *= fontSize.cx; cellsToPixels.right *= fontSize.cx; cellsToPixels.top *= fontSize.cy; cellsToPixels.bottom *= fontSize.cy; } // Routine Description: // - This is unused by this renderer. // Arguments: // - pForcePaint - always filled with false. // Return Value: // - S_FALSE because this is unused. [[nodiscard]] HRESULT DxEngine::PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept { RETURN_HR_IF_NULL(E_INVALIDARG, pForcePaint); *pForcePaint = false; return S_FALSE; } // Routine description: // - Prepares the surfaces for painting and begins a drawing batch // Arguments: // - // Return Value: // - Any DirectX error, a memory error, etc. [[nodiscard]] HRESULT DxEngine::StartPaint() noexcept try { RETURN_HR_IF(E_NOT_VALID_STATE, _isPainting); // invalid to start a paint while painting. // If full repaints are needed then we need to invalidate everything // so the entire frame is repainted. if (_FullRepaintNeeded()) { RETURN_IF_FAILED(InvalidateAll()); } if (TraceLoggingProviderEnabled(g_hDxRenderProvider, WINEVENT_LEVEL_VERBOSE, TIL_KEYWORD_TRACE)) { const auto invalidatedStr = _invalidMap.to_string(); const auto invalidated = invalidatedStr.c_str(); #pragma warning(suppress : 26477 26485 26494 26482 26446 26447) // We don't control TraceLoggingWrite TraceLoggingWrite(g_hDxRenderProvider, "Invalid", TraceLoggingWideString(invalidated), TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } if (_isEnabled) { const auto clientSize = _GetClientSize(); const auto glyphCellSize = _fontRenderData->GlyphCell(); // If we don't have device resources or if someone has requested that we // recreate the device... then make new resources. (Create will dump the old ones.) if (!_haveDeviceResources || _recreateDeviceRequested) { RETURN_IF_FAILED(_CreateDeviceResources(true)); } else if (_displaySizePixels != clientSize || _prevScale != _scale) { // OK, we're going to play a dangerous game here for the sake of optimizing resize // First, set up a complete clear of all device resources if something goes terribly wrong. auto resetDeviceResourcesOnFailure = wil::scope_exit([&]() noexcept { _ReleaseDeviceResources(); }); // Now let go of a few of the device resources that get in the way of resizing buffers in the swap chain _dxgiSurface.Reset(); _d2dDeviceContext->SetTarget(nullptr); _d2dBitmap.Reset(); // Change the buffer size and recreate the render target (and surface) RETURN_IF_FAILED(_dxgiSwapChain->ResizeBuffers(2, clientSize.width(), clientSize.height(), _swapChainDesc.Format, _swapChainDesc.Flags)); RETURN_IF_FAILED(_PrepareRenderTarget()); // OK we made it past the parts that can cause errors. We can release our failure handler. resetDeviceResourcesOnFailure.release(); // And persist the new size. _displaySizePixels = clientSize; } if (const auto size = clientSize / glyphCellSize; size != _invalidMap.size()) { _invalidMap.resize(size); RETURN_IF_FAILED(InvalidateAll()); } _d2dDeviceContext->BeginDraw(); _isPainting = true; { // Get the baseline for this font as that's where we draw from DWRITE_LINE_SPACING spacing; RETURN_IF_FAILED(_fontRenderData->DefaultTextFormat()->GetLineSpacing(&spacing.method, &spacing.height, &spacing.baseline)); // Assemble the drawing context information _drawingContext = std::make_unique(_d2dDeviceContext.Get(), _d2dBrushForeground.Get(), _d2dBrushBackground.Get(), _ShouldForceGrayscaleAA(), _dwriteFactory.Get(), spacing, glyphCellSize, _d2dDeviceContext->GetSize(), std::nullopt, D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT); } } return S_OK; } CATCH_RETURN() // Routine Description: // - Ends batch drawing and captures any state necessary for presentation // Arguments: // - // Return Value: // - Any DirectX error, a memory error, etc. [[nodiscard]] HRESULT DxEngine::EndPaint() noexcept try { RETURN_HR_IF(E_INVALIDARG, !_isPainting); // invalid to end paint when we're not painting HRESULT hr = S_OK; if (_haveDeviceResources) { _isPainting = false; // If there's still a clip hanging around, remove it. We're all done. LOG_IF_FAILED(_customRenderer->EndClip(_drawingContext.get())); hr = _d2dDeviceContext->EndDraw(); if (SUCCEEDED(hr)) { if (_invalidScroll != til::point{ 0, 0 }) { // Copy `til::rectangles` into RECT map. _presentDirty.assign(_invalidMap.begin(), _invalidMap.end()); // Scale all dirty rectangles into pixels std::transform(_presentDirty.begin(), _presentDirty.end(), _presentDirty.begin(), [&](til::rectangle rc) { return rc.scale_up(_fontRenderData->GlyphCell()); }); // Invalid scroll is in characters, convert it to pixels. const auto scrollPixels = (_invalidScroll * _fontRenderData->GlyphCell()); // The scroll rect is the entire field of cells, but in pixels. til::rectangle scrollArea{ _invalidMap.size() * _fontRenderData->GlyphCell() }; // Reduce the size of the rectangle by the scroll. scrollArea -= til::size{} - scrollPixels; // Assign the area to the present storage _presentScroll = scrollArea; // Pass the offset. _presentOffset = scrollPixels; // Now fill up the parameters structure from the member variables. _presentParams.DirtyRectsCount = gsl::narrow(_presentDirty.size()); _presentParams.pDirtyRects = _presentDirty.data(); _presentParams.pScrollOffset = &_presentOffset; _presentParams.pScrollRect = &_presentScroll; // The scroll rect will be empty if we scrolled >= 1 full screen size. // Present1 doesn't like that. So clear it out. Everything will be dirty anyway. if (IsRectEmpty(&_presentScroll)) { _presentParams.pScrollRect = nullptr; _presentParams.pScrollOffset = nullptr; } } _presentReady = true; } else { _presentReady = false; _ReleaseDeviceResources(); } } _invalidMap.reset_all(); _allInvalid = false; _invalidScroll = {}; return hr; } CATCH_RETURN() // Routine Description: // - Copies the front surface of the swap chain (the one being displayed) // to the back surface of the swap chain (the one we draw on next) // so we can draw on top of what's already there. // Arguments: // - // Return Value: // - Any DirectX error, a memory error, etc. [[nodiscard]] HRESULT DxEngine::_CopyFrontToBack() noexcept { try { Microsoft::WRL::ComPtr backBuffer; Microsoft::WRL::ComPtr frontBuffer; RETURN_IF_FAILED(_dxgiSwapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer))); RETURN_IF_FAILED(_dxgiSwapChain->GetBuffer(1, IID_PPV_ARGS(&frontBuffer))); _d3dDeviceContext->CopyResource(backBuffer.Get(), frontBuffer.Get()); } CATCH_RETURN(); return S_OK; } // Method Description: // - When the shaders are on, say that we need to keep redrawing every // possible frame in case they have some smooth action on every frame tick. // It is presumed that if you're using shaders, you're not about performance... // You're instead about OOH SHINY. And that's OK. But returning true here is 100% // a perf detriment. [[nodiscard]] bool DxEngine::RequiresContinuousRedraw() noexcept { // We're only going to request continuous redraw if someone is using // a pixel shader from a path because we cannot tell if those are using the // time parameter or not. // And if they are using time, they probably need it to tick continuously. // // By contrast, the in-built retro effect does NOT need it, // so let's not tick for it and save some amount of performance. // // Finally... if we're not using effects at all... let the render thread // go to sleep. It deserves it. That thread works hard. Also it sleeping // saves battery power and all sorts of related perf things. return _terminalEffectsEnabled && !_pixelShaderPath.empty(); } // Method Description: // - Blocks until the engine is able to render without blocking. // - See https://docs.microsoft.com/en-us/windows/uwp/gaming/reduce-latency-with-dxgi-1-3-swap-chains. void DxEngine::WaitUntilCanRender() noexcept { // Throttle the DxEngine a bit down to ~60 FPS. // This improves throughput for rendering complex or colored text. Sleep(8); if (_swapChainFrameLatencyWaitableObject) { WaitForSingleObjectEx(_swapChainFrameLatencyWaitableObject.get(), 100, true); } } // Routine Description: // - Takes queued drawing information and presents it to the screen. // - This is separated out so it can be done outside the lock as it's expensive. // Arguments: // - // Return Value: // - S_OK on success, E_PENDING to indicate a retry or a relevant DirectX error [[nodiscard]] HRESULT DxEngine::Present() noexcept { if (_presentReady) { if (_HasTerminalEffects() && _pixelShaderLoaded) { const HRESULT hr2 = _PaintTerminalEffects(); if (FAILED(hr2)) { _pixelShaderLoaded = false; LOG_HR_MSG(hr2, "Failed to paint terminal effects. Disabling."); } } try { HRESULT hr = S_OK; bool recreate = false; // On anything but the first frame, try partial presentation. // We'll do it first because if it fails, we'll try again with full presentation. if (!_firstFrame) { hr = _dxgiSwapChain->Present1(1, 0, &_presentParams); // These two error codes are indicated for destroy-and-recreate // If we were told to destroy-and-recreate, we're going to skip straight into doing that // and not try again with full presentation. recreate = hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET; // Log this as we actually don't expect it to happen, we just will try again // below for robustness of our drawing. if (FAILED(hr) && !recreate) { LOG_HR(hr); } } // If it's the first frame through, we cannot do partial presentation. // Also if partial presentation failed above and we weren't told to skip straight to // device recreation. // In both of these circumstances, do a full presentation. if (_firstFrame || (FAILED(hr) && !recreate)) { hr = _dxgiSwapChain->Present(1, 0); _firstFrame = false; // These two error codes are indicated for destroy-and-recreate recreate = hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET; } // Now check for failure cases from either presentation mode. if (FAILED(hr)) { // If we were told to recreate the device surface, do that. if (recreate) { // We don't need to end painting here, as the renderer has done it for us. _ReleaseDeviceResources(); FAIL_FAST_IF_FAILED(InvalidateAll()); return E_PENDING; // Indicate a retry to the renderer. } // Otherwise, we don't know what to do with this error. Report it. else { FAIL_FAST_HR(hr); } } // If we are doing full repaints we don't need to copy front buffer to back buffer if (!_FullRepaintNeeded()) { // Finally copy the front image (being presented now) onto the backing buffer // (where we are about to draw the next frame) so we can draw only the differences // next frame. RETURN_IF_FAILED(_CopyFrontToBack()); } _presentReady = false; _presentDirty.clear(); _presentOffset = { 0 }; _presentScroll = { 0 }; _presentParams = { 0 }; } CATCH_RETURN(); } return S_OK; } // Routine Description: // - This is currently unused. // Arguments: // - // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::ScrollFrame() noexcept { return S_OK; } // Routine Description: // - This paints in the back most layer of the frame with the background color. // Arguments: // - // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::PaintBackground() noexcept try { D2D1_COLOR_F nothing{ 0 }; if (_chainMode == SwapChainMode::ForHwnd) { // When we're drawing over an HWND target, we need to fully paint the background color. nothing = _backgroundColor; } // If the entire thing is invalid, just use one big clear operation. if (_invalidMap.all()) { _d2dDeviceContext->Clear(nothing); } else { // Runs are counts of cells. // Use a transform by the size of one cell to convert cells-to-pixels // as we clear. _d2dDeviceContext->SetTransform(D2D1::Matrix3x2F::Scale(_fontRenderData->GlyphCell())); for (const auto& rect : _invalidMap.runs()) { // Use aliased. // For graphics reasons, it'll look better because it will ensure that // the edges are cut nice and sharp (not blended by anti-aliasing). // For performance reasons, it takes a lot less work to not // do anti-alias blending. _d2dDeviceContext->PushAxisAlignedClip(rect, D2D1_ANTIALIAS_MODE_ALIASED); _d2dDeviceContext->Clear(nothing); _d2dDeviceContext->PopAxisAlignedClip(); } _d2dDeviceContext->SetTransform(D2D1::Matrix3x2F::Identity()); } return S_OK; } CATCH_RETURN() // Routine Description: // - Places one line of text onto the screen at the given position // Arguments: // - clusters - Iterable collection of cluster information (text and columns it should consume) // - coord - Character coordinate position in the cell grid // - fTrimLeft - Whether or not to trim off the left half of a double wide character // Return Value: // - S_OK or relevant DirectX error [[nodiscard]] HRESULT DxEngine::PaintBufferLine(gsl::span const clusters, COORD const coord, const bool /*trimLeft*/, const bool /*lineWrapped*/) noexcept try { // Calculate positioning of our origin. const D2D1_POINT_2F origin = til::point{ coord } * _fontRenderData->GlyphCell(); // Create the text layout RETURN_IF_FAILED(_customLayout->Reset()); RETURN_IF_FAILED(_customLayout->AppendClusters(clusters)); // Layout then render the text RETURN_IF_FAILED(_customLayout->Draw(_drawingContext.get(), _customRenderer.Get(), origin.x, origin.y)); return S_OK; } CATCH_RETURN() // Routine Description: // - Paints lines around cells (draws in pieces of the grid) // Arguments: // - lines - Which grid lines (top, left, bottom, right) to draw // - color - The color to use for drawing the lines // - cchLine - Length of the line to draw in character cells // - coordTarget - The X,Y character position in the grid where we should start drawing // - We will draw rightward (+X) from here // Return Value: // - S_OK or relevant DirectX error [[nodiscard]] HRESULT DxEngine::PaintBufferGridLines(GridLineSet const lines, COLORREF const color, size_t const cchLine, COORD const coordTarget) noexcept try { const auto existingColor = _d2dBrushForeground->GetColor(); const auto restoreBrushOnExit = wil::scope_exit([&]() noexcept { _d2dBrushForeground->SetColor(existingColor); }); _d2dBrushForeground->SetColor(_ColorFFromColorRef(color)); const D2D1_SIZE_F font = _fontRenderData->GlyphCell(); const D2D_POINT_2F target = { coordTarget.X * font.width, coordTarget.Y * font.height }; const auto fullRunWidth = font.width * gsl::narrow_cast(cchLine); const auto DrawLine = [=](const auto x0, const auto y0, const auto x1, const auto y1, const auto strokeWidth) noexcept { _d2dDeviceContext->DrawLine({ x0, y0 }, { x1, y1 }, _d2dBrushForeground.Get(), strokeWidth, _strokeStyle.Get()); }; const auto DrawHyperlinkLine = [=](const auto x0, const auto y0, const auto x1, const auto y1, const auto strokeWidth) noexcept { _d2dDeviceContext->DrawLine({ x0, y0 }, { x1, y1 }, _d2dBrushForeground.Get(), strokeWidth, _hyperlinkStrokeStyle.Get()); }; // NOTE: Line coordinates are centered within the line, so they need to be // offset by half the stroke width. For the start coordinate we add half // the stroke width, and for the end coordinate we subtract half the width. const DxFontRenderData::LineMetrics lineMetrics = _fontRenderData->GetLineMetrics(); if (lines.any(GridLines::Left, GridLines::Right)) { const auto halfGridlineWidth = lineMetrics.gridlineWidth / 2.0f; const auto startY = target.y + halfGridlineWidth; const auto endY = target.y + font.height - halfGridlineWidth; if (lines.test(GridLines::Left)) { auto x = target.x + halfGridlineWidth; for (size_t i = 0; i < cchLine; i++, x += font.width) { DrawLine(x, startY, x, endY, lineMetrics.gridlineWidth); } } if (lines.test(GridLines::Right)) { auto x = target.x + font.width - halfGridlineWidth; for (size_t i = 0; i < cchLine; i++, x += font.width) { DrawLine(x, startY, x, endY, lineMetrics.gridlineWidth); } } } if (lines.any(GridLines::Top, GridLines::Bottom)) { const auto halfGridlineWidth = lineMetrics.gridlineWidth / 2.0f; const auto startX = target.x + halfGridlineWidth; const auto endX = target.x + fullRunWidth - halfGridlineWidth; if (lines.test(GridLines::Top)) { const auto y = target.y + halfGridlineWidth; DrawLine(startX, y, endX, y, lineMetrics.gridlineWidth); } if (lines.test(GridLines::Bottom)) { const auto y = target.y + font.height - halfGridlineWidth; DrawLine(startX, y, endX, y, lineMetrics.gridlineWidth); } } // In the case of the underline and strikethrough offsets, the stroke width // is already accounted for, so they don't require further adjustments. if (lines.any(GridLines::Underline, GridLines::DoubleUnderline, GridLines::HyperlinkUnderline)) { const auto halfUnderlineWidth = lineMetrics.underlineWidth / 2.0f; const auto startX = target.x + halfUnderlineWidth; const auto endX = target.x + fullRunWidth - halfUnderlineWidth; const auto y = target.y + lineMetrics.underlineOffset; if (lines.test(GridLines::Underline)) { DrawLine(startX, y, endX, y, lineMetrics.underlineWidth); } if (lines.test(GridLines::HyperlinkUnderline)) { DrawHyperlinkLine(startX, y, endX, y, lineMetrics.underlineWidth); } if (lines.test(GridLines::DoubleUnderline)) { DrawLine(startX, y, endX, y, lineMetrics.underlineWidth); const auto y2 = target.y + lineMetrics.underlineOffset2; DrawLine(startX, y2, endX, y2, lineMetrics.underlineWidth); } } if (lines.test(GridLines::Strikethrough)) { const auto halfStrikethroughWidth = lineMetrics.strikethroughWidth / 2.0f; const auto startX = target.x + halfStrikethroughWidth; const auto endX = target.x + fullRunWidth - halfStrikethroughWidth; const auto y = target.y + lineMetrics.strikethroughOffset; DrawLine(startX, y, endX, y, lineMetrics.strikethroughWidth); } return S_OK; } CATCH_RETURN() // Routine Description: // - Paints an overlay highlight on a portion of the frame to represent selected text // Arguments: // - rect - Rectangle to invert or highlight to make the selection area // Return Value: // - S_OK or relevant DirectX error. [[nodiscard]] HRESULT DxEngine::PaintSelection(const SMALL_RECT rect) noexcept try { // If a clip rectangle is in place from drawing the text layer, remove it here. LOG_IF_FAILED(_customRenderer->EndClip(_drawingContext.get())); const auto existingColor = _d2dBrushForeground->GetColor(); _d2dBrushForeground->SetColor(_selectionBackground); const auto resetColorOnExit = wil::scope_exit([&]() noexcept { _d2dBrushForeground->SetColor(existingColor); }); const D2D1_RECT_F draw = til::rectangle{ Viewport::FromExclusive(rect).ToInclusive() }.scale_up(_fontRenderData->GlyphCell()); _d2dDeviceContext->FillRectangle(draw, _d2dBrushForeground.Get()); return S_OK; } CATCH_RETURN() // Routine Description: // - Does nothing. Our cursor is drawn in CustomTextRenderer::DrawGlyphRun, // either above or below the text. // Arguments: // - options - unused // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::PaintCursor(const CursorOptions& /*options*/) noexcept { return S_OK; } // Routine Description: // - Paint terminal effects. // Arguments: // Return Value: // - S_OK or relevant DirectX error. [[nodiscard]] HRESULT DxEngine::_PaintTerminalEffects() noexcept try { // Should have been initialized. RETURN_HR_IF(E_NOT_VALID_STATE, !_framebufferCapture); // Capture current frame in swap chain to a texture. ::Microsoft::WRL::ComPtr swapBuffer; RETURN_IF_FAILED(_dxgiSwapChain->GetBuffer(0, IID_PPV_ARGS(&swapBuffer))); _d3dDeviceContext->CopyResource(_framebufferCapture.Get(), swapBuffer.Get()); // Prepare captured texture as input resource to shader program. D3D11_TEXTURE2D_DESC desc; _framebufferCapture->GetDesc(&desc); D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc; srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; srvDesc.Texture2D.MostDetailedMip = 0; srvDesc.Texture2D.MipLevels = desc.MipLevels; srvDesc.Format = desc.Format; ::Microsoft::WRL::ComPtr shaderResource; RETURN_IF_FAILED(_d3dDevice->CreateShaderResourceView(_framebufferCapture.Get(), &srvDesc, &shaderResource)); // Render the screen quad with shader effects. const UINT stride = sizeof(ShaderInput); const UINT offset = 0; _d3dDeviceContext->OMSetRenderTargets(1, _renderTargetView.GetAddressOf(), nullptr); _d3dDeviceContext->IASetVertexBuffers(0, 1, _screenQuadVertexBuffer.GetAddressOf(), &stride, &offset); _d3dDeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); _d3dDeviceContext->IASetInputLayout(_vertexLayout.Get()); _d3dDeviceContext->VSSetShader(_vertexShader.Get(), nullptr, 0); _d3dDeviceContext->PSSetShader(_pixelShader.Get(), nullptr, 0); _d3dDeviceContext->PSSetShaderResources(0, 1, shaderResource.GetAddressOf()); _d3dDeviceContext->PSSetSamplers(0, 1, _samplerState.GetAddressOf()); _d3dDeviceContext->PSSetConstantBuffers(0, 1, _pixelShaderSettingsBuffer.GetAddressOf()); _d3dDeviceContext->Draw(ARRAYSIZE(_screenQuadVertices), 0); return S_OK; } CATCH_RETURN() [[nodiscard]] bool DxEngine::_FullRepaintNeeded() const noexcept { // If someone explicitly requested differential rendering off, then we need to invalidate everything // so the entire frame is repainted. // // If terminal effects are on, we must invalidate everything for them to draw correctly. // Yes, this will further impact the performance of terminal effects. // But we're talking about running the entire display pipeline through a shader for // cosmetic effect, so performance isn't likely the top concern with this feature. return _forceFullRepaintRendering || _HasTerminalEffects(); } // Routine Description: // - Updates the default brush colors used for drawing // Arguments: // - textAttributes - Text attributes to use for the brush color // - pData - The interface to console data structures required for rendering // - usingSoftFont - Whether we're rendering characters from a soft font // - isSettingDefaultBrushes - Lets us know that these are the default brushes to paint the swapchain background or selection // Return Value: // - S_OK or relevant DirectX error. [[nodiscard]] HRESULT DxEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, const bool /*usingSoftFont*/, const bool isSettingDefaultBrushes) noexcept { const auto [colorForeground, colorBackground] = pData->GetAttributeColors(textAttributes); const bool usingCleartype = _antialiasingMode == D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE; const bool usingTransparency = _defaultBackgroundIsTransparent; const bool forceOpaqueBG = usingCleartype && !usingTransparency; _foregroundColor = _ColorFFromColorRef(OPACITY_OPAQUE | colorForeground); // October 2021: small changes were made to the way BG color interacts with // grayscale AA, esp. with regards to acrylic and GH#5098. See comment in // _ShouldForceGrayscaleAA for more details. _backgroundColor = _ColorFFromColorRef((forceOpaqueBG ? OPACITY_OPAQUE : 0) | colorBackground); _d2dBrushForeground->SetColor(_foregroundColor); _d2dBrushBackground->SetColor(_backgroundColor); // If this flag is set, then we need to update the default brushes too and the swap chain background. if (isSettingDefaultBrushes) { _defaultForegroundColor = _foregroundColor; _defaultBackgroundColor = _backgroundColor; // If we have a swap chain, set the background color there too so the area // outside the chain on a resize can be filled in with an appropriate color value. /*if (_dxgiSwapChain) { const auto dxgiColor = s_RgbaFromColorF(_defaultBackgroundColor); RETURN_IF_FAILED(_dxgiSwapChain->SetBackgroundColor(&dxgiColor)); }*/ } // If we have a drawing context, it may be choosing its antialiasing based // on the colors. Update it if it exists. // Also record whether we need to render the text with an italic font. // We only need to do this here because this is called all the time on painting frames // and will update it in a timely fashion. Changing the AA mode or opacity do affect // it, but we will always hit updating the drawing brushes so we don't // need to update this in those locations. if (_drawingContext) { _drawingContext->forceGrayscaleAA = _ShouldForceGrayscaleAA(); _drawingContext->useBoldFont = _intenseIsBold && textAttributes.IsBold(); _drawingContext->useItalicFont = textAttributes.IsItalic(); } if (textAttributes.IsHyperlink()) { _hyperlinkStrokeStyle = (textAttributes.GetHyperlinkId() == _hyperlinkHoveredId) ? _strokeStyle : _dashStrokeStyle; } // Update pixel shader settings as background color might have changed _ComputePixelShaderSettings(); return S_OK; } // Routine Description: // - Updates the font used for drawing // - This is the version that complies with the IRenderEngine interface // Arguments: // - pfiFontInfoDesired - Information specifying the font that is requested // - fiFontInfo - Filled with the nearest font actually chosen for drawing // Return Value: // - S_OK or relevant DirectX error [[nodiscard]] HRESULT DxEngine::UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo) noexcept { return UpdateFont(pfiFontInfoDesired, fiFontInfo, {}, {}); } // Routine Description: // - Updates the font used for drawing // Arguments: // - pfiFontInfoDesired - Information specifying the font that is requested // - fiFontInfo - Filled with the nearest font actually chosen for drawing // - features - The map of font features to use // - axes - The map of font axes to use // Return Value: // - S_OK or relevant DirectX error [[nodiscard]] HRESULT DxEngine::UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept try { RETURN_IF_FAILED(_fontRenderData->UpdateFont(pfiFontInfoDesired, fiFontInfo, _dpi, features, axes)); // Prepare the text layout. _customLayout = WRL::Make(_fontRenderData.get()); return S_OK; } CATCH_RETURN(); [[nodiscard]] Viewport DxEngine::GetViewportInCharacters(const Viewport& viewInPixels) const noexcept { const short widthInChars = base::saturated_cast(viewInPixels.Width() / _fontRenderData->GlyphCell().width()); const short heightInChars = base::saturated_cast(viewInPixels.Height() / _fontRenderData->GlyphCell().height()); return Viewport::FromDimensions(viewInPixels.Origin(), { widthInChars, heightInChars }); } [[nodiscard]] Viewport DxEngine::GetViewportInPixels(const Viewport& viewInCharacters) const noexcept { const short widthInPixels = base::saturated_cast(viewInCharacters.Width() * _fontRenderData->GlyphCell().width()); const short heightInPixels = base::saturated_cast(viewInCharacters.Height() * _fontRenderData->GlyphCell().height()); return Viewport::FromDimensions(viewInCharacters.Origin(), { widthInPixels, heightInPixels }); } // Routine Description: // - Sets the DPI in this renderer // Arguments: // - iDpi - DPI // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::UpdateDpi(int const iDpi) noexcept { _dpi = iDpi; // The scale factor may be necessary for composition contexts, so save it once here. _scale = _dpi / static_cast(USER_DEFAULT_SCREEN_DPI); RETURN_IF_FAILED(InvalidateAll()); // Update pixel shader settings as scale might have changed _ComputePixelShaderSettings(); return S_OK; } // Method Description: // - Get the current scale factor of this renderer. The actual DPI the renderer // is USER_DEFAULT_SCREEN_DPI * GetScaling() // Arguments: // - // Return Value: // - the scaling multiplier of this render engine float DxEngine::GetScaling() const noexcept { return _scale; } // Method Description: // - This method will update our internal reference for how big the viewport is. // Does nothing for DX. // Arguments: // - srNewViewport - The bounds of the new viewport. // Return Value: // - HRESULT S_OK [[nodiscard]] HRESULT DxEngine::UpdateViewport(const SMALL_RECT /*srNewViewport*/) noexcept { return S_OK; } // Routine Description: // - Currently unused by this renderer // Arguments: // - pfiFontInfoDesired - // - pfiFontInfo - // - iDpi - // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::GetProposedFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& pfiFontInfo, int const iDpi) noexcept { DxFontRenderData fontRenderData(_dwriteFactory); return fontRenderData.UpdateFont(pfiFontInfoDesired, pfiFontInfo, iDpi); } // Routine Description: // - Gets the area that we currently believe is dirty within the character cell grid // Arguments: // - area - Rectangle describing dirty area in characters. // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::GetDirtyArea(gsl::span& area) noexcept try { area = _invalidMap.runs(); return S_OK; } CATCH_RETURN(); // Routine Description: // - Gets the current font size // Arguments: // - pFontSize - Filled with the font size. // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::GetFontSize(_Out_ COORD* const pFontSize) noexcept try { *pFontSize = _fontRenderData->GlyphCell(); return S_OK; } CATCH_RETURN(); // Routine Description: // - Currently unused by this renderer. // Arguments: // - glyph - The glyph run to process for column width. // - pResult - True if it should take two columns. False if it should take one. // Return Value: // - S_OK or relevant DirectWrite error. [[nodiscard]] HRESULT DxEngine::IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept try { RETURN_HR_IF_NULL(E_INVALIDARG, pResult); const Cluster cluster(glyph, 0); // columns don't matter, we're doing analysis not layout. RETURN_IF_FAILED(_customLayout->Reset()); RETURN_IF_FAILED(_customLayout->AppendClusters({ &cluster, 1 })); UINT32 columns = 0; RETURN_IF_FAILED(_customLayout->GetColumns(&columns)); *pResult = columns != 1; return S_OK; } CATCH_RETURN(); // Method Description: // - Updates the window's title string. // Arguments: // - newTitle: the new string to use for the title of the window // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::_DoUpdateTitle(_In_ const std::wstring_view /*newTitle*/) noexcept { if (_hwndTarget != INVALID_HANDLE_VALUE) { return PostMessageW(_hwndTarget, CM_UPDATE_TITLE, 0, 0) ? S_OK : E_FAIL; } return S_FALSE; } // Routine Description: // - Helps convert a GDI COLORREF into a Direct2D ColorF // Arguments: // - color - GDI color // Return Value: // - D2D color [[nodiscard]] D2D1_COLOR_F DxEngine::_ColorFFromColorRef(const COLORREF color) noexcept { // Converts BGR color order to RGB. const UINT32 rgb = ((color & 0x0000FF) << 16) | (color & 0x00FF00) | ((color & 0xFF0000) >> 16); switch (_chainMode) { case SwapChainMode::ForHwnd: { return D2D1::ColorF(rgb); } case SwapChainMode::ForComposition: { // Get the A value we've snuck into the highest byte const BYTE a = ((color >> 24) & 0xFF); const float aFloat = a / 255.0f; return D2D1::ColorF(rgb, aFloat); } default: FAIL_FAST_HR(E_NOTIMPL); } } // Routine Description: // - Updates the selection background color of the DxEngine // Arguments: // - color - GDI Color // Return Value: // - N/A void DxEngine::SetSelectionBackground(const COLORREF color, const float alpha) noexcept { _selectionBackground = D2D1::ColorF(GetRValue(color) / 255.0f, GetGValue(color) / 255.0f, GetBValue(color) / 255.0f, alpha); } // Routine Description: // - Changes the antialiasing mode of the renderer. This must be called before // _PrepareRenderTarget, otherwise the renderer will default to // D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE. // Arguments: // - antialiasingMode: a value from the D2D1_TEXT_ANTIALIAS_MODE enum. See: // https://docs.microsoft.com/en-us/windows/win32/api/d2d1/ne-d2d1-d2d1_text_antialias_mode // Return Value: // - N/A void DxEngine::SetAntialiasingMode(const D2D1_TEXT_ANTIALIAS_MODE antialiasingMode) noexcept try { if (_antialiasingMode != antialiasingMode) { _antialiasingMode = antialiasingMode; _recreateDeviceRequested = true; LOG_IF_FAILED(InvalidateAll()); } } CATCH_LOG() // Method Description: // - Update our tracker of the opacity of our background. We can only // effectively render cleartype text onto fully-opaque backgrounds. If we're // rendering onto a transparent surface (like acrylic), then cleartype won't // work correctly, and will actually just additively blend with the // background. This is here to support GH#5098. // - We'll use this, along with whether cleartype was requested, to manually set // the alpha channel of the background brush to 1.0. We need to do that to // make cleartype work without blending. However, we don't want to do that too // often - if we do that on top of a transparent BG, then the entire swap // chain will be fully opaque. // Arguments: // - isTransparent: true if our BG is transparent (acrylic, or anything that's not fully opaque) // Return Value: // - void DxEngine::EnableTransparentBackground(const bool isTransparent) noexcept try { _defaultBackgroundIsTransparent = isTransparent; // Make sure we redraw all the cells, to update whether they're actually // drawn with cleartype or not. // We don't terribly care if this fails. LOG_IF_FAILED(InvalidateAll()); } CATCH_LOG() // Method Description: // - Updates our internal tracker for which hyperlink ID we are hovering over // This is needed for UpdateDrawingBrushes to know where we need to set a different style // Arguments: // - The new link ID we are hovering over void DxEngine::UpdateHyperlinkHoveredId(const uint16_t hoveredId) noexcept { _hyperlinkHoveredId = hoveredId; } // Method Description: // - Informs this render engine about certain state for this frame at the // beginning of this frame. We'll use it to get information about the cursor // before PaintCursor is called. This enables the DX renderer to draw the // cursor underneath the text. // - This is called every frame. When the cursor is Off or out of frame, the // info's cursorInfo will be set to std::nullopt; // Arguments: // - info - a RenderFrameInfo with information about the state of the cursor in this frame. // Return Value: // - S_OK [[nodiscard]] HRESULT DxEngine::PrepareRenderInfo(const RenderFrameInfo& info) noexcept { _drawingContext->cursorInfo = info.cursorInfo; return S_OK; }