diff --git a/tests/TestSvc/source/test_thread_creation.arch.arm64.s b/tests/TestSvc/source/test_thread_creation.arch.arm64.s
new file mode 100644
index 000000000..22aa7fcde
--- /dev/null
+++ b/tests/TestSvc/source/test_thread_creation.arch.arm64.s
@@ -0,0 +1,43 @@
+/*
+ * 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 .
+ */
+
+/* ams::test::TestThreadCreateRegistersOnFunctionEntry(void *ctx) */
+.section .text._ZN3ams4test40TestThreadCreateRegistersOnFunctionEntryEPv, "ax", %progbits
+.global _ZN3ams4test40TestThreadCreateRegistersOnFunctionEntryEPv
+.type _ZN3ams4test40TestThreadCreateRegistersOnFunctionEntryEPv, %function
+_ZN3ams4test40TestThreadCreateRegistersOnFunctionEntryEPv:
+ /* Save all registers to our context. */
+ stp x0, x1, [x0, #0x00]
+ stp x2, x3, [x0, #0x10]
+ stp x4, x5, [x0, #0x20]
+ stp x6, x7, [x0, #0x30]
+ stp x8, x9, [x0, #0x40]
+ stp x10, x11, [x0, #0x50]
+ stp x12, x13, [x0, #0x60]
+ stp x14, x15, [x0, #0x70]
+ stp x16, x17, [x0, #0x80]
+ stp x18, x19, [x0, #0x90]
+ stp x20, x21, [x0, #0xA0]
+ stp x22, x23, [x0, #0xB0]
+ stp x24, x25, [x0, #0xC0]
+ stp x26, x27, [x0, #0xD0]
+ stp x28, x29, [x0, #0xE0]
+
+ mov x1, sp
+ stp x30, x1, [x0, #0xF0]
+
+ /* Exit the thread. */
+ svc 0xa
\ No newline at end of file
diff --git a/tests/TestSvc/source/test_thread_creation.cpp b/tests/TestSvc/source/test_thread_creation.cpp
new file mode 100644
index 000000000..e5a1e715c
--- /dev/null
+++ b/tests/TestSvc/source/test_thread_creation.cpp
@@ -0,0 +1,65 @@
+/*
+ * 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 {
+
+ void TestThreadCreateRegistersOnFunctionEntry(void *ctx);
+
+ DOCTEST_TEST_CASE( "Creating a thread results in fixed register contents." ) {
+ /* Create heap. */
+ ScopedHeap heap(os::MemoryPageSize);
+
+ /* Create register buffer. */
+ u64 thread_registers[32];
+ std::memset(thread_registers, 0xCC, sizeof(thread_registers));
+
+ /* Create thread. */
+ svc::Handle thread_handle;
+ DOCTEST_CHECK(R_SUCCEEDED(svc::CreateThread(std::addressof(thread_handle), reinterpret_cast(&TestThreadCreateRegistersOnFunctionEntry), reinterpret_cast(thread_registers), heap.GetAddress() + os::MemoryPageSize, HighestTestPriority, NumCores - 1)));
+
+ /* Start thread. */
+ DOCTEST_CHECK(R_SUCCEEDED(svc::StartThread(thread_handle)));
+
+ /* Wait for thread to exit. */
+ s32 dummy;
+ DOCTEST_CHECK(R_SUCCEEDED(svc::WaitSynchronization(std::addressof(dummy), std::addressof(thread_handle), 1, -1)));
+
+ /* Close thread handle. */
+ DOCTEST_CHECK(R_SUCCEEDED(svc::CloseHandle(thread_handle)));
+
+ /* Check thread initial registers. */
+ for (size_t i = 0; i < util::size(thread_registers); ++i) {
+ if (i == 0) {
+ /* X0 is argument. */
+ DOCTEST_CHECK(thread_registers[i] == reinterpret_cast(thread_registers));
+ } else if (i == 18) {
+ /* X18 is an odd cfi value. */
+ DOCTEST_CHECK(thread_registers[i] != 0);
+ DOCTEST_CHECK((thread_registers[i] & 0x1) != 0);
+ } else if (i == 31) {
+ /* SP is user-provided sp. */
+ DOCTEST_CHECK(thread_registers[i] == (heap.GetAddress() + os::MemoryPageSize));
+ } else {
+ /* All other registers are zero. */
+ DOCTEST_CHECK(thread_registers[i] == 0);
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/tests/TestSvc/source/util_common.hpp b/tests/TestSvc/source/util_common.hpp
index 0ba2e9c3b..86bfa9f9e 100644
--- a/tests/TestSvc/source/util_common.hpp
+++ b/tests/TestSvc/source/util_common.hpp
@@ -46,7 +46,7 @@ namespace ams::test {
/* 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;
+ constexpr auto MinimumTicksToGuaranteeInterruptFlag = ::ams::svc::Tick(PreemptionTimeSpan) + ::ams::svc::Tick(PreemptionTimeSpan) + 2;
auto GetSystemTickForPinnedThread = []() ALWAYS_INLINE_LAMBDA -> ::ams::svc::Tick {
s64 v;