Compare commits

..

No commits in common. "dev" and "v0.12.0-preview2" have entirely different histories.

209 changed files with 1890 additions and 8857 deletions

View file

@ -12,7 +12,11 @@
"avatar_url": "https://avatars.githubusercontent.com/u/34770031?v=4",
"profile": "https://github.com/Blinue",
"contributions": [
"maintenance"
"maintenance",
"code",
"review",
"doc",
"question"
]
},
{
@ -297,51 +301,6 @@
"contributions": [
"doc"
]
},
{
"login": "kangurek-kao",
"name": "Krzysztof",
"avatar_url": "https://avatars.githubusercontent.com/u/116571935?v=4",
"profile": "https://github.com/kangurek-kao",
"contributions": [
"translation"
]
},
{
"login": "Howard20181",
"name": "Howard Wu",
"avatar_url": "https://avatars.githubusercontent.com/u/40033067?v=4",
"profile": "https://github.com/Howard20181",
"contributions": [
"code"
]
},
{
"login": "arifpedia",
"name": "Arif Budiman",
"avatar_url": "https://avatars.githubusercontent.com/u/4081293?v=4",
"profile": "https://github.com/arifpedia",
"contributions": [
"translation"
]
},
{
"login": "Androidlate",
"name": "Raphael",
"avatar_url": "https://avatars.githubusercontent.com/u/194900061?v=4",
"profile": "https://github.com/Androidlate",
"contributions": [
"translation"
]
},
{
"login": "rezorrand",
"name": "Pate L",
"avatar_url": "https://avatars.githubusercontent.com/u/7170353?v=4",
"profile": "https://github.com/rezorrand",
"contributions": [
"translation"
]
}
],
"contributorsPerLine": 7,

View file

@ -2,40 +2,40 @@ name: build
on:
push:
paths: [ '.github/workflows/build.yml', 'Magpie.slnx', '*.props', 'scripts/publish.py', 'src/**' ]
paths: [ '.github/workflows/build.yml', 'Magpie.sln', '*.props', 'publish.py', 'src/**' ]
pull_request:
paths: [ '.github/workflows/build.yml', 'Magpie.slnx', '*.props', 'scripts/publish.py', 'src/**' ]
paths: [ '.github/workflows/build.yml', 'Magpie.sln', '*.props', 'publish.py', 'src/**' ]
jobs:
build:
runs-on: windows-2025-vs2026
runs-on: windows-latest
strategy:
matrix:
compiler: ["MSVC", "ClangCL"]
platform: ["x64", "ARM64"]
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: actions/setup-python@v6
- uses: actions/setup-python@v5
with:
python-version: '3.13'
python-version: '3.11'
- name: Setup Conan
run: pip install conan
- name: Load Conan cache
uses: actions/cache@v5
uses: actions/cache@v4
with:
path: ~/.conan2/p
key: Conan-${{ hashFiles('src/**/conanfile.txt') }}-${{ matrix.compiler }}-${{ matrix.platform }}
- name: Build
if: github.ref != 'refs/heads/dev' && github.ref != 'refs/heads/main'
if: github.event_name == 'pull_request'
run: python scripts/publish.py --compiler=${{ matrix.compiler }} --platform=${{ matrix.platform }}
- name: Build and sign
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main'
- name: Build
if: github.event_name != 'pull_request'
run: python scripts/publish.py --compiler=${{ matrix.compiler }} --platform=${{ matrix.platform }} --pfx-path=certs\Magpie.pfx --pfx-password="${{ secrets.MAGPIE_PFX_PASSWORD }}"
- name: Save hash
@ -43,7 +43,7 @@ jobs:
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $env:GITHUB_OUTPUT
- name: Store build
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: Magpie-dev-${{ steps.hash.outputs.sha_short }}-${{ matrix.compiler }}-${{ matrix.platform }}
path: ./publish/${{ matrix.platform }}

View file

@ -25,21 +25,27 @@ on:
type: boolean
jobs:
build:
runs-on: windows-2025-vs2026
runs-on: windows-latest
outputs:
tag: ${{ steps.tag.outputs.tag }}
strategy:
matrix:
platform: ["x64", "ARM64"]
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: actions/setup-python@v6
- uses: actions/setup-python@v5
with:
python-version: '3.13'
python-version: '3.11'
- name: Setup Conan
run: pip install conan
- name: Load Conan cache
uses: actions/cache@v4
with:
path: ~/.conan2/p
key: Conan-${{ hashFiles('src/**/conanfile.txt') }}-${{ matrix.platform }}
- name: Generate tag
id: tag
@ -48,12 +54,10 @@ jobs:
echo "tag=$tag" >> $env:GITHUB_OUTPUT
- name: Build
run: |
$versionString = "${{ steps.tag.outputs.tag }}" -replace "^v(?=\d)", ""
python scripts/publish.py --compiler=ClangCL --platform=${{ matrix.platform }} --version-major=${{ inputs.major }} --version-minor=${{ inputs.minor }} --version-patch=${{ inputs.patch }} --version-string=$versionString --pfx-path=certs\Magpie.pfx --pfx-password="${{ secrets.MAGPIE_PFX_PASSWORD }}"
run: python scripts/publish.py --compiler=ClangCL --platform=${{ matrix.platform }} --version-major=${{ inputs.major }} --version-minor=${{ inputs.minor }} --version-patch=${{ inputs.patch }} --version-tag=${{ steps.tag.outputs.tag }} --pfx-path=certs\Magpie.pfx --pfx-password="${{ secrets.MAGPIE_PFX_PASSWORD }}"
- name: Store artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: Magpie-${{ steps.tag.outputs.tag }}-${{ matrix.platform }}
path: publish/${{ matrix.platform }}
@ -61,17 +65,17 @@ jobs:
runs-on: windows-latest
needs: build
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: actions/setup-python@v6
- uses: actions/setup-python@v5
with:
python-version: '3.13'
python-version: '3.11'
- name: Setup Requests
run: pip install requests
- name: Restore artifacts
uses: actions/download-artifact@v8
uses: actions/download-artifact@v4
with:
path: publish

View file

@ -3,18 +3,18 @@ name: Publish wiki
on:
push:
branches: [ main ]
paths: [ '.github/workflows/wiki.yml', 'docs/**', 'scripts/wiki.py' ]
paths: [ '.github/workflows/wiki.yml', 'docs/**', 'ci/wiki.py' ]
jobs:
publish:
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: actions/setup-python@v6
- uses: actions/setup-python@v5
with:
python-version: '3.13'
python-version: '3.11'
- name: Upload documentations to wiki
run: python scripts/wiki.py ${{ secrets.CONTENTS_ACCESS_TOKEN }}

119
Magpie.sln Normal file
View file

@ -0,0 +1,119 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32228.430
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Magpie", "src\Magpie\Magpie.vcxproj", "{1239537C-E5B8-427A-9E7F-EA443D1F3529}"
ProjectSection(ProjectDependencies) = postProject
{05B51BB8-08CB-4907-884F-8E2AD6BF6052} = {05B51BB8-08CB-4907-884F-8E2AD6BF6052}
{456CCAE4-2C51-4CF2-8D3A-1EFCE8C41A2D} = {456CCAE4-2C51-4CF2-8D3A-1EFCE8C41A2D}
{62503530-B84B-4CC2-80B6-3F89618172B7} = {62503530-B84B-4CC2-80B6-3F89618172B7}
{E82B7A20-0557-4DC1-B418-87977D7450A4} = {E82B7A20-0557-4DC1-B418-87977D7450A4}
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{00AB63C3-0CD3-4944-B8E6-58C86138618D}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
src\BuildOptions.props = src\BuildOptions.props
src\Common.Post.props = src\Common.Post.props
src\Common.Pre.props = src\Common.Pre.props
Directory.Build.props = Directory.Build.props
src\HybridCRT.props = src\HybridCRT.props
src\WinUI.targets = src\WinUI.targets
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "_ConanDeps", "src\_ConanDeps\_ConanDeps.vcxproj", "{456CCAE4-2C51-4CF2-8D3A-1EFCE8C41A2D}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Effects", "src\Effects\Effects.vcxproj", "{62503530-B84B-4CC2-80B6-3F89618172B7}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Magpie.Core", "src\Magpie.Core\Magpie.Core.vcxproj", "{0E5205AE-DFA9-4CB8-B662-E43CD6512E2A}"
ProjectSection(ProjectDependencies) = postProject
{456CCAE4-2C51-4CF2-8D3A-1EFCE8C41A2D} = {456CCAE4-2C51-4CF2-8D3A-1EFCE8C41A2D}
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Natvis", "Natvis", "{9808D34F-5715-4D02-B216-4CB80F46BBC0}"
ProjectSection(SolutionItems) = preProject
natvis\magpie.natvis = natvis\magpie.natvis
natvis\phmap.natvis = natvis\phmap.natvis
natvis\rapidjson.natvis = natvis\rapidjson.natvis
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Updater", "src\Updater\Updater.vcxproj", "{E82B7A20-0557-4DC1-B418-87977D7450A4}"
ProjectSection(ProjectDependencies) = postProject
{456CCAE4-2C51-4CF2-8D3A-1EFCE8C41A2D} = {456CCAE4-2C51-4CF2-8D3A-1EFCE8C41A2D}
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TouchHelper", "src\TouchHelper\TouchHelper.vcxproj", "{05B51BB8-08CB-4907-884F-8E2AD6BF6052}"
ProjectSection(ProjectDependencies) = postProject
{456CCAE4-2C51-4CF2-8D3A-1EFCE8C41A2D} = {456CCAE4-2C51-4CF2-8D3A-1EFCE8C41A2D}
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Shared", "src\Shared\Shared.vcxitems", "{AABDA3A3-7B23-4189-895B-F68A4C6B14C2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
Debug|x64 = Debug|x64
Release|ARM64 = Release|ARM64
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1239537C-E5B8-427A-9E7F-EA443D1F3529}.Debug|ARM64.ActiveCfg = Debug|ARM64
{1239537C-E5B8-427A-9E7F-EA443D1F3529}.Debug|ARM64.Build.0 = Debug|ARM64
{1239537C-E5B8-427A-9E7F-EA443D1F3529}.Debug|x64.ActiveCfg = Debug|x64
{1239537C-E5B8-427A-9E7F-EA443D1F3529}.Debug|x64.Build.0 = Debug|x64
{1239537C-E5B8-427A-9E7F-EA443D1F3529}.Release|ARM64.ActiveCfg = Release|ARM64
{1239537C-E5B8-427A-9E7F-EA443D1F3529}.Release|ARM64.Build.0 = Release|ARM64
{1239537C-E5B8-427A-9E7F-EA443D1F3529}.Release|x64.ActiveCfg = Release|x64
{1239537C-E5B8-427A-9E7F-EA443D1F3529}.Release|x64.Build.0 = Release|x64
{456CCAE4-2C51-4CF2-8D3A-1EFCE8C41A2D}.Debug|ARM64.ActiveCfg = Debug|ARM64
{456CCAE4-2C51-4CF2-8D3A-1EFCE8C41A2D}.Debug|ARM64.Build.0 = Debug|ARM64
{456CCAE4-2C51-4CF2-8D3A-1EFCE8C41A2D}.Debug|x64.ActiveCfg = Debug|x64
{456CCAE4-2C51-4CF2-8D3A-1EFCE8C41A2D}.Debug|x64.Build.0 = Debug|x64
{456CCAE4-2C51-4CF2-8D3A-1EFCE8C41A2D}.Release|ARM64.ActiveCfg = Release|ARM64
{456CCAE4-2C51-4CF2-8D3A-1EFCE8C41A2D}.Release|ARM64.Build.0 = Release|ARM64
{456CCAE4-2C51-4CF2-8D3A-1EFCE8C41A2D}.Release|x64.ActiveCfg = Release|x64
{456CCAE4-2C51-4CF2-8D3A-1EFCE8C41A2D}.Release|x64.Build.0 = Release|x64
{62503530-B84B-4CC2-80B6-3F89618172B7}.Debug|ARM64.ActiveCfg = Debug|ARM64
{62503530-B84B-4CC2-80B6-3F89618172B7}.Debug|ARM64.Build.0 = Debug|ARM64
{62503530-B84B-4CC2-80B6-3F89618172B7}.Debug|x64.ActiveCfg = Debug|x64
{62503530-B84B-4CC2-80B6-3F89618172B7}.Debug|x64.Build.0 = Debug|x64
{62503530-B84B-4CC2-80B6-3F89618172B7}.Release|ARM64.ActiveCfg = Release|ARM64
{62503530-B84B-4CC2-80B6-3F89618172B7}.Release|ARM64.Build.0 = Release|ARM64
{62503530-B84B-4CC2-80B6-3F89618172B7}.Release|x64.ActiveCfg = Release|x64
{62503530-B84B-4CC2-80B6-3F89618172B7}.Release|x64.Build.0 = Release|x64
{0E5205AE-DFA9-4CB8-B662-E43CD6512E2A}.Debug|ARM64.ActiveCfg = Debug|ARM64
{0E5205AE-DFA9-4CB8-B662-E43CD6512E2A}.Debug|ARM64.Build.0 = Debug|ARM64
{0E5205AE-DFA9-4CB8-B662-E43CD6512E2A}.Debug|x64.ActiveCfg = Debug|x64
{0E5205AE-DFA9-4CB8-B662-E43CD6512E2A}.Debug|x64.Build.0 = Debug|x64
{0E5205AE-DFA9-4CB8-B662-E43CD6512E2A}.Release|ARM64.ActiveCfg = Release|ARM64
{0E5205AE-DFA9-4CB8-B662-E43CD6512E2A}.Release|ARM64.Build.0 = Release|ARM64
{0E5205AE-DFA9-4CB8-B662-E43CD6512E2A}.Release|x64.ActiveCfg = Release|x64
{0E5205AE-DFA9-4CB8-B662-E43CD6512E2A}.Release|x64.Build.0 = Release|x64
{E82B7A20-0557-4DC1-B418-87977D7450A4}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E82B7A20-0557-4DC1-B418-87977D7450A4}.Debug|ARM64.Build.0 = Debug|ARM64
{E82B7A20-0557-4DC1-B418-87977D7450A4}.Debug|x64.ActiveCfg = Debug|x64
{E82B7A20-0557-4DC1-B418-87977D7450A4}.Debug|x64.Build.0 = Debug|x64
{E82B7A20-0557-4DC1-B418-87977D7450A4}.Release|ARM64.ActiveCfg = Release|ARM64
{E82B7A20-0557-4DC1-B418-87977D7450A4}.Release|ARM64.Build.0 = Release|ARM64
{E82B7A20-0557-4DC1-B418-87977D7450A4}.Release|x64.ActiveCfg = Release|x64
{E82B7A20-0557-4DC1-B418-87977D7450A4}.Release|x64.Build.0 = Release|x64
{05B51BB8-08CB-4907-884F-8E2AD6BF6052}.Debug|ARM64.ActiveCfg = Debug|ARM64
{05B51BB8-08CB-4907-884F-8E2AD6BF6052}.Debug|ARM64.Build.0 = Debug|ARM64
{05B51BB8-08CB-4907-884F-8E2AD6BF6052}.Debug|x64.ActiveCfg = Debug|x64
{05B51BB8-08CB-4907-884F-8E2AD6BF6052}.Debug|x64.Build.0 = Debug|x64
{05B51BB8-08CB-4907-884F-8E2AD6BF6052}.Release|ARM64.ActiveCfg = Release|ARM64
{05B51BB8-08CB-4907-884F-8E2AD6BF6052}.Release|ARM64.Build.0 = Release|ARM64
{05B51BB8-08CB-4907-884F-8E2AD6BF6052}.Release|x64.ActiveCfg = Release|x64
{05B51BB8-08CB-4907-884F-8E2AD6BF6052}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{9808D34F-5715-4D02-B216-4CB80F46BBC0} = {00AB63C3-0CD3-4944-B8E6-58C86138618D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0114F74A-3B0C-43A4-AA0E-AB36FD4935F8}
EndGlobalSection
EndGlobal

View file

@ -1,37 +0,0 @@
<Solution>
<Configurations>
<Platform Name="ARM64" />
<Platform Name="x64" />
</Configurations>
<Folder Name="/Solution Items/">
<File Path="Directory.Build.props" />
<File Path="src/BuildOptions.props" />
<File Path="src/Common.Post.props" />
<File Path="src/Common.Pre.props" />
<File Path="src/HybridCRT.props" />
<File Path="src/WinUI.targets" />
</Folder>
<Folder Name="/Solution Items/Natvis/">
<File Path="natvis/magpie.natvis" />
<File Path="natvis/phmap.natvis" />
<File Path="natvis/rapidjson.natvis" />
</Folder>
<Project Path="src/Effects/Effects.vcxproj" Id="62503530-b84b-4cc2-80b6-3f89618172b7" />
<Project Path="src/Magpie.Core/Magpie.Core.vcxproj" Id="0e5205ae-dfa9-4cb8-b662-e43cd6512e2a">
<BuildDependency Project="src/_ConanDeps/_ConanDeps.vcxproj" />
</Project>
<Project Path="src/Magpie/Magpie.vcxproj" Id="1239537c-e5b8-427a-9e7f-ea443d1f3529" DefaultStartup="true">
<BuildDependency Project="src/Effects/Effects.vcxproj" />
<BuildDependency Project="src/TouchHelper/TouchHelper.vcxproj" />
<BuildDependency Project="src/Updater/Updater.vcxproj" />
<BuildDependency Project="src/_ConanDeps/_ConanDeps.vcxproj" />
</Project>
<Project Path="src/Shared/Shared.vcxitems" Id="aabda3a3-7b23-4189-895b-f68a4c6b14c2" />
<Project Path="src/TouchHelper/TouchHelper.vcxproj" Id="05b51bb8-08cb-4907-884f-8e2ad6bf6052">
<BuildDependency Project="src/_ConanDeps/_ConanDeps.vcxproj" />
</Project>
<Project Path="src/Updater/Updater.vcxproj" Id="e82b7a20-0557-4dc1-b418-87977d7450a4">
<BuildDependency Project="src/_ConanDeps/_ConanDeps.vcxproj" />
</Project>
<Project Path="src/_ConanDeps/_ConanDeps.vcxproj" Id="456ccae4-2c51-4cf2-8d3a-1efce8c41a2d" />
</Solution>

View file

