Skip to content

Commit aaf16e5

Browse files
committed
add osx workgroups and global task scheduler
1 parent 032003c commit aaf16e5

16 files changed

Lines changed: 777 additions & 55 deletions

framework/audio/common/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ target_sources(muse_audio_common PRIVATE
3333
iaudiothreadsecurer.h
3434
audiothreadsecurer.cpp
3535
audiothreadsecurer.h
36+
audioworkgroup.h
37+
audioworkgroup.cpp
38+
audiotaskscheduler.h
39+
inplace_function_mv.h
3640
workmode.cpp
3741
workmode.h
3842
alignmentbuffer.h
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* SPDX-License-Identifier: GPL-3.0-only
3+
* MuseScore-CLA-applies
4+
*
5+
* MuseScore
6+
* Music Composition & Notation
7+
*
8+
* Copyright (C) 2026 MuseScore Limited and others
9+
*
10+
* This program is free software: you can redistribute it and/or modify
11+
* it under the terms of the GNU General Public License version 3 as
12+
* published by the Free Software Foundation.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU General Public License
20+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
21+
*/
22+
23+
#pragma once
24+
25+
#include "common/audioworkgroup.h"
26+
#include "common/realtimethreadpool.h"
27+
#include "iaudiodriver.h"
28+
#include "iaudiotaskscheduler.h"
29+
#include "audiosanitizer.h"
30+
#include <mutex>
31+
32+
namespace muse::audio {
33+
class AudioTaskScheduler : public IAudioTaskScheduler, public kors::async::Asyncable
34+
{
35+
constexpr static const char* threadpoolName = "audio_realtime_thread";
36+
public:
37+
void submitRealtimeTasksAndWait(const std::vector<Task>& tasks) override
38+
{
39+
std::lock_guard lock(m_threadPoolMutex);
40+
for (const auto& task : tasks) {
41+
m_threadPool->enqueue(task);
42+
}
43+
m_threadPool->participateAndWait();
44+
}
45+
46+
void setAudioDriver(const IAudioDriverPtr& audioDriver)
47+
{
48+
setWorkgroup(audioDriver->getAudioWorkGroup());
49+
audioDriver->currentWorkgroupChanged().onNotify(
50+
this, [this, audioDriver]() {
51+
setWorkgroup(audioDriver->getAudioWorkGroup());
52+
});
53+
}
54+
55+
private:
56+
57+
void setWorkgroup(const AudioWorkGroup& workGroup)
58+
{
59+
std::lock_guard lock(m_threadPoolMutex);
60+
ensureThreadPoolSize(workGroup);
61+
m_threadPool->setAudioWorkgroup(workGroup);
62+
}
63+
64+
int getIdealThreadCount(AudioWorkGroup workGroup) const
65+
{
66+
int bestThreadHint = std::thread::hardware_concurrency();
67+
if (workGroup.getProvider() != nullptr) {
68+
bestThreadHint = workGroup.getMaxParallelThreadCount();
69+
}
70+
constexpr int hardwareToRealtimeRatio = 2; // This is a heuristic value. The optimal value may vary depending on the workload and system.
71+
return bestThreadHint > 0 ? static_cast<int>(bestThreadHint / hardwareToRealtimeRatio) : 1;
72+
}
73+
74+
void ensureThreadPoolSize(const AudioWorkGroup& currentWorkGroup)
75+
{
76+
auto idealWorkerCount = getIdealThreadCount(currentWorkGroup);
77+
std::lock_guard lock(m_threadPoolMutex);
78+
if (m_threadPool->getNumberOfWorkers() != idealWorkerCount) {
79+
m_threadPool = std::make_unique<RealtimeThreadPool>(threadpoolName, idealWorkerCount);
80+
AudioSanitizer::setMixerThreads(m_threadPool->threadIdSet());
81+
}
82+
}
83+
84+
std::unique_ptr<RealtimeThreadPool> m_threadPool{ std::make_unique<RealtimeThreadPool>(threadpoolName, getIdealThreadCount({})) };
85+
86+
std::recursive_mutex m_threadPoolMutex;
87+
};
88+
}
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/*
2+
* SPDX-License-Identifier: GPL-3.0-only
3+
* MuseScore-CLA-applies
4+
*
5+
* MuseScore
6+
* Music Composition & Notation
7+
*
8+
* Copyright (C) 2026 MuseScore Limited and others
9+
*
10+
* This program is free software: you can redistribute it and/or modify
11+
* it under the terms of the GNU General Public License version 3 as
12+
* published by the Free Software Foundation.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU General Public License
20+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
21+
*/
22+
23+
#include "audioworkgroup.h"
24+
25+
#include <thread>
26+
27+
#ifdef __APPLE__
28+
#include <utility>
29+
#include <os/object.h>
30+
#include <os/workgroup.h>
31+
#endif
32+
33+
namespace muse::audio {
34+
#ifdef __APPLE__
35+
class AudioWorkgroupTokenProvider
36+
{
37+
public:
38+
AudioWorkgroupTokenProvider(os_workgroup_t workgroup)
39+
: m_workgroup(workgroup)
40+
{
41+
if (!m_workgroup) {
42+
return;
43+
}
44+
45+
if (__builtin_available(macOS 11.0, *)) {
46+
os_retain(m_workgroup);
47+
auto status = os_workgroup_join(m_workgroup, &m_joinToken);
48+
if (status == 0) {
49+
return;
50+
}
51+
52+
os_release(m_workgroup);
53+
m_workgroup = nullptr;
54+
m_joinToken = {};
55+
return;
56+
}
57+
58+
m_workgroup = nullptr;
59+
}
60+
61+
bool isAttachedTo(os_workgroup_t wg) const { return m_workgroup == wg; }
62+
bool isValid() const { return m_workgroup != nullptr; }
63+
64+
AudioWorkgroupTokenProvider(const AudioWorkgroupTokenProvider& other) = delete;
65+
66+
AudioWorkgroupTokenProvider& operator=(const AudioWorkgroupTokenProvider&) = delete;
67+
68+
AudioWorkgroupTokenProvider(AudioWorkgroupTokenProvider&& other) noexcept
69+
: m_workgroup(other.m_workgroup), m_joinToken(other.m_joinToken)
70+
{
71+
other.m_workgroup = nullptr;
72+
other.m_joinToken = {};
73+
}
74+
75+
~AudioWorkgroupTokenProvider()
76+
{
77+
leave();
78+
}
79+
80+
private:
81+
void leave() noexcept
82+
{
83+
if (!m_workgroup) {
84+
return;
85+
}
86+
87+
if (__builtin_available(macOS 11.0, *)) {
88+
os_workgroup_leave(m_workgroup, &m_joinToken);
89+
}
90+
os_release(m_workgroup);
91+
m_workgroup = nullptr;
92+
m_joinToken = {};
93+
}
94+
95+
os_workgroup_t m_workgroup;
96+
os_workgroup_join_token_s m_joinToken;
97+
};
98+
99+
class AudioWorkgroupProvider
100+
{
101+
public:
102+
explicit AudioWorkgroupProvider(os_workgroup_t wg)
103+
: m_workgroup(wg)
104+
{
105+
os_retain(m_workgroup);
106+
}
107+
108+
~AudioWorkgroupProvider()
109+
{
110+
if (m_workgroup) {
111+
os_release(m_workgroup);
112+
}
113+
}
114+
115+
AudioWorkgroupProvider(const AudioWorkgroupProvider& other)
116+
: m_workgroup(other.m_workgroup)
117+
{
118+
os_retain(m_workgroup);
119+
}
120+
121+
AudioWorkgroupProvider& operator=(const AudioWorkgroupProvider& other)
122+
{
123+
if (this != &other) {
124+
os_retain(other.m_workgroup);
125+
if (m_workgroup) {
126+
os_release(m_workgroup);
127+
}
128+
m_workgroup = other.m_workgroup;
129+
}
130+
return *this;
131+
}
132+
133+
AudioWorkgroupProvider(AudioWorkgroupProvider&& other) noexcept
134+
: m_workgroup(other.m_workgroup)
135+
{
136+
other.m_workgroup = nullptr;
137+
}
138+
139+
bool join(AudioWorkgroupToken& tokenProvider) const
140+
{
141+
if (auto existingProvider = tokenProvider.getProvider();
142+
existingProvider != nullptr && existingProvider->isAttachedTo(m_workgroup)) {
143+
return true;
144+
}
145+
tokenProvider.reset();
146+
AudioWorkgroupTokenProvider provider(m_workgroup);
147+
if (!provider.isValid()) {
148+
return false;
149+
}
150+
151+
tokenProvider.join([provider = std::move(provider)]() {
152+
return &provider;
153+
});
154+
return true;
155+
}
156+
157+
size_t getMaxParallelThreadCount() const
158+
{
159+
if (__builtin_available(macOS 11.0, *)) {
160+
return (size_t)os_workgroup_max_parallel_threads(m_workgroup, nullptr);
161+
}
162+
return 0;
163+
}
164+
165+
private:
166+
167+
os_workgroup_t m_workgroup;
168+
};
169+
170+
bool AudioWorkGroup::join(AudioWorkgroupToken& token)
171+
{
172+
auto provider = getProvider();
173+
if (!provider) {
174+
return false;
175+
}
176+
return provider->join(token);
177+
}
178+
179+
AudioWorkGroup makeAudioWorkgroup(void* opaqueHandle)
180+
{
181+
if (opaqueHandle == nullptr) {
182+
return AudioWorkGroup{ []() -> const AudioWorkgroupProvider* { return nullptr; } };
183+
}
184+
185+
os_workgroup_t handle = reinterpret_cast<os_workgroup_t>(opaqueHandle);
186+
187+
return AudioWorkGroup { [provider = AudioWorkgroupProvider { handle }] { return &provider; } };
188+
}
189+
190+
size_t AudioWorkGroup::getMaxParallelThreadCount() const
191+
{
192+
if (auto provider = getProvider(); provider) {
193+
return provider->getMaxParallelThreadCount();
194+
}
195+
return 0;
196+
}
197+
198+
#else
199+
200+
size_t AudioWorkGroup::getMaxParallelThreadCount() const
201+
{
202+
return std::thread::hardware_concurrency();
203+
}
204+
205+
bool AudioWorkGroup::join(AudioWorkgroupToken&)
206+
{
207+
return false;
208+
}
209+
210+
AudioWorkGroup makeAudioWorkgroup(void*)
211+
{
212+
return {};
213+
}
214+
215+
#endif
216+
217+
AudioWorkGroup::AudioWorkGroup(const AudioWorkGroup& other)
218+
: m_provider(other.m_provider) {}
219+
220+
AudioWorkGroup& AudioWorkGroup::operator=(const AudioWorkGroup& other)
221+
{
222+
if (this != &other) {
223+
m_provider = other.m_provider;
224+
}
225+
return *this;
226+
}
227+
} // namespace muse::audio

0 commit comments

Comments
 (0)