diff --git a/tests/TestSvc/source/test_thread_pinning.cpp b/tests/TestSvc/source/test_thread_pinning.cpp
new file mode 100644
index 000000000..35fb9d4b5
--- /dev/null
+++ b/tests/TestSvc/source/test_thread_pinning.cpp
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) Atmosphère-NX
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU General Public License,
+ * version 2, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+#include
+#include "util_common.hpp"
+#include "util_scoped_heap.hpp"
+
+namespace ams::test {
+
+ DOCTEST_TEST_CASE( "Setting a thread's disable count will cause it to become pinned." ) {
+ DoWithThreadPinning([]() {
+ __asm__ __volatile__("" ::: "memory");
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/tests/TestSvc/source/util_common.hpp b/tests/TestSvc/source/util_common.hpp
index eeee4484b..0ba2e9c3b 100644
--- a/tests/TestSvc/source/util_common.hpp
+++ b/tests/TestSvc/source/util_common.hpp
@@ -32,4 +32,70 @@ namespace ams::test {
return priority == ((core == (NumCores - 1)) ? DpcManagerPreemptionThreadPriority : DpcManagerNormalThreadPriority);
}
+ template
+ void DoWithThreadPinning(F f) {
+ /* Get the thread local region. */
+ auto * const tlr = svc::GetThreadLocalRegion();
+
+ /* Require that we're not currently pinned. */
+ DOCTEST_CHECK((tlr->disable_count == 0));
+ DOCTEST_CHECK((!tlr->interrupt_flag));
+
+ /* Request to pin ourselves. */
+ tlr->disable_count = 1;
+
+ /* Wait long enough that we can be confident preemption will occur, and therefore our interrupt flag will be set. */
+ {
+ constexpr auto MinimumTicksToGuaranteeInterruptFlag = ::ams::svc::Tick(PreemptionTimeSpan) + 1;
+
+ auto GetSystemTickForPinnedThread = []() ALWAYS_INLINE_LAMBDA -> ::ams::svc::Tick {
+ s64 v;
+ __asm__ __volatile__ ("mrs %x[v], cntpct_el0" : [v]"=r"(v) :: "memory");
+ return ::ams::svc::Tick(v);
+ };
+
+ const auto start_tick = GetSystemTickForPinnedThread();
+ while (true) {
+ if (tlr->interrupt_flag) {
+ break;
+ }
+
+ if (const auto cur_tick = GetSystemTickForPinnedThread(); (cur_tick - start_tick) > MinimumTicksToGuaranteeInterruptFlag) {
+ break;
+ }
+ }
+ }
+
+ /* We're pinned. Execute the user callback. */
+ bool callback_succeeded = true;
+ {
+ if constexpr (requires { { f() } -> std::convertible_to; }) {
+ callback_succeeded = f();
+ } else {
+ f();
+ }
+ }
+
+ /* Clear our disable count. */
+ tlr->disable_count = 0;
+
+ /* Get our interrupt flag. */
+ const auto interrupt_flag_while_pinned = tlr->interrupt_flag;
+
+ /* Unpin ourselves. */
+ if (interrupt_flag_while_pinned) {
+ svc::SynchronizePreemptionState();
+ }
+
+ /* Get our interrupt flag. */
+ const auto interrupt_flag_after_unpin = tlr->interrupt_flag;
+
+ /* We have access to all SVCs again. Check that our pinning happened as expected. */
+ DOCTEST_CHECK(interrupt_flag_while_pinned);
+ DOCTEST_CHECK(!interrupt_flag_after_unpin);
+
+ /* Check that our callback succeeded. */
+ DOCTEST_CHECK(callback_succeeded);
+ }
+
}
\ No newline at end of file