@ -9,13 +9,17 @@
[![License](https://img.shields.io/github/license/Blinue/Magpie)](./LICENSE)
[![build](https://github.com/Blinue/Magpie/actions/workflows/build.yml/badge.svg)](https://github.com/Blinue/Magpie/actions/workflows/build.yml)
[![All Contributors](https://img.shields.io/github/all-contributors/Blinue/Magpie)](#acknowledgement-)
[![Translation status](https://hosted.weblate.org/widget/magpie/svg-badge.svg)](https://hosted.weblate.org/engage/magpie)
[![GitHub all releases](https://img.shields.io/github/downloads/Blinue/Magpie/total)](https://github.com/Blinue/Magpie/releases)
</div>
🌍 **English** | [简体中文](./README_ZH.md)
Magpie is a lightweight window upscaling tool that comes equipped with a variety of efficient scaling algorithms and filters.
Magpie is a lightweight window scaling tool that comes equipped with various efficient scaling algorithms and filters. Its primary purpose is to enhance game graphics and enable non-fullscreen games to display in fullscreen mode.
We are using [Weblate](https://weblate.org/) for localization work and would appreciate your help in translating Magpie into more languages.
[![Translation status](https://hosted.weblate.org/widgets/magpie/-/287x66-white.png)](https://hosted.weblate.org/engage/magpie/)
👉 [Download](https://github.com/Blinue/Magpie/releases)
@ -25,19 +29,42 @@ Magpie is a lightweight window upscaling tool that comes equipped with a variety
👉 [Compilation guide](https://github.com/Blinue/Magpie/wiki/Compilation%20guide)
👉 [Contributing](./CONTRIBUTING.md)
## Features
* Supports both fullscreen and windowed scaling
* Includes a variety of built-in algorithms and filters, including [Anime4K](https://github.com/bloc97/Anime4K), [FSR](https://github.com/GPUOpen-Effects/FidelityFX-FSR), CRT shaders, and more
* Scale any window to fullscreen
* Numerous built-in algorithms, including Lanczos, [Anime4K](https://github.com/bloc97/Anime4K), [FSR](https://github.com/GPUOpen-Effects/FidelityFX-FSR), Adaptive Sharpen, various CRT shaders, and more
* WinUI-based user interface with support for light and dark themes
* Create configuration profiles for specific windows
* Multi-monitor support
## How to use
1. Configuring scaling modes
Magpie provides some simple scaling modes by default, but it is recommended to configure them according to your specific use case. Then, change the global scaling mode on the "Profiles"-"Defaults" page.
2. Scaling a window
To scale a window, bring the desired window to the foreground and press the shortcut key (default is Win+Shift+A) to display it in fullscreen mode. Note that the window to be scaled must be in windowed mode, not maximized or fullscreen mode. You can also use the "Scale after xs" button on the "Home" page, and Magpie will automatically scale the foreground window after a few seconds.
3. Creating profiles for windows
This allows you to save configurations specific to a particular window. Magpie also supports automatically activate scaling when that window is brought to the foreground.
4. Customizing effects
Magpie uses Direct3D compute shader to implement effects, but the syntax has been extended to define resources and organize multiple passes. For more information, please refer to [MagpieFX](https://github.com/Blinue/Magpie/wiki/MagpieFX%20(EN)). Those with experience in shader writing can easily create custom effects.
## Screenshots
<div style="display:flex; gap:10px;">
<img src="img/main-window.png" alt= "Main window" height="300">
<img src="img/screenshot.png" alt= "Main window" height="300">
</div>
<img src="img/Main window.png" alt= "Main window" height="300">
## System requirements
1. Windows 10 v1903+ or Windows 11
2. DirectX feature level 11
## Hints
@ -45,18 +72,7 @@ Magpie is a lightweight window upscaling tool that comes equipped with a variety
2. Some games support zooming the window, but with extremely naive algorithms. Please set the resolution to the built-in (best) option.
## System requirements
1. Windows 10 v1903+ or Windows 11
2. DirectX feature level 11
## Localization
Thanks to [Weblate](https://weblate.org) for hosting! Click the image below to visit the translation page.
[![Translation status](https://hosted.weblate.org/widget/magpie/multi-auto.svg)](https://hosted.weblate.org/engage/magpie)
## Acknowledgement
## Acknowledgement ✨
Thanks go to these wonderful people:
@ -66,7 +82,7 @@ Thanks go to these wonderful people:
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Blinue"><img src="https://avatars.githubusercontent.com/u/34770031?v=4?s=100" width="100px;" alt="Xu"/><br /><sub><b>Xu</b></sub></a><br /><a href="#maintenance-Blinue" title="Maintenance">🚧</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Blinue"><img src="https://avatars.githubusercontent.com/u/34770031?v=4?s=100" width="100px;" alt="Xu"/><br /><sub><b>Xu</b></sub></a><br /><a href="#maintenance-Blinue" title="Maintenance">🚧</a> <a href="https://github.com/Blinue/Magpie/commits?author=Blinue" title="Code">💻</a> <a href="https://github.com/Blinue/Magpie/pulls?q=is%3Apr+reviewed-by%3ABlinue" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/Blinue/Magpie/commits?author=Blinue" title="Documentation">📖</a> <a href="#question-Blinue" title="Answering Questions">💬</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hooke007"><img src="https://avatars.githubusercontent.com/u/41094733?v=4?s=100" width="100px;" alt="hooke007"/><br /><sub><b>hooke007</b></sub></a><br /><a href="https://github.com/Blinue/Magpie/commits?author=hooke007" title="Documentation">📖</a> <a href="#question-hooke007" title="Answering Questions">💬</a> <a href="#userTesting-hooke007" title="User Testing">📓</a> <a href="https://github.com/Blinue/Magpie/commits?author=hooke007" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://palxex.ys168.com"><img src="https://avatars.githubusercontent.com/u/58222?v=4?s=100" width="100px;" alt="Pal Lockheart"/><br /><sub><b>Pal Lockheart</b></sub></a><br /><a href="#userTesting-palxex" title="User Testing">📓</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.stevedonaghy.com/"><img src="https://avatars.githubusercontent.com/u/1029699?v=4?s=100" width="100px;" alt="Steve Donaghy"/><br /><sub><b>Steve Donaghy</b></sub></a><br /><a href="https://github.com/Blinue/Magpie/commits?author=neoKushan" title="Code">💻</a> <a href="#translation-neoKushan" title="Translation">🌍</a></td>
@ -106,13 +122,6 @@ Thanks go to these wonderful people:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/eriforce"><img src="https://avatars.githubusercontent.com/u/8393109?v=4?s=100" width="100px;" alt="Erich Yu"/><br /><sub><b>Erich Yu</b></sub></a><br /><a href="https://github.com/Blinue/Magpie/commits?author=eriforce" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TamilNeram"><img src="https://avatars.githubusercontent.com/u/67970539?v=4?s=100" width="100px;" alt="தமிழ் நேரம்"/><br /><sub><b>தமிழ் நேரம்</b></sub></a><br /><a href="#translation-TamilNeram" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mhtvsSFrpHdE"><img src="https://avatars.githubusercontent.com/u/10773245?v=4?s=100" width="100px;" alt="mhtvsSFrpHdE"/><br /><sub><b>mhtvsSFrpHdE</b></sub></a><br /><a href="https://github.com/Blinue/Magpie/commits?author=mhtvsSFrpHdE" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kangurek-kao"><img src="https://avatars.githubusercontent.com/u/116571935?v=4?s=100" width="100px;" alt="Krzysztof"/><br /><sub><b>Krzysztof</b></sub></a><br /><a href="#translation-kangurek-kao" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Howard20181"><img src="https://avatars.githubusercontent.com/u/40033067?v=4?s=100" width="100px;" alt="Howard Wu"/><br /><sub><b>Howard Wu</b></sub></a><br /><a href="https://github.com/Blinue/Magpie/commits?author=Howard20181" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/arifpedia"><img src="https://avatars.githubusercontent.com/u/4081293?v=4?s=100" width="100px;" alt="Arif Budiman"/><br /><sub><b>Arif Budiman</b></sub></a><br /><a href="#translation-arifpedia" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Androidlate"><img src="https://avatars.githubusercontent.com/u/194900061?v=4?s=100" width="100px;" alt="Raphael"/><br /><sub><b>Raphael</b></sub></a><br /><a href="#translation-Androidlate" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rezorrand"><img src="https://avatars.githubusercontent.com/u/7170353?v=4?s=100" width="100px;" alt="Pate L"/><br /><sub><b>Pate L</b></sub></a><br /><a href="#translation-rezorrand" title="Translation">🌍</a></td>
</tr>
</tbody>
</table>
@ -123,7 +132,3 @@ Thanks go to these wonderful people:
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://allcontributors.org/) specification. Contributions of any kind are welcome!
## License
This project is licensed under GPLv3.

View file

@ -9,13 +9,17 @@
[![许可协议](https://img.shields.io/github/license/Blinue/Magpie)](./LICENSE)
[![build](https://github.com/Blinue/Magpie/actions/workflows/build.yml/badge.svg)](https://github.com/Blinue/Magpie/actions/workflows/build.yml)
[![All Contributors](https://img.shields.io/github/all-contributors/Blinue/Magpie)](#%E8%B4%A1%E7%8C%AE%E8%80%85-)
[![翻译状态](https://hosted.weblate.org/widget/magpie/svg-badge.svg)](https://hosted.weblate.org/engage/magpie)
[![GitHub all releases](https://img.shields.io/github/downloads/Blinue/Magpie/total)](https://github.com/Blinue/Magpie/releases)
</div>
🌍 [English](./README.md) | **简体中文**
Magpie 是一个轻量级的窗口超分辨率工具,内置众多高效的算法和滤镜。
Magpie 是一个轻量级的窗口缩放工具,内置了多种高效的缩放算法和滤镜。它主要用于提升游戏画质和让不支持全屏化的游戏也能全屏显示等。
我们使用 [Weblate](https://weblate.org) 进行本地化工作,请帮助我们把 Magpie 翻译成更多语言。
[![翻译状态](https://hosted.weblate.org/widgets/magpie/-/287x66-white.png)](https://hosted.weblate.org/engage/magpie/)
👉 [下载](https://github.com/Blinue/Magpie/releases)
@ -25,39 +29,51 @@ Magpie 是一个轻量级的窗口超分辨率工具,内置众多高效的算
👉 [编译指南](https://github.com/Blinue/Magpie/wiki/编译指南)
👉 [贡献指南](./CONTRIBUTING_ZH.md)
## 功能
* 支持全屏和窗口模式缩放
* 众多内置算法和滤镜,如 [Anime4K](https://github.com/bloc97/Anime4K)、[FSR](https://github.com/GPUOpen-Effects/FidelityFX-FSR)、CRT 着色器等
* 将任何窗口放大至全屏
* 众多内置算法,包括 Lanczos、[Anime4K](https://github.com/bloc97/Anime4K)、[FSR](https://github.com/GPUOpen-Effects/FidelityFX-FSR)、Adaptive Sharpen、多种 CRT 着色器等
* 基于 WinUI 的用户界面,支持浅色和深色主题
* 支持多屏幕
* 为特定窗口创建配置文件
* 多屏幕支持
## 如何使用
1. 配置缩放模式
Magpie 预设了一些简单的缩放模式,但建议根据使用场景自行配置。然后在“配置文件”-“默认”页面更改全局缩放模式。
2. 缩放窗口
把要缩放的窗口置于前台,按下快捷键(默认为 Win+Shift+A即可全屏显示。请注意要缩放的窗口必须处于窗口化状态而不是最大化或全屏化。也可以使用“主页”上的“x 秒后缩放”按钮Magpie 将在数秒后自动缩放前台窗口。
3. 为窗口创建配置文件
这使你可以保存针对某个窗口的配置,也支持在该窗口位于前台时自动执行缩放。
4. 自定义效果
Magpie 使用 Direct3D 计算着色器实现效果,但扩展了语法来定义资源、组织多个通道等,详见 [MagpieFX](https://github.com/Blinue/Magpie/wiki/MagpieFX) 。有着色器编写经验者可以轻松创建自定义效果。
## 截图
<div style="display:flex; gap:10px;">
<img src="img/main-window-zh.png" alt= "Main window" height="300">
<img src="img/screenshot.png" alt= "Main window" height="300">
</div>
## 使用提示
1. 如果你设置了 DPI 缩放,而要放大的窗口没有高 DPI 支持(这在老游戏中很常见),推荐首先进入该程序的兼容性设置,将“高 DPI 缩放替代”设置为“应用程序”。
2. 一些游戏支持调整窗口的大小,但只使用简单的缩放算法,这时请先将其设为原始(最佳)分辨率。
<img src="img/主窗口.png" alt= "主窗口" height="300">
## 系统需求
1. Windows 10 v1903+ 或 Windows 11
2. DirectX 功能级别 11
## 本地化
## 使用提示
感谢 [Weblate](https://weblate.org) 提供托管服务!点击下面的图片可以进入翻译页面。
1. 如果你设置了 DPI 缩放,而要放大的窗口没有高 DPI 支持(这在老游戏中很常见),推荐首先进入该程序的兼容性设置,将“高 DPI 缩放替代”设置为“应用程序”。
2. 一些游戏支持调整窗口的大小,但只使用简单的缩放算法,这时请先将其设为原始(最佳)分辨率。
[![翻译状态](https://hosted.weblate.org/widget/magpie/multi-auto.svg)](https://hosted.weblate.org/engage/magpie)
## 贡献者 ✨
## 贡献者
衷心感谢所有为本项目做出贡献的人:
感谢每一位参与贡献的人:
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
@ -65,7 +81,7 @@ Magpie 是一个轻量级的窗口超分辨率工具,内置众多高效的算
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Blinue"><img src="https://avatars.githubusercontent.com/u/34770031?v=4?s=100" width="100px;" alt="Xu"/><br /><sub><b>Xu</b></sub></a><br /><a href="#maintenance-Blinue" title="Maintenance">🚧</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Blinue"><img src="https://avatars.githubusercontent.com/u/34770031?v=4?s=100" width="100px;" alt="Xu"/><br /><sub><b>Xu</b></sub></a><br /><a href="#maintenance-Blinue" title="Maintenance">🚧</a> <a href="https://github.com/Blinue/Magpie/commits?author=Blinue" title="Code">💻</a> <a href="https://github.com/Blinue/Magpie/pulls?q=is%3Apr+reviewed-by%3ABlinue" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/Blinue/Magpie/commits?author=Blinue" title="Documentation">📖</a> <a href="#question-Blinue" title="Answering Questions">💬</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hooke007"><img src="https://avatars.githubusercontent.com/u/41094733?v=4?s=100" width="100px;" alt="hooke007"/><br /><sub><b>hooke007</b></sub></a><br /><a href="https://github.com/Blinue/Magpie/commits?author=hooke007" title="Documentation">📖</a> <a href="#question-hooke007" title="Answering Questions">💬</a> <a href="#userTesting-hooke007" title="User Testing">📓</a> <a href="https://github.com/Blinue/Magpie/commits?author=hooke007" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://palxex.ys168.com"><img src="https://avatars.githubusercontent.com/u/58222?v=4?s=100" width="100px;" alt="Pal Lockheart"/><br /><sub><b>Pal Lockheart</b></sub></a><br /><a href="#userTesting-palxex" title="User Testing">📓</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.stevedonaghy.com/"><img src="https://avatars.githubusercontent.com/u/1029699?v=4?s=100" width="100px;" alt="Steve Donaghy"/><br /><sub><b>Steve Donaghy</b></sub></a><br /><a href="https://github.com/Blinue/Magpie/commits?author=neoKushan" title="Code">💻</a> <a href="#translation-neoKushan" title="Translation">🌍</a></td>
@ -105,13 +121,6 @@ Magpie 是一个轻量级的窗口超分辨率工具,内置众多高效的算
<td align="center" valign="top" width="14.28%"><a href="https://github.com/eriforce"><img src="https://avatars.githubusercontent.com/u/8393109?v=4?s=100" width="100px;" alt="Erich Yu"/><br /><sub><b>Erich Yu</b></sub></a><br /><a href="https://github.com/Blinue/Magpie/commits?author=eriforce" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TamilNeram"><img src="https://avatars.githubusercontent.com/u/67970539?v=4?s=100" width="100px;" alt="தமிழ் நேரம்"/><br /><sub><b>தமிழ் நேரம்</b></sub></a><br /><a href="#translation-TamilNeram" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mhtvsSFrpHdE"><img src="https://avatars.githubusercontent.com/u/10773245?v=4?s=100" width="100px;" alt="mhtvsSFrpHdE"/><br /><sub><b>mhtvsSFrpHdE</b></sub></a><br /><a href="https://github.com/Blinue/Magpie/commits?author=mhtvsSFrpHdE" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kangurek-kao"><img src="https://avatars.githubusercontent.com/u/116571935?v=4?s=100" width="100px;" alt="Krzysztof"/><br /><sub><b>Krzysztof</b></sub></a><br /><a href="#translation-kangurek-kao" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Howard20181"><img src="https://avatars.githubusercontent.com/u/40033067?v=4?s=100" width="100px;" alt="Howard Wu"/><br /><sub><b>Howard Wu</b></sub></a><br /><a href="https://github.com/Blinue/Magpie/commits?author=Howard20181" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/arifpedia"><img src="https://avatars.githubusercontent.com/u/4081293?v=4?s=100" width="100px;" alt="Arif Budiman"/><br /><sub><b>Arif Budiman</b></sub></a><br /><a href="#translation-arifpedia" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Androidlate"><img src="https://avatars.githubusercontent.com/u/194900061?v=4?s=100" width="100px;" alt="Raphael"/><br /><sub><b>Raphael</b></sub></a><br /><a href="#translation-Androidlate" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rezorrand"><img src="https://avatars.githubusercontent.com/u/7170353?v=4?s=100" width="100px;" alt="Pate L"/><br /><sub><b>Pate L</b></sub></a><br /><a href="#translation-rezorrand" title="Translation">🌍</a></td>
</tr>
</tbody>
</table>
@ -121,8 +130,4 @@ Magpie 是一个轻量级的窗口超分辨率工具,内置众多高效的算
<!-- ALL-CONTRIBUTORS-LIST:END -->
本项目遵循 [all-contributors](https://allcontributors.org/) 规范,欢迎任何形式的贡献!
## 许可协议
本项目采用 GPLv3 许可协议。
本项目遵循 [all-contributors](https://allcontributors.org/) 规范。欢迎任何形式的贡献!

View file

@ -126,9 +126,6 @@ Magpie ships with a handful of effects that can be used in combinations. Most of
* CuNNy familySuitable for visual novel-style images. The DS variants offer a subtle denoise effect. Provided by [CuNNy](https://github.com/funnyplanter/CuNNy)
* Output size: twice that of the input
* CuNNy2 family: An enhanced version of CuNNy
* Output size: twice that of the input
* Deband
* Output size: the same as the input
* Parameters
@ -196,9 +193,6 @@ Magpie ships with a handful of effects that can be used in combinations. Most of
* Sinc Param: The larger the value is the sharper the images become. Must be greater than 0. Default value: 0.825
* Anti-ringing Strength: The greater the value is the better the effect becomes, but the images will be more blurry.
* k7_modernAnime: anime-targeted super-resolution algorithm
* Output size: twice that of the input
* Lanczos: Scaling with the Lanczos algorithm.
* Output size: determined by scale configuration
* Parameters
@ -250,12 +244,6 @@ Magpie ships with a handful of effects that can be used in combinations. Most of
* SharpBilinear: Scale with the Sharp-Bilinear algorithm. Suitable for upscaling pixel arts.
* Output size: determined by scale configuration
* SGSR: Port of [Snapdragon Game Super Resolution v1](https://github.com/SnapdragonStudios/snapdragon-gsr/tree/main/sgsr/v1).
* Output size: determined by scale configuration
* Parameter
* Edge Sharpness: Edge sharpening intensity (The larger the value, the sharper the image.)
* Edge Threshold: Edge detection threshold
* SMAA_Low, SMAA_Medium, SMAA_High, and SMAA_Ultra: SMAA anti-aliasing. In increasing order of demand for computing power.
* Output size: the same as the input

View file

@ -6,7 +6,7 @@ Magpie provides several capture methods. They have their pros and cons in differ
| Supports recording/streaming | No under extreme conditions<sup>[1]</sup> | No | Yes | Yes |
| Support the source window to span multiple screens | No under extreme conditions<sup>[1]</sup> | No | Yes | Yes |
| Ignores DPI virtualization<sup>[2]</sup> | No | No | Yes| Yes |
| Notes | The most recommended capture method | Requires Win10 v2004 | | Not recommended due to unstable performance |
| Notes | The most recommended capture method | Requires Win10 v2004 | | Low VRAM usage |
[1]: (1) The source window does not support regular window capture. (2) The operating system is Windows 11.

View file

@ -2,7 +2,7 @@
In order to compile Magpie, you need to first install:
1. The latest version of Visual Studio 2022 or 2026. You need to install both "Desktop development with C++" and "Universal Windows Platform development" workloads and Windows SDK build 26100 or newer.
1. The latest version of Visual Studio 2022. You need to install both "Desktop development with C++" and "Universal Windows Platform development" workloads and Windows SDK build 26100 or newer.
2. [CMake](https://cmake.org/)
You can also use the built-in CMake of Visual Studio, which is located at `%ProgramFiles%\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin`.
@ -28,7 +28,7 @@ In order to compile Magpie, you need to first install:
git clone https://github.com/Blinue/Magpie
```
2. Open the Magpie.slnx in the root directory and build the solution.
2. Open the Magpie.sln in the root directory and build the solution.
### Enabling Touch Support
@ -39,7 +39,7 @@ To enable touch input support, TouchHelper.exe needs to be signed. While signing
3. Run the following command in the root directory of the repository:
```bash
python scripts/publish.py --pfx-path=<pfx path> --pfx-password=<pfx password>
python publish.py x64 unpackaged <pfx path> <pfx password>
```
This will compile Magpie and sign TouchHelper.exe. The compiled files will be located in `publish\x64`.

View file

@ -2,11 +2,15 @@
When displaying performance monitor like RTSS (Rivatuner Statistics Server), there might be 2 OSD layers displayed with Magpie scaling. This is caused by d3d, the screen capture method since v0.7.0. It will be captured by RTSS as well. You can fix this issue by adding it to the blacklist.
## The hot keys don't work, but "Scale after `x`s" works.
## The hot keys don't work, but "Scale after x s" works.
1. Try changing the hot keys.
2. Try running Magpie as Administrator.
## Does Magpie support multiple monitors?
Supported from v0.8.
## Lagging/latency
Please check the [Performance optimization](https://github.com/Blinue/Magpie/wiki/Performance%20optimization) page.

View file

@ -2,11 +2,15 @@
使用性能计数器屏显时,例如 RTSS (Rivatuner Statistics Server),你可能会在缩放时看到两个叠加层。这是由于 Magpie 使用 Direct3D 呈现画面,它也会被 RTSS 捕捉。如果你不关心 Magpie 的性能,请将 Magpie 添加到黑名单。
### 快捷键不起作用,但可以使用 "`x` 秒后缩放"
### 快捷键不起作用,但可以使用 "x秒后缩放"
1. 尝试更换快捷键
2. 尝试以管理员身份运行 Magpie
### 是否支持多屏?
从 v0.8 开始支持。
### 卡顿/延迟
请查看[性能优化建议](https://github.com/Blinue/Magpie/wiki/性能优化建议)。

View file

@ -126,9 +126,6 @@ Magpie 内置了大量效果供组合使用,大部分提供了参数选项以
* CuNNy 族:适合视觉小说风格图像的缩放,由 [CuNNy](https://github.com/funnyplanter/CuNNy) 提供。DS 变体有轻微降噪效果
* 输出尺寸:输入的两倍
* CuNNy2 族CuNNy 的改进版
* 输出尺寸:输入的两倍
* Deband去除色带
* 输出尺寸:和输入相同
* 参数
@ -196,9 +193,6 @@ Magpie 内置了大量效果供组合使用,大部分提供了参数选项以
* Sinc Param值越大图像越锐利
* Anti-ringing Strength抗振铃强度
* k7_modernAnime适合动漫类风格的超分算法
* 输出尺寸:输入的两倍
* Lanczos使用 Lanczos 算法缩放输入。
* 输出尺寸:取决于缩放选项
* 参数
@ -247,12 +241,6 @@ Magpie 内置了大量效果供组合使用,大部分提供了参数选项以
* 输出尺寸:取决于缩放选项
* 备注:只支持放大
* SGSR[Snapdragon Game Super Resolution v1](https://github.com/SnapdragonStudios/snapdragon-gsr/tree/main/sgsr/v1) 的移植
* 输出尺寸:取决于缩放选项
* 参数
* Edge Sharpness边缘锐化强度值越大图像越锐利
* Edge Threshold边缘检测阈值
* SharpBilinear使用 Sharp-Bilinear 算法缩放输入。适合放大像素画
* 输出尺寸:取决于缩放选项

View file

@ -6,7 +6,8 @@ Magpie 提供数种捕获方式,根据使用场景,它们各有优劣。无
| 支持录制/串流 | 特殊情况下不支持<sup>[1]</sup> | 否 | 是 | 是 |
| 支持源窗口跨越多个屏幕 | 特殊情况下不支持<sup>[1]</sup> | 否 | 是 | 是 |
| 无视 DPI 虚拟化<sup>[2]</sup> | 否 | 否 | 是| 是 |
| 备注 | 首选捕获方式 | 要求 Win10 v2004 | | 性能不稳定,不建议使用 |
| 备注 | 首选捕获方式 | 要求 Win10 v2004 | | 占用的显存较少 |
[1]: (1) 源窗口不支持常规的窗口捕获 (2) 操作系统为 Windows 11

View file

@ -2,7 +2,7 @@
为了编译 Magpie你首先需要安装
1. Visual Studio 2022 或 2026 的最新版本,需要安装“使用 C++ 的桌面开发”和“通用 Windows 平台开发”两个工作负荷以及 Windows SDK build 26100 或更高版本。
1. Visual Studio 2022 的最新版本,需要安装“使用 C++ 的桌面开发”和“通用 Windows 平台开发”两个工作负荷以及 Windows SDK build 26100 或更高版本。
2. [CMake](https://cmake.org/)
你也可以使用 Visual Studio 内置的 CMake它位于 `%ProgramFiles%\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin`
@ -28,7 +28,7 @@
git clone https://github.com/Blinue/Magpie
```
2. 打开根目录的 Magpie.slnx 然后生成解决方案。
2. 打开根目录的 Magpie.sln 然后生成解决方案。
## 启用触控支持
@ -39,7 +39,7 @@
3. 在存储库根目录下执行以下命令:
```bash
python scripts/publish.py --pfx-path=<pfx 路径> --pfx-password=<pfx 密码>
python publish.py x64 unpackaged <pfx 路径> <pfx 密码>
```
这将编译 Magpie 并为 TouchHelper.exe 签名。编译出的程序位于 `publish\x64`

BIN
img/Main window.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

BIN
img/Repo card.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

BIN
img/主窗口.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

View file

@ -2,6 +2,7 @@ import sys
import os
import subprocess
import glob
import re
import argparse
try:
@ -20,7 +21,7 @@ argParser.add_argument("--use-native-march", action="store_true")
argParser.add_argument("--version-major", type=int, default=0)
argParser.add_argument("--version-minor", type=int, default=0)
argParser.add_argument("--version-patch", type=int, default=0)
argParser.add_argument("--version-string", default="")
argParser.add_argument("--version-tag", default="")
argParser.add_argument("--pfx-path", default="")
argParser.add_argument("--pfx-password", default="")
args = argParser.parse_args()
@ -56,11 +57,44 @@ os.chdir(os.path.dirname(__file__) + "\\..")
p = subprocess.run("git rev-parse --short HEAD", capture_output=True)
commitId = str(p.stdout, encoding="utf-8")[0:-1]
versionNumProps = f";MajorVersion={args.version_major};MinorVersion={args.version_minor};PatchVersion={args.version_patch}"
versionStrProp = "" if args.version_string == "" else f";VersionString={args.version_string}"
versionNumProps = ""
if args.version_major != 0 or args.version_minor != 0 or args.version_patch != 0:
versionNumProps = f";MajorVersion={args.version_major};MinorVersion={args.version_minor};PatchVersion={args.version_patch}"
# 更新 RC 文件中的版本号
version = f"{args.version_major}.{args.version_minor}.{args.version_patch}.0"
version_comma = version.replace(".", ",")
for project in os.listdir("src"):
rcPath = f"src\\{project}\\{project}.rc"
if not os.access(rcPath, os.R_OK | os.W_OK):
continue
with open(rcPath, mode="r+", encoding="utf-8") as f:
src = f.read()
src = re.sub(
r"FILEVERSION .*?\n", "FILEVERSION " + version_comma + "\n", src
)
src = re.sub(
r"PRODUCTVERSION .*?\n", "PRODUCTVERSION " + version_comma + "\n", src
)
src = re.sub(
r'"FileVersion", *?".*?"\n', '"FileVersion", "' + version + '"\n', src
)
src = re.sub(
r'"ProductVersion", *?".*?"\n',
'"ProductVersion", "' + version + '"\n',
src,
)
f.seek(0)
f.truncate()
f.write(src)
versionTagProp = "" if args.version_tag == "" else f";VersionTag={args.version_tag}"
p = subprocess.run(
f'"{msbuildPath}" Magpie.slnx -m -t:Rebuild -restore -p:RestorePackagesConfig=true;Configuration=Release;Platform={args.platform};DisablePDB=true;UseClangCL={args.compiler == "ClangCL"};UseNativeMicroArch={args.use_native_march};OutDir={os.getcwd()}\\publish\\{args.platform}\\;CommitId={commitId}{versionNumProps}{versionStrProp}'
f'"{msbuildPath}" Magpie.sln -m -t:Rebuild -restore -p:RestorePackagesConfig=true;Configuration=Release;Platform={args.platform};UseClangCL={args.compiler == "ClangCL"};UseNativeMicroArch={args.use_native_march};OutDir={os.getcwd()}\\publish\\{args.platform}\\;CommitId={commitId}{versionNumProps}{versionTagProp}'
)
if p.returncode != 0:
raise Exception("编译失败")
@ -82,8 +116,9 @@ def remove_file(file):
pass
for file in glob.glob("*.lib"):
remove_file(file)
for pattern in ["*.pdb", "*.lib", "*.exp"]:
for file in glob.glob(pattern):
remove_file(file)
print("清理完毕", flush=True)

View file

@ -59,8 +59,8 @@ try:
# 发布预发行版与最新的版本(无论是正式版还是预发行版)对比
response = requests.get(
f"https://api.github.com/repos/{repo}/releases",
json={"per_page": 1},
headers=headers,
params={"per_page": 1}
)
if response.ok:
prevReleaseTag = response.json()[0]["tag_name"]

View file

@ -19,7 +19,7 @@ argParser = argparse.ArgumentParser()
argParser.add_argument("access_token")
args = argParser.parse_args()
wikiRepoUrl = f'https://{args.access_token}@github.com/{os.environ["GITHUB_REPOSITORY"]}.wiki.git'
wikiRepoUrl = f"https://{args.access_token}@github.com/{os.environ["GITHUB_REPOSITORY"]}.wiki.git"
# 创建临时目录
wikiRepoDir = tempfile.mkdtemp()

View file

@ -14,12 +14,12 @@
<DebugInfoOnOverlay>false</DebugInfoOnOverlay>
<!-- 使用 composition swapchain 呈现 -->
<UseCompSwapchain>false</UseCompSwapchain>
<CommitId></CommitId>
<MajorVersion></MajorVersion>
<MinorVersion></MinorVersion>
<PatchVersion></PatchVersion>
<VersionString></VersionString>
<CommitId></CommitId>
<VersionTag></VersionTag>
</PropertyGroup>
<!-- 用户自定义编译选项 -->

View file

@ -1,19 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Microsoft.Cpp.props 之后导入 -->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(Configuration)' == 'Debug'" Label="Configuration">
<UseDebugLibraries>true</UseDebugLibraries>
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'" Label="Configuration">
<UseDebugLibraries>false</UseDebugLibraries>
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<PrecompiledHeaderOutputFile>$(IntDir)pch.pch</PrecompiledHeaderOutputFile>
<PreprocessorDefinitions>_WINDOWS;WIN32_LEAN_AND_MEAN;WINRT_LEAN_AND_MEAN;WINRT_NO_MODULE_LOCK;WIL_SUPPRESS_EXCEPTIONS;WIL_USE_STL=1;NOGDICAPMASKS;NOICONS;NOATOM;NOCLIPBOARD;NODRAWTEXT;NOMEMMGR;NOMETAFILE;NOMINMAX;NOOPENFILE;NOSCROLL;NOSERVICE;NOSOUND;NOTEXTMETRIC;NOCOMM;NOKANJI;NOHELP;NOPROFILER;NOMCX;NO_SHLWAPI_PATH;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>MP_MAJOR_VERSION=$(MajorVersion);MP_MINOR_VERSION=$(MinorVersion);MP_PATCH_VERSION=$(PatchVersion);%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="'$(VersionString)' != ''">MP_VERSION_STRING=$(VersionString);%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>_WINDOWS;WIN32_LEAN_AND_MEAN;WINRT_LEAN_AND_MEAN;WINRT_NO_MODULE_LOCK;WIL_SUPPRESS_EXCEPTIONS;WIL_USE_STL=1;NOGDICAPMASKS;NOICONS;NOATOM;NOCLIPBOARD;NODRAWTEXT;NOMEMMGR;NOMETAFILE;NOMINMAX;NOOPENFILE;NOSCROLL;NOSERVICE;NOSOUND;NOTEXTMETRIC;NOCOMM;NOKANJI;NOHELP;NOPROFILER;NODEFERWINDOWPOS;NOMCX;NO_SHLWAPI_PATH;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="'$(CommitId)' != ''">MP_COMMIT_ID=$(CommitId);%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="'$(MajorVersion)' != '' And '$(MinorVersion)' != '' And '$(PatchVersion)' != ''">MP_MAJOR_VERSION=$(MajorVersion);MP_MINOR_VERSION=$(MinorVersion);MP_PATCH_VERSION=$(PatchVersion);%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="'$(VersionTag)' != ''">MP_VERSION_TAG=$(VersionTag);%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="$(DebugBorder)">MP_DEBUG_BORDER;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="$(DebugInfoOnOverlay)">MP_DEBUG_INFO_ON_OVERLAY;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="$(UseCompSwapchain)">MP_USE_COMPSWAPCHAIN;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<DebugInformationFormat Condition="'$(DisablePDB)' == 'true'">None</DebugInformationFormat>
<AdditionalOptions>/bigobj %(AdditionalOptions)</AdditionalOptions>
<!-- /await:strict: 禁用协程的非标准语言扩展 -->
<AdditionalOptions Condition="!$(UseClangCL)">/await:strict %(AdditionalOptions)</AdditionalOptions>
@ -25,41 +32,20 @@
<AdditionalOptions Condition="$(UseClangCL) And '$(Platform)' == 'x64'">/clang:-mcx16 %(AdditionalOptions)</AdditionalOptions>
<!-- -march=native: 针对当前硬件生成优化代码 -->
<AdditionalOptions Condition="$(UseClangCL) And $(UseNativeMicroArch)">/clang:-march=native %(AdditionalOptions)</AdditionalOptions>
<!-- 使用 clang-cl 编译时禁用 cppwinrt 头文件中的编译警告 -->
<AdditionalOptions Condition="$(UseClangCL) And '$(GeneratedFilesDir)' != ''">/clang:-isystem /clang:"$(GeneratedFilesDir)\" %(AdditionalOptions)</AdditionalOptions>
<!-- 修复编译 Shared 中源文件找不到 pch.h 的问题 -->
<AdditionalIncludeDirectories Condition="$(UseClangCL)">.;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<ResourceCompile>
<PreprocessorDefinitions>MP_MAJOR_VERSION=$(MajorVersion);MP_MINOR_VERSION=$(MinorVersion);MP_PATCH_VERSION=$(PatchVersion);%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="'$(VersionString)' != ''">MP_VERSION_STRING=$(VersionString);%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="'$(CommitId)' != ''">MP_COMMIT_ID=$(CommitId);%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)\Shared;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ResourceCompile>
<Link>
<GenerateDebugInformation Condition="'$(DisablePDB)' == 'true'">false</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)' == 'Debug'">
<ClCompile>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<ResourceCompile>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ResourceCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)' == 'Release'">
<ClCompile>
<!-- Release 下不允许编译警告 -->
<TreatWarningAsError>true</TreatWarningAsError>
<!-- 存在同名全局属性,但表现很奇怪。首先,在导入 Microsoft.Cpp.Default.props 之前和之后定义 -->
<!-- 效果不同,似乎在导入前定义才能起作用;其次,全局 WholeProgramOptimization 属性起作用时由 -->
<!-- Microsoft.Cpp.targets 而不是 Microsoft.Cpp.props 更改编译选项,将用户设置覆盖;最后,该 -->
<!-- 属性起作用时会将 LinkTimeCodeGeneration 改为 UseFastLinkTimeCodeGeneration而我们想要 -->
<!-- UseLinkTimeCodeGeneration。为了精确控制编译参数我们不使用全局 -->
<!-- WholeProgramOptimization而是自己定义编译选项。全局 WholeProgramOptimization 影响的属 -->
<!-- 性见 $(VCTargetsPath)\Microsoft.Cpp.WholeProgramOptimization.props。 -->
<WholeProgramOptimization>true</WholeProgramOptimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
@ -76,9 +62,6 @@
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
<Lib>
<LinkTimeCodeGeneration>true</LinkTimeCodeGeneration>
</Lib>
</ItemDefinitionGroup>
<!-- 所有项目共享的头文件 -->

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Microsoft.Cpp.Default.props 之后导入 -->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
@ -23,23 +22,12 @@
<!-- 编译选项 -->
<Import Project="$(MSBuildThisFileDirectory)\BuildOptions.props" />
<PropertyGroup Label="Globals">
<VS17 Condition="$([System.String]::new('$(MSBuildVersion)').StartsWith('17'))">true</VS17>
<VS17 Condition="'$(VS17)' != 'true'">false</VS17>
<VCProjectVersion Condition="$(VS17)">17.0</VCProjectVersion>
<VCProjectVersion Condition="!$(VS17)">18.0</VCProjectVersion>
<PropertyGroup>
<DefaultLanguage>en-US</DefaultLanguage>
<MajorVersion Condition="'$(MajorVersion)' == ''">0</MajorVersion>
<MinorVersion Condition="'$(MinorVersion)' == ''">0</MinorVersion>
<PatchVersion Condition="'$(PatchVersion)' == ''">0</PatchVersion>
<!-- 可通过 VersionString 区分开发版本和发布版本 -->
<VersionString Condition="'$(VersionString)' == '' And ('$(MajorVersion)' != '0' Or '$(MinorVersion)' != '0' Or '$(PatchVersion)' != '0')">$(MajorVersion).$(MinorVersion).$(PatchVersion)</VersionString>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<PlatformToolset Condition="$(UseClangCL)">ClangCL</PlatformToolset>
<PlatformToolset Condition="!$(UseClangCL) And $(VS17)">v143</PlatformToolset>
<PlatformToolset Condition="!$(UseClangCL) And !$(VS17)">v145</PlatformToolset>
<UseDebugLibraries Condition="'$(Configuration)' == 'Debug'">true</UseDebugLibraries>
<PlatformToolset Condition="!$(UseClangCL)">v143</PlatformToolset>
</PropertyGroup>
</Project>

View file

@ -106,9 +106,6 @@ float4 Pass1(float2 pos)
// Sample the source pixel
float3 col = INPUT.SampleLevel(sam1, pos, 0).rgb;
#ifdef MP_INLINE_PARAMS
[unroll]
#endif
for (int i = 1; i <= iterations; i++) {
// Use the average instead if the difference is below the threshold
float3 avg = average(pos, i*range, h);

View file

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\Common.Pre.props" />
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{62503530-b84b-4cc2-80b6-3f89618172b7}</ProjectGuid>
<WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
@ -8,7 +10,6 @@
<OutDir>$(SolutionDir)\bin\$(Platform)\$(Configuration)\</OutDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<Import Project="..\Common.Pre.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>Utility</ConfigurationType>
</PropertyGroup>
@ -427,10 +428,14 @@
<CopyFileToFolders Include="CuNNy\CuNNy-16x16C-NVL-DN.hlsl">
<FileType>Document</FileType>
</CopyFileToFolders>
</ItemGroup>
<ItemGroup>
<CopyFileToFolders Include="NIS\NIS_Scaler.hlsli">
<FileType>Document</FileType>
</CopyFileToFolders>
<None Include="StubDefs.hlsli" />
</ItemGroup>
<ItemGroup>
<CopyFileToFolders Include="CuNNy2\CuNNy-3x12-NVL.hlsl">
<FileType>Document</FileType>
</CopyFileToFolders>
@ -458,12 +463,6 @@
<CopyFileToFolders Include="CuNNy2\CuNNy-veryfast-NVL.hlsl">
<FileType>Document</FileType>
</CopyFileToFolders>
<CopyFileToFolders Include="SGSR.hlsl">
<FileType>Document</FileType>
</CopyFileToFolders>
<CopyFileToFolders Include="k7_modernAnime_FHD_x2.hlsl">
<FileType>Document</FileType>
</CopyFileToFolders>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>

View file

@ -451,8 +451,6 @@
<CopyFileToFolders Include="CuNNy2\CuNNy-veryfast-NVL.hlsl">
<Filter>CuNNy2</Filter>
</CopyFileToFolders>
<CopyFileToFolders Include="SGSR.hlsl" />
<CopyFileToFolders Include="k7_modernAnime_FHD_x2.hlsl" />
</ItemGroup>
<ItemGroup>
<Filter Include="Anime4K">

View file

@ -1,170 +0,0 @@
// Snapdragon™ Game Super Resolution
// 移植自 https://github.com/SnapdragonStudios/snapdragon-gsr/blob/main/sgsr/v1/include/hlsl/sgsr1_shader_mobile.hlsl
//!MAGPIE EFFECT
//!VERSION 4
//!PARAMETER
//!LABEL Edge Sharpness
//!DEFAULT 2.0
//!MIN 0.0
//!MAX 10.0
//!STEP 0.1
float EdgeSharpness;
//!PARAMETER
//!LABEL Edge Threshold
//!DEFAULT 8.0
//!MIN 0.0
//!MAX 16.0
//!STEP 0.1
float EdgeThreshold;
//!TEXTURE
Texture2D INPUT;
//!TEXTURE
Texture2D OUTPUT;
//!SAMPLER
//!FILTER POINT
SamplerState sam;
//!PASS 1
//!STYLE PS
//!IN INPUT
//!OUT OUTPUT
#define UseEdgeDirection
float fastLanczos2(float x)
{
float wA = x- float(4.0);
float wB = x*wA-wA;
wA *= wA;
return wB*wA;
}
#if defined(UseEdgeDirection)
float2 weightY(float dx, float dy, float c, float3 data)
#else
float2 weightY(float dx, float dy, float c, float data)
#endif
{
#if defined(UseEdgeDirection)
float std = data.x;
float2 dir = data.yz;
float edgeDis = ((dx*dir.y)+(dy*dir.x));
float x = (((dx*dx)+(dy*dy))+((edgeDis*edgeDis)*((clamp(((c*c)*std),0.0,1.0)*0.7)+-1.0)));
#else
float std = data;
float x = ((dx*dx)+(dy* dy))* float(0.5) + clamp(abs(c)*std, 0.0, 1.0);
#endif
float w = fastLanczos2(x);
return float2(w, w * c);
}
float2 edgeDirection(float4 left, float4 right)
{
float2 dir;
float RxLz = (right.x + (-left.z));
float RwLy = (right.w + (-left.y));
float2 delta;
delta.x = (RxLz + RwLy);
delta.y = (RxLz + (-RwLy));
float lengthInv = rsqrt((delta.x * delta.x+ 3.075740e-05) + (delta.y * delta.y));
dir.x = (delta.x * lengthInv);
dir.y = (delta.y * lengthInv);
return dir;
}
float4 SGSRH(float2 p)
{
return INPUT.GatherGreen(sam, p);
}
float4 SGSRRGBH(float2 p)
{
return INPUT.SampleLevel(sam, p, 0);
}
float3 SgsrYuvH(float2 uv, float4 con1)
{
float3 pix;
float edgeThreshold = EdgeThreshold / 255.0;
float edgeSharpness = EdgeSharpness;
pix = SGSRRGBH(uv).xyz;
float xCenter;
xCenter = abs(uv.x+-0.5);
float yCenter;
yCenter = abs(uv.y+-0.5);
float2 imgCoord = ((uv.xy*con1.zw)+ float2(-0.5,0.5));
float2 imgCoordPixel = floor(imgCoord);
float2 coord = (imgCoordPixel*con1.xy);
float2 pl = (imgCoord+(-imgCoordPixel));
float4 left = SGSRH(coord);
float edgeVote = abs(left.z - left.y) + abs(pix[1] - left.y) + abs(pix[1] - left.z) ;
if (edgeVote > edgeThreshold)
{
coord.x += con1.x;
float4 right = SGSRH(coord + float2(con1.x, 0.0));
float4 upDown;
upDown.xy = SGSRH(coord + float2(0.0, -con1.y)).wz;
upDown.zw = SGSRH(coord + float2(0.0, con1.y)).yx;
float mean = (left.y+left.z+right.x+right.w)* float(0.25);
left = left - float4(mean,mean,mean,mean);
right = right - float4(mean, mean, mean, mean);
upDown = upDown - float4(mean, mean, mean, mean);
float pix_G = pix[1] - mean;
float sum = (((((abs(left.x)+abs(left.y))+abs(left.z))+abs(left.w))+(((abs(right.x)+abs(right.y))+abs(right.z))+abs(right.w)))+(((abs(upDown.x)+abs(upDown.y))+abs(upDown.z))+abs(upDown.w)));
float sumMean = 1.014185e+01/sum;
float std = (sumMean*sumMean);
#if defined(UseEdgeDirection)
float3 data = float3(std, edgeDirection(left, right));
#else
float data = std;
#endif
float2 aWY = weightY(pl.x, pl.y+1.0, upDown.x,data);
aWY += weightY(pl.x-1.0, pl.y+1.0, upDown.y,data);
aWY += weightY(pl.x-1.0, pl.y-2.0, upDown.z,data);
aWY += weightY(pl.x, pl.y-2.0, upDown.w,data);
aWY += weightY(pl.x+1.0, pl.y-1.0, left.x,data);
aWY += weightY(pl.x, pl.y-1.0, left.y,data);
aWY += weightY(pl.x, pl.y, left.z,data);
aWY += weightY(pl.x+1.0, pl.y, left.w,data);
aWY += weightY(pl.x-1.0, pl.y-1.0, right.x,data);
aWY += weightY(pl.x-2.0, pl.y-1.0, right.y,data);
aWY += weightY(pl.x-2.0, pl.y, right.z,data);
aWY += weightY(pl.x-1.0, pl.y, right.w,data);
float finalY = aWY.y/aWY.x;
float max4 = max(max(left.y,left.z),max(right.x,right.w));
float min4 = min(min(left.y,left.z),min(right.x,right.w));
finalY = clamp(edgeSharpness*finalY, min4, max4);
float deltaY = finalY - pix_G;
pix = saturate(pix+deltaY);
}
return pix;
}
MF4 Pass1(float2 texCoord)
{
float2 inputSize = GetInputSize();
float2 inputPt = GetInputPt();
float4 viewportInfo = float4(inputPt.x, inputPt.y, inputSize.x, inputSize.y);
return float4(SgsrYuvH(texCoord, viewportInfo), 1);
}

File diff suppressed because it is too large Load diff

View file

@ -47,7 +47,7 @@ bool AdaptivePresenter::_Initialize(HWND hwndAttach) noexcept {
};
ID3D11Device5* d3dDevice = _deviceResources->GetD3DDevice();
winrt::com_ptr<IDXGISwapChain1> dxgiSwapChain;
winrt::com_ptr<IDXGISwapChain1> dxgiSwapChain = nullptr;
HRESULT hr = _deviceResources->GetDXGIFactory()->CreateSwapChainForHwnd(
d3dDevice,
hwndAttach,
@ -130,14 +130,12 @@ bool AdaptivePresenter::BeginFrame(
return true;
}
void AdaptivePresenter::EndFrame(bool waitForGpu) noexcept {
void AdaptivePresenter::EndFrame(bool waitForRenderComplete) noexcept {
if (_isDCompPresenting) {
_dcompSurface->EndDraw();
}
if (waitForGpu || _isResized) {
_isResized = false;
if (waitForRenderComplete || _isResized) {
// 下面两个调用用于减少调整窗口尺寸时的边缘闪烁。
//
// 我们希望 DWM 绘制新的窗口框架时刚好合成新帧,但这不是我们能控制的,尤其是混合架构
@ -154,10 +152,10 @@ void AdaptivePresenter::EndFrame(bool waitForGpu) noexcept {
// 实用价值。
// 等待渲染完成
_WaitForGpu();
_WaitForRenderComplete();
// 等待 DWM 开始合成新一帧
Win32Helper::WaitForDwmComposition();
_WaitForDwmComposition();
}
if (_isDCompPresenting) {
@ -174,14 +172,21 @@ void AdaptivePresenter::EndFrame(bool waitForGpu) noexcept {
_isSwitchingToSwapChain = false;
// 等待交换链呈现新帧
_WaitForGpu();
Win32Helper::WaitForDwmComposition();
_WaitForRenderComplete();
_WaitForDwmComposition();
// 清除 DirectCompostion 内容
_dcompVisual->SetContent(nullptr);
_dcompDevice->Commit();
}
}
if (_isResized) {
_isResized = false;
} else {
// 确保前一帧渲染完成再渲染下一帧,既降低了 GPU 负载,也能降低延迟
_WaitForRenderComplete();
}
}
bool AdaptivePresenter::OnResize() noexcept {

View file

@ -18,7 +18,7 @@ public:
POINT& drawOffset
) noexcept override;
void EndFrame(bool waitForGpu = false) noexcept override;
void EndFrame(bool waitForRenderComplete = false) noexcept override;
bool OnResize() noexcept override;

View file

@ -31,7 +31,9 @@ bool CompSwapchainPresenter::_Initialize(HWND hwndAttach) noexcept {
return false;
}
HRESULT hr = DCompositionCreateDevice3(nullptr, IID_PPV_ARGS(&_dcompDevice));
ID3D11Device5* d3dDevice = _deviceResources->GetD3DDevice();
HRESULT hr = DCompositionCreateDevice3(d3dDevice, IID_PPV_ARGS(&_dcompDevice));
if (FAILED(hr)) {
Logger::Get().ComError("DCompositionCreateDevice3 失败", hr);
return false;
@ -56,7 +58,7 @@ bool CompSwapchainPresenter::_Initialize(HWND hwndAttach) noexcept {
}
winrt::com_ptr<IPresentationFactory> presentationFactory =
CreatePresentationFactory(_deviceResources->GetD3DDevice());
CreatePresentationFactory(d3dDevice);
if (!presentationFactory) {
Logger::Get().Error("CreatePresentationFactory 失败");
return false;
@ -226,15 +228,15 @@ bool CompSwapchainPresenter::BeginFrame(
return true;
}
void CompSwapchainPresenter::EndFrame(bool waitForGpu) noexcept {
if (waitForGpu || _isResized) {
void CompSwapchainPresenter::EndFrame(bool waitForRenderComplete) noexcept {
if (waitForRenderComplete || _isResized) {
// 下面两个调用用于减少调整窗口尺寸时的边缘闪烁,参见 AdaptivePresenter::EndFrame
// 等待渲染完成
_WaitForGpu();
_WaitForRenderComplete();
// 等待 DWM 开始合成新一帧
Win32Helper::WaitForDwmComposition();
_WaitForDwmComposition();
}
_presentationManager->Present();
@ -243,7 +245,7 @@ void CompSwapchainPresenter::EndFrame(bool waitForGpu) noexcept {
_isResized = false;
} else {
// 确保前一帧渲染完成再渲染下一帧,既降低了 GPU 负载,也能降低延迟
_WaitForGpu();
_WaitForRenderComplete();
}
}

View file

@ -16,7 +16,7 @@ public:
POINT& drawOffset
) noexcept override;
void EndFrame(bool waitForGpu = false) noexcept override;
void EndFrame(bool waitForRenderComplete = false) noexcept override;
bool OnResize() noexcept override;

View file

@ -39,8 +39,9 @@ struct VertexPositionTexture {
XMFLOAT2 position;
XMFLOAT2 textureCoordinate;
static constexpr D3D11_INPUT_ELEMENT_DESC InputElements[] = {
{ "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
static constexpr D3D11_INPUT_ELEMENT_DESC InputElements[] =
{
{ "SV_POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
};
@ -85,64 +86,63 @@ bool CursorDrawer::Initialize(DeviceResources& deviceResources) noexcept {
}
void CursorDrawer::Draw(ID3D11Texture2D* backBuffer, POINT drawOffset) noexcept {
const ScalingWindow& scalingWindow = ScalingWindow::Get();
bool isCursorActive = false;
const auto [cursorHandle, cursorPos] = _GetCursorState(isCursorActive);
if (isCursorActive) {
// 启用自动隐藏时光标形状或位置变化后应记录新的形状、位置和变化时间。位置由
// _lastCursorPos 记录。
_lastRawCursorHandle = scalingWindow.CursorManager().CursorHandle();
_lastCursorActiveTime = std::chrono::steady_clock::now();
if (!_isCursorVisible) {
// 截屏时暂时不渲染光标
return;
}
_lastCursorHandle = cursorHandle;
const CursorManager& cursorManager = ScalingWindow::Get().CursorManager();
const HCURSOR hCursor = cursorManager.CursorHandle();
POINT cursorPos = cursorManager.CursorPos();
_lastCursorHandle = NULL;
if (!hCursor) {
return;
}
const _CursorInfo* ci = _ResolveCursor(hCursor);
if (!ci) {
return;
}
// 转换为渲染矩形局部坐标
const RECT& rendererRect = ScalingWindow::Get().RendererRect();
cursorPos.x -= rendererRect.left;
cursorPos.y -= rendererRect.top;
_lastCursorHandle = hCursor;
_lastCursorPos = cursorPos;
if (!cursorHandle) {
return;
}
const _CursorInfo* cursorInfo = _ResolveCursor(cursorHandle);
if (!cursorInfo) {
return;
}
const ScalingOptions& options = scalingWindow.Options();
const ScalingOptions& options = ScalingWindow::Get().Options();
float cursorScaling = options.cursorScaling;
if (cursorScaling < FLOAT_EPSILON<float>) {
// 光标缩放和源窗口相同
const Renderer& renderer = scalingWindow.Renderer();
const Renderer& renderer = ScalingWindow::Get().Renderer();
const SIZE srcSize = Win32Helper::GetSizeOfRect(renderer.SrcRect());
const SIZE destSize = Win32Helper::GetSizeOfRect(renderer.DestRect());
cursorScaling = (((float)destSize.cx / srcSize.cx) + ((float)destSize.cy / srcSize.cy)) / 2;
}
const SIZE cursorSize{
lroundf(cursorInfo->size.cx * cursorScaling),
lroundf(cursorInfo->size.cy * cursorScaling)
lroundf(ci->size.cx * cursorScaling),
lroundf(ci->size.cy * cursorScaling)
};
RECT cursorRect{
.left = lroundf(cursorPos.x - cursorInfo->hotSpot.x * cursorScaling),
.top = lroundf(cursorPos.y - cursorInfo->hotSpot.y * cursorScaling),
.left = lroundf(cursorPos.x - ci->hotSpot.x * cursorScaling),
.top = lroundf(cursorPos.y - ci->hotSpot.y * cursorScaling),
.right = cursorRect.left + cursorSize.cx,
.bottom = cursorRect.top + cursorSize.cy
};
RECT viewportRect;
{
const RECT& rendererRect = scalingWindow.RendererRect();
const RECT& destRect = scalingWindow.Renderer().DestRect();
viewportRect = {
destRect.left - rendererRect.left,
destRect.top - rendererRect.top,
destRect.right - rendererRect.left,
destRect.bottom - rendererRect.top
};
}
const bool isSrcFocused = ScalingWindow::Get().SrcTracker().IsFocused();
const RECT& destRect = ScalingWindow::Get().Renderer().DestRect();
const RECT viewportRect{
isSrcFocused ? destRect.left - rendererRect.left : 0,
isSrcFocused ? destRect.top - rendererRect.top : 0,
(isSrcFocused ? destRect.right : rendererRect.right) - rendererRect.left,
(isSrcFocused ? destRect.bottom : rendererRect.bottom) - rendererRect.top
};
if (cursorRect.left >= viewportRect.right ||
cursorRect.top >= viewportRect.bottom ||
@ -202,7 +202,7 @@ void CursorDrawer::Draw(ID3D11Texture2D* backBuffer, POINT drawOffset) noexcept
d3dDC->RSSetState(nullptr);
}
if (cursorInfo->type == _CursorType::Color) {
if (ci->type == _CursorType::Color) {
// 配置像素着色器
if (!_simplePS) {
HRESULT hr = _deviceResources->GetD3DDevice()->CreatePixelShader(
@ -215,7 +215,7 @@ void CursorDrawer::Draw(ID3D11Texture2D* backBuffer, POINT drawOffset) noexcept
d3dDC->PSSetShader(_simplePS.get(), nullptr, 0);
d3dDC->PSSetConstantBuffers(0, 0, nullptr);
ID3D11ShaderResourceView* cursorSrv = cursorInfo->textureSrv.get();
ID3D11ShaderResourceView* cursorSrv = ci->textureSrv.get();
d3dDC->PSSetShaderResources(0, 1, &cursorSrv);
const bool useBilinear = options.cursorInterpolationMode == CursorInterpolationMode::Bilinear &&
@ -259,26 +259,26 @@ void CursorDrawer::Draw(ID3D11Texture2D* backBuffer, POINT drawOffset) noexcept
_tempCursorTextureSize = cursorSize;
}
{
D3D11_BOX srcBox{
UINT(std::max(cursorRect.left, viewportRect.left) + drawOffset.x),
UINT(std::max(cursorRect.top, viewportRect.top) + drawOffset.y),
0,
UINT(std::min(cursorRect.right, viewportRect.right) + drawOffset.x),
UINT(std::min(cursorRect.bottom, viewportRect.bottom) + drawOffset.y),
1
};
UINT destLeft = UINT(std::max(0l, viewportRect.left - cursorRect.left));
UINT destTop = UINT(std::max(0l, viewportRect.top - cursorRect.top));
D3D11_BOX srcBox{
UINT(std::max(cursorRect.left, viewportRect.left) + drawOffset.x),
UINT(std::max(cursorRect.top, viewportRect.top) + drawOffset.y),
0,
UINT(std::min(cursorRect.right, viewportRect.right) + drawOffset.x),
UINT(std::min(cursorRect.bottom, viewportRect.bottom) + drawOffset.y),
1
};
d3dDC->CopySubresourceRegion(
_tempCursorTexture.get(),
0,
UINT(std::max(0l, viewportRect.left - cursorRect.left)) ,
UINT(std::max(0l, viewportRect.top - cursorRect.left)),
0,
backBuffer,
0,
&srcBox
);
assert(LONG(destLeft + srcBox.right - srcBox.left) <= cursorSize.cx);
assert(LONG(destTop + srcBox.bottom - srcBox.top) <= cursorSize.cy);
d3dDC->CopySubresourceRegion(_tempCursorTexture.get(),
0, destLeft, destTop, 0, backBuffer, 0, &srcBox);
}
if (cursorInfo->type == _CursorType::MaskedColor) {
if (ci->type == _CursorType::MaskedColor) {
if (!_maskedCursorPS) {
HRESULT hr = _deviceResources->GetD3DDevice()->CreatePixelShader(
MaskedCursorPS, sizeof(MaskedCursorPS), nullptr, _maskedCursorPS.put());
@ -303,7 +303,7 @@ void CursorDrawer::Draw(ID3D11Texture2D* backBuffer, POINT drawOffset) noexcept
d3dDC->PSSetConstantBuffers(0, 0, nullptr);
{
ID3D11ShaderResourceView* srvs[2]{ _tempCursorTextureRtv.get(), cursorInfo->textureSrv.get() };
ID3D11ShaderResourceView* srvs[2]{ _tempCursorTextureRtv.get(), ci->textureSrv.get() };
d3dDC->PSSetShaderResources(0, 2, srvs);
}
@ -319,53 +319,25 @@ void CursorDrawer::Draw(ID3D11Texture2D* backBuffer, POINT drawOffset) noexcept
}
bool CursorDrawer::NeedRedraw() const noexcept {
bool isCursorActive = false;
const auto [cursorHandle, cursorPos] = _GetCursorState(isCursorActive);
// 光标形状或位置变化时需要重新绘制
return cursorHandle != _lastCursorHandle || (cursorHandle && cursorPos != _lastCursorPos);
}
std::pair<HCURSOR, POINT> CursorDrawer::_GetCursorState(bool& isActive) const noexcept {
assert(!isActive);
using namespace std::chrono;
const ScalingWindow& scalingWindow = ScalingWindow::Get();
const ScalingOptions& options = scalingWindow.Options();
const CursorManager& cursorManager = scalingWindow.CursorManager();
HCURSOR cursorHandle = cursorManager.CursorHandle();
const CursorManager& cursorManager = ScalingWindow::Get().CursorManager();
const HCURSOR hCursor = cursorManager.CursorHandle();
POINT cursorPos = cursorManager.CursorPos();
// 转换为渲染矩形局部坐标
const RECT& rendererRect = scalingWindow.RendererRect();
// 检查光标形状是否变化
if (hCursor != _lastCursorHandle) {
return true;
}
if (!hCursor) {
// 一直不可见
return false;
}
// 光标可见则检查位置是否变化。为了适配缩放窗口位置变化,比较在缩放窗口中的相对位置
const RECT& rendererRect = ScalingWindow::Get().RendererRect();
cursorPos.x -= rendererRect.left;
cursorPos.y -= rendererRect.top;
// 检查自动隐藏光标
if (options.autoHideCursorDelay.has_value()) {
// 光标在叠加层上或拖动窗口时禁用自动隐藏。光标处于隐藏状态视为形状不变,考虑形状
// 变化:箭头->隐藏->箭头,只要位置不变,自动隐藏功能应让光标始终隐藏;反之如果光
// 标隐藏时移动了或显示时形状变化了应正常显示。
if (cursorManager.IsCursorCaptured() &&
!scalingWindow.IsResizingOrMoving() &&
!scalingWindow.SrcTracker().IsMoving() &&
_lastCursorPos == cursorPos &&
(_lastRawCursorHandle == cursorHandle || !cursorHandle))
{
const duration<float> hideDelay(*options.autoHideCursorDelay);
if (steady_clock::now() - _lastCursorActiveTime > hideDelay) {
cursorHandle = NULL;
}
} else {
isActive = true;
}
}
// 截屏时暂时不渲染光标
if (!_isCursorVisible) {
cursorHandle = NULL;
}
return { cursorHandle, cursorPos };
return cursorPos != _lastCursorPos;
}
const CursorDrawer::_CursorInfo* CursorDrawer::_ResolveCursor(HCURSOR hCursor) noexcept {

View file

@ -26,8 +26,6 @@ public:
bool NeedRedraw() const noexcept;
private:
std::pair<HCURSOR, POINT> _GetCursorState(bool& isActive) const noexcept;
enum class _CursorType {
// 彩色光标,此时纹理中 RGB 通道已预乘 A 通道premultiplied alphaA 通道已预先取反
// 这是为了减少着色器的计算量以及确保(可能进行的)双线性差值的准确性
@ -72,10 +70,6 @@ private:
winrt::com_ptr<ID3D11ShaderResourceView> _tempCursorTextureRtv;
SIZE _tempCursorTextureSize{};
// 这两个成员用于检查自动隐藏光标
HCURSOR _lastRawCursorHandle = NULL;
std::chrono::steady_clock::time_point _lastCursorActiveTime;
// 上次绘制的光标形状和位置
HCURSOR _lastCursorHandle = NULL;
POINT _lastCursorPos{ std::numeric_limits<LONG>::max(), std::numeric_limits<LONG>::max() };

View file

@ -101,13 +101,12 @@ CursorManager::~CursorManager() noexcept {
if (_isUnderCapture) {
POINT cursorPos;
if (GetCursorPos(&cursorPos)) {
_StopCapture(cursorPos, true);
_ReliableSetCursorPos(cursorPos);
} else {
if (!GetCursorPos(&cursorPos)) {
Logger::Get().Win32Error("GetCursorPos 失败");
_RestoreClipCursor();
}
_StopCapture(cursorPos, true);
_ReliableSetCursorPos(cursorPos);
}
}
@ -396,7 +395,7 @@ static bool PtInWindow(HWND hWnd, POINT pt) noexcept {
// 也会考虑自定义形状的窗口。反之如果位于非客户区,我们需手动处理后者。
//
// 可以参考 ChildWindowFromPointEx 的实现:
// https://github.com/Blinue/nt5src/blob/daad8a087a4e75422ec96b7911f1df4669989611/Source/XPSP1/NT/windows/core/ntuser/kernel/winwhere.c#L47
// https://github.com/tongzx/nt5src/blob/daad8a087a4e75422ec96b7911f1df4669989611/Source/XPSP1/NT/windows/core/ntuser/kernel/winwhere.c#L47
RECT clientRect;
if (!Win32Helper::GetClientScreenRect(hWnd, clientRect)) {
@ -580,6 +579,7 @@ void CursorManager::_UpdateCursorState() noexcept {
POINT cursorPos;
if (!GetCursorPos(&cursorPos)) {
Logger::Get().Win32Error("GetCursorPos 失败");
_RestoreClipCursor();
return;
}
@ -966,7 +966,7 @@ void CursorManager::_ClipCursorOnSrcMoving() noexcept {
if (!monitorRects.empty()) {
// 移动源窗口时,如果只有一个显示器,应将光标限制在工作矩形内。一旦超出工作矩形,
// 源窗口将无法继续移动。还需检查窗口样式,以和 OS 保持一致,见
// https://github.com/Blinue/nt5src/blob/daad8a087a4e75422ec96b7911f1df4669989611/Source/XPSP1/NT/windows/core/ntuser/kernel/movesize.c#L1142
// https://github.com/tongzx/nt5src/blob/daad8a087a4e75422ec96b7911f1df4669989611/Source/XPSP1/NT/windows/core/ntuser/kernel/movesize.c#L1142
if (monitorRects.size() == 1) {
const DWORD exStyle = GetWindowExStyle(ScalingWindow::Get().SrcTracker().Handle());
if ((exStyle & (WS_EX_TOPMOST | WS_EX_TOOLWINDOW)) == 0) {

View file

@ -72,24 +72,21 @@ protected:
winrt::com_ptr<ID3D11ComputeShader> _dupFrameCS;
std::pair<uint32_t, uint32_t> _dispatchCount;
bool _roundCornerDisabled = false;
private:
bool _InitCheckingForDuplicateFrame();
bool _IsDuplicateFrame();
// (预测错误帧数, 总计跳过帧数)
std::atomic<std::pair<uint32_t, uint32_t>> _statistics;
// 用于检查重复帧
winrt::com_ptr<ID3D11Texture2D> _prevFrame;
winrt::com_ptr<ID3D11ShaderResourceView> _prevFrameSrv;
uint16_t _nextSkipCount;
uint16_t _framesLeft;
// (预测错误帧数, 总计跳过帧数)
std::atomic<std::pair<uint32_t, uint32_t>> _statistics;
bool _isCheckingForDuplicateFrame = true;
protected:
bool _roundCornerDisabled = false;
};
}

View file

@ -90,15 +90,6 @@ FrameSourceState GraphicsCaptureFrameSource::_Update() noexcept {
return FrameSourceState::Waiting;
}
// 取最新帧,帧率较低时可以有效降低延迟
while (true) {
if (winrt::Direct3D11CaptureFrame nextFrame = _captureFramePool.TryGetNextFrame()) {
frame = std::move(nextFrame);
} else {
break;
}
}
// 从帧获取 IDXGISurface
winrt::IDirect3DSurface d3dSurface = frame.Surface();
@ -184,26 +175,6 @@ static bool CalcWindowCapturedFrameBounds(HWND hWnd, RECT& rect) noexcept {
return true;
}
// 部分使用 Kirikiri 引擎的游戏有着这样的架构: 游戏窗口并非顶级窗口,而是被一个零尺寸
// 的窗口所有。此时 Alt+Tab 列表中的窗口和任务栏图标实际上是所有者窗口,这会导致 WGC
// 捕获失败。我们特殊处理这类窗口。
static bool IsKirikiriWindow(HWND hwndSrc) noexcept {
const HWND hwndOwner = GetWindowOwner(hwndSrc);
if (!hwndOwner) {
return false;
}
RECT ownerRect;
if (!GetWindowRect(hwndOwner, &ownerRect)) {
Logger::Get().Win32Error("GetWindowRect 失败");
return false;
}
// 所有者窗口尺寸为零,而且是顶级窗口
return ownerRect.left == ownerRect.right && ownerRect.top == ownerRect.bottom &&
!GetWindowOwner(hwndOwner);
}
bool GraphicsCaptureFrameSource::_CaptureWindow(IGraphicsCaptureItemInterop* interop) noexcept {
const SrcTracker& srcTracker = ScalingWindow::Get().SrcTracker();
const HWND hwndSrc = srcTracker.Handle();
@ -231,67 +202,64 @@ bool GraphicsCaptureFrameSource::_CaptureWindow(IGraphicsCaptureItemInterop* int
1
};
const DWORD srcExStyle = GetWindowExStyle(hwndSrc);
// WS_EX_APPWINDOW 样式使窗口始终在 Alt+Tab 列表中显示
if (srcExStyle & WS_EX_APPWINDOW) {
return _TryCreateGraphicsCaptureItem(interop);
}
const bool isSrcKirikiri = IsKirikiriWindow(hwndSrc);
if (isSrcKirikiri) {
Logger::Get().Info("源窗口有零尺寸的所有者窗口");
} else {
// 第一次尝试捕获。Kirikiri 窗口必定失败,无需尝试
if (_TryCreateGraphicsCaptureItem(interop)) {
return true;
}
}
// 添加 WS_EX_APPWINDOW 样式
if (!SetWindowLongPtr(hwndSrc, GWL_EXSTYLE, srcExStyle | WS_EX_APPWINDOW)) {
Logger::Get().Win32Error("SetWindowLongPtr 失败");
return false;
}
Logger::Get().Info("已改变源窗口样式");
_isSrcStyleChanged = true;
// Kirikiri 窗口改变样式后所有者窗口和游戏窗口将同时出现在 Alt+Tab 列表和任务栏中。
// 虽然所有窗口都会如此,但 Kirikiri 的特殊之处在于两个窗口的图标和标题相同,为了不
// 引起困惑应隐藏所有者窗口的图标。
if (isSrcKirikiri) {
_taskbarList = winrt::try_create_instance<ITaskbarList>(CLSID_TaskbarList);
if (_taskbarList) {
HRESULT hr = _taskbarList->HrInit();
if (SUCCEEDED(hr)) {
// 修正任务栏图标
_taskbarList->DeleteTab(GetWindowOwner(hwndSrc));
_taskbarList->AddTab(hwndSrc);
// 修正 Alt+Tab 切换顺序
if (GetForegroundWindow() == hwndSrc) {
SetForegroundWindow(GetDesktopWindow());
SetForegroundWindow(hwndSrc);
}
} else {
Logger::Get().ComError("ITaskbarList::HrInit 失败", hr);
_taskbarList = nullptr;
}
} else {
Logger::Get().Error("创建 ITaskbarList 失败");
}
}
// 再次尝试捕获
if (_TryCreateGraphicsCaptureItem(interop)) {
return true;
} else {
if (_isSrcStyleChanged) {
// 恢复源窗口样式
SetWindowLongPtr(hwndSrc, GWL_EXSTYLE, srcExStyle);
}
return false;
}
// 尝试设置源窗口样式,因为 WGC 只能捕获位于 Alt+Tab 列表中的窗口
LONG_PTR srcExStyle = GetWindowLongPtr(hwndSrc, GWL_EXSTYLE);
if ((srcExStyle & WS_EX_APPWINDOW) == 0) {
// 添加 WS_EX_APPWINDOW 样式,确保源窗口可被 Alt+Tab 选中
if (SetWindowLongPtr(hwndSrc, GWL_EXSTYLE, srcExStyle | WS_EX_APPWINDOW)) {
Logger::Get().Info("已改变源窗口样式");
_originalSrcExStyle = srcExStyle;
if (_TryCreateGraphicsCaptureItem(interop)) {
_RemoveOwnerFromAltTabList(hwndSrc);
return true;
}
} else {
Logger::Get().Win32Error("SetWindowLongPtr 失败");
}
}
// 如果窗口使用 ITaskbarList 隐藏了任务栏图标也不会出现在 Alt+Tab 列表。这种情况很罕见
_taskbarList = winrt::try_create_instance<ITaskbarList>(CLSID_TaskbarList);
if (_taskbarList && SUCCEEDED(_taskbarList->HrInit())) {
HRESULT hr = _taskbarList->AddTab(hwndSrc);
if (SUCCEEDED(hr)) {
Logger::Get().Info("已添加任务栏图标");
if (_TryCreateGraphicsCaptureItem(interop)) {
_RemoveOwnerFromAltTabList(hwndSrc);
return true;
}
} else {
_taskbarList = nullptr;
Logger::Get().Error("ITaskbarList::AddTab 失败");
}
} else {
_taskbarList = nullptr;
Logger::Get().Error("创建 ITaskbarList 失败");
}
// 上面的尝试失败了则还原更改
if (_taskbarList) {
_taskbarList->DeleteTab(hwndSrc);
_taskbarList = nullptr;
}
if (_originalSrcExStyle) {
// 首先还原所有者窗口的样式以压制任务栏的动画
if (_originalOwnerExStyle) {
SetWindowLongPtr(GetWindowOwner(hwndSrc), GWL_EXSTYLE, _originalOwnerExStyle);
_originalOwnerExStyle = 0;
}
SetWindowLongPtr(hwndSrc, GWL_EXSTYLE, _originalSrcExStyle);
_originalSrcExStyle = 0;
}
return false;
}
bool GraphicsCaptureFrameSource::_TryCreateGraphicsCaptureItem(IGraphicsCaptureItemInterop* interop) noexcept {
@ -313,6 +281,43 @@ bool GraphicsCaptureFrameSource::_TryCreateGraphicsCaptureItem(IGraphicsCaptureI
return true;
}
// 部分使用 Kirikiri 引擎的游戏有着这样的架构: 游戏窗口并非根窗口,它被一个尺寸为 0 的窗口
// 所有。此时 Alt+Tab 列表中的窗口和任务栏图标实际上是所有者窗口,这会导致 WGC 捕获游戏窗
// 口时失败。_CaptureWindow 在初次捕获失败后会将 WS_EX_APPWINDOW 样式添加到游戏窗口,这
// 可以工作,但也导致所有者窗口和游戏窗口同时出现在 Alt+Tab 列表中,引起用户的困惑。
//
// 此函数检测这种情况并改变所有者窗口的样式将它从 Alt+Tab 列表中移除。
void GraphicsCaptureFrameSource::_RemoveOwnerFromAltTabList(HWND hwndSrc) noexcept {
HWND hwndOwner = GetWindowOwner(hwndSrc);
if (!hwndOwner) {
return;
}
RECT ownerRect{};
if (!GetWindowRect(hwndOwner, &ownerRect)) {
Logger::Get().Win32Error("GetWindowRect 失败");
return;
}
// 检查所有者窗口尺寸
if (ownerRect.right != ownerRect.left || ownerRect.bottom != ownerRect.top) {
return;
}
LONG_PTR ownerExStyle = GetWindowLongPtr(hwndOwner, GWL_EXSTYLE);
if (ownerExStyle == 0) {
Logger::Get().Win32Error("GetWindowLongPtr 失败");
return;
}
if (!SetWindowLongPtr(hwndOwner, GWL_EXSTYLE, ownerExStyle | WS_EX_TOOLWINDOW)) {
Logger::Get().Win32Error("SetWindowLongPtr 失败");
return;
}
_originalOwnerExStyle = ownerExStyle;
}
bool GraphicsCaptureFrameSource::_StartCapture() noexcept {
if (_captureSession) {
return true;
@ -323,7 +328,7 @@ bool GraphicsCaptureFrameSource::_StartCapture() noexcept {
_captureFramePool = winrt::Direct3D11CaptureFramePool::Create(
_wrappedD3DDevice,
winrt::DirectXPixelFormat::B8G8R8A8UIntNormalized,
4, // 帧的缓存数量,更大的值有利于在低帧率下降低延迟
1, // 帧的缓存数量
{ (int)_frameBox.right, (int)_frameBox.bottom } // 帧的尺寸为包含源窗口的最小尺寸
);
@ -381,22 +386,18 @@ GraphicsCaptureFrameSource::~GraphicsCaptureFrameSource() {
const HWND hwndSrc = ScalingWindow::Get().SrcTracker().Handle();
// 还原源窗口样式
if (_isSrcStyleChanged) {
const DWORD srcExStyle = GetWindowExStyle(hwndSrc);
SetWindowLongPtr(hwndSrc, GWL_EXSTYLE, srcExStyle & ~WS_EX_APPWINDOW);
}
// 还原 Kirikiri 窗口
if (_taskbarList) {
_taskbarList->DeleteTab(hwndSrc);
_taskbarList->AddTab(GetWindowOwner(hwndSrc));
}
// 修正任务栏焦点窗口和 Alt+Tab 切换顺序
if (GetForegroundWindow() == hwndSrc) {
SetForegroundWindow(GetDesktopWindow());
SetForegroundWindow(hwndSrc);
// 还原源窗口样式
if (_originalSrcExStyle) {
// 首先还原所有者窗口的样式以压制任务栏的动画
if (_originalOwnerExStyle) {
SetWindowLongPtr(GetWindowOwner(hwndSrc), GWL_EXSTYLE, _originalOwnerExStyle);
}
SetWindowLongPtr(hwndSrc, GWL_EXSTYLE, _originalSrcExStyle);
}
}

View file

@ -38,15 +38,18 @@ private:
bool _TryCreateGraphicsCaptureItem(IGraphicsCaptureItemInterop* interop) noexcept;
void _RemoveOwnerFromAltTabList(HWND hwndSrc) noexcept;
LONG_PTR _originalSrcExStyle = 0;
LONG_PTR _originalOwnerExStyle = 0;
winrt::com_ptr<ITaskbarList> _taskbarList;
D3D11_BOX _frameBox{};
winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice _wrappedD3DDevice{ nullptr };
winrt::Windows::Graphics::Capture::GraphicsCaptureItem _captureItem{ nullptr };
winrt::Windows::Graphics::Capture::GraphicsCaptureSession _captureSession{ nullptr };
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool _captureFramePool{ nullptr };
winrt::com_ptr<ITaskbarList> _taskbarList;
bool _isSrcStyleChanged = false;
};
}

View file

@ -226,7 +226,7 @@ bool ImGuiBackend::_CreateDeviceObjects() noexcept {
}
static constexpr D3D11_INPUT_ELEMENT_DESC LOCAL_LAYOUT[] = {
{ "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (UINT)IM_OFFSETOF(ImDrawVert, pos), D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "SV_POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (UINT)IM_OFFSETOF(ImDrawVert, pos), D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (UINT)IM_OFFSETOF(ImDrawVert, uv), D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, (UINT)IM_OFFSETOF(ImDrawVert, col), D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

View file

@ -139,7 +139,7 @@ struct serializer<
namespace Magpie {
// 缓存版本号。当缓存文件结构有更改时更新它,使旧缓存失效
static constexpr uint32_t FONTS_CACHE_VERSION = 7;
static constexpr uint32_t FONTS_CACHE_VERSION = 4;
static std::wstring GetCacheFileName(const std::wstring_view& language, uint32_t dpi) noexcept {
return fmt::format(L"{}\\fonts_{}_{}", CommonSharedConstants::CACHE_DIR, language, dpi);

View file

@ -53,10 +53,6 @@ bool ImGuiImpl::Initialize(DeviceResources& deviceResources) noexcept {
io.ConfigFlags |= ImGuiConfigFlags_NavNoCaptureKeyboard | ImGuiConfigFlags_NoMouseCursorChange;
// 禁用 ini 配置文件
io.IniFilename = nullptr;
#ifndef _DEBUG
// Release 配置下禁用重复 ID 检查
io.ConfigDebugHighlightIdConflicts = false;
#endif
if (!_backend.Initialize(deviceResources)) {
Logger::Get().Error("初始化 ImGuiBackend 失败");

View file

@ -48,6 +48,11 @@ private:
ImGuiBackend _backend;
phmap::flat_hash_map<std::string, ImVec4> _windowRects;
uint32_t _handlerId = 0;
HANDLE _hHookThread = NULL;
DWORD _hookThreadId = 0;
};
}

View file

@ -1,12 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
<Import Project="..\Common.Pre.props" />
<PropertyGroup Label="Globals">
<CppWinRTFastAbi>true</CppWinRTFastAbi>
<CppWinRTOptimized>true</CppWinRTOptimized>
<CppWinRTRootNamespaceAutoMerge>true</CppWinRTRootNamespaceAutoMerge>
<CppWinRTGenerateWindowsMetadata>false</CppWinRTGenerateWindowsMetadata>
<CppWinRTVerbosity>low</CppWinRTVerbosity>
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{0e5205ae-dfa9-4cb8-b662-e43cd6512e2a}</ProjectGuid>
<WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
@ -15,7 +17,6 @@
<GeneratedFilesDir>$(IntDir)\Generated Files\</GeneratedFilesDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<Import Project="..\Common.Pre.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<CharacterSet>Unicode</CharacterSet>
@ -153,15 +154,15 @@
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>这台计算机上缺少此项目引用的 NuGet 程序包。使用“NuGet 程序包还原”可下载这些程序包。有关更多信息,请参见 http://go.microsoft.com/fwlink/?LinkID=322105。缺少的文件是 {0}。</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
<Error Condition="!Exists('..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

View file

@ -110,14 +110,12 @@ void OverlayDrawer::Draw(
_imguiImpl.NewFrame(_overlayOptions->windows, fittsLawAdjustment, _dpiScale);
bool needRedraw = false;
// 防止 ID 冲突
int itemId = 0;
if (_isToolbarVisible && _DrawToolbar(fps, itemId)) {
if (_isToolbarVisible && _DrawToolbar(fps)) {
needRedraw = true;
}
if (_isProfilerVisible && _DrawProfiler(effectTimings, fps, itemId)) {
if (_isProfilerVisible && _DrawProfiler(effectTimings, fps)) {
needRedraw = true;
}
@ -340,12 +338,10 @@ SmallVector<ImWchar> OverlayDrawer::_BuildFontUI(
SetGlyphRanges(ranges, OverlayHelper::BASIC_LATIN_RANGES);
} else if (language == L"ru" || language == L"uk") {
SetGlyphRanges(ranges, fontAtlas.GetGlyphRangesCyrillic());
} else if (language == L"tr" || language == L"pl" || language == L"fi") {
} else if (language == L"tr" || language == L"pl") {
SetGlyphRanges(ranges, OverlayHelper::EXTENDED_LATIN_RANGES);
} else if (language == L"vi") {
SetGlyphRanges(ranges, fontAtlas.GetGlyphRangesVietnamese());
} else if (language == L"fr") {
SetGlyphRanges(ranges, OverlayHelper::FRENCH_RANGES);
} else {
// Basic Latin 使用默认字体
SetGlyphRanges(ranges, OverlayHelper::BASIC_LATIN_RANGES);
@ -646,7 +642,7 @@ static std::string IconLabel(ImWchar iconChar) noexcept {
return StrHelper::UTF16ToUTF8(text);
}
bool OverlayDrawer::_DrawToolbar(uint32_t fps, int& itemId) noexcept {
bool OverlayDrawer::_DrawToolbar(uint32_t fps) noexcept {
bool needRedraw = false;
const float windowWidth = 360 * _dpiScale;
@ -657,8 +653,6 @@ bool OverlayDrawer::_DrawToolbar(uint32_t fps, int& itemId) noexcept {
_lastToolbarAlpha = _CalcToolbarAlpha();
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, _lastToolbarAlpha);
ImGui::PushStyleColor(ImGuiCol_WindowBg, (ImU32)ImColor(15, 15, 15, 180));
const ImVec2 originalWindowPadding = ImGui::GetStyle().WindowPadding;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 6 * _dpiScale,0.0f });
_isToolbarItemActive = false;
@ -683,7 +677,6 @@ bool OverlayDrawer::_DrawToolbar(uint32_t fps, int& itemId) noexcept {
ImGui::SetCursorPosY((CORNER_ROUNDING + 3) * _dpiScale);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, originalWindowPadding);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, { 4 * _dpiScale,4 * _dpiScale });
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4 * _dpiScale);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0);
@ -774,7 +767,6 @@ bool OverlayDrawer::_DrawToolbar(uint32_t fps, int& itemId) noexcept {
if (isDeveloperMode && effectDesc.passes.size() > 1) {
// 开发者模式允许保存任意通道的输出
ImGui::PushID(itemId++);
if (ImGui::BeginMenu(effectName.data())) {
const uint32_t passCount = (uint32_t)effectDesc.passes.size();
for (uint32_t j = 0; j < passCount; ++j) {
@ -782,37 +774,28 @@ bool OverlayDrawer::_DrawToolbar(uint32_t fps, int& itemId) noexcept {
const uint32_t outputCount = (uint32_t)passDesc.outputs.size();
if (outputCount == 1) {
ImGui::PushID(itemId++);
if (ImGui::MenuItem(passDesc.desc.c_str())) {
ScalingWindow::Get().Renderer().TakeScreenshot(i, j);
}
ImGui::PopID();
} else {
ImGui::PushID(itemId++);
if (ImGui::BeginMenu(passDesc.desc.c_str())) {
for (uint32_t k = 0; k < outputCount; ++k) {
ImGui::PushID(itemId++);
if (ImGui::MenuItem(effectDesc.textures[passDesc.outputs[k]].name.c_str())) {
ScalingWindow::Get().Renderer().TakeScreenshot(i, j, k);
}
ImGui::PopID();
}
ImGui::EndMenu();
}
ImGui::PopID();
}
}
ImGui::EndMenu();
}
ImGui::PopID();
} else {
ImGui::PushID(itemId++);
if (ImGui::MenuItem(effectName.data())) {
ScalingWindow::Get().Renderer().TakeScreenshot(i);
}
ImGui::PopID();
}
}
@ -831,25 +814,7 @@ bool OverlayDrawer::_DrawToolbar(uint32_t fps, int& itemId) noexcept {
ImGui::SameLine();
ImGui::SetCursorPosY((CORNER_ROUNDING + 3) * _dpiScale);
// 源窗口支持最小化时才显示最小化按钮
const HWND hwndSrc = ScalingWindow::Get().SrcTracker().Handle();
const bool canSrcMinimized = GetWindowStyle(hwndSrc) & WS_MINIMIZEBOX;
ImGui::SetCursorPosX(ImGui::GetContentRegionMax().x -
((canSrcMinimized ? 3 : 2) * 28 - 4) * _dpiScale);
if (canSrcMinimized) {
const std::string& minimizeStr = _GetResourceString(L"Overlay_Toolbar_Minimize");
const ImWchar icon = Win32Helper::GetOSVersion().IsWin11() ?
OverlayHelper::SegoeIcons::CheckboxIndeterminate : OverlayHelper::SegoeIcons::Remove;
if (drawButton(icon, minimizeStr.c_str())) {
// 模拟通过标题栏最小化,失败则回落到 ShowWindow
if (!PostMessage(hwndSrc, WM_SYSCOMMAND, SC_MINIMIZE, 0)) {
ShowWindowAsync(hwndSrc, SW_SHOWMINIMIZED);
}
}
ImGui::SameLine();
}
ImGui::SetCursorPosX(ImGui::GetContentRegionMax().x - 50 * _dpiScale);
{
const bool isWindowedMode = ScalingWindow::Get().Options().IsWindowedMode();
@ -859,10 +824,11 @@ bool OverlayDrawer::_DrawToolbar(uint32_t fps, int& itemId) noexcept {
isWindowedMode ? L"Overlay_Toolbar_SwitchToFullscreen" : L"Overlay_Toolbar_SwitchToWindowed");
if (drawButton(icon, switchScalingStr.c_str())) {
ScalingWindow::Dispatcher().TryEnqueue([]() {
ScalingWindow::Get().ToggleScaling(!ScalingWindow::Get().Options().IsWindowedMode());
ScalingWindow::Get().SwitchScalingState(!ScalingWindow::Get().Options().IsWindowedMode());
});
}
}
ImGui::SameLine();
// 和主窗口保持一致 (#C42B1C)
@ -888,14 +854,14 @@ bool OverlayDrawer::_DrawToolbar(uint32_t fps, int& itemId) noexcept {
ImGui::EndDisabled();
ImGui::PopStyleColor(5);
ImGui::PopStyleVar(6);
ImGui::PopStyleVar(5);
} else {
_isCursorOnCaptionArea = false;
}
ImGui::End();
ImGui::PopStyleColor();
ImGui::PopStyleVar(2);
ImGui::PopStyleVar();
return needRedraw;
}
@ -909,7 +875,7 @@ static std::string RectToStr(const RECT& rect) noexcept {
#endif
// 返回 true 表示应再渲染一次
bool OverlayDrawer::_DrawProfiler(const SmallVector<float>& effectTimings, uint32_t fps, int& itemId) noexcept {
bool OverlayDrawer::_DrawProfiler(const SmallVector<float>& effectTimings, uint32_t fps) noexcept {
const ScalingOptions& options = ScalingWindow::Get().Options();
const Renderer& renderer = ScalingWindow::Get().Renderer();
@ -1111,6 +1077,8 @@ bool OverlayDrawer::_DrawProfiler(const SmallVector<float>& effectTimings, uint3
}
static int selectedIdx = -1;
// 防止 ID 冲突
int itemId = 0;
if (nEffect > 1 || showPasses) {
ImGui::Spacing();

View file

@ -77,9 +77,9 @@ private:
bool selected = false
);
bool _DrawToolbar(uint32_t fps, int& itemId) noexcept;
bool _DrawToolbar(uint32_t fps) noexcept;
bool _DrawProfiler(const SmallVector<float>& effectTimings, uint32_t fps, int& itemId) noexcept;
bool _DrawProfiler(const SmallVector<float>& effectTimings, uint32_t fps) noexcept;
const std::string& _GetResourceString(const std::wstring_view& key) noexcept;

View file

@ -18,8 +18,6 @@ struct OverlayHelper {
// Basic Latin + Latin-1 Supplement + Latin Extended-A用于土耳其语、波兰语等。
// 参见 https://en.wikipedia.org/wiki/Latin_Extended-A
static constexpr ImWchar EXTENDED_LATIN_RANGES[] = { 0x20, 0x17F };
// 法语字符,包含 EXTENDED_LATIN_RANGES + General Punctuation
static constexpr ImWchar FRENCH_RANGES[] = { 0x20, 0x17F, 0x2000, 0x206F };
/////////////////////////////////////////////////////
//
@ -51,13 +49,10 @@ struct OverlayHelper {
//
/////////////////////////////////////////////////////
// 更改图标后记得更新 FONTS_CACHE_VERSION
struct SegoeIcons {
static const ImWchar Cancel = 0xE711;
static const ImWchar Camera = 0xE722;
static const ImWchar Favicon = 0xE737;
static const ImWchar Remove = 0xE738;
static const ImWchar CheckboxIndeterminate = 0xE73C;
static const ImWchar FullScreen = 0xE740;
static const ImWchar Pinned = 0xE840;
static const ImWchar Diagnostic = 0xE9D9;
@ -70,8 +65,6 @@ struct OverlayHelper {
SegoeIcons::Cancel, SegoeIcons::Cancel,
SegoeIcons::Camera, SegoeIcons::Camera,
SegoeIcons::Favicon, SegoeIcons::Favicon,
SegoeIcons::Remove, SegoeIcons::Remove,
SegoeIcons::CheckboxIndeterminate, SegoeIcons::CheckboxIndeterminate,
SegoeIcons::FullScreen, SegoeIcons::FullScreen,
SegoeIcons::Pinned, SegoeIcons::Pinned,
SegoeIcons::Diagnostic, SegoeIcons::Diagnostic,

View file

@ -3,6 +3,9 @@
#include "DeviceResources.h"
#include "Logger.h"
#include "ScalingWindow.h"
#include "Win32Helper.h"
#include <dcomp.h>
#include <dwmapi.h>
namespace Magpie {
@ -27,12 +30,64 @@ bool PresenterBase::Initialize(HWND hwndAttach, const DeviceResources& deviceRes
return _Initialize(hwndAttach);
}
void PresenterBase::_WaitForDwmComposition() noexcept {
// Win11 可以使用准确的 DCompositionWaitForCompositorClock
if (Win32Helper::GetOSVersion().IsWin11()) {
static const auto dCompositionWaitForCompositorClock =
Win32Helper::LoadSystemFunction<decltype(DCompositionWaitForCompositorClock)>(
L"dcomp.dll", "DCompositionWaitForCompositorClock");
if (dCompositionWaitForCompositorClock) {
dCompositionWaitForCompositorClock(0, nullptr, INFINITE);
return;
}
}
LARGE_INTEGER qpf;
QueryPerformanceFrequency(&qpf);
qpf.QuadPart /= 10000000;
DWM_TIMING_INFO info{};
info.cbSize = sizeof(info);
DwmGetCompositionTimingInfo(NULL, &info);
LARGE_INTEGER time;
QueryPerformanceCounter(&time);
if (time.QuadPart >= (LONGLONG)info.qpcCompose) {
return;
}
// 提前 1ms 结束然后忙等待
time.QuadPart += 10000;
if (time.QuadPart < (LONGLONG)info.qpcCompose) {
LARGE_INTEGER liDueTime{
.QuadPart = -((LONGLONG)info.qpcCompose - time.QuadPart) / qpf.QuadPart
};
static HANDLE timer = CreateWaitableTimerEx(nullptr, nullptr,
CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
SetWaitableTimerEx(timer, &liDueTime, 0, NULL, NULL, 0, 0);
WaitForSingleObject(timer, INFINITE);
} else {
Sleep(0);
}
while (true) {
QueryPerformanceCounter(&time);
if (time.QuadPart >= (LONGLONG)info.qpcCompose) {
return;
}
Sleep(0);
}
}
uint32_t PresenterBase::_CalcBufferCount() noexcept {
// 缓冲区数量取决于 ScalingRuntime::_ScalingThreadProc 中检查光标移动的频率
return ScalingWindow::Get().Options().Is3DGameMode() ? 4 : 8;
}
void PresenterBase::_WaitForGpu() noexcept {
void PresenterBase::_WaitForRenderComplete() noexcept {
ID3D11DeviceContext4* d3dDC = _deviceResources->GetD3DDC();
// 等待渲染完成

View file

@ -16,7 +16,7 @@ public:
POINT& drawOffset
) noexcept = 0;
virtual void EndFrame(bool waitForGpu = false) noexcept = 0;
virtual void EndFrame(bool waitForRenderComplete = false) noexcept = 0;
virtual bool OnResize() noexcept = 0;
@ -27,7 +27,10 @@ public:
protected:
virtual bool _Initialize(HWND hwndAttach) noexcept = 0;
void _WaitForGpu() noexcept;
void _WaitForRenderComplete() noexcept;
// 和 DwmFlush 效果相同但更准确
static void _WaitForDwmComposition() noexcept;
static uint32_t _CalcBufferCount() noexcept;

View file

@ -199,7 +199,7 @@ winrt::fire_and_forget Renderer::TakeScreenshot(
}
}
void Renderer::_FrontendRender(bool waitForGpu) noexcept {
void Renderer::_FrontendRender(bool waitForRenderComplete) noexcept {
winrt::com_ptr<ID3D11Texture2D> frameTex;
winrt::com_ptr<ID3D11RenderTargetView> frameRtv;
POINT drawOffset;
@ -267,10 +267,10 @@ void Renderer::_FrontendRender(bool waitForGpu) noexcept {
// 绘制光标
_cursorDrawer.Draw(frameTex.get(), drawOffset);
_presenter->EndFrame(waitForGpu);
_presenter->EndFrame(waitForRenderComplete);
}
bool Renderer::Render(bool force, bool waitForGpu) noexcept {
bool Renderer::Render(bool force, bool waitForRenderComplete) noexcept {
if (!force && _lastAccessMutexKey == _sharedTextureMutexKey.load(std::memory_order_relaxed)) {
if (_lastAccessMutexKey == 0) {
// 第一帧尚未完成
@ -282,7 +282,7 @@ bool Renderer::Render(bool force, bool waitForGpu) noexcept {
}
}
_FrontendRender(waitForGpu);
_FrontendRender(waitForRenderComplete);
return true;
}
@ -699,43 +699,14 @@ ID3D11Texture2D* Renderer::_ResizeEffects() noexcept {
void Renderer::_UpdateDestRect() noexcept {
const RECT& rendererRect = ScalingWindow::Get().RendererRect();
DestAlignment alignment = ScalingWindow::Get().Options().destAlignment;
LONG destWidth;
LONG destHeight;
{
D3D11_TEXTURE2D_DESC desc;
_frontendSharedTexture->GetDesc(&desc);
destWidth = (LONG)desc.Width;
destHeight = (LONG)desc.Height;
}
D3D11_TEXTURE2D_DESC desc;
_frontendSharedTexture->GetDesc(&desc);
using enum DestAlignment;
if (alignment == LeftTop || alignment == Left || alignment == LeftBottom) {
_destRect.left = 0;
_destRect.right = destWidth;
} else if (alignment == Top || alignment == Center || alignment == Bottom) {
_destRect.left = (rendererRect.left + rendererRect.right - destWidth) / 2;
_destRect.right = _destRect.left + destWidth;
} else {
_destRect.left = rendererRect.right - destWidth;
_destRect.right = rendererRect.right;
}
if (alignment == LeftTop || alignment == Top || alignment == RightTop) {
_destRect.top = 0;
_destRect.bottom = destHeight;
} else if (alignment == Left || alignment == Center || alignment == Right) {
_destRect.top = (rendererRect.top + rendererRect.bottom - destHeight) / 2;
_destRect.bottom = _destRect.top + destHeight;
} else {
_destRect.top = rendererRect.bottom - destHeight;
_destRect.bottom = rendererRect.bottom;
}
assert(_destRect.left + destWidth == _destRect.right);
assert(_destRect.top + destHeight == _destRect.bottom);
_destRect.left = (rendererRect.left + rendererRect.right - (LONG)desc.Width) / 2;
_destRect.top = (rendererRect.top + rendererRect.bottom - (LONG)desc.Height) / 2;
_destRect.right = _destRect.left + (LONG)desc.Width;
_destRect.bottom = _destRect.top + (LONG)desc.Height;
}
HANDLE Renderer::_CreateSharedTexture(ID3D11Texture2D* effectsOutput) noexcept {

View file

@ -22,7 +22,7 @@ public:
ScalingError Initialize(HWND hwndAttach, OverlayOptions& overlayOptions) noexcept;
bool Render(bool force = false, bool waitForGpu = false) noexcept;
bool Render(bool force = false, bool waitForRenderComplete = false) noexcept;
bool OnResize() noexcept;
@ -66,7 +66,7 @@ public:
) noexcept;
private:
void _FrontendRender(bool waitForGpu = false) noexcept;
void _FrontendRender(bool waitForRenderComplete = false) noexcept;
void _BackendThreadProc() noexcept;

View file

@ -41,7 +41,6 @@ void ScalingOptions::Log() const noexcept {
IsWindowedMode: {}
IsDebugMode: {}
IsBenchmarkMode: {}
IsTopmostDisabled: {}
IsFP16Disabled: {}
IsEffectCacheDisabled: {}
IsFontCacheDisabled: {}
@ -75,7 +74,6 @@ void ScalingOptions::Log() const noexcept {
IsWindowedMode(),
IsDebugMode(),
IsBenchmarkMode(),
IsTopmostDisabled(),
IsFP16Disabled(),
IsEffectCacheDisabled(),
IsFontCacheDisabled(),

View file

@ -14,6 +14,8 @@ ScalingRuntime::ScalingRuntime() : _scalingThread(&ScalingRuntime::_ScalingThrea
}
ScalingRuntime::~ScalingRuntime() {
Stop();
if (_scalingThread.joinable()) {
const HANDLE hScalingThread = _scalingThread.native_handle();
@ -48,30 +50,27 @@ ScalingRuntime::~ScalingRuntime() {
}
}
bool ScalingRuntime::Start(HWND hwndSrc, ScalingOptions&& options, bool force) {
bool ScalingRuntime::Start(HWND hwndSrc, ScalingOptions&& options) {
assert(!options.screenshotsDir.empty() && options.showToast && options.showError && options.save);
_Dispatcher().TryEnqueue([this, hwndSrc, options(std::move(options)), force]() mutable {
_Dispatcher().TryEnqueue([this, hwndSrc, options(std::move(options))]() mutable {
ScalingWindow& scalingWindow = ScalingWindow::Get();
// 如果正在缩放且 force 为假则忽略
if (scalingWindow && !force) {
return;
if (scalingWindow.IsSrcRepositioning()) {
scalingWindow.CleanAfterSrcRepositioned();
}
scalingWindow.Stop();
// 初始化时视为处于缩放状态
_State(ScalingState::Scaling);
_IsScaling(true);
scalingWindow.Start(hwndSrc, std::move(options));
});
return true;
}
void ScalingRuntime::ToggleScaling(bool isWindowedMode) {
void ScalingRuntime::SwitchScalingState(bool isWindowedMode) {
_Dispatcher().TryEnqueue([isWindowedMode]() {
if (ScalingWindow& scalingWindow = ScalingWindow::Get()) {
scalingWindow.ToggleScaling(isWindowedMode);
scalingWindow.SwitchScalingState(isWindowedMode);
};
});
}
@ -90,39 +89,38 @@ void ScalingRuntime::Stop() {
});
}
static std::optional<bool> IsSrcRepositioning(HWND hwndSrc) noexcept {
// 返回值:
// -1: 应取消缩放
// 0: 仍在调整中
// 1: 调整完毕
static int GetSrcRepositionState(HWND hwndSrc) noexcept {
if (!IsWindow(hwndSrc)) {
Logger::Get().Info("源窗口已销毁");
return std::nullopt;
}
// 窗口不可见或最小化则继续等待。注意 showCmd 不能准确判断窗口可见性,
// 应使用 IsWindowVisible。
if (!IsWindowVisible(hwndSrc)) {
return true;
return -1;
}
if (Win32Helper::IsWindowHung(hwndSrc)) {
Logger::Get().Info("源窗口已挂起");
return std::nullopt;
return -1;
}
const UINT showCmd = Win32Helper::GetWindowShowCmd(hwndSrc);
if (showCmd == SW_SHOWMAXIMIZED) {
if (showCmd == SW_HIDE) {
return -1;
} else if (showCmd == SW_SHOWMAXIMIZED) {
// 窗口最大化则尝试缩放,失败会显示错误消息
return false;
return 1;
} else if (showCmd == SW_SHOWMINIMIZED) {
return true;
// 窗口最小化则继续等待
return 0;
}
// 检查源窗口是否正在调整大小或移动
GUITHREADINFO guiThreadInfo{ .cbSize = sizeof(GUITHREADINFO) };
if (!GetGUIThreadInfo(GetWindowThreadProcessId(hwndSrc, nullptr), &guiThreadInfo)) {
Logger::Get().Win32Error("GetGUIThreadInfo 失败");
return std::nullopt;
return -1;
}
return bool(guiThreadInfo.flags & GUI_INMOVESIZE);
return (guiThreadInfo.flags & GUI_INMOVESIZE) ? 0 : 1;
}
void ScalingRuntime::_ScalingThreadProc() noexcept {
@ -162,9 +160,9 @@ void ScalingRuntime::_ScalingThreadProc() noexcept {
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
scalingWindow.Stop();
_IsScaling(false);
return;
} else if (msg.message == CommonSharedConstants::WM_FRONTEND_RENDER &&
msg.hwnd == scalingWindow.Handle()) {
} else if (msg.message == CommonSharedConstants::WM_FRONTEND_RENDER && msg.hwnd == scalingWindow.Handle()) {
// 缩放窗口收到 WM_FRONTEND_RENDER 将执行渲染
lastRenderTime = steady_clock::now();
}
@ -172,9 +170,9 @@ void ScalingRuntime::_ScalingThreadProc() noexcept {
DispatchMessage(&msg);
}
if (scalingWindow) {
_State(ScalingState::Scaling);
_IsScaling(scalingWindow);
if (scalingWindow) {
const auto now = steady_clock::now();
// 限制检测光标移动的频率
const milliseconds timeout(scalingWindow.Options().Is3DGameMode() ? 8 : 2);
@ -191,25 +189,18 @@ void ScalingRuntime::_ScalingThreadProc() noexcept {
const DWORD restMs = DWORD((rest.count() + ratio - 1) / ratio);
MsgWaitForMultipleObjectsEx(0, nullptr, restMs, QS_ALLINPUT, MWMO_INPUTAVAILABLE);
} else if (scalingWindow.IsSrcRepositioning()) {
std::optional<bool> repositioning =
IsSrcRepositioning(scalingWindow.SrcTracker().Handle());
if (repositioning.has_value()) {
if (*repositioning) {
// 等待调整完成
_State(ScalingState::Waiting);
MsgWaitForMultipleObjectsEx(0, nullptr, 10, QS_ALLINPUT, MWMO_INPUTAVAILABLE);
} else {
// 重新缩放。初始化时视为处于缩放状态
_State(ScalingState::Scaling);
ScalingWindow::Get().RestartAfterSrcRepositioned();
}
const int state = GetSrcRepositionState(scalingWindow.SrcTracker().Handle());
if (state == 0) {
// 等待调整完成
MsgWaitForMultipleObjectsEx(0, nullptr, 10, QS_ALLINPUT, MWMO_INPUTAVAILABLE);
} else if (state == 1) {
// 重新缩放
ScalingWindow::Get().RestartAfterSrcRepositioned();
} else {
// 取消缩放
ScalingWindow::Get().CleanAfterSrcRepositioned();
_State(ScalingState::Idle);
}
} else {
_State(ScalingState::Idle);
lastRenderTime = {};
WaitMessage();
}
@ -225,9 +216,9 @@ const winrt::DispatcherQueue& ScalingRuntime::_Dispatcher() noexcept {
return _dispatcher;
}
void ScalingRuntime::_State(ScalingState value) {
if (_state.exchange(value, std::memory_order_relaxed) != value) {
StateChanged.Invoke(value);
void ScalingRuntime::_IsScaling(bool value) {
if (_isScaling.exchange(value, std::memory_order_relaxed) != value) {
IsScalingChanged.Invoke(value);
}
}

View file

@ -27,10 +27,6 @@ static void InitMessage() noexcept {
}();
}
static bool IsTopmostWindow(HWND hWnd) noexcept {
return GetWindowExStyle(hWnd) & WS_EX_TOPMOST;
}
ScalingWindow::ScalingWindow() noexcept :
_resourceLoader(winrt::ResourceLoader::GetForViewIndependentUse(CommonSharedConstants::APP_RESOURCE_MAP_ID)) {}
@ -52,10 +48,8 @@ static void LogRects(const RECT& srcRect, const RECT& rendererRect, const RECT&
ScalingError ScalingWindow::_StartImpl(HWND hwndSrc) noexcept {
Logger::Get().Info(fmt::format("缩放开始\n\t程序版本: {}\n\tOS 版本: {}\n\t管理员: {}",
#ifdef MP_VERSION_STRING
STRINGIFY(MP_VERSION_STRING),
#elif defined(MP_COMMIT_ID)
"dev (" STRINGIFY(MP_COMMIT_ID) ")",
#ifdef MP_VERSION_TAG
STRING(MP_VERSION_TAG),
#else
"dev",
#endif
@ -92,20 +86,11 @@ ScalingError ScalingWindow::_StartImpl(HWND hwndSrc) noexcept {
InitMessage();
bool isSrcInvisibleOrMinimized = false;
if (ScalingError error = _srcTracker.Set(hwndSrc, _options, isSrcInvisibleOrMinimized);
error != ScalingError::NoError
) {
if (ScalingError error = _srcTracker.Set(hwndSrc, _options); error != ScalingError::NoError) {
Logger::Get().Error("初始化 SrcTracker 失败");
return error;
}
if (isSrcInvisibleOrMinimized || (_srcTracker.IsMoving() && !_options.IsWindowedMode())) {
// 等待源窗口状态改变
_isSrcRepositioning = true;
return ScalingError::NoError;
}
if (_srcTracker.IsZoomed()) {
if (_options.IsWindowedMode()) {
Logger::Get().Info("已最大化的窗口不支持窗口模式缩放");
@ -116,6 +101,11 @@ ScalingError ScalingWindow::_StartImpl(HWND hwndSrc) noexcept {
}
}
if (_srcTracker.IsMoving() && !_options.IsWindowedMode()) {
_isSrcRepositioning = true;
return ScalingError::NoError;
}
[[maybe_unused]] static Ignore _ = []() {
WNDCLASSEXW wcex{
.cbSize = sizeof(wcex),
@ -137,7 +127,7 @@ ScalingError ScalingWindow::_StartImpl(HWND hwndSrc) noexcept {
const bool isWin11 = Win32Helper::GetOSVersion().IsWin11();
// 不存在非客户区,渲染无需创建在子窗口里
const bool isAllClient = !isWin11 &&
(srcWindowKind == SrcWindowKind::NoBorder || srcWindowKind == SrcWindowKind::NoNativeFrame);
(srcWindowKind == SrcWindowKind::NoBorder || srcWindowKind == SrcWindowKind::NoDecoration);
if (_options.IsWindowedMode()) {
const RECT& srcWindowRect = _srcTracker.WindowRect();
@ -153,8 +143,8 @@ ScalingError ScalingWindow::_StartImpl(HWND hwndSrc) noexcept {
} else {
GetDpiForMonitor(hMon, MDT_EFFECTIVE_DPI, &_currentDpi, &_currentDpi);
if (isWin11 && srcWindowKind == SrcWindowKind::NoNativeFrame) {
// NoNativeFrame 在 Win11 中和 NoTitleBar 一样处理并禁用边框,优点是只需一个辅助窗口
if (isWin11 && srcWindowKind == SrcWindowKind::NoDecoration) {
// NoDecoration 在 Win11 中和 NoTitleBar 一样处理并禁用边框,优点是只需一个辅助窗口
_topBorderThicknessInClient = 0;
} else {
_topBorderThicknessInClient = Win32Helper::GetNativeWindowBorderThickness(_currentDpi);
@ -304,7 +294,7 @@ ScalingError ScalingWindow::_StartImpl(HWND hwndSrc) noexcept {
if (!_options.RealIsAllowScalingMaximized()) {
// 检查源窗口是否是无边框全屏窗口
if (srcWindowKind == SrcWindowKind::NoNativeFrame && _srcTracker.WindowRect() == _rendererRect) {
if (srcWindowKind == SrcWindowKind::NoDecoration && _srcTracker.WindowRect() == _rendererRect) {
Logger::Get().Info("源窗口已全屏");
return ScalingError::Maximized;
}
@ -328,18 +318,10 @@ ScalingError ScalingWindow::_StartImpl(HWND hwndSrc) noexcept {
}
void ScalingWindow::Start(HWND hwndSrc, ScalingOptions&& options) noexcept {
assert(!Handle());
assert(!options.effects.empty());
assert(options.cropping.Left >= 0 && options.cropping.Top >= 0 &&
options.cropping.Right >= 0 && options.cropping.Bottom >= 0);
assert(options.minFrameRate >= 0);
assert(!options.maxFrameRate.has_value() || *options.maxFrameRate > 0);
assert(options.cursorScaling >= 0);
assert(!options.autoHideCursorDelay.has_value() || *options.autoHideCursorDelay > 0);
assert(options.initialWindowedScaleFactor >= 0);
assert(!options.screenshotsDir.empty());
assert(options.showToast && options.showError && options.save);
if (Handle()) {
options.showError(hwndSrc, ScalingError::ScalingFailedGeneral);
return;
}
options.Log();
// 缩放结束后失效
@ -359,7 +341,7 @@ void ScalingWindow::Stop() noexcept {
CleanAfterSrcRepositioned();
}
void ScalingWindow::ToggleScaling(bool isWindowedMode) noexcept {
void ScalingWindow::SwitchScalingState(bool isWindowedMode) noexcept {
assert(Handle());
if (_options.IsWindowedMode() == isWindowedMode || !_srcTracker.IsFocused()) {
@ -386,15 +368,16 @@ void ScalingWindow::SwitchToolbarState() noexcept {
void ScalingWindow::Render() noexcept {
bool isSrcRepositioning = false;
bool srcFocusedChanged = false;
if (!_UpdateSrcState(isSrcRepositioning, srcFocusedChanged)) {
bool srcOwnedWindowFocusedChanged = false;
if (!_UpdateSrcState(isSrcRepositioning, srcFocusedChanged, srcOwnedWindowFocusedChanged)) {
Logger::Get().Info("源窗口状态改变");
_DelayedStop(false, isSrcRepositioning);
return;
}
if (srcFocusedChanged) {
_UpdateFocusStateAsync();
}
if (srcFocusedChanged || srcOwnedWindowFocusedChanged) {
_UpdateFocusStateAsync(!srcFocusedChanged && srcOwnedWindowFocusedChanged, false);
}
// 虽然可以在第一帧渲染完成后再隐藏系统光标,但某些设备上显示窗口时光标状态会变成忙,
// 提前隐藏光标可以提高观感。缩放窗口显示后再隐藏光标还可能造成光标闪烁两次,第一次是
@ -461,8 +444,8 @@ LRESULT ScalingWindow::_MessageHandler(UINT msg, WPARAM wParam, LPARAM lParam) n
DwmExtendFrameIntoClientArea(Handle(), &margins);
}
if (_srcTracker.WindowKind() == SrcWindowKind::NoNativeFrame && Win32Helper::GetOSVersion().IsWin11()) {
// Win11 中禁用边框和圆角以模仿 NoNativeFrame 的样式
if (_srcTracker.WindowKind() == SrcWindowKind::NoDecoration && Win32Helper::GetOSVersion().IsWin11()) {
// Win11 中禁用边框和圆角以模仿 NoDecoration 的样式
COLORREF color = DWMWA_COLOR_NONE;
DwmSetWindowAttribute(Handle(), DWMWA_BORDER_COLOR, &color, sizeof(color));
@ -484,20 +467,6 @@ LRESULT ScalingWindow::_MessageHandler(UINT msg, WPARAM wParam, LPARAM lParam) n
{
// 调整窗口大小时会进入 OS 的内部循环,我们的消息循环没有机会调用 Render。幸运的是
// 内部循环会正常分发消息,因此有必要在窗口过程中执行渲染以避免调整大小时渲染暂停。
if (!_renderer) {
return 0;
}
// 删除消息队列中的其他 WM_FRONTEND_RENDER 以避免重复渲染
{
MSG msg1;
while (PeekMessage(&msg1, Handle(), CommonSharedConstants::WM_FRONTEND_RENDER,
CommonSharedConstants::WM_FRONTEND_RENDER, PM_REMOVE)
) {
// 不做处理
}
}
Render();
return 0;
}
@ -725,18 +694,6 @@ LRESULT ScalingWindow::_MessageHandler(UINT msg, WPARAM wParam, LPARAM lParam) n
{
WINDOWPOS& windowPos = *(WINDOWPOS*)lParam;
// 阻止 OS 修改置顶状态。当源窗口中途置顶/取消置顶时OS 会试图修改缩放窗口的置顶
// 状态,这不是我们想要的。
if (!(windowPos.flags & SWP_NOZORDER) && !_options.IsDebugMode()) {
if (_CalcTopmostState()) {
if (windowPos.hwndInsertAfter != HWND_TOP) {
windowPos.hwndInsertAfter = HWND_TOPMOST;
}
} else if (windowPos.hwndInsertAfter == HWND_TOPMOST) {
windowPos.hwndInsertAfter = HWND_NOTOPMOST;
}
}
// 如果全屏模式缩放包含 WS_MAXIMIZE 样式,创建窗口时将收到 WM_WINDOWPOSCHANGING
// 应该忽略。
if (!_renderer || (windowPos.flags & (SWP_NOSIZE | SWP_NOMOVE)) == (SWP_NOSIZE | SWP_NOMOVE)) {
@ -1125,15 +1082,24 @@ ScalingError ScalingWindow::_CalcFullscreenRendererRect(uint32_t& monitorCount)
}
}
// 全屏模式缩放无需保持比例,但要限制最大尺寸
// 全屏模式缩放无需保持比例,但要限制最小和最大尺寸
SIZE ScalingWindow::_AdjustFullscreenWindowSize(SIZE size, uint32_t dpi) const noexcept {
if (dpi == 0) {
dpi = _currentDpi;
}
const RECT& srcFrameRect = _srcTracker.WindowFrameRect();
const LONG spaceAround = lroundf(WINDOWED_MODE_MIN_SPACE_AROUND *
dpi / float(USER_DEFAULT_SCREEN_DPI));
const LONG minWidth = srcFrameRect.right - srcFrameRect.left + spaceAround;
const LONG minHeight = srcFrameRect.bottom - srcFrameRect.top + spaceAround;
const LONG maxWidth = GetSystemMetricsForDpi(SM_CXMAXTRACK, dpi);
const LONG maxHeight = GetSystemMetricsForDpi(SM_CYMAXTRACK, dpi);
return SIZE{ std::clamp(size.cx, 1l, maxWidth), std::clamp(size.cy, 1l, maxHeight) };
return SIZE{
std::clamp(size.cx, minWidth, maxWidth),
std::clamp(size.cy, minHeight, maxHeight)
};
}
ScalingError ScalingWindow::_InitialMoveSrcWindowInFullscreen() noexcept {
@ -1215,7 +1181,7 @@ void ScalingWindow::_Show() noexcept {
Handle(),
NULL,
0, 0, 0, 0,
SWP_SHOWWINDOW | SWP_NO_ACTIVATE_MOVE_SIZE
SWP_SHOWWINDOW | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE
);
// 广播开始缩放
@ -1229,7 +1195,7 @@ void ScalingWindow::_Show() noexcept {
// 如果源窗口位于前台则将缩放窗口置顶
if (_srcTracker.IsFocused()) {
_UpdateFocusStateAsync();
_UpdateFocusStateAsync(false, true);
}
if (_options.IsTouchSupportEnabled()) {
@ -1285,7 +1251,8 @@ void ScalingWindow::_MoveRenderer() noexcept {
bool ScalingWindow::_UpdateSrcState(
bool& isSrcRepositioning,
bool& srcFocusedChanged
bool& srcFocusedChanged,
bool& srcOwnedWindowFocusedChanged
) noexcept {
HWND hwndFore = GetForegroundWindow();
@ -1302,23 +1269,24 @@ bool ScalingWindow::_UpdateSrcState(
return false;
}
bool isSrcInvisibleOrMinimized = false;
bool srcMinimized = false;
bool srcRectChanged = false;
bool srcSizeChanged = false;
bool srcMovingChanged = false;
if (!_srcTracker.UpdateState(hwndFore, _options.IsWindowedMode(), _isResizingOrMoving,
isSrcInvisibleOrMinimized, srcFocusedChanged, srcRectChanged, srcSizeChanged, srcMovingChanged)) {
srcFocusedChanged, srcOwnedWindowFocusedChanged,
srcMinimized, srcRectChanged, srcSizeChanged, srcMovingChanged)) {
return false;
}
if (isSrcInvisibleOrMinimized || srcSizeChanged || (!_options.IsWindowedMode() && srcRectChanged)) {
if (srcMinimized || srcSizeChanged || (!_options.IsWindowedMode() && srcRectChanged)) {
// 不要立刻设置 _isSrcSizing销毁窗口是异步的
isSrcRepositioning = true;
if (srcSizeChanged) {
// 源窗口大小改变则清除记忆
_lastWindowedRendererWidth = 0;
} else if (isSrcInvisibleOrMinimized) {
} else if (srcMinimized) {
if (_options.IsWindowedMode()) {
_lastWindowedRendererWidth = _rendererRect.right - _rendererRect.left;
}
@ -1870,149 +1838,77 @@ void ScalingWindow::_UpdateFrameMargins() const noexcept {
DwmExtendFrameIntoClientArea(Handle(), &margins);
}
winrt::fire_and_forget ScalingWindow::_UpdateFocusStateAsync() const noexcept {
if (_options.IsWindowedMode()) {
winrt::fire_and_forget ScalingWindow::_UpdateFocusStateAsync(
bool onSrcOwnedWindowFocusedChanged,
bool onShow
) const noexcept {
if (!onSrcOwnedWindowFocusedChanged && _options.IsWindowedMode()) {
// 根据源窗口状态绘制非客户区,我们必须自己控制非客户区是绘制成焦点状态还是非焦点
// 状态,因为缩放窗口实际上永远不会得到焦点。
DefWindowProc(Handle(), WM_NCACTIVATE, _srcTracker.IsFocused(), 0);
}
if (!_options.IsDebugMode()) {
if (_srcTracker.IsOwnedWindowFocused() ||
(_options.RealIsKeepOnTop() && !onSrcOwnedWindowFocusedChanged))
{
if (!onShow) {
const uint32_t runId = RunId();
// 前台窗口变化后立刻调用 SetWindowPos 有时会导致 Z 顺序混乱,因此等待一段时间
co_await 20ms;
co_await _dispatcher;
if (runId != RunId()) {
co_return;
}
}
if (Win32Helper::IsWindowHung(_srcTracker.Handle())) {
Logger::Get().Error("源窗口已挂起");
_DelayedStop();
Destroy();
co_return;
}
// 这里搞得很复杂,是我反复实验得到的,若要修改应测试下列情形:
// 1. 缩放 WindowCase 中的 TopmostWindow 和 PopupHostWindow
// 2. 缩放常规窗口然后切换到管理员身份的窗口。测试这一条时应直接运行,不要调试,因
// 为调试状态下 SetWindowPos 的行为有变化
// 3. 缩放时将任意窗口最小化然后还原
// 4. 缩放时拖动任意窗口
const bool oldTopmost = IsTopmostWindow(Handle());
const bool newTopmost = _CalcTopmostState();
if (oldTopmost != newTopmost) {
if (newTopmost) {
for (int i = 0; i < 10; ++i) {
SetWindowPos(Handle(), HWND_TOPMOST, 0, 0, 0, 0, SWP_NO_ACTIVATE_MOVE_SIZE);
if (IsTopmostWindow(Handle()) == newTopmost) {
break;
}
}
// 全屏模式缩放时确保缩放窗口在所有置顶窗口之上,这使不支持 MPO 的显卡更容易激
// 活 DirectFlip。
if (!_options.IsWindowedMode()) {
SetWindowPos(Handle(), HWND_TOP, 0, 0, 0, 0, SWP_NO_ACTIVATE_MOVE_SIZE);
if (_srcTracker.IsOwnedWindowFocused()) {
// 前台窗口是源窗口的弹窗则将缩放窗口置于源窗口之前
const HWND hwndPrev = GetWindow(_srcTracker.Handle(), GW_HWNDPREV);
SetWindowPos(Handle(), hwndPrev,
0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE);
} else if (_options.RealIsKeepOnTop() && !onSrcOwnedWindowFocusedChanged) {
// 源窗口位于前台时将缩放窗口置顶
if (_srcTracker.IsFocused()) {
if (!_options.IsDebugMode()) {
SetWindowPos(Handle(), HWND_TOPMOST,
0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE);
}
// 非调试模式时再次调用 SetWindowPos 确保缩放窗口在所有置顶窗口之上
SetWindowPos(Handle(), HWND_TOP,
0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE);
} else {
const uint32_t runId = ScalingWindow::RunId();
bool isInBackground = false;
HWND hwndFore = GetForegroundWindow();
if (!hwndFore) {
// 切换窗口时有一个瞬间无前台窗口,这里等待切换完成
co_await winrt::resume_after(1ms);
isInBackground = true;
hwndFore = GetForegroundWindow();
}
bool isForeMovable = true;
if (hwndFore) {
DWORD windowIL;
isForeMovable = Win32Helper::GetWindowIntegrityLevel(hwndFore, windowIL) &&
windowIL <= Win32Helper::GetCurrentProcessIntegrityLevel();
}
if (!isForeMovable) {
if (!isInBackground) {
co_await winrt::resume_background();
isInBackground = true;
}
// 等待 DWM 开始合成新帧以避免显示中间状态
Win32Helper::WaitForDwmComposition();
}
if (isInBackground) {
co_await ScalingWindow::Get().Dispatcher();
// 等待时源窗口重新回到前台了应放弃后续操作
if (runId != ScalingWindow::RunId() || _srcTracker.IsFocused()) {
co_return;
}
}
for (int i = 0; i < 10; ++i) {
HDWP hDwp = BeginDeferWindowPos(isForeMovable ? 2 : 3);
// 改变置顶状态
hDwp = DeferWindowPos(hDwp, Handle(), HWND_NOTOPMOST,
0, 0, 0, 0, SWP_NO_ACTIVATE_MOVE_SIZE);
// 将缩放窗口恰好置于源窗口前
if (isForeMovable) {
// 这个方式没有中间状态,但会导致源窗口遮挡前台窗口,之后会手动将前台窗口移到顶部
hDwp = DeferWindowPos(hDwp, Handle(), _srcTracker.Handle(),
0, 0, 0, 0, SWP_NO_ACTIVATE_MOVE_SIZE);
} else {
// 这个方式不会移动源窗口,但有中间状态。如果前台窗口 IL 更高,这是唯一的办法
hDwp = DeferWindowPos(hDwp, Handle(), _srcTracker.Handle(),
0, 0, 0, 0, SWP_NO_ACTIVATE_MOVE_SIZE | SWP_NOOWNERZORDER);
hDwp = DeferWindowPos(hDwp, _srcTracker.Handle(), Handle(),
0, 0, 0, 0, SWP_NO_ACTIVATE_MOVE_SIZE | SWP_NOOWNERZORDER);
}
EndDeferWindowPos(hDwp);
// 确保缩放窗口刚好在源窗口前
if (IsTopmostWindow(Handle()) == newTopmost &&
GetWindow(_srcTracker.Handle(), GW_HWNDPREV) == Handle()) {
break;
}
}
if (isForeMovable && hwndFore && GetForegroundWindow() == hwndFore) {
SetWindowPos(hwndFore, HWND_TOP, 0, 0, 0, 0, SWP_NO_ACTIVATE_MOVE_SIZE);
}
SetWindowPos(Handle(), HWND_NOTOPMOST,
0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE);
}
}
}
if (_srcTracker.IsFocused()) {
PostMessage(HWND_BROADCAST, WM_MAGPIE_SCALINGCHANGED, 1, (LPARAM)Handle());
} else {
// lParam 传 1 表示转到后台而非结束缩放
PostMessage(HWND_BROADCAST, WM_MAGPIE_SCALINGCHANGED, 0, 1);
if (!onSrcOwnedWindowFocusedChanged) {
if (_srcTracker.IsFocused()) {
PostMessage(HWND_BROADCAST, WM_MAGPIE_SCALINGCHANGED, 1, (LPARAM)Handle());
} else {
// lParam 传 1 表示转到后台而非结束缩放
PostMessage(HWND_BROADCAST, WM_MAGPIE_SCALINGCHANGED, 0, 1);
}
}
}
bool ScalingWindow::_CalcTopmostState() const noexcept {
// 源窗口置顶时缩放窗口必须置顶
if (IsTopmostWindow(_srcTracker.Handle())) {
return true;
}
// 源窗口位于前台时一般将缩放窗口置顶,这是为了防止有些窗口突破 OS 维护的所有者关系
// 顺序,如 GH#1232。一个例外是源窗口有弹窗时缩放窗口应在弹窗下方除了常规弹窗
// 应检查模拟模态弹窗(见 ScalingService.cpp 的 IsPopupWindow
return !_options.IsTopmostDisabled() &&
_srcTracker.IsFocused() &&
!GetWindow(_srcTracker.Handle(), GW_ENABLEDPOPUP) &&
IsWindowEnabled(_srcTracker.Handle());
}
bool ScalingWindow::_IsBorderless() const noexcept {
assert(_options.IsWindowedMode());
const SrcWindowKind srcWindowKind = _srcTracker.WindowKind();
// NoBorder: Win11 中这类窗口有着特殊的边框,因此和 Win10 的处理方式相同。
// NoNativeFrame: Win11 中实现为无标题栏并隐藏边框。
// NoDecoration: Win11 中实现为无标题栏并隐藏边框。
return srcWindowKind == SrcWindowKind::NoBorder ||
(srcWindowKind == SrcWindowKind::NoNativeFrame && Win32Helper::GetOSVersion().IsWin10());
(srcWindowKind == SrcWindowKind::NoDecoration && Win32Helper::GetOSVersion().IsWin10());
}
void ScalingWindow::_UpdateRendererRect() noexcept {
@ -2031,8 +1927,7 @@ void ScalingWindow::_UpdateRendererRect() noexcept {
const bool resized = Win32Helper::GetSizeOfRect(_rendererRect) !=
Win32Helper::GetSizeOfRect(oldRendererRect);
// 全屏模式缩放时不移动源窗口,因为我们不限制最小尺寸,而且源窗口可能处于最大化或全屏状态
if (_options.IsWindowedMode() && !_isMovingDueToSrcMoved && !_srcTracker.IsMoving()) {
if (!_isMovingDueToSrcMoved && !_srcTracker.IsMoving()) {
// 确保源窗口中心点和缩放窗口中心点相同。应先移动源窗口,因为之后需要调整光标位置
const RECT& srcRect = _srcTracker.WindowRect();
const int offsetX = (_windowRect.left + _windowRect.right - srcRect.left - srcRect.right) / 2;
@ -2069,7 +1964,7 @@ void ScalingWindow::_UpdateRendererRect() noexcept {
}
// OS 有类似的机制,但我们很少能触发,只能自己处理。参考自
// https://github.com/Blinue/nt5src/blob/daad8a087a4e75422ec96b7911f1df4669989611/Source/XPSP1/NT/windows/core/ntuser/kernel/movesize.c#L592
// https://github.com/tongzx/nt5src/blob/daad8a087a4e75422ec96b7911f1df4669989611/Source/XPSP1/NT/windows/core/ntuser/kernel/movesize.c#L592
bool ScalingWindow::_EnsureCaptionVisibleOnScreen() noexcept {
struct EnumData {
RECT windowRect;
@ -2143,10 +2038,10 @@ void ScalingWindow::_UpdateWindowRectFromWindowPos(const WINDOWPOS& windowPos) n
void ScalingWindow::_DelayedStop(bool onSrcHung, bool onSrcRepositioning) const noexcept {
if (!onSrcHung) {
const HWND hwndSrc = _srcTracker.Handle();
if (IsTopmostWindow(Handle()) && !(IsWindow(hwndSrc) && Win32Helper::IsWindowHung(hwndSrc))) {
if (!(IsWindow(hwndSrc) && Win32Helper::IsWindowHung(hwndSrc))) {
// 提前取消置顶,这样销毁时出现问题不会影响和桌面环境交互
SetWindowPos(Handle(), HWND_NOTOPMOST, 0, 0, 0, 0,
SWP_NO_ACTIVATE_MOVE_SIZE | SWP_NOOWNERZORDER);
SetWindowPos(Handle(), HWND_NOTOPMOST,
0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE);
}
}

View file

@ -34,7 +34,7 @@ public:
void Stop() noexcept;
void ToggleScaling(bool isWindowedMode) noexcept;
void SwitchScalingState(bool isWindowedMode) noexcept;
void SwitchToolbarState() noexcept;
@ -48,11 +48,7 @@ public:
return _options;
}
class SrcTracker& SrcTracker() noexcept {
return _srcTracker;
}
const class SrcTracker& SrcTracker() const noexcept {
SrcTracker& SrcTracker() noexcept {
return _srcTracker;
}
@ -60,15 +56,7 @@ public:
return *_renderer;
}
const class Renderer& Renderer() const noexcept {
return *_renderer;
}
class CursorManager& CursorManager() noexcept {
return *_cursorManager;
}
const class CursorManager& CursorManager() const noexcept {
CursorManager& CursorManager() noexcept {
return *_cursorManager;
}
@ -120,7 +108,8 @@ private:
bool _UpdateSrcState(
bool& isSrcRepositioning,
bool& srcFocusedChanged
bool& srcFocusedChanged,
bool& srcOwnedWindowFocusedChanged
) noexcept;
bool _CheckForegroundFor3DGameMode(HWND hwndFore) const noexcept;
@ -151,9 +140,10 @@ private:
void _UpdateFrameMargins() const noexcept;
winrt::fire_and_forget _UpdateFocusStateAsync() const noexcept;
bool _CalcTopmostState() const noexcept;
winrt::fire_and_forget _UpdateFocusStateAsync(
bool onSrcOwnedWindowFocusedChanged,
bool onShow
) const noexcept;
bool _IsBorderless() const noexcept;

View file

@ -11,6 +11,32 @@
namespace Magpie {
static bool GetWindowIntegrityLevel(HWND hWnd, DWORD& integrityLevel) noexcept {
wil::unique_process_handle hProc = Win32Helper::GetWindowProcessHandle(hWnd);
if (!hProc) {
Logger::Get().Error("GetWindowProcessHandle 失败");
return false;
}
wil::unique_handle hQueryToken;
if (!OpenProcessToken(hProc.get(), TOKEN_QUERY, hQueryToken.put())) {
Logger::Get().Win32Error("OpenProcessToken 失败");
return false;
}
return Win32Helper::GetProcessIntegrityLevel(hQueryToken.get(), integrityLevel);
}
static bool CheckIL(HWND hwndSrc) noexcept {
static DWORD thisIL = []() -> DWORD {
DWORD il;
return Win32Helper::GetProcessIntegrityLevel(NULL, il) ? il : 0;
}();
DWORD windowIL;
return GetWindowIntegrityLevel(hwndSrc, windowIL) && windowIL <= thisIL;
}
static bool IsWindowMoving(HWND hWnd) noexcept {
GUITHREADINFO guiThreadInfo{ .cbSize = sizeof(GUITHREADINFO) };
if (GetGUIThreadInfo(GetWindowThreadProcessId(hWnd, nullptr), &guiThreadInfo)) {
@ -21,9 +47,7 @@ static bool IsWindowMoving(HWND hWnd) noexcept {
}
}
ScalingError SrcTracker::Set(HWND hWnd, const ScalingOptions& options, bool& isInvisibleOrMinimized) noexcept {
assert(!isInvisibleOrMinimized);
ScalingError SrcTracker::Set(HWND hWnd, const ScalingOptions& options) noexcept {
_hWnd = hWnd;
// 这里不检查源窗口是否挂起,将在创建缩放窗口前检查
@ -33,34 +57,19 @@ ScalingError SrcTracker::Set(HWND hWnd, const ScalingOptions& options, bool& isI
return ScalingError::InvalidSourceWindow;
}
// 不可见和最小化的窗口将等待源窗口状态改变,这里提前返回。注意 showCmd 不能准确
// 判断窗口可见性,应使用 IsWindowVisible。
if (!IsWindowVisible(_hWnd)) {
isInvisibleOrMinimized = true;
return ScalingError::NoError;
Logger::Get().Error("不支持缩放隐藏的窗口");
return ScalingError::InvalidSourceWindow;
}
const UINT showCmd = Win32Helper::GetWindowShowCmd(hWnd);
if (showCmd == SW_SHOWMINIMIZED) {
isInvisibleOrMinimized = true;
return ScalingError::NoError;
}
_isMaximized = showCmd == SW_SHOWMAXIMIZED;
if (Win32Helper::GetWindowClassName(hWnd) == L"Ghost") {
Logger::Get().Error("不支持缩放幽灵窗口");
return ScalingError::InvalidSourceWindow;
}
// 检查 integrity level
{
DWORD windowIL;
if (!Win32Helper::GetWindowIntegrityLevel(hWnd, windowIL) ||
windowIL > Win32Helper::GetCurrentProcessIntegrityLevel()) {
Logger::Get().Error("不支持缩放 IL 更高的窗口");
return ScalingError::LowIntegrityLevel;
}
if (!CheckIL(hWnd)) {
Logger::Get().Error("不支持缩放 IL 更高的窗口");
return ScalingError::LowIntegrityLevel;
}
// 已在 ScalingService 中阻止
@ -77,7 +86,10 @@ ScalingError SrcTracker::Set(HWND hWnd, const ScalingOptions& options, bool& isI
return ScalingError::InvalidSourceWindow;
}
_isFocused = GetForegroundWindow() == hWnd;
const HWND hwndFore = GetForegroundWindow();
_isFocused = hwndFore == hWnd;
_UpdateIsOwnedWindowFocused(hwndFore);
_isMoving = IsWindowMoving(_hWnd);
if (!GetWindowRect(hWnd, &_windowRect)) {
@ -98,15 +110,21 @@ ScalingError SrcTracker::Set(HWND hWnd, const ScalingOptions& options, bool& isI
return ScalingError::ScalingFailedGeneral;
}
const UINT showCmd = Win32Helper::GetWindowShowCmd(hWnd);
if (showCmd == SW_SHOWMINIMIZED) {
Logger::Get().Error("不支持缩放最小化的窗口");
return ScalingError::InvalidSourceWindow;
}
_isMaximized = showCmd == SW_SHOWMAXIMIZED;
// 计算窗口样式
BOOL hasBorder = TRUE;
bool hasCustomNonclient = false;
hr = DwmGetWindowAttribute(hWnd, DWMWA_NCRENDERING_ENABLED, &hasBorder, sizeof(hasBorder));
if (FAILED(hr) || !hasBorder) {
// 无原生框架要么是因为无 WS_CAPTION 和 WS_THICKFRAME 样式,要么禁用了原生窗口框架
// 以自绘标题栏和边框。
_windowKind = SrcWindowKind::NoNativeFrame;
hasCustomNonclient = GetWindowStyle(hWnd) & (WS_CAPTION | WS_THICKFRAME);
// 凡是没有原生框架的窗口都视为 NoDecoration这类窗口可能没有 WS_CAPTION 和 WS_THICKFRAME 样式,
// 或者禁用了原生窗口框架以自绘标题栏和边框。
_windowKind = SrcWindowKind::NoDecoration;
} else {
// 最大化窗口的上边框很可能存在非客户区,这使得 NoTitleBar 类型的窗口最大化时会被归类到 Native。
// 技术上说这很合理,上边框处的非客户区当然可以视为标题栏,对后续计算也没有影响。
@ -120,7 +138,7 @@ ScalingError SrcTracker::Set(HWND hWnd, const ScalingOptions& options, bool& isI
// 一个窗口要么有边框,要么没有。只要左右下三边中有一条边没有边框,我们就将它视为无边框窗口。
//
// FIXME: 有的窗口(如微信)通过 WM_NCCALCSIZE 移除边框,但不使用 DwmExtendFrameIntoClientArea
// 还原阴影,这种窗口实际上是 NoNativeFrame 类型。遗憾的是没有办法获知窗口是否调用了
// 还原阴影,这种窗口实际上是 NoDecoration 类型。遗憾的是没有办法获知窗口是否调用了
// DwmExtendFrameIntoClientArea因此我们假设所有使用 WM_NCCALCSIZE 移除边框的窗口都有阴影,
// 一方面有阴影的情况更多,比如基于 electron 的窗口,另一方面如果假设没有阴影会使得 Win11 中
// 不能正确裁剪边框导致黑边,而如果假设有阴影,猜错的后果相对较轻。
@ -137,11 +155,11 @@ ScalingError SrcTracker::Set(HWND hWnd, const ScalingOptions& options, bool& isI
}
}
// * 最大化窗口、NoNativeFrame 窗口和 Win10 中 NoBorder 窗口不存在边框
// * 最大化窗口、NoDecoration 窗口和 Win10 中 NoBorder 窗口不存在边框
LONG borderThicknessInFrame = 0;
const bool isWin11 = Win32Helper::GetOSVersion().IsWin11();
if (!_isMaximized &&
_windowKind != SrcWindowKind::NoNativeFrame &&
_windowKind != SrcWindowKind::NoDecoration &&
(_windowKind != SrcWindowKind::NoBorder || isWin11))
{
// 使用屏幕而非窗口的 DPI 来计算边框宽度
@ -151,7 +169,7 @@ ScalingError SrcTracker::Set(HWND hWnd, const ScalingOptions& options, bool& isI
borderThicknessInFrame = (LONG)Win32Helper::GetNativeWindowBorderThickness(dpi);
}
return _CalcSrcRect(options, hasCustomNonclient, borderThicknessInFrame);
return _CalcSrcRect(options, borderThicknessInFrame);
}
static bool IsPrimaryMouseButtonDown() noexcept {
@ -164,36 +182,27 @@ bool SrcTracker::UpdateState(
HWND hwndFore,
bool isWindowedMode,
bool isResizingOrMoving,
bool& isInvisibleOrMinimized,
bool& focusedChanged,
bool& ownedWindowFocusedChanged,
bool& minimized,
bool& rectChanged,
bool& sizeChanged,
bool& movingChanged
) noexcept {
assert(!isInvisibleOrMinimized && !focusedChanged &&
!rectChanged && !sizeChanged && !movingChanged);
assert(!focusedChanged && !ownedWindowFocusedChanged && !rectChanged && !sizeChanged && !movingChanged);
if (!IsWindow(_hWnd)) {
Logger::Get().Info("源窗口已销毁");
return false;
}
isInvisibleOrMinimized = !IsWindowVisible(_hWnd);
// 缩放中途对源窗口响应性的检查更宽松,即使源窗口挂起一段时间,只要用户不做额
// 外的操作就不会终止缩放,直到源窗口被替换为幽灵窗口。
//
// 不要使用 IsHungAppWindow它有误报的情况见 GH#1244。这里用了未记录函数
// GhostWindowFromHungWindow它可以准确检查源窗口是否已被替换为幽灵窗口。
static const auto ghostWindowFromHungWindow =
Win32Helper::LoadSystemFunction<HWND WINAPI(HWND)>(
L"user32.dll", "GhostWindowFromHungWindow");
if (ghostWindowFromHungWindow && ghostWindowFromHungWindow(_hWnd)) {
// 检查源窗口是否真的处于无响应状态
if (Win32Helper::IsWindowHung(_hWnd)) {
Logger::Get().Info("源窗口已挂起");
return false;
}
// Win32Helper::IsWindowHung 更准确,但它会向源窗口发送消息,比较耗时。
// IsHungAppWindow 的另一个好处是它不如 Win32Helper::IsWindowHung 严
// 格,因此即使源窗口挂起一段时间,只要用户不做额外的操作就不会结束缩放,
// 直到源窗口被替换为幽灵窗口。
if (IsHungAppWindow(_hWnd)) {
Logger::Get().Info("源窗口已挂起");
return false;
}
if (_isFocused != (hwndFore == _hWnd)) {
@ -201,39 +210,29 @@ bool SrcTracker::UpdateState(
focusedChanged = true;
}
const bool oldMaximized = _isMaximized;
ownedWindowFocusedChanged = _UpdateIsOwnedWindowFocused(hwndFore);
WINDOWPLACEMENT wp{ sizeof(wp) };
if (!GetWindowPlacement(_hWnd, &wp)) {
Logger::Get().Win32Error("GetWindowPlacement 失败");
const bool oldMaximized = _isMaximized;
const UINT showCmd = Win32Helper::GetWindowShowCmd(_hWnd);
if (showCmd == SW_HIDE) {
Logger::Get().Info("源窗口已隐藏");
return false;
} else if (showCmd == SW_SHOWMINIMIZED) {
Logger::Get().Info("源窗口已最小化");
_isMaximized = false;
minimized = true;
} else {
_isMaximized = showCmd == SW_SHOWMAXIMIZED;
}
_isMaximized = wp.showCmd == SW_SHOWMAXIMIZED;
RECT curWindowRect;
if (wp.showCmd == SW_SHOWMINIMIZED) {
// 窗口最小化有两步:先将窗口状态设为最小化,然后将窗口移出屏幕 (左上角坐标
// (-32000,-32000))。如果我们刚好在两步之间停止缩放,第二步将无法执行,这和缩
// 放窗口被源窗口所有有关,不确定是否是 OS 的 bug。这个检查确保第二步完成后再
// 停止缩放。
if (wp.rcNormalPosition.left == wp.ptMinPosition.x) {
return true;
}
isInvisibleOrMinimized = true;
// rcNormalPosition 使用工作区坐标,应转换为屏幕坐标
HMONITOR hMon = MonitorFromWindow(_hWnd, MONITOR_DEFAULTTOPRIMARY);
MONITORINFO mi{ sizeof(mi) };
if (!GetMonitorInfo(hMon, &mi)) {
Logger::Get().Win32Error("GetMonitorInfo 失败");
if (minimized) {
WINDOWPLACEMENT wp{ sizeof(wp) };
if (!GetWindowPlacement(_hWnd, &wp)) {
Logger::Get().Win32Error("GetWindowPlacement 失败");
return false;
}
curWindowRect = wp.rcNormalPosition;
Win32Helper::OffsetRect(
curWindowRect, mi.rcWork.left - mi.rcMonitor.left, mi.rcWork.top - mi.rcMonitor.top);
} else {
if (!GetWindowRect(_hWnd, &curWindowRect)) {
Logger::Get().Win32Error("GetWindowRect 失败");
@ -267,7 +266,7 @@ bool SrcTracker::UpdateState(
// 处理自己实现拖拽逻辑的窗口:将鼠标左键按下视为开始拖拽,释放视为拖拽结束。
// 可能会有误判,但幸好后果不太严重。
const bool isMoving = !isInvisibleOrMinimized &&
const bool isMoving = !minimized &&
(IsWindowMoving(_hWnd) || (rectChanged && IsPrimaryMouseButtonDown()));
if (_isMoving != isMoving) {
movingChanged = true;
@ -436,68 +435,20 @@ static bool GetClientRectOfUWP(HWND hWnd, RECT& rect) noexcept {
}
bool SrcTracker::SetFocus() const noexcept {
// 不要把被禁用的窗口设为前台,检查是否存在弹窗
if (IsWindowEnabled(_hWnd)) {
return SetForegroundWindow(_hWnd);
if (_isOwnedWindowFocused) {
return true;
}
// 如果源窗口存在弹窗(即被源窗口所有的窗口),应把弹窗设为前台窗口
const HWND hwndPopup = GetWindow(_hWnd, GW_ENABLEDPOPUP);
if (hwndPopup && IsWindowEnabled(hwndPopup)) {
return SetForegroundWindow(hwndPopup);
}
// 源窗口被禁用视为成功
return true;
return SetForegroundWindow(hwndPopup ? hwndPopup : _hWnd);
}
ScalingError SrcTracker::_CalcSrcRect(
const ScalingOptions& options,
bool hasCustomNonclient,
LONG borderThicknessInFrame
) noexcept {
if (_windowKind == SrcWindowKind::NoNativeFrame) {
if (hasCustomNonclient) {
if (options.RealIsCaptureTitleBar()) {
// 窗口的非客户区是自绘的,无法模拟,因此启用捕获标题栏时捕获整个窗口
_srcRect = _windowRect;
} else {
RECT clientRect;
if (!Win32Helper::GetClientScreenRect(_hWnd, clientRect)) {
Logger::Get().Error("GetClientScreenRect 失败");
return ScalingError::ScalingFailedGeneral;
}
// 如果有滚动条需特殊处理
if (const DWORD srcStyle = GetWindowStyle(_hWnd); srcStyle & (WS_VSCROLL | WS_HSCROLL)) {
// 左右两边不可能都有滚动条,据此找到边框宽度
const LONG borderThickness = std::min(
clientRect.left - _windowRect.left,
_windowRect.right - clientRect.right
);
if (srcStyle & WS_VSCROLL) {
_srcRect.left = _windowRect.left + borderThickness;
_srcRect.right = _windowRect.right - borderThickness;
} else {
_srcRect.left = clientRect.left;
_srcRect.right = clientRect.right;
}
if (srcStyle & WS_HSCROLL) {
_srcRect.bottom = _windowRect.bottom - borderThickness;
} else {
_srcRect.bottom = clientRect.bottom;
}
_srcRect.top = clientRect.top;
} else {
_srcRect = clientRect;
}
}
} else {
// 不存在非客户区
_srcRect = _windowRect;
}
ScalingError SrcTracker::_CalcSrcRect(const ScalingOptions& options, LONG borderThicknessInFrame) noexcept {
if (_windowKind == SrcWindowKind::NoDecoration) {
// NoDecoration 类型的窗口不裁剪非客户区。它们要么没有非客户区,要么非客户区不是由
// DWM 绘制,前者无需裁剪,后者不能裁剪。
_srcRect = _windowRect;
} else {
const bool isCaptureTitleBar = options.RealIsCaptureTitleBar();
@ -505,7 +456,6 @@ ScalingError SrcTracker::_CalcSrcRect(
if (_windowKind == SrcWindowKind::NoTitleBar && !isCaptureTitleBar && GetClientRectOfUWP(_hWnd, _srcRect)) {
_srcRect.top = std::max(_srcRect.top, _windowFrameRect.top + borderThicknessInFrame);
} else {
// 不要使用客户区矩形,它不包含滚动条
_srcRect.left = _windowFrameRect.left + borderThicknessInFrame;
_srcRect.top = _windowFrameRect.top + borderThicknessInFrame;
_srcRect.right = _windowFrameRect.right - borderThicknessInFrame;
@ -557,4 +507,27 @@ ScalingError SrcTracker::_CalcSrcRect(
return ScalingError::NoError;
}
static bool IsOwnedWindow(HWND hwndOwner, HWND hwndTest) noexcept {
HWND hwndCur = hwndTest;
while (bool(hwndCur = GetWindowOwner(hwndCur))) {
if (hwndCur == hwndOwner) {
return true;
}
}
return false;
}
bool SrcTracker::_UpdateIsOwnedWindowFocused(HWND hwndFore) noexcept {
// 支持两种形式的弹窗
// 1. 弹窗被源窗口所有
// 2. 弹窗没有被源窗口所有,但弹出时源窗口被禁用
bool newValue = !_isFocused && (IsOwnedWindow(_hWnd, hwndFore) || !IsWindowEnabled(_hWnd));
if (_isOwnedWindowFocused == newValue) {
return false;
} else {
_isOwnedWindowFocused = newValue;
return true;
}
}
}

View file

@ -13,8 +13,8 @@ enum class SrcWindowKind {
// 无标题栏,是否有边框取决于 OS 版本Win10 中不存在边框Win11 中边框
// 被绘制到客户区内有阴影在窗口内调整大小Win11 中有圆角
NoBorder,
// 无标题栏、边框和阴影在窗口内调整大小Win11 中无圆角。可能自绘非客户区
NoNativeFrame,
// 无标题栏、边框和阴影在窗口内调整大小Win11 中无圆角
NoDecoration,
// 无标题栏系统边框但上边框较粗有阴影左右下三边在窗口外调整大小Win11 中有圆角
OnlyThickFrame
};
@ -27,14 +27,15 @@ public:
SrcTracker(const SrcTracker&) = delete;
SrcTracker(SrcTracker&&) = delete;
ScalingError Set(HWND hWnd, const ScalingOptions& options, bool& isInvisibleOrMinimized) noexcept;
ScalingError Set(HWND hWnd, const ScalingOptions& options) noexcept;
bool UpdateState(
HWND hwndFore,
bool isWindowedMode,
bool isResizingOrMoving,
bool& isInvisibleOrMinimized,
bool& focusedChanged,
bool& ownedWindowFocusedChanged,
bool& minimized,
bool& rectChanged,
bool& sizeChanged,
bool& movingChanged
@ -64,6 +65,10 @@ public:
return _isFocused;
}
bool IsOwnedWindowFocused() const noexcept {
return _isOwnedWindowFocused;
}
bool SetFocus() const noexcept;
// IsMaximized 已定义为宏
@ -81,11 +86,9 @@ public:
}
private:
ScalingError _CalcSrcRect(
const ScalingOptions& options,
bool hasCustomNonclient,
LONG borderThicknessInFrame
) noexcept;
ScalingError _CalcSrcRect(const ScalingOptions& options, LONG borderThicknessInFrame) noexcept;
bool _UpdateIsOwnedWindowFocused(HWND hwndFore) noexcept;
HWND _hWnd = NULL;
RECT _windowRect{};
@ -94,6 +97,7 @@ private:
SrcWindowKind _windowKind = SrcWindowKind::Native;
bool _isFocused = false;
bool _isOwnedWindowFocused = false;
bool _isMaximized = false;
bool _isMoving = false;
};

View file

@ -1,7 +1,6 @@
#include "pch.h"
#include "Win32Helper.h"
#include "StrHelper.h"
#include <dcomp.h>
#include <dwmapi.h>
#include <io.h>
#pragma push_macro("ShellExecute")
@ -291,9 +290,8 @@ int16_t Win32Helper::AdvancedWindowHitTest(HWND hWnd, POINT ptScreen, UINT timeo
}
bool Win32Helper::IsWindowHung(HWND hWnd) noexcept {
// 保险起见不使用 SMTO_ABORTIFHUNG。我不知道 OS 怎么判断线程是否处于无响应
// 状态,考虑到 IsHungAppWindow 有误报的情况 (GH#1244),最好不要依赖。
return 0 == SendMessageTimeout(hWnd, WM_NULL, 0, 0, SMTO_ERRORONEXIT, 500, nullptr);
return 0 == SendMessageTimeout(hWnd, WM_NULL, 0, 0,
SMTO_ABORTIFHUNG | SMTO_ERRORONEXIT, 500, nullptr);
}
bool Win32Helper::ReadFile(const wchar_t* fileName, std::vector<uint8_t>& result) noexcept {
@ -698,30 +696,6 @@ bool Win32Helper::GetProcessIntegrityLevel(HANDLE hQueryToken, DWORD& integrityL
return true;
}
DWORD Win32Helper::GetCurrentProcessIntegrityLevel() noexcept {
static DWORD result = []() -> DWORD {
DWORD il;
return Win32Helper::GetProcessIntegrityLevel(NULL, il) ? il : 0;
}();
return result;
}
bool Win32Helper::GetWindowIntegrityLevel(HWND hWnd, DWORD& integrityLevel) noexcept {
wil::unique_process_handle hProc = GetWindowProcessHandle(hWnd);
if (!hProc) {
Logger::Get().Error("GetWindowProcessHandle 失败");
return false;
}
wil::unique_handle hQueryToken;
if (!OpenProcessToken(hProc.get(), TOKEN_QUERY, hQueryToken.put())) {
Logger::Get().Win32Error("OpenProcessToken 失败");
return false;
}
return GetProcessIntegrityLevel(hQueryToken.get(), integrityLevel);
}
static winrt::com_ptr<IShellView> FindDesktopFolderView() noexcept {
winrt::com_ptr<IShellWindows> shellWindows =
winrt::try_create_instance<IShellWindows>(CLSID_ShellWindows, CLSCTX_LOCAL_SERVER);
@ -881,56 +855,4 @@ const std::filesystem::path& Win32Helper::GetExePath() noexcept {
return result;
}
void Win32Helper::WaitForDwmComposition() noexcept {
// Win11 可以使用准确的 DCompositionWaitForCompositorClock
if (Win32Helper::GetOSVersion().IsWin11()) {
static const auto dCompositionWaitForCompositorClock =
Win32Helper::LoadSystemFunction<decltype(DCompositionWaitForCompositorClock)>(
L"dcomp.dll", "DCompositionWaitForCompositorClock");
if (dCompositionWaitForCompositorClock) {
dCompositionWaitForCompositorClock(0, nullptr, INFINITE);
return;
}
}
LARGE_INTEGER qpf;
QueryPerformanceFrequency(&qpf);
qpf.QuadPart /= 10000000;
DWM_TIMING_INFO info{};
info.cbSize = sizeof(info);
DwmGetCompositionTimingInfo(NULL, &info);
LARGE_INTEGER time;
QueryPerformanceCounter(&time);
if (time.QuadPart >= (LONGLONG)info.qpcCompose) {
return;
}
// 提前 1ms 结束然后忙等待
time.QuadPart += 10000;
if (time.QuadPart < (LONGLONG)info.qpcCompose) {
LARGE_INTEGER liDueTime{
.QuadPart = -((LONGLONG)info.qpcCompose - time.QuadPart) / qpf.QuadPart
};
static HANDLE timer = CreateWaitableTimerEx(nullptr, nullptr,
CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
SetWaitableTimerEx(timer, &liDueTime, 0, NULL, NULL, 0, 0);
WaitForSingleObject(timer, INFINITE);
} else {
Sleep(0);
}
while (true) {
QueryPerformanceCounter(&time);
if (time.QuadPart >= (LONGLONG)info.qpcCompose) {
return;
}
Sleep(0);
}
}
}

View file

@ -21,7 +21,6 @@ enum class MultiMonitorUsage {
enum class CursorInterpolationMode {
NearestNeighbor,
Bilinear,
COUNT
};
struct Cropping {
@ -42,17 +41,27 @@ struct GraphicsCardId {
uint32_t deviceId = 0;
};
enum class DestAlignment {
LeftTop,
Top,
RightTop,
Left,
Center,
Right,
LeftBottom,
Bottom,
RightBottom,
COUNT
struct ScalingFlags {
static constexpr uint32_t WindowedMode = 1;
static constexpr uint32_t DebugMode = 1 << 1;
static constexpr uint32_t DisableEffectCache = 1 << 2;
static constexpr uint32_t SaveEffectSources = 1 << 3;
static constexpr uint32_t WarningsAreErrors = 1 << 4;
static constexpr uint32_t SimulateExclusiveFullscreen = 1 << 5;
static constexpr uint32_t Is3DGameMode = 1 << 6;
static constexpr uint32_t CaptureTitleBar = 1 << 10;
static constexpr uint32_t AdjustCursorSpeed = 1 << 11;
static constexpr uint32_t DisableDirectFlip = 1 << 13;
static constexpr uint32_t DisableFontCache = 1 << 14;
static constexpr uint32_t AllowScalingMaximized = 1 << 15;
static constexpr uint32_t EnableStatisticsForDynamicDetection = 1 << 16;
// 只影响缩放行为Magpie.Core 不负责启动 TouchHelper.exe
static constexpr uint32_t TouchSupportEnabled = 1 << 17;
static constexpr uint32_t InlineParams = 1 << 18;
static constexpr uint32_t FP16Disabled = 1 << 19;
static constexpr uint32_t BenchmarkMode = 1 << 20;
static constexpr uint32_t DeveloperMode = 1 << 21;
static constexpr uint32_t KeepOnTop = 1 << 22;
};
enum class ScalingType {
@ -146,36 +155,12 @@ enum class ScalingError {
CreateFenceFailed
};
struct ScalingFlags {
static constexpr uint32_t WindowedMode = 1;
static constexpr uint32_t DebugMode = 1 << 1;
static constexpr uint32_t DisableEffectCache = 1 << 2;
static constexpr uint32_t SaveEffectSources = 1 << 3;
static constexpr uint32_t WarningsAreErrors = 1 << 4;
static constexpr uint32_t SimulateExclusiveFullscreen = 1 << 5;
static constexpr uint32_t Is3DGameMode = 1 << 6;
static constexpr uint32_t CaptureTitleBar = 1 << 10;
static constexpr uint32_t AdjustCursorSpeed = 1 << 11;
static constexpr uint32_t DisableDirectFlip = 1 << 13;
static constexpr uint32_t DisableFontCache = 1 << 14;
static constexpr uint32_t AllowScalingMaximized = 1 << 15;
static constexpr uint32_t EnableStatisticsForDynamicDetection = 1 << 16;
// 只影响缩放行为Magpie.Core 不负责启动 TouchHelper.exe
static constexpr uint32_t TouchSupportEnabled = 1 << 17;
static constexpr uint32_t InlineParams = 1 << 18;
static constexpr uint32_t DisableFP16 = 1 << 19;
static constexpr uint32_t BenchmarkMode = 1 << 20;
static constexpr uint32_t DeveloperMode = 1 << 21;
static constexpr uint32_t DisableTopmost = 1 << 22;
};
struct ScalingOptions {
DEFINE_FLAG_ACCESSOR(IsWindowedMode, ScalingFlags::WindowedMode, flags)
DEFINE_FLAG_ACCESSOR(IsDeveloperMode, ScalingFlags::DeveloperMode, flags)
DEFINE_FLAG_ACCESSOR(IsDebugMode, ScalingFlags::DebugMode, flags)
DEFINE_FLAG_ACCESSOR(IsBenchmarkMode, ScalingFlags::BenchmarkMode, flags)
DEFINE_FLAG_ACCESSOR(IsTopmostDisabled, ScalingFlags::DisableTopmost, flags)
DEFINE_FLAG_ACCESSOR(IsFP16Disabled, ScalingFlags::DisableFP16, flags)
DEFINE_FLAG_ACCESSOR(IsFP16Disabled, ScalingFlags::FP16Disabled, flags)
DEFINE_FLAG_ACCESSOR(IsEffectCacheDisabled, ScalingFlags::DisableEffectCache, flags)
DEFINE_FLAG_ACCESSOR(IsFontCacheDisabled, ScalingFlags::DisableFontCache, flags)
DEFINE_FLAG_ACCESSOR(IsSaveEffectSources, ScalingFlags::SaveEffectSources, flags)
@ -184,6 +169,7 @@ struct ScalingOptions {
DEFINE_FLAG_ACCESSOR(IsInlineParams, ScalingFlags::InlineParams, flags)
DEFINE_FLAG_ACCESSOR(IsTouchSupportEnabled, ScalingFlags::TouchSupportEnabled, flags)
DEFINE_FLAG_ACCESSOR(IsAllowScalingMaximized, ScalingFlags::AllowScalingMaximized, flags)
DEFINE_FLAG_ACCESSOR(IsKeepOnTop, ScalingFlags::KeepOnTop, flags)
DEFINE_FLAG_ACCESSOR(IsSimulateExclusiveFullscreen, ScalingFlags::SimulateExclusiveFullscreen, flags)
DEFINE_FLAG_ACCESSOR(Is3DGameMode, ScalingFlags::Is3DGameMode, flags)
DEFINE_FLAG_ACCESSOR(IsCaptureTitleBar, ScalingFlags::CaptureTitleBar, flags)
@ -199,9 +185,7 @@ struct ScalingOptions {
float cursorScaling = 1.0f;
CaptureMethod captureMethod = CaptureMethod::GraphicsCapture;
MultiMonitorUsage multiMonitorUsage = MultiMonitorUsage::Closest;
DestAlignment destAlignment = DestAlignment::Center;
CursorInterpolationMode cursorInterpolationMode = CursorInterpolationMode::NearestNeighbor;
std::optional<float> autoHideCursorDelay;
DuplicateFrameDetectionMode duplicateFrameDetectionMode = DuplicateFrameDetectionMode::Dynamic;
ToolbarState fullscreenInitialToolbarState = ToolbarState::AutoHide;
ToolbarState windowedInitialToolbarState = ToolbarState::AutoHide;
@ -227,6 +211,10 @@ struct ScalingOptions {
return IsAllowScalingMaximized() && !IsWindowedMode();
}
bool RealIsKeepOnTop() const noexcept {
return IsKeepOnTop() && !IsWindowedMode();
}
bool RealIsSimulateExclusiveFullscreen() const noexcept {
return IsSimulateExclusiveFullscreen() && !IsWindowedMode();
}

View file

@ -3,31 +3,25 @@
namespace Magpie {
enum class ScalingState {
Idle,
Scaling,
Waiting
};
class ScalingRuntime {
public:
ScalingRuntime();
~ScalingRuntime();
bool Start(HWND hwndSrc, struct ScalingOptions&& options, bool force);
bool Start(HWND hwndSrc, struct ScalingOptions&& options);
void ToggleScaling(bool isWindowedMode);
void SwitchScalingState(bool isWindowedMode);
void SwitchToolbarState();
void Stop();
ScalingState State() const noexcept {
return _state.load(std::memory_order_relaxed);
bool IsScaling() const noexcept {
return _isScaling.load(std::memory_order_relaxed);
}
// 调用者应处理线程同步
MultithreadEvent<ScalingState> StateChanged;
MultithreadEvent<bool> IsScalingChanged;
private:
void _ScalingThreadProc() noexcept;
@ -35,7 +29,7 @@ private:
// 确保 _dispatcher 完成初始化
const winrt::DispatcherQueue& _Dispatcher() noexcept;
void _State(ScalingState value);
void _IsScaling(bool value);
std::thread _scalingThread;
@ -44,7 +38,7 @@ private:
// 只能在主线程访问,省下检查 _dispatcherInitialized 的开销
bool _dispatcherInitializedCache = false;
std::atomic<ScalingState> _state = ScalingState::Idle;
std::atomic<bool> _isScaling = false;
};
}

View file

@ -117,10 +117,6 @@ struct Win32Helper {
static bool GetProcessIntegrityLevel(HANDLE hQueryToken, DWORD& integrityLevel) noexcept;
static DWORD GetCurrentProcessIntegrityLevel() noexcept;
static bool GetWindowIntegrityLevel(HWND hWnd, DWORD& integrityLevel) noexcept;
// VARIANT 封装,自动管理生命周期,比 WIL 提供更多功能
struct Variant : public VARIANT {
Variant() noexcept {
@ -137,7 +133,7 @@ struct Win32Helper {
}
Variant(VARIANT&& varSrc) noexcept {
std::memcpy((VARIANT*)this, &varSrc, sizeof(varSrc));
std::memcpy(this, &varSrc, sizeof(varSrc));
varSrc.vt = VT_EMPTY;
}
@ -170,7 +166,7 @@ struct Win32Helper {
}
Variant& operator=(VARIANT&& other) noexcept {
std::memcpy((VARIANT*)this, &other, sizeof(other));
std::memcpy(this, &other, sizeof(other));
other.vt = VT_EMPTY;
return *this;
}
@ -214,9 +210,6 @@ struct Win32Helper {
// 先转成 void* 以避免警告
return reinterpret_cast<T*>(reinterpret_cast<void*>(address));
}
// 和 DwmFlush 效果相同但更准确
static void WaitForDwmComposition() noexcept;
};
}

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.250303.1" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.260126.7" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.250325.1" targetFramework="native" />
</packages>

View file

@ -1,6 +1,6 @@
SamplerState sam : register(s0);
Texture2D tex : register(t0);
float4 main(noperspective float2 coord : TEXCOORD, noperspective float4 color : COLOR) : SV_Target {
float4 main(float2 coord : TEXCOORD, float4 color : COLOR) : SV_Target {
return color * float4(1, 1, 1, tex.Sample(sam, coord).r);
}

View file

@ -3,14 +3,14 @@ cbuffer vertexBuffer : register(b0) {
};
void main(
float2 pos : POSITION,
float4 pos : SV_POSITION,
float2 coord : TEXCOORD,
float4 color : COLOR,
out noperspective float2 outCoord : TEXCOORD,
out noperspective float4 outColor : COLOR,
out noperspective float4 outPos : SV_POSITION
out float2 outCoord : TEXCOORD,
out float4 outColor : COLOR,
out float4 outPos : SV_POSITION
) {
outPos = mul(projectionMatrix, float4(pos, 0, 1));
outPos = mul(projectionMatrix, float4(pos.xy, 0.f, 1.f));
outCoord = coord;
outColor = color;
}

View file

@ -3,7 +3,7 @@ Texture2D cursorTex : register(t1);
SamplerState pointSampler : register(s0);
float4 main(noperspective float2 coord : TEXCOORD) : SV_TARGET {
float4 main(float2 coord : TEXCOORD) : SV_TARGET {
float4 mask = cursorTex.Sample(pointSampler, coord);
if (mask.a < 0.5f) {

View file

@ -3,7 +3,7 @@ Texture2D<float2> cursorTex : register(t1);
SamplerState pointSampler : register(s0);
float4 main(noperspective float2 coord : TEXCOORD) : SV_TARGET {
float4 main(float2 coord : TEXCOORD) : SV_TARGET {
float2 mask = cursorTex.Sample(pointSampler, coord);
if (mask.x > 0.5f) {

View file

@ -1,6 +1,6 @@
Texture2D tex : register(t0);
SamplerState sam : register(s0);
float4 main(noperspective float2 coord : TEXCOORD) : SV_Target {
float4 main(float2 coord : TEXCOORD) : SV_Target {
return tex.Sample(sam, coord);
}

View file

@ -1,9 +1,9 @@
void main(
float2 pos : POSITION,
float4 pos : SV_POSITION,
float2 coord : TEXCOORD,
out noperspective float2 outCoord : TEXCOORD,
out noperspective float4 outPos : SV_POSITION
out float2 outCoord : TEXCOORD,
out float4 outPos : SV_POSITION
) {
outPos = float4(pos, 0, 1);
outPos = pos;
outCoord = coord;
}

View file

@ -58,15 +58,16 @@ hstring AboutViewModel::Version() const noexcept {
ResourceLoader::GetForCurrentView(CommonSharedConstants::APP_RESOURCE_MAP_ID);
return hstring(StrHelper::Concat(
resourceLoader.GetString(L"About_Version_Version"),
#ifdef MP_VERSION_STRING
L" " WIDEN_STRINGIFY(MP_VERSION_STRING),
#ifdef MP_VERSION_TAG
L" ",
&WIDEN(STRING(MP_VERSION_TAG))[1],
#else
L" dev",
#endif
#ifdef MP_COMMIT_ID
L" | ",
resourceLoader.GetString(L"About_Version_CommitId"),
L" " WIDEN_STRINGIFY(MP_COMMIT_ID),
L" " WIDEN(STRING(MP_COMMIT_ID)),
#endif
L" | "
#ifdef _M_X64
@ -102,7 +103,7 @@ void AboutViewModel::IsCheckForPreviewUpdates(bool value) {
bool AboutViewModel::IsCheckForUpdatesButtonEnabled() const noexcept {
// 只有发布版本能检查更新
#ifdef MP_VERSION_STRING
#ifdef MP_VERSION_TAG
return !IsCheckingForUpdates() && !IsDownloadingOrLater();
#else
return false;

View file

@ -127,7 +127,7 @@ bool AdaptersService::_GatherAdapterInfos(
return info.idx == std::numeric_limits<uint32_t>::max();
});
App::Get().Dispatcher().TryEnqueue([this, adapterInfos(std::move(adapterInfos))]() {
App::Get().Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [this, adapterInfos(std::move(adapterInfos))]() {
_adapterInfos = std::move(adapterInfos);
_UpdateProfiles();
AdaptersChanged.Invoke();

View file

@ -127,17 +127,15 @@ bool App::Initialize(const wchar_t* arguments) {
// 初始化 XAML 框架。退出时也不要关闭,如果正在播放动画会崩溃。文档中的清空消息队列的做法无用。
_windowsXamlManager = Hosting::WindowsXamlManager::InitializeForCurrentThread();
// Win10 中 CoreDispatcher.RunAsync 存在内存泄露,因此我们始终使用 DispatcherQueue。
// 初始化 WindowsXamlManager 时已经创建 DispatcherQueue。
_dispatcher = winrt::DispatcherQueue::GetForCurrentThread();
// Win10 中隐藏 DesktopWindowXamlSource 窗口
if (Win32Helper::GetOSVersion().IsWin10()) {
if (CoreWindow coreWindow = CoreWindow::GetForCurrentThread()) {
if (CoreWindow coreWindow = CoreWindow::GetForCurrentThread()) {
// Win10 中隐藏 DesktopWindowXamlSource 窗口
if (Win32Helper::GetOSVersion().IsWin10()) {
HWND hwndDWXS;
coreWindow.try_as<ICoreWindowInterop>()->get_WindowHandle(&hwndDWXS);
ShowWindow(hwndDWXS, SW_HIDE);
}
_dispatcher = coreWindow.Dispatcher();
}
LocalizationService::Get().EarlyInitialize();
@ -195,7 +193,7 @@ bool App::Initialize(const wchar_t* arguments) {
// 再检查显卡的功能级别。
_mainWindow->Content()->Loaded([](const auto&, const auto&) {
// 低优先级回调确保在初始化完毕后执行
App::Get().Dispatcher().TryEnqueue(DispatcherQueuePriority::Low, []() {
App::Get().Dispatcher().RunAsync(CoreDispatcherPriority::Low, []() {
AdaptersService::Get().StartMonitor();
});
});
@ -357,7 +355,9 @@ void App::_UpdateColorValuesChangedRevoker() {
_colorValuesChangedRevoker = _uiSettings.ColorValuesChanged(
auto_revoke,
[this](const auto&, const auto&) {
_dispatcher.TryEnqueue([this] { _UpdateTheme(); });
_dispatcher.RunAsync(CoreDispatcherPriority::Normal, [this] {
_UpdateTheme();
});
}
);
} else {

View file

@ -25,7 +25,7 @@ public:
int Run();
const DispatcherQueue& Dispatcher() const noexcept {
const CoreDispatcher& Dispatcher() const noexcept {
return _dispatcher;
}
@ -74,7 +74,7 @@ private:
std::unique_ptr<::Magpie::MainWindow> _mainWindow;
DispatcherQueue _dispatcher{ nullptr };
CoreDispatcher _dispatcher{ nullptr };
::Magpie::Event<::Magpie::AppTheme>::EventRevoker _themeChangedRevoker;
Windows::UI::ViewManagement::UISettings _uiSettings;

View file

@ -24,7 +24,7 @@ using namespace winrt::Magpie;
namespace Magpie {
// 如果配置文件和已发布的正式版本不再兼容,应提高此版本号
static constexpr uint32_t CONFIG_VERSION = 4;
static constexpr uint32_t CONFIG_VERSION = 3;
_AppSettingsData::_AppSettingsData() {}
@ -121,10 +121,6 @@ static void WriteProfile(rapidjson::PrettyWriter<rapidjson::StringBuffer>& write
writer.Double(profile.customCursorScaling);
writer.Key("cursorInterpolationMode");
writer.Uint((uint32_t)profile.cursorInterpolationMode);
writer.Key("autoHideCursorEnabled");
writer.Bool(profile.isAutoHideCursorEnabled);
writer.Key("autoHideCursorDelay");
writer.Double(profile.autoHideCursorDelay);
writer.Key("croppingEnabled");
writer.Bool(profile.isCroppingEnabled);
@ -140,9 +136,6 @@ static void WriteProfile(rapidjson::PrettyWriter<rapidjson::StringBuffer>& write
writer.Double(profile.cropping.Bottom);
writer.EndObject();
writer.Key("destAlignment");
writer.Uint((uint32_t)profile.destAlignment);
writer.EndObject();
}
@ -515,20 +508,9 @@ void AppSettings::_UpdateWindowPlacement() noexcept {
return;
}
// rcNormalPosition 使用工作区坐标,应转换为屏幕坐标。
// 见 https://github.com/Blinue/nt5src/blob/daad8a087a4e75422ec96b7911f1df4669989611/Source/XPSP1/NT/windows/core/ntuser/kernel/winmgr.c#L752
HMONITOR hMon = MonitorFromWindow(hwndMain, MONITOR_DEFAULTTOPRIMARY);
MONITORINFO mi{ sizeof(mi) };
if (!GetMonitorInfo(hMon, &mi)) {
Logger::Get().Win32Error("GetMonitorInfo 失败");
return;
}
const LONG workingAreaOffsetX = mi.rcWork.left - mi.rcMonitor.left;
const LONG workingAreaOffsetY = mi.rcWork.top - mi.rcMonitor.top;
_mainWindowCenter = {
(wp.rcNormalPosition.left + wp.rcNormalPosition.right) / 2.0f + workingAreaOffsetX,
(wp.rcNormalPosition.top + wp.rcNormalPosition.bottom) / 2.0f + workingAreaOffsetY,
(wp.rcNormalPosition.left + wp.rcNormalPosition.right) / 2.0f,
(wp.rcNormalPosition.top + wp.rcNormalPosition.bottom) / 2.0f
};
const float dpiFactor = GetDpiForWindow(hwndMain) / float(USER_DEFAULT_SCREEN_DPI);
@ -593,8 +575,6 @@ bool AppSettings::_Save(const _AppSettingsData& data) noexcept {
writer.Bool(data._isDebugMode);
writer.Key("benchmarkMode");
writer.Bool(data._isBenchmarkMode);
writer.Key("disableTopmost");
writer.Bool(data._isTopmostDisabled);
writer.Key("disableEffectCache");
writer.Bool(data._isEffectCacheDisabled);
writer.Key("disableFontCache");
@ -627,6 +607,8 @@ bool AppSettings::_Save(const _AppSettingsData& data) noexcept {
writer.Double(data._minFrameRate);
writer.Key("disableFP16");
writer.Bool(data._isFP16Disabled);
writer.Key("keepOnTop");
writer.Bool(data._isKeepOnTop);
ScalingModesService::Get().Export(writer);
@ -792,7 +774,6 @@ void AppSettings::_LoadSettings(const rapidjson::GenericObject<true, rapidjson::
JsonHelper::ReadBool(root, "developerMode", _isDeveloperMode);
JsonHelper::ReadBool(root, "debugMode", _isDebugMode);
JsonHelper::ReadBool(root, "benchmarkMode", _isBenchmarkMode);
JsonHelper::ReadBool(root, "disableTopmost", _isTopmostDisabled);
JsonHelper::ReadBool(root, "disableEffectCache", _isEffectCacheDisabled);
JsonHelper::ReadBool(root, "disableFontCache", _isFontCacheDisabled);
JsonHelper::ReadBool(root, "saveEffectSources", _isSaveEffectSources);
@ -828,6 +809,7 @@ void AppSettings::_LoadSettings(const rapidjson::GenericObject<true, rapidjson::
JsonHelper::ReadBool(root, "enableStatisticsForDynamicDetection", _isStatisticsForDynamicDetectionEnabled);
JsonHelper::ReadFloat(root, "minFrameRate", _minFrameRate);
JsonHelper::ReadBool(root, "disableFP16", _isFP16Disabled);
JsonHelper::ReadBool(root, "keepOnTop", _isKeepOnTop);
[[maybe_unused]] bool result = ScalingModesService::Get().Import(root, true);
assert(result);
@ -1070,9 +1052,7 @@ bool AppSettings::_LoadProfile(
JsonHelper::ReadBool(profileObj, "frameRateLimiterEnabled", profile.isFrameRateLimiterEnabled);
JsonHelper::ReadFloat(profileObj, "maxFrameRate", profile.maxFrameRate);
if (profile.maxFrameRate <= 10.0f - FLOAT_EPSILON<float> ||
profile.maxFrameRate >= 1000.0f + FLOAT_EPSILON<float>)
{
if (profile.maxFrameRate < 10.0f || profile.maxFrameRate > 1000.0f) {
profile.maxFrameRate = 60.0f;
}
@ -1101,20 +1081,12 @@ bool AppSettings::_LoadProfile(
{
uint32_t cursorInterpolationMode = (uint32_t)CursorInterpolationMode::NearestNeighbor;
JsonHelper::ReadUInt(profileObj, "cursorInterpolationMode", cursorInterpolationMode);
if (cursorInterpolationMode >= (uint32_t)CursorInterpolationMode::COUNT) {
if (cursorInterpolationMode > 1) {
cursorInterpolationMode = (uint32_t)CursorInterpolationMode::NearestNeighbor;
}
profile.cursorInterpolationMode = (CursorInterpolationMode)cursorInterpolationMode;
}
JsonHelper::ReadBool(profileObj, "autoHideCursorEnabled", profile.isAutoHideCursorEnabled);
JsonHelper::ReadFloat(profileObj, "autoHideCursorDelay", profile.autoHideCursorDelay);
if (profile.autoHideCursorDelay <= 0.1f - FLOAT_EPSILON<float> ||
profile.autoHideCursorDelay >= 5.0f + FLOAT_EPSILON<float>)
{
profile.autoHideCursorDelay = 3.0f;
}
JsonHelper::ReadBool(profileObj, "croppingEnabled", profile.isCroppingEnabled);
auto croppingNode = profileObj.FindMember("cropping");
@ -1134,15 +1106,6 @@ bool AppSettings::_LoadProfile(
}
}
{
uint32_t destAlignment = (uint32_t)DestAlignment::Center;
JsonHelper::ReadUInt(profileObj, "destAlignment", destAlignment);
if (destAlignment >= (uint32_t)DestAlignment::COUNT) {
destAlignment = (uint32_t)DestAlignment::Center;
}
profile.destAlignment = (DestAlignment)destAlignment;
}
return true;
}

View file

@ -62,7 +62,6 @@ struct _AppSettingsData {
bool _isDeveloperMode = false;
bool _isDebugMode = false;
bool _isBenchmarkMode = false;
bool _isTopmostDisabled = false;
bool _isEffectCacheDisabled = false;
bool _isFontCacheDisabled = false;
bool _isSaveEffectSources = false;
@ -76,6 +75,7 @@ struct _AppSettingsData {
bool _isCheckForPreviewUpdates = false;
bool _isStatisticsForDynamicDetectionEnabled = false;
bool _isFP16Disabled = false;
bool _isKeepOnTop = false;
};
class AppSettings : private _AppSettingsData {
@ -162,15 +162,6 @@ public:
SaveAsync();
}
bool IsTopmostDisabled() const noexcept {
return _isTopmostDisabled;
}
void IsTopmostDisabled(bool value) noexcept {
_isTopmostDisabled = value;
SaveAsync();
}
bool IsEffectCacheDisabled() const noexcept {
return _isEffectCacheDisabled;
}
@ -216,6 +207,15 @@ public:
SaveAsync();
}
bool IsKeepOnTop() const noexcept {
return _isKeepOnTop;
}
void IsKeepOnTop(bool value) noexcept {
_isKeepOnTop = value;
SaveAsync();
}
bool IsAllowScalingMaximized() const noexcept {
return _isAllowScalingMaximized;
}

View file

@ -113,7 +113,8 @@ fire_and_forget CandidateWindowItem::_ResolveWindow(bool resolveIcon, bool resol
co_return;
}
App::Get().Dispatcher().TryEnqueue(
App::Get().Dispatcher().RunAsync(
CoreDispatcherPriority::Normal,
[this, defaultProfileName(std::move(defaultProfileName)), aumid(reader.AUMID())]() {
if (!defaultProfileName.empty()) {
_defaultProfileName = defaultProfileName;

View file

@ -59,9 +59,7 @@
</local:ShortcutControl.Action>
</local:ShortcutControl>
</local:SettingsCard>
<local:SettingsExpander x:Uid="Home_Activation_Timer"
Description="{x:Bind ViewModel.TimerDescription, Mode=OneWay}"
IsWrapEnabled="True">
<local:SettingsExpander x:Uid="Home_Activation_Timer">
<local:SettingsExpander.HeaderIcon>
<FontIcon Glyph="&#xE916;" />
</local:SettingsExpander.HeaderIcon>
@ -82,48 +80,19 @@
VerticalAlignment="Center"
Text="{x:Bind ViewModel.TimerLabelText, Mode=OneWay}" />
</Grid>
<Button Click="{x:Bind ViewModel.ToggleTimerFullscreen}"
IsEnabled="{x:Bind ViewModel.IsNotRunning, Mode=OneWay}">
<local:SimpleStackPanel Orientation="Horizontal"
Spacing="8">
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xE740;" />
<TextBlock Text="{x:Bind ViewModel.TimerButtonText(x:False), Mode=OneWay}" />
</local:SimpleStackPanel>
</Button>
<Button Click="{x:Bind ViewModel.ToggleTimerWindowed}"
IsEnabled="{x:Bind ViewModel.IsNotRunning, Mode=OneWay}">
<local:SimpleStackPanel Orientation="Horizontal"
Spacing="8">
<FontIcon FontSize="{StaticResource StandardIconSize}"
Glyph="&#xE737;" />
<TextBlock Text="{x:Bind ViewModel.TimerButtonText(x:True), Mode=OneWay}" />
</local:SimpleStackPanel>
</Button>
<Button Click="{x:Bind ViewModel.ToggleTimer}"
Content="{x:Bind ViewModel.TimerButtonText, Mode=OneWay}"
IsEnabled="{x:Bind ViewModel.IsNotRunning, Mode=OneWay}" />
</local:SimpleStackPanel>
</local:SettingsExpander.Content>
<local:SettingsExpander.Items>
<local:SettingsCard x:Uid="Home_Activation_Timer_Delay"
IsWrapEnabled="True">
<Grid MinWidth="{StaticResource SettingsCardContentMinWidth}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Slider Grid.Column="0"
MinWidth="0"
IsThumbToolTipEnabled="False"
Loaded="TimerSlider_Loaded"
Maximum="5"
Minimum="1"
TickFrequency="1"
Value="{x:Bind ViewModel.Delay, Mode=TwoWay}" />
<TextBlock Grid.Column="1"
Width="20"
VerticalAlignment="Center"
Text="{x:Bind ViewModel.DelayText, Mode=OneWay}"
TextAlignment="Right" />
</Grid>
<Slider Loaded="TimerSlider_Loaded"
Maximum="5"
Minimum="1"
TickFrequency="1"
Value="{x:Bind ViewModel.Delay, Mode=TwoWay}" />
</local:SettingsCard>
</local:SettingsExpander.Items>
</local:SettingsExpander>
@ -224,6 +193,13 @@
<ToggleSwitch x:Uid="ToggleSwitch"
IsOn="{x:Bind ViewModel.IsAllowScalingMaximized, Mode=TwoWay}" />
</local:SettingsCard>
<local:SettingsCard x:Uid="Home_Advanced_KeepOnTop">
<local:SettingsCard.HeaderIcon>
<FontIcon Glyph="&#xE718;" />
</local:SettingsCard.HeaderIcon>
<ToggleSwitch x:Uid="ToggleSwitch"
IsOn="{x:Bind ViewModel.IsKeepOnTop, Mode=TwoWay}" />
</local:SettingsCard>
<local:SettingsCard x:Uid="Home_Advanced_SimulateExclusiveFullscreen">
<local:SettingsCard.HeaderIcon>
<FontIcon Glyph="&#xEC46;" />
@ -296,10 +272,6 @@
<CheckBox x:Uid="Home_Advanced_DeveloperOptions_BenchmarkMode"
IsChecked="{x:Bind ViewModel.IsBenchmarkMode, Mode=TwoWay}" />
</local:SettingsCard>
<local:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="Home_Advanced_DeveloperOptions_DisableTopmost"
IsChecked="{x:Bind ViewModel.IsTopmostDisabled, Mode=TwoWay}" />
</local:SettingsCard>
<local:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="Home_Advanced_DeveloperOptions_DisableEffectCache"
IsChecked="{x:Bind ViewModel.IsEffectCacheDisabled, Mode=TwoWay}" />

View file

@ -42,16 +42,6 @@ HomeViewModel::HomeViewModel() {
);
}
hstring HomeViewModel::TimerDescription() const noexcept {
ResourceLoader resourceLoader =
ResourceLoader::GetForCurrentView(CommonSharedConstants::APP_RESOURCE_MAP_ID);
hstring fmtStr = resourceLoader.GetString(L"Home_Activation_Timer_Description");
return hstring(fmt::format(
fmt::runtime(std::wstring_view(fmtStr)),
AppSettings::Get().CountdownSeconds()
));
}
bool HomeViewModel::IsTimerOn() const noexcept {
return ScalingService::Get().IsTimerOn();
}
@ -66,28 +56,34 @@ hstring HomeViewModel::TimerLabelText() const noexcept {
return to_hstring((int)std::ceil(ScalingService.SecondsLeft()));
}
hstring HomeViewModel::TimerButtonText() const noexcept {
ScalingService& ScalingService = ScalingService::Get();
ResourceLoader resourceLoader =
ResourceLoader::GetForCurrentView(CommonSharedConstants::APP_RESOURCE_MAP_ID);
if (ScalingService.IsTimerOn()) {
return resourceLoader.GetString(L"Home_Activation_Timer_Cancel");
} else {
hstring fmtStr = resourceLoader.GetString(L"Home_Activation_Timer_ButtonText");
return hstring(fmt::format(
fmt::runtime(std::wstring_view(fmtStr)),
AppSettings::Get().CountdownSeconds()
));
}
}
bool HomeViewModel::IsNotRunning() const noexcept {
return !ScalingService::Get().IsScaling();
}
hstring HomeViewModel::TimerButtonText(bool windowedMode) const noexcept {
ResourceLoader resourceLoader =
ResourceLoader::GetForCurrentView(CommonSharedConstants::APP_RESOURCE_MAP_ID);
if (ScalingService::Get().IsTimerOn(windowedMode)) {
return resourceLoader.GetString(L"Home_Activation_Timer_Cancel");
void HomeViewModel::ToggleTimer() const noexcept {
ScalingService& scalingService = ScalingService::Get();
if (scalingService.IsTimerOn()) {
scalingService.StopTimer();
} else {
return resourceLoader.GetString(L"Home_Activation_Timer_Start");
scalingService.StartTimer();
}
}
void HomeViewModel::ToggleTimerFullscreen() const noexcept {
_ToggleTimer(false);
}
void HomeViewModel::ToggleTimerWindowed() const noexcept {
_ToggleTimer(true);
}
uint32_t HomeViewModel::Delay() const noexcept {
return AppSettings::Get().CountdownSeconds();
}
@ -95,15 +91,10 @@ uint32_t HomeViewModel::Delay() const noexcept {
void HomeViewModel::Delay(uint32_t value) {
AppSettings::Get().CountdownSeconds(value);
RaisePropertyChanged(L"Delay");
RaisePropertyChanged(L"DelayText");
RaisePropertyChanged(L"TimerDescription");
RaisePropertyChanged(L"TimerButtonText");
}
hstring HomeViewModel::DelayText() const noexcept {
return App::Get().DoubleFormatter().FormatDouble(Delay());
}
void HomeViewModel::ShowUpdateCard(bool value) noexcept {
inline void HomeViewModel::ShowUpdateCard(bool value) noexcept {
_showUpdateCard = value;
if (!value) {
UpdateService::Get().IsShowOnHomePage(false);
@ -337,6 +328,21 @@ void HomeViewModel::IsAllowScalingMaximized(bool value) {
}
}
bool HomeViewModel::IsKeepOnTop() const noexcept {
return AppSettings::Get().IsKeepOnTop();
}
void HomeViewModel::IsKeepOnTop(bool value) {
AppSettings& settings = AppSettings::Get();
if (settings.IsKeepOnTop() == value) {
return;
}
settings.IsKeepOnTop(value);
RaisePropertyChanged(L"IsKeepOnTop");
}
bool HomeViewModel::IsSimulateExclusiveFullscreen() const noexcept {
return AppSettings::Get().IsSimulateExclusiveFullscreen();
}
@ -454,21 +460,6 @@ void HomeViewModel::LocateUpdaterLogs() noexcept {
LocateTempLogs(CommonSharedConstants::UPDATER_LOG_NAME);
}
bool HomeViewModel::IsDebugMode() const noexcept {
return AppSettings::Get().IsDebugMode();
}
void HomeViewModel::IsDebugMode(bool value) {
AppSettings& settings = AppSettings::Get();
if (settings.IsDebugMode() == value) {
return;
}
settings.IsDebugMode(value);
RaisePropertyChanged(L"IsDebugMode");
}
bool HomeViewModel::IsBenchmarkMode() const noexcept {
return AppSettings::Get().IsBenchmarkMode();
}
@ -484,19 +475,19 @@ void HomeViewModel::IsBenchmarkMode(bool value) {
RaisePropertyChanged(L"IsBenchmarkMode");
}
bool HomeViewModel::IsTopmostDisabled() const noexcept {
return AppSettings::Get().IsTopmostDisabled();
bool HomeViewModel::IsDebugMode() const noexcept {
return AppSettings::Get().IsDebugMode();
}
void HomeViewModel::IsTopmostDisabled(bool value) {
void HomeViewModel::IsDebugMode(bool value) {
AppSettings& settings = AppSettings::Get();
if (settings.IsTopmostDisabled() == value) {
if (settings.IsDebugMode() == value) {
return;
}
settings.IsTopmostDisabled(value);
RaisePropertyChanged(L"IsTopmostDisabled");
settings.IsDebugMode(value);
RaisePropertyChanged(L"IsDebugMode");
}
bool HomeViewModel::IsEffectCacheDisabled() const noexcept {
@ -620,7 +611,7 @@ void HomeViewModel::IsStatisticsForDynamicDetectionEnabled(bool value) {
RaisePropertyChanged(L"IsStatisticsForDynamicDetectionEnabled");
}
void HomeViewModel::_ScalingService_IsTimerOnChanged(bool value, bool) {
void HomeViewModel::_ScalingService_IsTimerOnChanged(bool value) {
if (!value) {
RaisePropertyChanged(L"TimerProgressRingValue");
}
@ -640,13 +631,4 @@ void HomeViewModel::_ScalingService_IsScalingChanged(bool) {
RaisePropertyChanged(L"IsNotRunning");
}
void HomeViewModel::_ToggleTimer(bool windowedMode) const noexcept {
ScalingService& scalingService = ScalingService::Get();
if (scalingService.IsTimerOn(windowedMode)) {
scalingService.StopTimer();
} else {
scalingService.StartTimer(windowedMode);
}
}
}

View file

@ -7,27 +7,21 @@ namespace winrt::Magpie::implementation {
struct HomeViewModel : HomeViewModelT<HomeViewModel>, wil::notify_property_changed_base<HomeViewModel> {
HomeViewModel();
hstring TimerDescription() const noexcept;
bool IsTimerOn() const noexcept;
double TimerProgressRingValue() const noexcept;
hstring TimerLabelText() const noexcept;
hstring TimerButtonText() const noexcept;
bool IsNotRunning() const noexcept;
hstring TimerButtonText(bool windowedMode) const noexcept;
void ToggleTimerFullscreen() const noexcept;
void ToggleTimerWindowed() const noexcept;
void ToggleTimer() const noexcept;
uint32_t Delay() const noexcept;
void Delay(uint32_t value);
hstring DelayText() const noexcept;
bool ShowUpdateCard() const noexcept {
return _showUpdateCard;
}
@ -69,6 +63,9 @@ struct HomeViewModel : HomeViewModelT<HomeViewModel>, wil::notify_property_chang
bool IsAllowScalingMaximized() const noexcept;
void IsAllowScalingMaximized(bool value);
bool IsKeepOnTop() const noexcept;
void IsKeepOnTop(bool value);
bool IsSimulateExclusiveFullscreen() const noexcept;
void IsSimulateExclusiveFullscreen(bool value);
@ -87,14 +84,11 @@ struct HomeViewModel : HomeViewModelT<HomeViewModel>, wil::notify_property_chang
void LocateTouchHelperLogs() noexcept;
void LocateUpdaterLogs() noexcept;
bool IsDebugMode() const noexcept;
void IsDebugMode(bool value);
bool IsBenchmarkMode() const noexcept;
void IsBenchmarkMode(bool value);
bool IsTopmostDisabled() const noexcept;
void IsTopmostDisabled(bool value);
bool IsDebugMode() const noexcept;
void IsDebugMode(bool value);
bool IsEffectCacheDisabled() const noexcept;
void IsEffectCacheDisabled(bool value);
@ -120,15 +114,13 @@ struct HomeViewModel : HomeViewModelT<HomeViewModel>, wil::notify_property_chang
void IsStatisticsForDynamicDetectionEnabled(bool value);
private:
void _ScalingService_IsTimerOnChanged(bool value, bool windowedMode);
void _ScalingService_IsTimerOnChanged(bool value);
void _ScalingService_TimerTick(double);
void _ScalingService_IsScalingChanged(bool);
void _ToggleTimer(bool windowedMode) const noexcept;
::Magpie::Event<bool, bool>::EventRevoker _isTimerOnRevoker;
::Magpie::Event<bool>::EventRevoker _isTimerOnRevoker;
::Magpie::Event<double>::EventRevoker _timerTickRevoker;
::Magpie::Event<bool>::EventRevoker _isScalingChangedRevoker;
::Magpie::Event<bool>::EventRevoker _isShowOnHomePageChangedRevoker;

View file

@ -7,17 +7,14 @@ namespace Magpie {
void ReleaseNotes();
void RemindMeLater();
String TimerDescription { get; };
Boolean IsTimerOn { get; };
Double TimerProgressRingValue { get; };
String TimerLabelText { get; };
String TimerButtonText { get; };
Boolean IsNotRunning { get; };
UInt32 Delay;
String DelayText { get; };
String TimerButtonText(Boolean windowedMode);
void ToggleTimerFullscreen();
void ToggleTimerWindowed();
void ToggleTimer();
String InitialToolbarStateDescription { get; };
Int32 FullscreenInitialToolbarState;
@ -31,6 +28,7 @@ namespace Magpie {
Boolean IsShowTouchSupportInfoBar { get; };
Boolean IsAllowScalingMaximized;
Boolean IsKeepOnTop;
Boolean IsSimulateExclusiveFullscreen;
Boolean IsInlineParams;
static IVector<IInspectable> MinFrameRateOptions { get; };
@ -42,7 +40,6 @@ namespace Magpie {
void LocateUpdaterLogs();
Boolean IsDebugMode;
Boolean IsBenchmarkMode;
Boolean IsTopmostDisabled;
Boolean IsEffectCacheDisabled;
Boolean IsFontCacheDisabled;
Boolean IsSaveEffectSources;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3 KiB

Before After
Before After

Some files were not shown because too many files have changed in this diff Show more