diff --git a/.gitignore b/.gitignore
index 17aa0aa..9dbfc12 100644
--- a/.gitignore
+++ b/.gitignore
@@ -69,3 +69,17 @@ initramfs/system/lib/liboscortex_libc.so
iso_root_aarch64/
oscortex-aarch64.iso
tools/flutter-engine/linux-arm64/
+apps/_x86_aot_backup/
+*.iso
+iso_root/
+*.kernel
+
+# Dart/Flutter package build artifacts
+packages/*/.dart_tool/
+packages/*/pubspec.lock
+packages/*/.flutter-plugins*
+packages/*/build/
+
+# osx CLI per-app build config + outputs
+apps/*/.osx/
+apps/*/build/osx/
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..559d1ae
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,30 @@
+# OSCortex — agent instructions
+
+OSCortex is a from-scratch OS: a Rust microkernel (x86_64 + aarch64, no glibc/std,
+software-composited framebuffer, no GPU) running a custom prebuilt Flutter embedder
+as the shell + apps. Single source tree; active branch `feat/native-engine-port`,
+base `develop`.
+
+## ‼️ READ `rules.md` BEFORE STARTING AND BEFORE REPORTING DONE — MANDATORY
+[rules.md](rules.md) is binding for every agent. The short version:
+- **Never present a stub/placeholder/mock as done.** Label it in code, report it as a
+ stub, and never call it "done/working/complete".
+- **"Done" = verified end-to-end on the REAL artifact** (the actual ISO/binary the
+ user runs), with saved proof. Compiles/renders/tests-pass ≠ done.
+- **Report status as exactly one of: DONE / UNVERIFIED / SCAFFOLD-STUB / NOT STARTED.**
+- **Finish the task or name the gap precisely** (what's missing, why, effort). No
+ silent scope-narrowing.
+- Keep [docs/FEATURE_STATUS.md](docs/FEATURE_STATUS.md) honest and current.
+
+Violating these wastes the maintainer's time and breaks trust. Slower-but-true beats
+fast-but-fake, always.
+
+## Build / test (x86, the primary target)
+- Release build with the SMP app-launch fix: `KERNEL_FEATURES=smp X86_AOT=1 SKIP_CORE_APPS=1 bash scripts/build-iso.sh` → `oscortex.iso`. Add `input-hud` to KERNEL_FEATURES only for local debugging (never in a release).
+- The SMP app-launch fix is inert on a single core — test under `-smp 2`+.
+- Verify on the real ISO before claiming anything works (Rule 5).
+
+## Hard constraints
+- **No Claude/Anthropic attribution** on commits or anything cloud-bound.
+- Only verified work goes to cloud. Published == locally verified.
+- Never enter the maintainer's credentials/tokens — that is the maintainer's action.
diff --git a/apps/oscortex_app/ios/.gitignore b/apps/oscortex_app/ios/.gitignore
deleted file mode 100644
index 7a7f987..0000000
--- a/apps/oscortex_app/ios/.gitignore
+++ /dev/null
@@ -1,34 +0,0 @@
-**/dgph
-*.mode1v3
-*.mode2v3
-*.moved-aside
-*.pbxuser
-*.perspectivev3
-**/*sync/
-.sconsign.dblite
-.tags*
-**/.vagrant/
-**/DerivedData/
-Icon?
-**/Pods/
-**/.symlinks/
-profile
-xcuserdata
-**/.generated/
-Flutter/App.framework
-Flutter/Flutter.framework
-Flutter/Flutter.podspec
-Flutter/Generated.xcconfig
-Flutter/ephemeral/
-Flutter/app.flx
-Flutter/app.zip
-Flutter/flutter_assets/
-Flutter/flutter_export_environment.sh
-ServiceDefinitions.json
-Runner/GeneratedPluginRegistrant.*
-
-# Exceptions to above rules.
-!default.mode1v3
-!default.mode2v3
-!default.pbxuser
-!default.perspectivev3
diff --git a/apps/oscortex_app/ios/Flutter/AppFrameworkInfo.plist b/apps/oscortex_app/ios/Flutter/AppFrameworkInfo.plist
deleted file mode 100644
index 391a902..0000000
--- a/apps/oscortex_app/ios/Flutter/AppFrameworkInfo.plist
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- en
- CFBundleExecutable
- App
- CFBundleIdentifier
- io.flutter.flutter.app
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- App
- CFBundlePackageType
- FMWK
- CFBundleShortVersionString
- 1.0
- CFBundleSignature
- ????
- CFBundleVersion
- 1.0
-
-
diff --git a/apps/oscortex_app/ios/Flutter/Debug.xcconfig b/apps/oscortex_app/ios/Flutter/Debug.xcconfig
deleted file mode 100644
index 592ceee..0000000
--- a/apps/oscortex_app/ios/Flutter/Debug.xcconfig
+++ /dev/null
@@ -1 +0,0 @@
-#include "Generated.xcconfig"
diff --git a/apps/oscortex_app/ios/Flutter/Release.xcconfig b/apps/oscortex_app/ios/Flutter/Release.xcconfig
deleted file mode 100644
index 592ceee..0000000
--- a/apps/oscortex_app/ios/Flutter/Release.xcconfig
+++ /dev/null
@@ -1 +0,0 @@
-#include "Generated.xcconfig"
diff --git a/apps/oscortex_app/ios/Runner.xcodeproj/project.pbxproj b/apps/oscortex_app/ios/Runner.xcodeproj/project.pbxproj
deleted file mode 100644
index 89eba7c..0000000
--- a/apps/oscortex_app/ios/Runner.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,623 +0,0 @@
-// !$*UTF8*$!
-{
- archiveVersion = 1;
- classes = {
- };
- objectVersion = 54;
- objects = {
-
-/* Begin PBXBuildFile section */
- 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
- 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
- 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
- 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
- 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
- 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
- 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
- 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXContainerItemProxy section */
- 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 97C146E61CF9000F007C117D /* Project object */;
- proxyType = 1;
- remoteGlobalIDString = 97C146ED1CF9000F007C117D;
- remoteInfo = Runner;
- };
-/* End PBXContainerItemProxy section */
-
-/* Begin PBXCopyFilesBuildPhase section */
- 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
- isa = PBXCopyFilesBuildPhase;
- buildActionMask = 2147483647;
- dstPath = "";
- dstSubfolderSpec = 10;
- files = (
- );
- name = "Embed Frameworks";
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXCopyFilesBuildPhase section */
-
-/* Begin PBXFileReference section */
- 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
- 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
- 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
- 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
- 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
- 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
- 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
- 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
- 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
- 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
- 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
- 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
- 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
- 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
- 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
- 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
-/* End PBXFileReference section */
-
-/* Begin PBXFrameworksBuildPhase section */
- 97C146EB1CF9000F007C117D /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXFrameworksBuildPhase section */
-
-/* Begin PBXGroup section */
- 331C8082294A63A400263BE5 /* RunnerTests */ = {
- isa = PBXGroup;
- children = (
- 331C807B294A618700263BE5 /* RunnerTests.swift */,
- );
- path = RunnerTests;
- sourceTree = "";
- };
- 9740EEB11CF90186004384FC /* Flutter */ = {
- isa = PBXGroup;
- children = (
- 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
- 9740EEB21CF90195004384FC /* Debug.xcconfig */,
- 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
- 9740EEB31CF90195004384FC /* Generated.xcconfig */,
- );
- name = Flutter;
- sourceTree = "";
- };
- 97C146E51CF9000F007C117D = {
- isa = PBXGroup;
- children = (
- 9740EEB11CF90186004384FC /* Flutter */,
- 97C146F01CF9000F007C117D /* Runner */,
- 97C146EF1CF9000F007C117D /* Products */,
- 331C8082294A63A400263BE5 /* RunnerTests */,
- );
- sourceTree = "";
- };
- 97C146EF1CF9000F007C117D /* Products */ = {
- isa = PBXGroup;
- children = (
- 97C146EE1CF9000F007C117D /* Runner.app */,
- 331C8081294A63A400263BE5 /* RunnerTests.xctest */,
- );
- name = Products;
- sourceTree = "";
- };
- 97C146F01CF9000F007C117D /* Runner */ = {
- isa = PBXGroup;
- children = (
- 97C146FA1CF9000F007C117D /* Main.storyboard */,
- 97C146FD1CF9000F007C117D /* Assets.xcassets */,
- 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
- 97C147021CF9000F007C117D /* Info.plist */,
- 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
- 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
- 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
- 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
- 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
- );
- path = Runner;
- sourceTree = "";
- };
-/* End PBXGroup section */
-
-/* Begin PBXNativeTarget section */
- 331C8080294A63A400263BE5 /* RunnerTests */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
- buildPhases = (
- 331C807D294A63A400263BE5 /* Sources */,
- 331C807F294A63A400263BE5 /* Resources */,
- );
- buildRules = (
- );
- dependencies = (
- 331C8086294A63A400263BE5 /* PBXTargetDependency */,
- );
- name = RunnerTests;
- productName = RunnerTests;
- productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
- productType = "com.apple.product-type.bundle.unit-test";
- };
- 97C146ED1CF9000F007C117D /* Runner */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
- buildPhases = (
- 9740EEB61CF901F6004384FC /* Run Script */,
- 97C146EA1CF9000F007C117D /* Sources */,
- 97C146EB1CF9000F007C117D /* Frameworks */,
- 97C146EC1CF9000F007C117D /* Resources */,
- 9705A1C41CF9048500538489 /* Embed Frameworks */,
- 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
- );
- buildRules = (
- );
- dependencies = (
- );
- name = Runner;
- productName = Runner;
- productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
- productType = "com.apple.product-type.application";
- };
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
- 97C146E61CF9000F007C117D /* Project object */ = {
- isa = PBXProject;
- attributes = {
- BuildIndependentTargetsInParallel = YES;
- LastUpgradeCheck = 1510;
- ORGANIZATIONNAME = "";
- TargetAttributes = {
- 331C8080294A63A400263BE5 = {
- CreatedOnToolsVersion = 14.0;
- TestTargetID = 97C146ED1CF9000F007C117D;
- };
- 97C146ED1CF9000F007C117D = {
- CreatedOnToolsVersion = 7.3.1;
- LastSwiftMigration = 1100;
- };
- };
- };
- buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
- compatibilityVersion = "Xcode 9.3";
- developmentRegion = en;
- hasScannedForEncodings = 0;
- knownRegions = (
- en,
- Base,
- );
- mainGroup = 97C146E51CF9000F007C117D;
- productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
- projectDirPath = "";
- projectRoot = "";
- targets = (
- 97C146ED1CF9000F007C117D /* Runner */,
- 331C8080294A63A400263BE5 /* RunnerTests */,
- );
- };
-/* End PBXProject section */
-
-/* Begin PBXResourcesBuildPhase section */
- 331C807F294A63A400263BE5 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- 97C146EC1CF9000F007C117D /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
- 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
- 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
- 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXShellScriptBuildPhase section */
- 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
- isa = PBXShellScriptBuildPhase;
- alwaysOutOfDate = 1;
- buildActionMask = 2147483647;
- files = (
- );
- inputPaths = (
- "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
- );
- name = "Thin Binary";
- outputPaths = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
- };
- 9740EEB61CF901F6004384FC /* Run Script */ = {
- isa = PBXShellScriptBuildPhase;
- alwaysOutOfDate = 1;
- buildActionMask = 2147483647;
- files = (
- );
- inputPaths = (
- );
- name = "Run Script";
- outputPaths = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
- };
-/* End PBXShellScriptBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
- 331C807D294A63A400263BE5 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- 97C146EA1CF9000F007C117D /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
- 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
- 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXSourcesBuildPhase section */
-
-/* Begin PBXTargetDependency section */
- 331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- target = 97C146ED1CF9000F007C117D /* Runner */;
- targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
- };
-/* End PBXTargetDependency section */
-
-/* Begin PBXVariantGroup section */
- 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
- isa = PBXVariantGroup;
- children = (
- 97C146FB1CF9000F007C117D /* Base */,
- );
- name = Main.storyboard;
- sourceTree = "";
- };
- 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
- isa = PBXVariantGroup;
- children = (
- 97C147001CF9000F007C117D /* Base */,
- );
- name = LaunchScreen.storyboard;
- sourceTree = "";
- };
-/* End PBXVariantGroup section */
-
-/* Begin XCBuildConfiguration section */
- 249021D3217E4FDB00AE95B9 /* Profile */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_NS_ASSERTIONS = NO;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = NO;
- GCC_C_LANGUAGE_STANDARD = gnu99;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
- MTL_ENABLE_DEBUG_INFO = NO;
- SDKROOT = iphoneos;
- SUPPORTED_PLATFORMS = iphoneos;
- TARGETED_DEVICE_FAMILY = "1,2";
- VALIDATE_PRODUCT = YES;
- };
- name = Profile;
- };
- 249021D4217E4FDB00AE95B9 /* Profile */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = NNXKAV4XD5;
- ENABLE_BITCODE = NO;
- INFOPLIST_FILE = Runner/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
- SWIFT_VERSION = 5.0;
- VERSIONING_SYSTEM = "apple-generic";
- };
- name = Profile;
- };
- 331C8088294A63A400263BE5 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- BUNDLE_LOADER = "$(TEST_HOST)";
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- GENERATE_INFOPLIST_FILE = YES;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp.RunnerTests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- SWIFT_VERSION = 5.0;
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
- };
- name = Debug;
- };
- 331C8089294A63A400263BE5 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- BUNDLE_LOADER = "$(TEST_HOST)";
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- GENERATE_INFOPLIST_FILE = YES;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp.RunnerTests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_VERSION = 5.0;
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
- };
- name = Release;
- };
- 331C808A294A63A400263BE5 /* Profile */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- BUNDLE_LOADER = "$(TEST_HOST)";
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- GENERATE_INFOPLIST_FILE = YES;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp.RunnerTests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_VERSION = 5.0;
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
- };
- name = Profile;
- };
- 97C147031CF9000F007C117D /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = dwarf;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_TESTABILITY = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = NO;
- GCC_C_LANGUAGE_STANDARD = gnu99;
- GCC_DYNAMIC_NO_PIC = NO;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_OPTIMIZATION_LEVEL = 0;
- GCC_PREPROCESSOR_DEFINITIONS = (
- "DEBUG=1",
- "$(inherited)",
- );
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
- MTL_ENABLE_DEBUG_INFO = YES;
- ONLY_ACTIVE_ARCH = YES;
- SDKROOT = iphoneos;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Debug;
- };
- 97C147041CF9000F007C117D /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_NS_ASSERTIONS = NO;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = NO;
- GCC_C_LANGUAGE_STANDARD = gnu99;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
- MTL_ENABLE_DEBUG_INFO = NO;
- SDKROOT = iphoneos;
- SUPPORTED_PLATFORMS = iphoneos;
- SWIFT_COMPILATION_MODE = wholemodule;
- SWIFT_OPTIMIZATION_LEVEL = "-O";
- TARGETED_DEVICE_FAMILY = "1,2";
- VALIDATE_PRODUCT = YES;
- };
- name = Release;
- };
- 97C147061CF9000F007C117D /* Debug */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = NNXKAV4XD5;
- ENABLE_BITCODE = NO;
- INFOPLIST_FILE = Runner/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- SWIFT_VERSION = 5.0;
- VERSIONING_SYSTEM = "apple-generic";
- };
- name = Debug;
- };
- 97C147071CF9000F007C117D /* Release */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = NNXKAV4XD5;
- ENABLE_BITCODE = NO;
- INFOPLIST_FILE = Runner/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
- SWIFT_VERSION = 5.0;
- VERSIONING_SYSTEM = "apple-generic";
- };
- name = Release;
- };
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
- 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 331C8088294A63A400263BE5 /* Debug */,
- 331C8089294A63A400263BE5 /* Release */,
- 331C808A294A63A400263BE5 /* Profile */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 97C147031CF9000F007C117D /* Debug */,
- 97C147041CF9000F007C117D /* Release */,
- 249021D3217E4FDB00AE95B9 /* Profile */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 97C147061CF9000F007C117D /* Debug */,
- 97C147071CF9000F007C117D /* Release */,
- 249021D4217E4FDB00AE95B9 /* Profile */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
-/* End XCConfigurationList section */
- };
- rootObject = 97C146E61CF9000F007C117D /* Project object */;
-}
diff --git a/apps/oscortex_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/apps/oscortex_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 919434a..0000000
--- a/apps/oscortex_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/apps/oscortex_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/oscortex_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d9810..0000000
--- a/apps/oscortex_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/apps/oscortex_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/apps/oscortex_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
deleted file mode 100644
index f9b0d7c..0000000
--- a/apps/oscortex_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- PreviewsEnabled
-
-
-
diff --git a/apps/oscortex_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/apps/oscortex_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
deleted file mode 100644
index e3773d4..0000000
--- a/apps/oscortex_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ /dev/null
@@ -1,101 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/oscortex_app/ios/Runner.xcworkspace/contents.xcworkspacedata b/apps/oscortex_app/ios/Runner.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 1d526a1..0000000
--- a/apps/oscortex_app/ios/Runner.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/apps/oscortex_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/oscortex_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d9810..0000000
--- a/apps/oscortex_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/apps/oscortex_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/apps/oscortex_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
deleted file mode 100644
index f9b0d7c..0000000
--- a/apps/oscortex_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- PreviewsEnabled
-
-
-
diff --git a/apps/oscortex_app/ios/Runner/AppDelegate.swift b/apps/oscortex_app/ios/Runner/AppDelegate.swift
deleted file mode 100644
index c30b367..0000000
--- a/apps/oscortex_app/ios/Runner/AppDelegate.swift
+++ /dev/null
@@ -1,16 +0,0 @@
-import Flutter
-import UIKit
-
-@main
-@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
- override func application(
- _ application: UIApplication,
- didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
- ) -> Bool {
- return super.application(application, didFinishLaunchingWithOptions: launchOptions)
- }
-
- func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
- GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
- }
-}
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index d36b1fa..0000000
--- a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,122 +0,0 @@
-{
- "images" : [
- {
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "83.5x83.5",
- "idiom" : "ipad",
- "filename" : "Icon-App-83.5x83.5@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "1024x1024",
- "idiom" : "ios-marketing",
- "filename" : "Icon-App-1024x1024@1x.png",
- "scale" : "1x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
deleted file mode 100644
index dc9ada4..0000000
Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
deleted file mode 100644
index 7353c41..0000000
Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
deleted file mode 100644
index 797d452..0000000
Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
deleted file mode 100644
index 6ed2d93..0000000
Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
deleted file mode 100644
index 4cd7b00..0000000
Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
deleted file mode 100644
index fe73094..0000000
Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
deleted file mode 100644
index 321773c..0000000
Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
deleted file mode 100644
index 797d452..0000000
Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
deleted file mode 100644
index 502f463..0000000
Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
deleted file mode 100644
index 0ec3034..0000000
Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
deleted file mode 100644
index 0ec3034..0000000
Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
deleted file mode 100644
index e9f5fea..0000000
Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
deleted file mode 100644
index 84ac32a..0000000
Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
deleted file mode 100644
index 8953cba..0000000
Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
deleted file mode 100644
index 0467bf1..0000000
Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
deleted file mode 100644
index 0bedcf2..0000000
--- a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "filename" : "LaunchImage.png",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "LaunchImage@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "filename" : "LaunchImage@3x.png",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
deleted file mode 100644
index 9da19ea..0000000
Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
deleted file mode 100644
index 9da19ea..0000000
Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
deleted file mode 100644
index 9da19ea..0000000
Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ
diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
deleted file mode 100644
index 89c2725..0000000
--- a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Launch Screen Assets
-
-You can customize the launch screen with your own desired assets by replacing the image files in this directory.
-
-You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/apps/oscortex_app/ios/Runner/Base.lproj/LaunchScreen.storyboard b/apps/oscortex_app/ios/Runner/Base.lproj/LaunchScreen.storyboard
deleted file mode 100644
index f2e259c..0000000
--- a/apps/oscortex_app/ios/Runner/Base.lproj/LaunchScreen.storyboard
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/oscortex_app/ios/Runner/Base.lproj/Main.storyboard b/apps/oscortex_app/ios/Runner/Base.lproj/Main.storyboard
deleted file mode 100644
index f3c2851..0000000
--- a/apps/oscortex_app/ios/Runner/Base.lproj/Main.storyboard
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/oscortex_app/ios/Runner/Info.plist b/apps/oscortex_app/ios/Runner/Info.plist
deleted file mode 100644
index 42a6122..0000000
--- a/apps/oscortex_app/ios/Runner/Info.plist
+++ /dev/null
@@ -1,70 +0,0 @@
-
-
-
-
- CADisableMinimumFrameDurationOnPhone
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- Oscortex App
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- oscortex_app
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleSignature
- ????
- CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
- LSRequiresIPhoneOS
-
- UIApplicationSceneManifest
-
- UIApplicationSupportsMultipleScenes
-
- UISceneConfigurations
-
- UIWindowSceneSessionRoleApplication
-
-
- UISceneClassName
- UIWindowScene
- UISceneConfigurationName
- flutter
- UISceneDelegateClassName
- $(PRODUCT_MODULE_NAME).SceneDelegate
- UISceneStoryboardFile
- Main
-
-
-
-
- UIApplicationSupportsIndirectInputEvents
-
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
-
-
diff --git a/apps/oscortex_app/ios/Runner/Runner-Bridging-Header.h b/apps/oscortex_app/ios/Runner/Runner-Bridging-Header.h
deleted file mode 100644
index 308a2a5..0000000
--- a/apps/oscortex_app/ios/Runner/Runner-Bridging-Header.h
+++ /dev/null
@@ -1 +0,0 @@
-#import "GeneratedPluginRegistrant.h"
diff --git a/apps/oscortex_app/ios/Runner/SceneDelegate.swift b/apps/oscortex_app/ios/Runner/SceneDelegate.swift
deleted file mode 100644
index b9ce8ea..0000000
--- a/apps/oscortex_app/ios/Runner/SceneDelegate.swift
+++ /dev/null
@@ -1,6 +0,0 @@
-import Flutter
-import UIKit
-
-class SceneDelegate: FlutterSceneDelegate {
-
-}
diff --git a/apps/oscortex_app/ios/RunnerTests/RunnerTests.swift b/apps/oscortex_app/ios/RunnerTests/RunnerTests.swift
deleted file mode 100644
index 86a7c3b..0000000
--- a/apps/oscortex_app/ios/RunnerTests/RunnerTests.swift
+++ /dev/null
@@ -1,12 +0,0 @@
-import Flutter
-import UIKit
-import XCTest
-
-class RunnerTests: XCTestCase {
-
- func testExample() {
- // If you add code to the Runner application, consider adding tests here.
- // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
- }
-
-}
diff --git a/apps/oscortex_app/linux/.gitignore b/apps/oscortex_app/linux/.gitignore
deleted file mode 100644
index d3896c9..0000000
--- a/apps/oscortex_app/linux/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-flutter/ephemeral
diff --git a/apps/oscortex_app/linux/CMakeLists.txt b/apps/oscortex_app/linux/CMakeLists.txt
deleted file mode 100644
index 8c3beeb..0000000
--- a/apps/oscortex_app/linux/CMakeLists.txt
+++ /dev/null
@@ -1,128 +0,0 @@
-# Project-level configuration.
-cmake_minimum_required(VERSION 3.13)
-project(runner LANGUAGES CXX)
-
-# The name of the executable created for the application. Change this to change
-# the on-disk name of your application.
-set(BINARY_NAME "oscortex_app")
-# The unique GTK application identifier for this application. See:
-# https://wiki.gnome.org/HowDoI/ChooseApplicationID
-set(APPLICATION_ID "com.example.oscortex_app")
-
-# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
-# versions of CMake.
-cmake_policy(SET CMP0063 NEW)
-
-# Load bundled libraries from the lib/ directory relative to the binary.
-set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
-
-# Root filesystem for cross-building.
-if(FLUTTER_TARGET_PLATFORM_SYSROOT)
- set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
- set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
- set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
- set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
- set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
- set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
-endif()
-
-# Define build configuration options.
-if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
- set(CMAKE_BUILD_TYPE "Debug" CACHE
- STRING "Flutter build mode" FORCE)
- set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
- "Debug" "Profile" "Release")
-endif()
-
-# Compilation settings that should be applied to most targets.
-#
-# Be cautious about adding new options here, as plugins use this function by
-# default. In most cases, you should add new options to specific targets instead
-# of modifying this function.
-function(APPLY_STANDARD_SETTINGS TARGET)
- target_compile_features(${TARGET} PUBLIC cxx_std_14)
- target_compile_options(${TARGET} PRIVATE -Wall -Werror)
- target_compile_options(${TARGET} PRIVATE "$<$>:-O3>")
- target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>")
-endfunction()
-
-# Flutter library and tool build rules.
-set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
-add_subdirectory(${FLUTTER_MANAGED_DIR})
-
-# System-level dependencies.
-find_package(PkgConfig REQUIRED)
-pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
-
-# Application build; see runner/CMakeLists.txt.
-add_subdirectory("runner")
-
-# Run the Flutter tool portions of the build. This must not be removed.
-add_dependencies(${BINARY_NAME} flutter_assemble)
-
-# Only the install-generated bundle's copy of the executable will launch
-# correctly, since the resources must in the right relative locations. To avoid
-# people trying to run the unbundled copy, put it in a subdirectory instead of
-# the default top-level location.
-set_target_properties(${BINARY_NAME}
- PROPERTIES
- RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
-)
-
-
-# Generated plugin build rules, which manage building the plugins and adding
-# them to the application.
-include(flutter/generated_plugins.cmake)
-
-
-# === Installation ===
-# By default, "installing" just makes a relocatable bundle in the build
-# directory.
-set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
-if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
- set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
-endif()
-
-# Start with a clean build bundle directory every time.
-install(CODE "
- file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
- " COMPONENT Runtime)
-
-set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
-set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
-
-install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
- COMPONENT Runtime)
-
-install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
- COMPONENT Runtime)
-
-install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
- COMPONENT Runtime)
-
-foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
- install(FILES "${bundled_library}"
- DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
- COMPONENT Runtime)
-endforeach(bundled_library)
-
-# Copy the native assets provided by the build.dart from all packages.
-set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
-install(DIRECTORY "${NATIVE_ASSETS_DIR}"
- DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
- COMPONENT Runtime)
-
-# Fully re-copy the assets directory on each build to avoid having stale files
-# from a previous install.
-set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
-install(CODE "
- file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
- " COMPONENT Runtime)
-install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
- DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
-
-# Install the AOT library on non-Debug builds only.
-if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
- install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
- COMPONENT Runtime)
-endif()
diff --git a/apps/oscortex_app/linux/flutter/CMakeLists.txt b/apps/oscortex_app/linux/flutter/CMakeLists.txt
deleted file mode 100644
index d5bd016..0000000
--- a/apps/oscortex_app/linux/flutter/CMakeLists.txt
+++ /dev/null
@@ -1,88 +0,0 @@
-# This file controls Flutter-level build steps. It should not be edited.
-cmake_minimum_required(VERSION 3.10)
-
-set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
-
-# Configuration provided via flutter tool.
-include(${EPHEMERAL_DIR}/generated_config.cmake)
-
-# TODO: Move the rest of this into files in ephemeral. See
-# https://github.com/flutter/flutter/issues/57146.
-
-# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
-# which isn't available in 3.10.
-function(list_prepend LIST_NAME PREFIX)
- set(NEW_LIST "")
- foreach(element ${${LIST_NAME}})
- list(APPEND NEW_LIST "${PREFIX}${element}")
- endforeach(element)
- set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
-endfunction()
-
-# === Flutter Library ===
-# System-level dependencies.
-find_package(PkgConfig REQUIRED)
-pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
-pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
-pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
-
-set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
-
-# Published to parent scope for install step.
-set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
-set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
-set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
-set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
-
-list(APPEND FLUTTER_LIBRARY_HEADERS
- "fl_basic_message_channel.h"
- "fl_binary_codec.h"
- "fl_binary_messenger.h"
- "fl_dart_project.h"
- "fl_engine.h"
- "fl_json_message_codec.h"
- "fl_json_method_codec.h"
- "fl_message_codec.h"
- "fl_method_call.h"
- "fl_method_channel.h"
- "fl_method_codec.h"
- "fl_method_response.h"
- "fl_plugin_registrar.h"
- "fl_plugin_registry.h"
- "fl_standard_message_codec.h"
- "fl_standard_method_codec.h"
- "fl_string_codec.h"
- "fl_value.h"
- "fl_view.h"
- "flutter_linux.h"
-)
-list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
-add_library(flutter INTERFACE)
-target_include_directories(flutter INTERFACE
- "${EPHEMERAL_DIR}"
-)
-target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
-target_link_libraries(flutter INTERFACE
- PkgConfig::GTK
- PkgConfig::GLIB
- PkgConfig::GIO
-)
-add_dependencies(flutter flutter_assemble)
-
-# === Flutter tool backend ===
-# _phony_ is a non-existent file to force this command to run every time,
-# since currently there's no way to get a full input/output list from the
-# flutter tool.
-add_custom_command(
- OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
- ${CMAKE_CURRENT_BINARY_DIR}/_phony_
- COMMAND ${CMAKE_COMMAND} -E env
- ${FLUTTER_TOOL_ENVIRONMENT}
- "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
- ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
- VERBATIM
-)
-add_custom_target(flutter_assemble DEPENDS
- "${FLUTTER_LIBRARY}"
- ${FLUTTER_LIBRARY_HEADERS}
-)
diff --git a/apps/oscortex_app/linux/flutter/generated_plugin_registrant.cc b/apps/oscortex_app/linux/flutter/generated_plugin_registrant.cc
deleted file mode 100644
index e71a16d..0000000
--- a/apps/oscortex_app/linux/flutter/generated_plugin_registrant.cc
+++ /dev/null
@@ -1,11 +0,0 @@
-//
-// Generated file. Do not edit.
-//
-
-// clang-format off
-
-#include "generated_plugin_registrant.h"
-
-
-void fl_register_plugins(FlPluginRegistry* registry) {
-}
diff --git a/apps/oscortex_app/linux/flutter/generated_plugin_registrant.h b/apps/oscortex_app/linux/flutter/generated_plugin_registrant.h
deleted file mode 100644
index e0f0a47..0000000
--- a/apps/oscortex_app/linux/flutter/generated_plugin_registrant.h
+++ /dev/null
@@ -1,15 +0,0 @@
-//
-// Generated file. Do not edit.
-//
-
-// clang-format off
-
-#ifndef GENERATED_PLUGIN_REGISTRANT_
-#define GENERATED_PLUGIN_REGISTRANT_
-
-#include
-
-// Registers Flutter plugins.
-void fl_register_plugins(FlPluginRegistry* registry);
-
-#endif // GENERATED_PLUGIN_REGISTRANT_
diff --git a/apps/oscortex_app/linux/flutter/generated_plugins.cmake b/apps/oscortex_app/linux/flutter/generated_plugins.cmake
deleted file mode 100644
index 2e1de87..0000000
--- a/apps/oscortex_app/linux/flutter/generated_plugins.cmake
+++ /dev/null
@@ -1,23 +0,0 @@
-#
-# Generated file, do not edit.
-#
-
-list(APPEND FLUTTER_PLUGIN_LIST
-)
-
-list(APPEND FLUTTER_FFI_PLUGIN_LIST
-)
-
-set(PLUGIN_BUNDLED_LIBRARIES)
-
-foreach(plugin ${FLUTTER_PLUGIN_LIST})
- add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
- target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
- list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
- list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
-endforeach(plugin)
-
-foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
- add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
- list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
-endforeach(ffi_plugin)
diff --git a/apps/oscortex_app/linux/runner/CMakeLists.txt b/apps/oscortex_app/linux/runner/CMakeLists.txt
deleted file mode 100644
index e97dabc..0000000
--- a/apps/oscortex_app/linux/runner/CMakeLists.txt
+++ /dev/null
@@ -1,26 +0,0 @@
-cmake_minimum_required(VERSION 3.13)
-project(runner LANGUAGES CXX)
-
-# Define the application target. To change its name, change BINARY_NAME in the
-# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
-# work.
-#
-# Any new source files that you add to the application should be added here.
-add_executable(${BINARY_NAME}
- "main.cc"
- "my_application.cc"
- "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
-)
-
-# Apply the standard set of build settings. This can be removed for applications
-# that need different build settings.
-apply_standard_settings(${BINARY_NAME})
-
-# Add preprocessor definitions for the application ID.
-add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
-
-# Add dependency libraries. Add any application-specific dependencies here.
-target_link_libraries(${BINARY_NAME} PRIVATE flutter)
-target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
-
-target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
diff --git a/apps/oscortex_app/linux/runner/main.cc b/apps/oscortex_app/linux/runner/main.cc
deleted file mode 100644
index e7c5c54..0000000
--- a/apps/oscortex_app/linux/runner/main.cc
+++ /dev/null
@@ -1,6 +0,0 @@
-#include "my_application.h"
-
-int main(int argc, char** argv) {
- g_autoptr(MyApplication) app = my_application_new();
- return g_application_run(G_APPLICATION(app), argc, argv);
-}
diff --git a/apps/oscortex_app/linux/runner/my_application.cc b/apps/oscortex_app/linux/runner/my_application.cc
deleted file mode 100644
index 6311b86..0000000
--- a/apps/oscortex_app/linux/runner/my_application.cc
+++ /dev/null
@@ -1,148 +0,0 @@
-#include "my_application.h"
-
-#include
-#ifdef GDK_WINDOWING_X11
-#include
-#endif
-
-#include "flutter/generated_plugin_registrant.h"
-
-struct _MyApplication {
- GtkApplication parent_instance;
- char** dart_entrypoint_arguments;
-};
-
-G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
-
-// Called when first Flutter frame received.
-static void first_frame_cb(MyApplication* self, FlView* view) {
- gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
-}
-
-// Implements GApplication::activate.
-static void my_application_activate(GApplication* application) {
- MyApplication* self = MY_APPLICATION(application);
- GtkWindow* window =
- GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
-
- // Use a header bar when running in GNOME as this is the common style used
- // by applications and is the setup most users will be using (e.g. Ubuntu
- // desktop).
- // If running on X and not using GNOME then just use a traditional title bar
- // in case the window manager does more exotic layout, e.g. tiling.
- // If running on Wayland assume the header bar will work (may need changing
- // if future cases occur).
- gboolean use_header_bar = TRUE;
-#ifdef GDK_WINDOWING_X11
- GdkScreen* screen = gtk_window_get_screen(window);
- if (GDK_IS_X11_SCREEN(screen)) {
- const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
- if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
- use_header_bar = FALSE;
- }
- }
-#endif
- if (use_header_bar) {
- GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
- gtk_widget_show(GTK_WIDGET(header_bar));
- gtk_header_bar_set_title(header_bar, "oscortex_app");
- gtk_header_bar_set_show_close_button(header_bar, TRUE);
- gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
- } else {
- gtk_window_set_title(window, "oscortex_app");
- }
-
- gtk_window_set_default_size(window, 1280, 720);
-
- g_autoptr(FlDartProject) project = fl_dart_project_new();
- fl_dart_project_set_dart_entrypoint_arguments(
- project, self->dart_entrypoint_arguments);
-
- FlView* view = fl_view_new(project);
- GdkRGBA background_color;
- // Background defaults to black, override it here if necessary, e.g. #00000000
- // for transparent.
- gdk_rgba_parse(&background_color, "#000000");
- fl_view_set_background_color(view, &background_color);
- gtk_widget_show(GTK_WIDGET(view));
- gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
-
- // Show the window when Flutter renders.
- // Requires the view to be realized so we can start rendering.
- g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
- self);
- gtk_widget_realize(GTK_WIDGET(view));
-
- fl_register_plugins(FL_PLUGIN_REGISTRY(view));
-
- gtk_widget_grab_focus(GTK_WIDGET(view));
-}
-
-// Implements GApplication::local_command_line.
-static gboolean my_application_local_command_line(GApplication* application,
- gchar*** arguments,
- int* exit_status) {
- MyApplication* self = MY_APPLICATION(application);
- // Strip out the first argument as it is the binary name.
- self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
-
- g_autoptr(GError) error = nullptr;
- if (!g_application_register(application, nullptr, &error)) {
- g_warning("Failed to register: %s", error->message);
- *exit_status = 1;
- return TRUE;
- }
-
- g_application_activate(application);
- *exit_status = 0;
-
- return TRUE;
-}
-
-// Implements GApplication::startup.
-static void my_application_startup(GApplication* application) {
- // MyApplication* self = MY_APPLICATION(object);
-
- // Perform any actions required at application startup.
-
- G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
-}
-
-// Implements GApplication::shutdown.
-static void my_application_shutdown(GApplication* application) {
- // MyApplication* self = MY_APPLICATION(object);
-
- // Perform any actions required at application shutdown.
-
- G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
-}
-
-// Implements GObject::dispose.
-static void my_application_dispose(GObject* object) {
- MyApplication* self = MY_APPLICATION(object);
- g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
- G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
-}
-
-static void my_application_class_init(MyApplicationClass* klass) {
- G_APPLICATION_CLASS(klass)->activate = my_application_activate;
- G_APPLICATION_CLASS(klass)->local_command_line =
- my_application_local_command_line;
- G_APPLICATION_CLASS(klass)->startup = my_application_startup;
- G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
- G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
-}
-
-static void my_application_init(MyApplication* self) {}
-
-MyApplication* my_application_new() {
- // Set the program name to the application ID, which helps various systems
- // like GTK and desktop environments map this running application to its
- // corresponding .desktop file. This ensures better integration by allowing
- // the application to be recognized beyond its binary name.
- g_set_prgname(APPLICATION_ID);
-
- return MY_APPLICATION(g_object_new(my_application_get_type(),
- "application-id", APPLICATION_ID, "flags",
- G_APPLICATION_NON_UNIQUE, nullptr));
-}
diff --git a/apps/oscortex_app/linux/runner/my_application.h b/apps/oscortex_app/linux/runner/my_application.h
deleted file mode 100644
index db16367..0000000
--- a/apps/oscortex_app/linux/runner/my_application.h
+++ /dev/null
@@ -1,21 +0,0 @@
-#ifndef FLUTTER_MY_APPLICATION_H_
-#define FLUTTER_MY_APPLICATION_H_
-
-#include
-
-G_DECLARE_FINAL_TYPE(MyApplication,
- my_application,
- MY,
- APPLICATION,
- GtkApplication)
-
-/**
- * my_application_new:
- *
- * Creates a new Flutter-based application.
- *
- * Returns: a new #MyApplication.
- */
-MyApplication* my_application_new();
-
-#endif // FLUTTER_MY_APPLICATION_H_
diff --git a/apps/oscortex_app/macos/.gitignore b/apps/oscortex_app/macos/.gitignore
deleted file mode 100644
index 746adbb..0000000
--- a/apps/oscortex_app/macos/.gitignore
+++ /dev/null
@@ -1,7 +0,0 @@
-# Flutter-related
-**/Flutter/ephemeral/
-**/Pods/
-
-# Xcode-related
-**/dgph
-**/xcuserdata/
diff --git a/apps/oscortex_app/macos/Flutter/Flutter-Debug.xcconfig b/apps/oscortex_app/macos/Flutter/Flutter-Debug.xcconfig
deleted file mode 100644
index c2efd0b..0000000
--- a/apps/oscortex_app/macos/Flutter/Flutter-Debug.xcconfig
+++ /dev/null
@@ -1 +0,0 @@
-#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/apps/oscortex_app/macos/Flutter/Flutter-Release.xcconfig b/apps/oscortex_app/macos/Flutter/Flutter-Release.xcconfig
deleted file mode 100644
index c2efd0b..0000000
--- a/apps/oscortex_app/macos/Flutter/Flutter-Release.xcconfig
+++ /dev/null
@@ -1 +0,0 @@
-#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/apps/oscortex_app/macos/Flutter/GeneratedPluginRegistrant.swift b/apps/oscortex_app/macos/Flutter/GeneratedPluginRegistrant.swift
deleted file mode 100644
index cccf817..0000000
--- a/apps/oscortex_app/macos/Flutter/GeneratedPluginRegistrant.swift
+++ /dev/null
@@ -1,10 +0,0 @@
-//
-// Generated file. Do not edit.
-//
-
-import FlutterMacOS
-import Foundation
-
-
-func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
-}
diff --git a/apps/oscortex_app/macos/Runner.xcodeproj/project.pbxproj b/apps/oscortex_app/macos/Runner.xcodeproj/project.pbxproj
deleted file mode 100644
index cc667c8..0000000
--- a/apps/oscortex_app/macos/Runner.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,705 +0,0 @@
-// !$*UTF8*$!
-{
- archiveVersion = 1;
- classes = {
- };
- objectVersion = 54;
- objects = {
-
-/* Begin PBXAggregateTarget section */
- 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
- isa = PBXAggregateTarget;
- buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
- buildPhases = (
- 33CC111E2044C6BF0003C045 /* ShellScript */,
- );
- dependencies = (
- );
- name = "Flutter Assemble";
- productName = FLX;
- };
-/* End PBXAggregateTarget section */
-
-/* Begin PBXBuildFile section */
- 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
- 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
- 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
- 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
- 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
- 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXContainerItemProxy section */
- 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
- proxyType = 1;
- remoteGlobalIDString = 33CC10EC2044A3C60003C045;
- remoteInfo = Runner;
- };
- 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
- proxyType = 1;
- remoteGlobalIDString = 33CC111A2044C6BA0003C045;
- remoteInfo = FLX;
- };
-/* End PBXContainerItemProxy section */
-
-/* Begin PBXCopyFilesBuildPhase section */
- 33CC110E2044A8840003C045 /* Bundle Framework */ = {
- isa = PBXCopyFilesBuildPhase;
- buildActionMask = 2147483647;
- dstPath = "";
- dstSubfolderSpec = 10;
- files = (
- );
- name = "Bundle Framework";
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXCopyFilesBuildPhase section */
-
-/* Begin PBXFileReference section */
- 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
- 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
- 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; };
- 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; };
- 33CC10ED2044A3C60003C045 /* oscortex_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "oscortex_app.app"; sourceTree = BUILT_PRODUCTS_DIR; };
- 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
- 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; };
- 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
- 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; };
- 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; };
- 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; };
- 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; };
- 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; };
- 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; };
- 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; };
- 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; };
- 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; };
- 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; };
-/* End PBXFileReference section */
-
-/* Begin PBXFrameworksBuildPhase section */
- 331C80D2294CF70F00263BE5 /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- 33CC10EA2044A3C60003C045 /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXFrameworksBuildPhase section */
-
-/* Begin PBXGroup section */
- 331C80D6294CF71000263BE5 /* RunnerTests */ = {
- isa = PBXGroup;
- children = (
- 331C80D7294CF71000263BE5 /* RunnerTests.swift */,
- );
- path = RunnerTests;
- sourceTree = "";
- };
- 33BA886A226E78AF003329D5 /* Configs */ = {
- isa = PBXGroup;
- children = (
- 33E5194F232828860026EE4D /* AppInfo.xcconfig */,
- 9740EEB21CF90195004384FC /* Debug.xcconfig */,
- 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
- 333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
- );
- path = Configs;
- sourceTree = "";
- };
- 33CC10E42044A3C60003C045 = {
- isa = PBXGroup;
- children = (
- 33FAB671232836740065AC1E /* Runner */,
- 33CEB47122A05771004F2AC0 /* Flutter */,
- 331C80D6294CF71000263BE5 /* RunnerTests */,
- 33CC10EE2044A3C60003C045 /* Products */,
- D73912EC22F37F3D000D13A0 /* Frameworks */,
- );
- sourceTree = "";
- };
- 33CC10EE2044A3C60003C045 /* Products */ = {
- isa = PBXGroup;
- children = (
- 33CC10ED2044A3C60003C045 /* oscortex_app.app */,
- 331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
- );
- name = Products;
- sourceTree = "";
- };
- 33CC11242044D66E0003C045 /* Resources */ = {
- isa = PBXGroup;
- children = (
- 33CC10F22044A3C60003C045 /* Assets.xcassets */,
- 33CC10F42044A3C60003C045 /* MainMenu.xib */,
- 33CC10F72044A3C60003C045 /* Info.plist */,
- );
- name = Resources;
- path = ..;
- sourceTree = "";
- };
- 33CEB47122A05771004F2AC0 /* Flutter */ = {
- isa = PBXGroup;
- children = (
- 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
- 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
- 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
- 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
- );
- path = Flutter;
- sourceTree = "";
- };
- 33FAB671232836740065AC1E /* Runner */ = {
- isa = PBXGroup;
- children = (
- 33CC10F02044A3C60003C045 /* AppDelegate.swift */,
- 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
- 33E51913231747F40026EE4D /* DebugProfile.entitlements */,
- 33E51914231749380026EE4D /* Release.entitlements */,
- 33CC11242044D66E0003C045 /* Resources */,
- 33BA886A226E78AF003329D5 /* Configs */,
- );
- path = Runner;
- sourceTree = "";
- };
- D73912EC22F37F3D000D13A0 /* Frameworks */ = {
- isa = PBXGroup;
- children = (
- );
- name = Frameworks;
- sourceTree = "";
- };
-/* End PBXGroup section */
-
-/* Begin PBXNativeTarget section */
- 331C80D4294CF70F00263BE5 /* RunnerTests */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
- buildPhases = (
- 331C80D1294CF70F00263BE5 /* Sources */,
- 331C80D2294CF70F00263BE5 /* Frameworks */,
- 331C80D3294CF70F00263BE5 /* Resources */,
- );
- buildRules = (
- );
- dependencies = (
- 331C80DA294CF71000263BE5 /* PBXTargetDependency */,
- );
- name = RunnerTests;
- productName = RunnerTests;
- productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
- productType = "com.apple.product-type.bundle.unit-test";
- };
- 33CC10EC2044A3C60003C045 /* Runner */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
- buildPhases = (
- 33CC10E92044A3C60003C045 /* Sources */,
- 33CC10EA2044A3C60003C045 /* Frameworks */,
- 33CC10EB2044A3C60003C045 /* Resources */,
- 33CC110E2044A8840003C045 /* Bundle Framework */,
- 3399D490228B24CF009A79C7 /* ShellScript */,
- );
- buildRules = (
- );
- dependencies = (
- 33CC11202044C79F0003C045 /* PBXTargetDependency */,
- );
- name = Runner;
- productName = Runner;
- productReference = 33CC10ED2044A3C60003C045 /* oscortex_app.app */;
- productType = "com.apple.product-type.application";
- };
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
- 33CC10E52044A3C60003C045 /* Project object */ = {
- isa = PBXProject;
- attributes = {
- BuildIndependentTargetsInParallel = YES;
- LastSwiftUpdateCheck = 0920;
- LastUpgradeCheck = 1510;
- ORGANIZATIONNAME = "";
- TargetAttributes = {
- 331C80D4294CF70F00263BE5 = {
- CreatedOnToolsVersion = 14.0;
- TestTargetID = 33CC10EC2044A3C60003C045;
- };
- 33CC10EC2044A3C60003C045 = {
- CreatedOnToolsVersion = 9.2;
- LastSwiftMigration = 1100;
- ProvisioningStyle = Automatic;
- SystemCapabilities = {
- com.apple.Sandbox = {
- enabled = 1;
- };
- };
- };
- 33CC111A2044C6BA0003C045 = {
- CreatedOnToolsVersion = 9.2;
- ProvisioningStyle = Manual;
- };
- };
- };
- buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
- compatibilityVersion = "Xcode 9.3";
- developmentRegion = en;
- hasScannedForEncodings = 0;
- knownRegions = (
- en,
- Base,
- );
- mainGroup = 33CC10E42044A3C60003C045;
- productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
- projectDirPath = "";
- projectRoot = "";
- targets = (
- 33CC10EC2044A3C60003C045 /* Runner */,
- 331C80D4294CF70F00263BE5 /* RunnerTests */,
- 33CC111A2044C6BA0003C045 /* Flutter Assemble */,
- );
- };
-/* End PBXProject section */
-
-/* Begin PBXResourcesBuildPhase section */
- 331C80D3294CF70F00263BE5 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- 33CC10EB2044A3C60003C045 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
- 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXShellScriptBuildPhase section */
- 3399D490228B24CF009A79C7 /* ShellScript */ = {
- isa = PBXShellScriptBuildPhase;
- alwaysOutOfDate = 1;
- buildActionMask = 2147483647;
- files = (
- );
- inputFileListPaths = (
- );
- inputPaths = (
- );
- outputFileListPaths = (
- );
- outputPaths = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
- };
- 33CC111E2044C6BF0003C045 /* ShellScript */ = {
- isa = PBXShellScriptBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- inputFileListPaths = (
- Flutter/ephemeral/FlutterInputs.xcfilelist,
- );
- inputPaths = (
- Flutter/ephemeral/tripwire,
- );
- outputFileListPaths = (
- Flutter/ephemeral/FlutterOutputs.xcfilelist,
- );
- outputPaths = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
- };
-/* End PBXShellScriptBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
- 331C80D1294CF70F00263BE5 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- 33CC10E92044A3C60003C045 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
- 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
- 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXSourcesBuildPhase section */
-
-/* Begin PBXTargetDependency section */
- 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- target = 33CC10EC2044A3C60003C045 /* Runner */;
- targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
- };
- 33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
- targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
- };
-/* End PBXTargetDependency section */
-
-/* Begin PBXVariantGroup section */
- 33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
- isa = PBXVariantGroup;
- children = (
- 33CC10F52044A3C60003C045 /* Base */,
- );
- name = MainMenu.xib;
- path = Runner;
- sourceTree = "";
- };
-/* End PBXVariantGroup section */
-
-/* Begin XCBuildConfiguration section */
- 331C80DB294CF71000263BE5 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- BUNDLE_LOADER = "$(TEST_HOST)";
- CURRENT_PROJECT_VERSION = 1;
- GENERATE_INFOPLIST_FILE = YES;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp.RunnerTests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_VERSION = 5.0;
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/oscortex_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/oscortex_app";
- };
- name = Debug;
- };
- 331C80DC294CF71000263BE5 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- BUNDLE_LOADER = "$(TEST_HOST)";
- CURRENT_PROJECT_VERSION = 1;
- GENERATE_INFOPLIST_FILE = YES;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp.RunnerTests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_VERSION = 5.0;
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/oscortex_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/oscortex_app";
- };
- name = Release;
- };
- 331C80DD294CF71000263BE5 /* Profile */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- BUNDLE_LOADER = "$(TEST_HOST)";
- CURRENT_PROJECT_VERSION = 1;
- GENERATE_INFOPLIST_FILE = YES;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp.RunnerTests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_VERSION = 5.0;
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/oscortex_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/oscortex_app";
- };
- name = Profile;
- };
- 338D0CE9231458BD00FA5F75 /* Profile */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CODE_SIGN_IDENTITY = "-";
- COPY_PHASE_STRIP = NO;
- DEAD_CODE_STRIPPING = YES;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_NS_ASSERTIONS = NO;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = NO;
- GCC_C_LANGUAGE_STANDARD = gnu11;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- MACOSX_DEPLOYMENT_TARGET = 10.15;
- MTL_ENABLE_DEBUG_INFO = NO;
- SDKROOT = macosx;
- SWIFT_COMPILATION_MODE = wholemodule;
- SWIFT_OPTIMIZATION_LEVEL = "-O";
- };
- name = Profile;
- };
- 338D0CEA231458BD00FA5F75 /* Profile */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CLANG_ENABLE_MODULES = YES;
- CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
- CODE_SIGN_STYLE = Automatic;
- COMBINE_HIDPI_IMAGES = YES;
- INFOPLIST_FILE = Runner/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/../Frameworks",
- );
- PROVISIONING_PROFILE_SPECIFIER = "";
- SWIFT_VERSION = 5.0;
- };
- name = Profile;
- };
- 338D0CEB231458BD00FA5F75 /* Profile */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- CODE_SIGN_STYLE = Manual;
- PRODUCT_NAME = "$(TARGET_NAME)";
- };
- name = Profile;
- };
- 33CC10F92044A3C60003C045 /* Debug */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CODE_SIGN_IDENTITY = "-";
- COPY_PHASE_STRIP = NO;
- DEAD_CODE_STRIPPING = YES;
- DEBUG_INFORMATION_FORMAT = dwarf;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_TESTABILITY = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = NO;
- GCC_C_LANGUAGE_STANDARD = gnu11;
- GCC_DYNAMIC_NO_PIC = NO;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_OPTIMIZATION_LEVEL = 0;
- GCC_PREPROCESSOR_DEFINITIONS = (
- "DEBUG=1",
- "$(inherited)",
- );
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- MACOSX_DEPLOYMENT_TARGET = 10.15;
- MTL_ENABLE_DEBUG_INFO = YES;
- ONLY_ACTIVE_ARCH = YES;
- SDKROOT = macosx;
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- };
- name = Debug;
- };
- 33CC10FA2044A3C60003C045 /* Release */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CODE_SIGN_IDENTITY = "-";
- COPY_PHASE_STRIP = NO;
- DEAD_CODE_STRIPPING = YES;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_NS_ASSERTIONS = NO;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = NO;
- GCC_C_LANGUAGE_STANDARD = gnu11;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- MACOSX_DEPLOYMENT_TARGET = 10.15;
- MTL_ENABLE_DEBUG_INFO = NO;
- SDKROOT = macosx;
- SWIFT_COMPILATION_MODE = wholemodule;
- SWIFT_OPTIMIZATION_LEVEL = "-O";
- };
- name = Release;
- };
- 33CC10FC2044A3C60003C045 /* Debug */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CLANG_ENABLE_MODULES = YES;
- CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
- CODE_SIGN_STYLE = Automatic;
- COMBINE_HIDPI_IMAGES = YES;
- INFOPLIST_FILE = Runner/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/../Frameworks",
- );
- PROVISIONING_PROFILE_SPECIFIER = "";
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- SWIFT_VERSION = 5.0;
- };
- name = Debug;
- };
- 33CC10FD2044A3C60003C045 /* Release */ = {
- isa = XCBuildConfiguration;
- baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CLANG_ENABLE_MODULES = YES;
- CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
- CODE_SIGN_STYLE = Automatic;
- COMBINE_HIDPI_IMAGES = YES;
- INFOPLIST_FILE = Runner/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/../Frameworks",
- );
- PROVISIONING_PROFILE_SPECIFIER = "";
- SWIFT_VERSION = 5.0;
- };
- name = Release;
- };
- 33CC111C2044C6BA0003C045 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- CODE_SIGN_STYLE = Manual;
- PRODUCT_NAME = "$(TARGET_NAME)";
- };
- name = Debug;
- };
- 33CC111D2044C6BA0003C045 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- CODE_SIGN_STYLE = Automatic;
- PRODUCT_NAME = "$(TARGET_NAME)";
- };
- name = Release;
- };
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
- 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 331C80DB294CF71000263BE5 /* Debug */,
- 331C80DC294CF71000263BE5 /* Release */,
- 331C80DD294CF71000263BE5 /* Profile */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 33CC10F92044A3C60003C045 /* Debug */,
- 33CC10FA2044A3C60003C045 /* Release */,
- 338D0CE9231458BD00FA5F75 /* Profile */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 33CC10FC2044A3C60003C045 /* Debug */,
- 33CC10FD2044A3C60003C045 /* Release */,
- 338D0CEA231458BD00FA5F75 /* Profile */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 33CC111C2044C6BA0003C045 /* Debug */,
- 33CC111D2044C6BA0003C045 /* Release */,
- 338D0CEB231458BD00FA5F75 /* Profile */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
-/* End XCConfigurationList section */
- };
- rootObject = 33CC10E52044A3C60003C045 /* Project object */;
-}
diff --git a/apps/oscortex_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/oscortex_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d9810..0000000
--- a/apps/oscortex_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/apps/oscortex_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/apps/oscortex_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
deleted file mode 100644
index a0457b0..0000000
--- a/apps/oscortex_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ /dev/null
@@ -1,99 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/oscortex_app/macos/Runner.xcworkspace/contents.xcworkspacedata b/apps/oscortex_app/macos/Runner.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 1d526a1..0000000
--- a/apps/oscortex_app/macos/Runner.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/apps/oscortex_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/oscortex_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d9810..0000000
--- a/apps/oscortex_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/apps/oscortex_app/macos/Runner/AppDelegate.swift b/apps/oscortex_app/macos/Runner/AppDelegate.swift
deleted file mode 100644
index b3c1761..0000000
--- a/apps/oscortex_app/macos/Runner/AppDelegate.swift
+++ /dev/null
@@ -1,13 +0,0 @@
-import Cocoa
-import FlutterMacOS
-
-@main
-class AppDelegate: FlutterAppDelegate {
- override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
- return true
- }
-
- override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
- return true
- }
-}
diff --git a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index a2ec33f..0000000
--- a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,68 +0,0 @@
-{
- "images" : [
- {
- "size" : "16x16",
- "idiom" : "mac",
- "filename" : "app_icon_16.png",
- "scale" : "1x"
- },
- {
- "size" : "16x16",
- "idiom" : "mac",
- "filename" : "app_icon_32.png",
- "scale" : "2x"
- },
- {
- "size" : "32x32",
- "idiom" : "mac",
- "filename" : "app_icon_32.png",
- "scale" : "1x"
- },
- {
- "size" : "32x32",
- "idiom" : "mac",
- "filename" : "app_icon_64.png",
- "scale" : "2x"
- },
- {
- "size" : "128x128",
- "idiom" : "mac",
- "filename" : "app_icon_128.png",
- "scale" : "1x"
- },
- {
- "size" : "128x128",
- "idiom" : "mac",
- "filename" : "app_icon_256.png",
- "scale" : "2x"
- },
- {
- "size" : "256x256",
- "idiom" : "mac",
- "filename" : "app_icon_256.png",
- "scale" : "1x"
- },
- {
- "size" : "256x256",
- "idiom" : "mac",
- "filename" : "app_icon_512.png",
- "scale" : "2x"
- },
- {
- "size" : "512x512",
- "idiom" : "mac",
- "filename" : "app_icon_512.png",
- "scale" : "1x"
- },
- {
- "size" : "512x512",
- "idiom" : "mac",
- "filename" : "app_icon_1024.png",
- "scale" : "2x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
diff --git a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
deleted file mode 100644
index 82b6f9d..0000000
Binary files a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ
diff --git a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
deleted file mode 100644
index 13b35eb..0000000
Binary files a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ
diff --git a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
deleted file mode 100644
index 0a3f5fa..0000000
Binary files a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ
diff --git a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
deleted file mode 100644
index bdb5722..0000000
Binary files a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ
diff --git a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
deleted file mode 100644
index f083318..0000000
Binary files a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ
diff --git a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
deleted file mode 100644
index 326c0e7..0000000
Binary files a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ
diff --git a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
deleted file mode 100644
index 2f1632c..0000000
Binary files a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ
diff --git a/apps/oscortex_app/macos/Runner/Base.lproj/MainMenu.xib b/apps/oscortex_app/macos/Runner/Base.lproj/MainMenu.xib
deleted file mode 100644
index 80e867a..0000000
--- a/apps/oscortex_app/macos/Runner/Base.lproj/MainMenu.xib
+++ /dev/null
@@ -1,343 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/oscortex_app/macos/Runner/Configs/AppInfo.xcconfig b/apps/oscortex_app/macos/Runner/Configs/AppInfo.xcconfig
deleted file mode 100644
index 3e49e0f..0000000
--- a/apps/oscortex_app/macos/Runner/Configs/AppInfo.xcconfig
+++ /dev/null
@@ -1,14 +0,0 @@
-// Application-level settings for the Runner target.
-//
-// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
-// future. If not, the values below would default to using the project name when this becomes a
-// 'flutter create' template.
-
-// The application's name. By default this is also the title of the Flutter window.
-PRODUCT_NAME = oscortex_app
-
-// The application's bundle identifier
-PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp
-
-// The copyright displayed in application information
-PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved.
diff --git a/apps/oscortex_app/macos/Runner/Configs/Debug.xcconfig b/apps/oscortex_app/macos/Runner/Configs/Debug.xcconfig
deleted file mode 100644
index 36b0fd9..0000000
--- a/apps/oscortex_app/macos/Runner/Configs/Debug.xcconfig
+++ /dev/null
@@ -1,2 +0,0 @@
-#include "../../Flutter/Flutter-Debug.xcconfig"
-#include "Warnings.xcconfig"
diff --git a/apps/oscortex_app/macos/Runner/Configs/Release.xcconfig b/apps/oscortex_app/macos/Runner/Configs/Release.xcconfig
deleted file mode 100644
index dff4f49..0000000
--- a/apps/oscortex_app/macos/Runner/Configs/Release.xcconfig
+++ /dev/null
@@ -1,2 +0,0 @@
-#include "../../Flutter/Flutter-Release.xcconfig"
-#include "Warnings.xcconfig"
diff --git a/apps/oscortex_app/macos/Runner/Configs/Warnings.xcconfig b/apps/oscortex_app/macos/Runner/Configs/Warnings.xcconfig
deleted file mode 100644
index 42bcbf4..0000000
--- a/apps/oscortex_app/macos/Runner/Configs/Warnings.xcconfig
+++ /dev/null
@@ -1,13 +0,0 @@
-WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
-GCC_WARN_UNDECLARED_SELECTOR = YES
-CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
-CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
-CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
-CLANG_WARN_PRAGMA_PACK = YES
-CLANG_WARN_STRICT_PROTOTYPES = YES
-CLANG_WARN_COMMA = YES
-GCC_WARN_STRICT_SELECTOR_MATCH = YES
-CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
-CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
-GCC_WARN_SHADOW = YES
-CLANG_WARN_UNREACHABLE_CODE = YES
diff --git a/apps/oscortex_app/macos/Runner/DebugProfile.entitlements b/apps/oscortex_app/macos/Runner/DebugProfile.entitlements
deleted file mode 100644
index dddb8a3..0000000
--- a/apps/oscortex_app/macos/Runner/DebugProfile.entitlements
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
- com.apple.security.app-sandbox
-
- com.apple.security.cs.allow-jit
-
- com.apple.security.network.server
-
-
-
diff --git a/apps/oscortex_app/macos/Runner/Info.plist b/apps/oscortex_app/macos/Runner/Info.plist
deleted file mode 100644
index 4789daa..0000000
--- a/apps/oscortex_app/macos/Runner/Info.plist
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIconFile
-
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- $(PRODUCT_NAME)
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
- LSMinimumSystemVersion
- $(MACOSX_DEPLOYMENT_TARGET)
- NSHumanReadableCopyright
- $(PRODUCT_COPYRIGHT)
- NSMainNibFile
- MainMenu
- NSPrincipalClass
- NSApplication
-
-
diff --git a/apps/oscortex_app/macos/Runner/MainFlutterWindow.swift b/apps/oscortex_app/macos/Runner/MainFlutterWindow.swift
deleted file mode 100644
index 3cc05eb..0000000
--- a/apps/oscortex_app/macos/Runner/MainFlutterWindow.swift
+++ /dev/null
@@ -1,15 +0,0 @@
-import Cocoa
-import FlutterMacOS
-
-class MainFlutterWindow: NSWindow {
- override func awakeFromNib() {
- let flutterViewController = FlutterViewController()
- let windowFrame = self.frame
- self.contentViewController = flutterViewController
- self.setFrame(windowFrame, display: true)
-
- RegisterGeneratedPlugins(registry: flutterViewController)
-
- super.awakeFromNib()
- }
-}
diff --git a/apps/oscortex_app/macos/Runner/Release.entitlements b/apps/oscortex_app/macos/Runner/Release.entitlements
deleted file mode 100644
index 852fa1a..0000000
--- a/apps/oscortex_app/macos/Runner/Release.entitlements
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- com.apple.security.app-sandbox
-
-
-
diff --git a/apps/oscortex_app/macos/RunnerTests/RunnerTests.swift b/apps/oscortex_app/macos/RunnerTests/RunnerTests.swift
deleted file mode 100644
index 61f3bd1..0000000
--- a/apps/oscortex_app/macos/RunnerTests/RunnerTests.swift
+++ /dev/null
@@ -1,12 +0,0 @@
-import Cocoa
-import FlutterMacOS
-import XCTest
-
-class RunnerTests: XCTestCase {
-
- func testExample() {
- // If you add code to the Runner application, consider adding tests here.
- // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
- }
-
-}
diff --git a/apps/oscortex_web_link/.gitignore b/apps/oscortex_web_link/.gitignore
new file mode 100644
index 0000000..54c072e
--- /dev/null
+++ b/apps/oscortex_web_link/.gitignore
@@ -0,0 +1,3 @@
+
+# OSCortex build artifacts
+build/osx/
diff --git a/apps/oscortex_web_link/lib/main.dart b/apps/oscortex_web_link/lib/main.dart
index 7e01229..4e081e5 100644
--- a/apps/oscortex_web_link/lib/main.dart
+++ b/apps/oscortex_web_link/lib/main.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:oscortex_ui/oscortex_ui.dart';
+import 'package:oscortex_webview/oscortex_webview.dart';
void main() {
runApp(const WebLinkApp());
@@ -18,6 +19,11 @@ class WebLinkApp extends StatelessWidget {
}
}
+/// Web Link — the OSCortex system browser. Chrome (address/search bar, nav,
+/// progress) wrapping the native [OscWebView], whose page is rendered by the
+/// userspace web-engine service into an OSCortex compositor surface beneath this
+/// scene. Until that engine service binds, navigation is inert and the web region
+/// shows a placeholder (the controller tolerates a missing engine).
class WebLinkHome extends StatefulWidget {
const WebLinkHome({super.key});
@@ -26,364 +32,261 @@ class WebLinkHome extends StatefulWidget {
}
class _WebLinkHomeState extends State {
- final _controller =
- TextEditingController(text: 'https://dccortec.com/docs/arch');
+ static const String _homeUrl = 'https://dotcorr.com/';
+
+ final OscWebViewController _web = OscWebViewController(1);
+ final TextEditingController _address = TextEditingController(text: _homeUrl);
+ final FocusNode _addressFocus = FocusNode();
+
+ int _progress = 0;
+ bool _loading = false;
+ bool _canBack = false;
+ bool _canForward = false;
+ bool _engineReady = false; // a render surface exists
+ bool _createRequested = false;
+
+ @override
+ void initState() {
+ super.initState();
+ _web.onUrlChanged = (String url) {
+ if (!_addressFocus.hasFocus) _address.text = url;
+ };
+ // onTitleChanged is wired when tabs land (which display the page title).
+ _web.onProgress = (int p) {
+ setState(() => _progress = p);
+ };
+ _web.onLoadStarted = (String _) {
+ setState(() => _loading = true);
+ };
+ _web.onLoadFinished = (String _) {
+ setState(() {
+ _loading = false;
+ _progress = 100;
+ });
+ };
+ _web.onLoadError = (int _, String _, String _) {
+ setState(() => _loading = false);
+ };
+ _web.onNavState = (bool back, bool fwd) {
+ setState(() {
+ _canBack = back;
+ _canForward = fwd;
+ });
+ };
+ }
@override
void dispose() {
- _controller.dispose();
+ _web.dispose();
+ _address.dispose();
+ _addressFocus.dispose();
super.dispose();
}
+ /// Turn raw omnibox input into a URL: keep explicit schemes, assume https for
+ /// bare hosts, otherwise treat it as a web search (the "search" wrapper).
+ String _toUrl(String input) {
+ final String s = input.trim();
+ if (s.isEmpty) return s;
+ if (RegExp(r'^[a-zA-Z][a-zA-Z0-9+.\-]*://').hasMatch(s)) return s;
+ if (!s.contains(' ') && RegExp(r'^[^\s/]+\.[^\s/]+').hasMatch(s)) {
+ return 'https://$s';
+ }
+ return 'https://duckduckgo.com/?q=${Uri.encodeQueryComponent(s)}';
+ }
+
+ void _navigate() {
+ final String url = _toUrl(_address.text);
+ if (url.isEmpty) return;
+ _addressFocus.unfocus();
+ setState(() {
+ _loading = true;
+ _progress = 0;
+ });
+ _web.loadUrl(url);
+ }
+
+ Future _ensureSurface(Size size, double dpr) async {
+ if (_createRequested || size.isEmpty) return;
+ _createRequested = true;
+ final int sid = await _web.create(
+ width: (size.width * dpr).round(),
+ height: (size.height * dpr).round(),
+ dpr: dpr,
+ initialUrl: _toUrl(_address.text),
+ );
+ if (mounted && sid >= 0) setState(() => _engineReady = true);
+ }
+
@override
Widget build(BuildContext context) {
+ final double dpr = MediaQuery.of(context).devicePixelRatio;
return Scaffold(
backgroundColor: OscColors.bodyBg,
- body: Stack(
- children: [
- // ── Subtle emerald radial gradient background
- Positioned.fill(
- child: DecoratedBox(
- decoration: BoxDecoration(
- gradient: RadialGradient(
- center: const Alignment(-0.6, -0.8),
- radius: 1.4,
- colors: [
- OscColors.green.withValues(alpha: 0.045),
- OscColors.bodyBg.withValues(alpha: 0.0),
- ],
- stops: const [0.0, 0.7],
- ),
+ body: SafeArea(
+ child: Column(
+ children: [
+ _toolbar(),
+ _progressBar(),
+ Expanded(
+ child: LayoutBuilder(
+ builder: (BuildContext context, BoxConstraints c) {
+ final Size size = Size(c.maxWidth, c.maxHeight);
+ WidgetsBinding.instance.addPostFrameCallback(
+ (_) => _ensureSurface(size, dpr));
+ return _engineReady
+ ? OscWebView(controller: _web)
+ : _placeholder();
+ },
),
),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _toolbar() {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ decoration: const BoxDecoration(
+ color: OscColors.canvasBg,
+ border: Border(bottom: BorderSide(color: OscColors.border)),
+ ),
+ child: Row(
+ children: [
+ OscIconButton(
+ icon: Icons.arrow_back_rounded,
+ tooltip: 'Back',
+ onPressed: _canBack ? _web.goBack : null,
),
- // ── Second subtle gradient from bottom-right
- Positioned.fill(
- child: DecoratedBox(
- decoration: BoxDecoration(
- gradient: RadialGradient(
- center: const Alignment(0.9, 1.0),
- radius: 1.2,
- colors: [
- OscColors.violet.withValues(alpha: 0.025),
- Colors.transparent,
- ],
- stops: const [0.0, 0.6],
- ),
- ),
- ),
+ const SizedBox(width: 4),
+ OscIconButton(
+ icon: Icons.arrow_forward_rounded,
+ tooltip: 'Forward',
+ onPressed: _canForward ? _web.goForward : null,
),
- // ── Main content
- SafeArea(
- child: SingleChildScrollView(
- padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 24),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // ── Title
- Text(
- 'Web Link',
- style: OscTypography.heading1.copyWith(
- color: OscColors.textPrimary,
- letterSpacing: -0.5,
- height: 1.1,
- ),
- ),
- const SizedBox(height: 6),
- Row(
- children: [
- Container(
- width: 6,
- height: 6,
- decoration: const BoxDecoration(
- color: OscColors.green,
- shape: BoxShape.circle,
- ),
- ),
- const SizedBox(width: 8),
- Text(
- 'Knowledge source & media gateway',
- style: OscTypography.caption.copyWith(
- fontSize: 14,
- color: OscColors.textMuted,
- fontWeight: FontWeight.w400,
- letterSpacing: 0.2,
- ),
- ),
- ],
- ),
- const SizedBox(height: 28),
-
- // ── URL Input
- TextField(
- controller: _controller,
- style: OscTypography.monoBody.copyWith(
- color: OscColors.textPrimary,
- fontSize: 14,
- ),
- decoration: const InputDecoration(
- labelText: 'Link target',
- prefixIcon: Icon(Icons.link_rounded),
- ),
- ),
- const SizedBox(height: 16),
-
- // ── Open Link button
- FilledButton.icon(
- onPressed: () {
- final url = _controller.text.trim();
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Row(
- children: [
- const Icon(Icons.open_in_new_rounded,
- color: OscColors.green, size: 16),
- const SizedBox(width: 10),
- Expanded(
- child: Text(
- 'Queued link: $url',
- overflow: TextOverflow.ellipsis,
- ),
- ),
- ],
- ),
- ),
- );
- },
- icon: const Icon(Icons.open_in_new_rounded, size: 18),
- label: const Text('Open Link'),
- ),
- const SizedBox(height: 32),
-
- // ── Knowledge Source Engine Card
- _buildKnowledgeSourceCard(),
- const SizedBox(height: 20),
-
- // ── Media Player Card
- _buildMediaPlayerCard(),
- ],
- ),
- ),
+ const SizedBox(width: 4),
+ OscIconButton(
+ icon: _loading ? Icons.close_rounded : Icons.refresh_rounded,
+ tooltip: _loading ? 'Stop' : 'Reload',
+ onPressed: _loading ? _web.stopLoading : _web.reload,
),
+ const SizedBox(width: 10),
+ Expanded(child: _addressBar()),
],
),
);
}
- Widget _buildKnowledgeSourceCard() {
- return OscCard(
- padding: EdgeInsets.zero,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // ── Card header bar
- Container(
- width: double.infinity,
- padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
- decoration: const BoxDecoration(
- border: Border(
- bottom: BorderSide(color: OscColors.border, width: 1),
- ),
- ),
- child: Row(
- children: [
- OscTag.green(label: 'KNOWLEDGE SOURCE ENGINE'),
- const Spacer(),
- Container(
- width: 8,
- height: 8,
- decoration: BoxDecoration(
- color: OscColors.green,
- shape: BoxShape.circle,
- boxShadow: [
- BoxShadow(
- color: OscColors.green.withValues(alpha: 0.5),
- blurRadius: 6,
- spreadRadius: 1,
- ),
- ],
- ),
- ),
- const SizedBox(width: 6),
- Text(
- 'LIVE',
- style: OscTypography.monoLabel.copyWith(
- color: OscColors.green,
- letterSpacing: 1.0,
- ),
- ),
- ],
- ),
- ),
-
- // ── Card content
- Padding(
- padding: const EdgeInsets.all(18),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // URL preview
- Row(
- children: [
- const Icon(Icons.language_rounded,
- color: OscColors.skyBlue, size: 14),
- const SizedBox(width: 8),
- Expanded(
- child: Text(
- _controller.text,
- style: OscTypography.monoBody.copyWith(
- fontSize: 12,
- color: OscColors.skyBlue,
- ),
- overflow: TextOverflow.ellipsis,
- ),
- ),
- ],
- ),
- const SizedBox(height: 16),
-
- // Page title
- Text(
- 'Operating Systems Architecture Evolution',
- style: OscTypography.heading3.copyWith(
- fontSize: 18,
- fontWeight: FontWeight.w700,
- color: OscColors.textPrimary,
- height: 1.3,
- ),
- ),
- const SizedBox(height: 10),
-
- // Description
- Text(
- 'Comprehensive guide covering microkernel design principles, '
- 'memory-safe driver isolation, capability-based security models, '
- 'and the convergence of embedded real-time constraints with '
- 'modern desktop compositing pipelines. Explores how OSCortex '
- 'synthesizes these paradigms into a unified bare-metal runtime.',
- style: OscTypography.body.copyWith(
- color: OscColors.textMuted,
- height: 1.65,
- fontWeight: FontWeight.w400,
- ),
- ),
- const SizedBox(height: 18),
-
- // Meta tags row
- Row(
- children: [
- _metaTag(Icons.schedule_rounded, '12 min read'),
- const SizedBox(width: 10),
- _metaTag(Icons.bookmark_outline_rounded, 'Saved'),
- const Spacer(),
- OscTag.green(label: 'Architecture'),
- ],
- ),
- ],
- ),
- ),
- ],
+ Widget _addressBar() {
+ return TextField(
+ controller: _address,
+ focusNode: _addressFocus,
+ textInputAction: TextInputAction.go,
+ onSubmitted: (_) => _navigate(),
+ style: OscTypography.monoBody.copyWith(
+ color: OscColors.textPrimary,
+ fontSize: 13,
),
+ decoration: InputDecoration(
+ isDense: true,
+ contentPadding:
+ const EdgeInsets.symmetric(horizontal: 14, vertical: 11),
+ hintText: 'Search or enter address',
+ prefixIcon: const Icon(Icons.travel_explore_rounded, size: 16),
+ suffixIcon: IconButton(
+ icon: const Icon(Icons.subdirectory_arrow_left_rounded, size: 16),
+ color: OscColors.textMuted,
+ tooltip: 'Go',
+ onPressed: _navigate,
+ ),
+ filled: true,
+ fillColor: OscColors.surface,
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(10),
+ borderSide: const BorderSide(color: OscColors.border),
+ ),
+ enabledBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(10),
+ borderSide: const BorderSide(color: OscColors.border),
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(10),
+ borderSide: const BorderSide(color: OscColors.green),
+ ),
+ ),
+ );
+ }
+
+ Widget _progressBar() {
+ // A 2px accent line while loading; invisible otherwise (keeps layout stable).
+ return SizedBox(
+ height: 2,
+ child: (_loading && _progress > 0 && _progress < 100)
+ ? LinearProgressIndicator(
+ value: _progress / 100.0,
+ minHeight: 2,
+ backgroundColor: Colors.transparent,
+ valueColor:
+ const AlwaysStoppedAnimation(OscColors.green),
+ )
+ : const SizedBox.shrink(),
);
}
- Widget _buildMediaPlayerCard() {
- return OscCard(
- padding: EdgeInsets.zero,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // ── Header bar
- Container(
- width: double.infinity,
- padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
- decoration: const BoxDecoration(
- border: Border(
- bottom: BorderSide(color: OscColors.border, width: 1),
+ Widget _placeholder() {
+ // Shown until the web-engine service binds and a render surface exists.
+ return DecoratedBox(
+ decoration: BoxDecoration(
+ gradient: RadialGradient(
+ center: const Alignment(-0.4, -0.7),
+ radius: 1.3,
+ colors: [
+ OscColors.green.withValues(alpha: 0.05),
+ OscColors.bodyBg.withValues(alpha: 0.0),
+ ],
+ stops: const [0.0, 0.7],
+ ),
+ ),
+ child: Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Icon(Icons.public_rounded, size: 44, color: OscColors.textDim),
+ const SizedBox(height: 16),
+ Text(
+ 'Web Link',
+ style: OscTypography.heading2.copyWith(
+ color: OscColors.textPrimary,
+ letterSpacing: -0.4,
),
),
- child: Row(
- children: [
- OscTag.violet(label: 'NOW PLAYING'),
- const Spacer(),
- Icon(Icons.volume_up_rounded,
- size: 16, color: OscColors.violetLight.withValues(alpha: 0.6)),
- ],
- ),
- ),
-
- // ── Body
- Padding(
- padding: const EdgeInsets.all(18),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // Track info
- Text(
- 'Ambient Waves',
- style: OscTypography.heading3.copyWith(
- fontSize: 17,
- fontWeight: FontWeight.w700,
- color: OscColors.textPrimary,
- height: 1.2,
+ const SizedBox(height: 8),
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: 6,
+ height: 6,
+ decoration: const BoxDecoration(
+ color: OscColors.amber,
+ shape: BoxShape.circle,
),
),
- const SizedBox(height: 4),
+ const SizedBox(width: 8),
Text(
- 'Cortex Generative Radio',
- style: OscTypography.body.copyWith(
+ 'web engine starting…',
+ style: OscTypography.caption.copyWith(
color: OscColors.textMuted,
- fontWeight: FontWeight.w400,
- ),
- ),
- const SizedBox(height: 20),
-
- // Waveform visualiser
- const Center(
- child: OscWaveform(
- barCount: 8,
- height: 48,
- barWidth: 6,
- barGap: 3,
+ letterSpacing: 0.2,
),
),
- const SizedBox(height: 20),
-
- // Tags
- Row(
- children: [
- OscTag.violet(
- label: 'Local Sync',
- icon: Icons.sync_rounded,
- ),
- const SizedBox(width: 10),
- OscTag.violet(
- label: 'Spatial Audio Active',
- icon: Icons.spatial_audio_off_rounded,
- ),
- ],
- ),
],
),
- ),
- ],
- ),
- );
- }
-
- Widget _metaTag(IconData icon, String label) {
- return Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(icon, size: 13, color: OscColors.textMuted),
- const SizedBox(width: 5),
- Text(
- label,
- style: OscTypography.caption.copyWith(
- fontWeight: FontWeight.w500,
- ),
+ ],
),
- ],
+ ),
);
}
}
diff --git a/apps/oscortex_web_link/pubspec.yaml b/apps/oscortex_web_link/pubspec.yaml
index 9910524..5e2fae1 100644
--- a/apps/oscortex_web_link/pubspec.yaml
+++ b/apps/oscortex_web_link/pubspec.yaml
@@ -11,6 +11,8 @@ dependencies:
sdk: flutter
oscortex_ui:
path: ../../packages/oscortex_ui
+ oscortex_webview:
+ path: ../../packages/oscortex_webview
dev_dependencies:
flutter_lints: ^6.0.0
diff --git a/dev-tools/README.md b/dev-tools/README.md
new file mode 100644
index 0000000..a1df506
--- /dev/null
+++ b/dev-tools/README.md
@@ -0,0 +1,113 @@
+# OSCortex dev-tools
+
+Tooling for **contributors** working on the OSCortex low level (kernel, drivers,
+boot, engine bring-up). These are the harnesses and recovery scripts used to
+diagnose and verify the boot/render/input stability of both architectures.
+
+> App-developer tooling (compiling `.osx` apps, the cross-arch emulator/preview)
+> lives separately — see the `osx` CLI package. This folder is for **OS/kernel
+> contributors**, not app developers.
+
+All scripts auto-detect the repo root (via `git rev-parse --show-toplevel`) and
+take overridable env vars — no hard-coded paths. Run them from anywhere in the repo.
+
+## Layout
+
+| Path | Purpose |
+|---|---|
+| `test/freeze-repro.py` | aarch64 `.kernel` boot (HVF) → launch app → sweep → assert `present_callback` keeps advancing (the post-app-launch freeze regression test). Env: `OSCORTEX_KERNEL`. |
+| `test/freeze-repro-x86.py` | x86_64 ISO boot (TCG, UEFI/OVMF) → render + launch + no-freeze. Env: `OSCORTEX_ISO`. |
+| `test/input-test.py` | Boots with UTM-exact devices (virtio-vga + qemu-xhci + usb-tablet) + injects QMP pointer events → verifies the kernel binds + processes the pointer. |
+| `build/restore-arm64.sh` | **Recover the arm64 build** after an x86 build clobbers the shared `initramfs/` (see "Build fragility" below). |
+| `build/regen-x86-snapshots.sh` | Regenerate **product-mode** x86 AOT snapshots (shell + apps) from the AOT dills using the matching `gen_snapshot`. |
+
+All scripts take env overrides (e.g. `OSCORTEX_REPO`, `OSCORTEX_KERNEL`, `OSCORTEX_ISO`,
+`OSCX_ENGINE_CONTAINER`) and default to repo-relative / `~/OSCortex-run` paths. For an
+arm64 **ISO** (UEFI) freeze test, run `freeze-repro.py` against a kernel built into a
+Limine ISO, or boot the ISO with the `freeze-repro-x86.py` QEMU pattern adapted to
+`-M virt,accel=hvf -bios -cdrom `.
+
+## Quick start
+
+```sh
+# Regression: does the freeze stay fixed on aarch64?
+python3 dev-tools/test/freeze-repro.py
+
+# Boot the x86 ISO headless and check it renders + no freeze
+OSCORTEX_ISO=/path/to/oscortex-x86_64.iso python3 dev-tools/test/freeze-repro-x86.py
+```
+
+## Hard-won knowledge (read before debugging boot/input)
+
+**Two real-hardware boot hangs that QEMU's default devices hid:**
+- **Legacy PIT calibration** (x86): TSC/APIC calibration spun forever on the i8254
+ PIT/speaker gate, which Apple firmware (and some UEFI) doesn't wire. Fixed by
+ CPUID-derived TSC freq + bounded PIT spins (`kernel/src/arch/x86_64/apic.rs`).
+ Reproduce the *path* with QEMU `-machine q35,pit=off`.
+- **Legacy i8042 PS/2 drain** (x86, UTM): `ps2::init` flushed the i8042 with
+ unbounded `while (status & OBF)` loops; an absent controller reads `0xFF` (OBF
+ always set) → infinite spin → hang at `cortex::drivers`. Fixed with a 0xFF guard
+ + bounded drains (`kernel/src/drivers/ps2.rs`). QEMU's own q35 i8042 clears OBF,
+ hiding it headless — only UTM/real-HW reproduce.
+
+**On-screen boot phases:** the boot splash shows `cortex::` (set by
+`bootscreen::set_phase` at each `shared_init_and_run` step). When a boot wedges on
+hardware QEMU can't reproduce, the frozen splash names the last stage reached —
+your fastest localizer when there's no serial.
+
+**Input device matrix (what the kernel binds):**
+- aarch64: **virtio-input** (`virtio-tablet-device`/`virtio-keyboard-device`) on the
+ `ramfb` display. USB is gated on the limine-boot ISO (QEMU-11+HVF xHCI
+ `assert(isv)` crash). NOTE: adding virtio-input alongside UTM's `virtio-ramfb`
+ (virtio-GPU) display breaks the display — use plain `ramfb`.
+- x86_64: **USB** via xHCI. Boot keyboard/mouse + **usb-tablet** (absolute, the
+ UTM/QEMU default pointer — protocol 0, parsed via the HID report descriptor).
+ PS/2 is skipped when no i8042 is present.
+
+**Serial:** the default kernel log level is `Error` (fast boot). Raise to `Info` in
+`kernel/src/logger.rs` for deep probe visibility. UTM VMs have no serial port by
+default — add one (or use the on-screen boot phases) to debug on-device.
+
+## On-screen input HUD (`--features input-hud`)
+
+When a VM/board shows **no cursor or no response to input** and there's no serial,
+build the kernel with `--features input-hud`. A compact panel is drawn over every
+frame (bottom-left):
+
+```
+INPUT src=virtio p=142 k=0 s=3 ← bound device + pointer/key/scroll event counts
+xy=640,360 btn=1 key=0x00- ← last pointer pos / buttons / last key
+```
+
+Read it like this:
+- **`src=none` + red border** → no input device bound. The kernel never found a
+ pointer/keyboard (wrong VM device set — see the device matrix above).
+- **`src=…` + cyan border but `p=` never climbs** as you move the mouse → a device
+ bound but events aren't arriving (IRQ not wired, queue not kicked, descriptor
+ mis-parse). The device is the suspect, not the cursor draw.
+- **`p=` climbs but `xy=` is pinned at an edge / off-screen** → events arrive but the
+ coordinates are scaled/clamped wrong (e.g. absolute-axis range mismatch).
+- **`btn=` flips on click, `key=0x..` changes on keypress** → input is fully working.
+
+The telemetry counters themselves (`wm::input_counts`, `wm::input_source`) are always
+compiled in (a few relaxed atomics); only the on-screen drawing is gated by the
+feature, so **release ISOs are unchanged and pay zero cost**. Build a debug image:
+
+```sh
+# x86_64 debug ISO with the HUD
+X86_AOT=1 SKIP_CORE_APPS=1 KERNEL_FEATURES=input-hud bash scripts/build-iso.sh
+# aarch64 .kernel with the HUD
+cargo +nightly build --release --target aarch64-unknown-none -p oscortex-kernel \
+ --no-default-features --features arch-aarch64,limine-boot,input-hud \
+ -Z build-std=core,compiler_builtins,alloc -Z build-std-features=compiler-builtins-mem
+```
+
+## Build fragility (important)
+
+The aarch64 and x86 builds **share the `initramfs/` directory** and clobber each
+other's staged artifacts (the x86 `build-iso.sh` strips `libapp.so`; the arm build
+stages its own engine/snapshots). After building one arch, **re-stage before
+building the other**, or use `build/restore-arm64.sh` to recover the aarch64 tree.
+The arch-specific AOT snapshots are backed up under `apps/*/build/oscortex/`
+(`libapp-arm64.so` / the x86 `libapp.so`). A proper fix (separate per-arch staging
+dirs) is a tracked cleanup item.
diff --git a/dev-tools/build/regen-x86-snapshots.sh b/dev-tools/build/regen-x86-snapshots.sh
new file mode 100755
index 0000000..034f39f
--- /dev/null
+++ b/dev-tools/build/regen-x86-snapshots.sh
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+# Regenerate PRODUCT-mode x86_64 AOT snapshots (shell + bundled apps) from the
+# prebuilt AOT dills, using a gen_snapshot that MATCHES the shipped release x64
+# engine. Needed when the prebuilt x86 libapp.so files are the wrong VM mode
+# (the Dart VM rejects a 'release' snapshot under a 'product' engine).
+#
+# Prereqs: the `oscx-engine` build container with a built product gen_snapshot at
+# out/oscortex_release_x64/gen_snapshot (build it with
+# `ninja -C out/oscortex_release_x64 gen_snapshot`), and the per-app AOT dills at
+# apps//build/app_aot.dill.
+#
+# GOTCHA: this gen_snapshot rejects `--dedup_instructions` as an unknown VM flag —
+# do not pass it.
+#
+# Usage: dev-tools/build/regen-x86-snapshots.sh
+set -euo pipefail
+
+ROOT="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel)"
+CONTAINER="${OSCX_ENGINE_CONTAINER:-oscx-engine}"
+GEN="${X64_GEN_SNAPSHOT:-/work/engine/engine/src/out/oscortex_release_x64/gen_snapshot}"
+
+regen() { #
+ local app="$1"
+ local dill="$ROOT/apps/$app/build/app_aot.dill"
+ [ -f "$dill" ] || { echo " MISSING dill: $dill" >&2; return 1; }
+ echo "[regen-x86] $app: app_aot.dill → product-mode libapp.so"
+ docker cp "$dill" "$CONTAINER:/work/${app}_aot.dill"
+ docker exec "$CONTAINER" bash -lc \
+ "$GEN --deterministic --snapshot_kind=app-aot-elf --elf=/work/${app}_x64.so --strip /work/${app}_aot.dill" 2>&1 | tail -1
+ docker cp "$CONTAINER:/work/${app}_x64.so" "$ROOT/apps/$app/build/oscortex/libapp.so"
+ echo " -> $(file "$ROOT/apps/$app/build/oscortex/libapp.so" | grep -oE 'x86-64') $(stat -f %z "$ROOT/apps/$app/build/oscortex/libapp.so" 2>/dev/null || stat -c %s "$ROOT/apps/$app/build/oscortex/libapp.so")b"
+}
+
+regen oscortex_app
+regen oscortex_files
+regen oscortex_canvas
+regen oscortex_web_link
+echo "[regen-x86] done. Stage with: X86_AOT=1 SKIP_CORE_APPS=1 scripts/build-iso.sh"
diff --git a/dev-tools/build/restore-arm64.sh b/dev-tools/build/restore-arm64.sh
new file mode 100755
index 0000000..fe3fdbf
--- /dev/null
+++ b/dev-tools/build/restore-arm64.sh
@@ -0,0 +1,43 @@
+#!/usr/bin/env bash
+# Recover the aarch64 build tree after an x86 build clobbered the shared
+# initramfs/ (the build-fragility issue — see dev-tools/README.md). Re-stages the
+# arm64 shell + app AOT snapshots, the arm64 engine, the arm64 embedder, drops the
+# x86 JIT kernel_blob, and rebuilds the arm64 kernel.
+#
+# Prereqs: the `oscx-engine` build container running (holds the arm64 engine), and
+# the prebuilt arm64 AOT snapshots under apps/*/build/oscortex/libapp-arm64.so.
+#
+# Usage: dev-tools/build/restore-arm64.sh
+set -euo pipefail
+
+ROOT="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel)"
+cd "$ROOT"
+CONTAINER="${OSCX_ENGINE_CONTAINER:-oscx-engine}"
+ARM_ENGINE_OUT="${ARM_ENGINE_OUT:-/work/engine/engine/src/out/linux_release_arm64}"
+
+echo "[arm-restore] 1/5 shell + app AOT snapshots (aarch64)"
+cp "apps/oscortex_app/build/oscortex/libapp-arm64.so" initramfs/system/flutter/libapp.so
+cp "apps/oscortex_files/build/oscortex/libapp-arm64.so" "initramfs/Applications/Files.app/libapp.so"
+cp "apps/oscortex_canvas/build/oscortex/libapp-arm64.so" "initramfs/Applications/Canvas.app/libapp.so"
+cp "apps/oscortex_web_link/build/oscortex/libapp-arm64.so" "initramfs/Applications/Web Link.app/libapp.so"
+
+echo "[arm-restore] 2/5 drop x86 JIT kernel_blob (arm is AOT)"
+rm -f initramfs/system/flutter/flutter_assets/kernel_blob.bin initramfs/system/flutter/kernel_blob.bin
+for a in Files Canvas "Web Link"; do rm -f "initramfs/Applications/$a.app/flutter_assets/kernel_blob.bin"; done
+
+echo "[arm-restore] 3/5 arm64 engine from the build container"
+docker cp "$CONTAINER:$ARM_ENGINE_OUT/libflutter_engine.so" initramfs/system/lib/libflutter_engine.so
+
+echo "[arm-restore] 4/5 arm64 embedder → /init"
+( cd tools/flutter-embedder && cargo +nightly build --release --target aarch64-unknown-none \
+ -Z build-std=core,compiler_builtins -Z build-std-features=compiler-builtins-mem >/dev/null 2>&1 )
+cp tools/flutter-embedder/target/aarch64-unknown-none/release/oscortex-host initramfs/init
+cp tools/flutter-embedder/target/aarch64-unknown-none/release/oscortex-host initramfs/bin/oscortex-host
+
+echo "[arm-restore] 5/5 build aarch64 kernel"
+touch kernel/src/fs/initramfs.rs
+cargo build --target aarch64-unknown-none -p oscortex-kernel \
+ --no-default-features --features arch-aarch64 \
+ -Z build-std=core,compiler_builtins,alloc -Z build-std-features=compiler-builtins-mem 2>&1 | tail -2
+
+echo "[arm-restore] done. engine=$(file initramfs/system/lib/libflutter_engine.so | grep -oE 'aarch64|x86-64') libapp=$(file initramfs/system/flutter/libapp.so | grep -oE 'aarch64|x86-64') init=$(file initramfs/init | grep -oE 'aarch64|x86-64')"
diff --git a/dev-tools/test/freeze-repro-x86.py b/dev-tools/test/freeze-repro-x86.py
new file mode 100755
index 0000000..c72f32d
--- /dev/null
+++ b/dev-tools/test/freeze-repro-x86.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+# Headless freeze repro for OSCortex x86_64 (Limine ISO, pure TCG — SLOW).
+# Boots the ISO, waits (patiently) for the shell, launches an app via QMP, sweeps,
+# and samples present_callback to decide FROZEN vs ADVANCING.
+import json, os, socket, subprocess, sys, time, signal
+
+ROOT = (os.environ.get("OSCORTEX_REPO") or __import__("subprocess").run(["git","rev-parse","--show-toplevel"],capture_output=True,text=True).stdout.strip())
+ISO = f"{ROOT}/oscortex.iso"
+VBLK = f"{ROOT}/vdisk.img"
+NVME = f"{ROOT}/nvme.img"
+SERIAL = "/tmp/oscortex-x86-serial.log"
+QMP = "/tmp/oscortex-x86-qmp.sock"
+for p in (SERIAL, QMP):
+ try: os.remove(p)
+ except OSError: pass
+for img, mb in ((VBLK, 8), (NVME, 16)):
+ if not os.path.exists(img):
+ subprocess.run(["dd", "if=/dev/zero", f"of={img}", "bs=1M", f"count={mb}"],
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+
+qemu = subprocess.Popen([
+ "qemu-system-x86_64", "-cdrom", ISO, "-m", "2048M", "-smp", "2",
+ "-cpu", "qemu64,+x2apic", "-machine", "q35",
+ "-device", "virtio-net-pci,netdev=net0", "-netdev", "user,id=net0",
+ "-device", "virtio-blk-pci,drive=vblk",
+ "-drive", f"file={VBLK},format=raw,id=vblk,if=none",
+ "-device", "nvme,drive=nvmedrive,serial=oscortex0",
+ "-drive", f"file={NVME},format=raw,id=nvmedrive,if=none",
+ "-device", "qemu-xhci,id=xhci",
+ "-serial", f"file:{SERIAL}", "-no-reboot", "-display", "none",
+ "-qmp", f"unix:{QMP},server,nowait",
+], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+
+def reads():
+ try:
+ with open(SERIAL, "rb") as f: return f.read()
+ except FileNotFoundError: return b""
+def present(): return reads().count(b"present_callback")
+def has(t): return t.encode() in reads()
+
+def qmp_connect(timeout=300):
+ t0 = time.time()
+ while time.time() - t0 < timeout:
+ try:
+ s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); s.connect(QMP); return s
+ except (FileNotFoundError, ConnectionRefusedError): time.sleep(0.5)
+ raise RuntimeError("QMP never came up")
+qmp = qmp_connect(); buf = b""
+def rcv():
+ global buf
+ while b"\n" not in buf: buf += qmp.recv(4096)
+ l, buf = buf.split(b"\n", 1); return json.loads(l)
+rcv(); qmp.sendall(b'{"execute":"qmp_capabilities"}\n'); rcv()
+def ev(e): qmp.sendall((json.dumps({"execute":"input-send-event","arguments":{"events":e}})+"\n").encode()); rcv()
+def move(x,y): ev([{"type":"abs","data":{"axis":"x","value":x}},{"type":"abs","data":{"axis":"y","value":y}}])
+def click(x,y):
+ move(x,y); ev([{"type":"btn","data":{"down":True,"button":"left"}}]); time.sleep(0.1); ev([{"type":"btn","data":{"down":False,"button":"left"}}])
+
+# 1. wait for shell (TCG is slow — be patient: up to 6 min)
+print("[x86] waiting for shell first frames (slow TCG)...", flush=True)
+t0 = time.time()
+while time.time() - t0 < 360:
+ if has("FATAL") and has("disallowed Dart VM flag"):
+ print("[x86] *** FATAL: engine rejected dart-flags (UNPATCHED x64 engine) ***", flush=True)
+ qemu.send_signal(signal.SIGTERM); sys.exit(2)
+ if present() > 20: break
+ time.sleep(3)
+pc0 = present()
+print(f"[x86] shell present_callback={pc0} after {int(time.time()-t0)}s", flush=True)
+if pc0 <= 20:
+ print("[x86] shell never rendered — abort. serial tail:", flush=True)
+ print(subprocess.run(["tail","-30",SERIAL],capture_output=True,text=True).stdout)
+ qemu.send_signal(signal.SIGTERM); sys.exit(3)
+
+# 2. launch an app (try a couple of grid positions; x86 res may differ)
+print("[x86] launching app (grid click)...", flush=True)
+for (lx,ly) in [(12380,11880),(8000,9000),(16000,9000)]:
+ click(lx,ly); time.sleep(8)
+ if present() - pc0 > 15: break
+time.sleep(15)
+pc_launch = present()
+print(f"[x86] post-launch present_callback={pc_launch} (delta {pc_launch-pc0})", flush=True)
+
+# 3. sweep + sample
+print("[x86] sweeping + sampling...", flush=True)
+samples = []
+sweep = [(8000,8000),(14000,8000),(20000,12000),(14000,16000),(9000,14000)]
+for w in range(6):
+ for (x,y) in sweep: move(x,y); time.sleep(0.3)
+ time.sleep(4)
+ samples.append(present()); print(f"[x86] window {w}: present={samples[-1]}", flush=True)
+
+advanced = samples[-1] - pc_launch
+stuck = sum(1 for i in range(1,len(samples)) if samples[i]==samples[i-1])
+data = reads().decode("latin1")
+crash = {m: data.count(m) for m in ["panic","KERNEL PANIC","FATAL","Unhandled Exception"] if data.count(m)>0}
+print("[x86] ===== VERDICT =====", flush=True)
+print(f"[x86] launch={pc_launch} final={samples[-1]} advanced={advanced} stuck={stuck}/{len(samples)-1}", flush=True)
+print(f"[x86] crash markers: {crash if crash else 'NONE'}", flush=True)
+ok = advanced > 5 and stuck <= 1 and not any(k in crash for k in ['panic','KERNEL PANIC','FATAL'])
+print("[x86] RESULT:", "ADVANCING — no freeze (FIX WORKS)" if ok else "FROZEN / PROBLEM", flush=True)
+qmp.close(); qemu.send_signal(signal.SIGTERM)
+try: qemu.wait(timeout=15)
+except subprocess.TimeoutExpired: qemu.kill()
+sys.exit(0 if ok else 1)
diff --git a/dev-tools/test/freeze-repro.py b/dev-tools/test/freeze-repro.py
new file mode 100755
index 0000000..d669b31
--- /dev/null
+++ b/dev-tools/test/freeze-repro.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+"""aarch64 post-app-launch freeze regression test.
+
+Boots the OSCortex aarch64 .kernel headless under HVF, waits for the shell to
+render, launches an app via QMP pointer events, sweeps the grid, and asserts that
+`present_callback` keeps advancing (the freeze symptom is present_callback
+sticking ~38 frames after launch).
+
+Env overrides:
+ OSCORTEX_KERNEL path to the aarch64 .kernel (default ~/OSCortex-run/oscortex-aarch64.kernel)
+Exit 0 = advancing (no freeze); non-zero = frozen / failed to render.
+"""
+import json, os, socket, subprocess, sys, time, signal
+
+KERNEL = os.environ.get("OSCORTEX_KERNEL", os.path.expanduser("~/OSCortex-run/oscortex-aarch64.kernel"))
+SERIAL = "/tmp/oscortex-freeze-serial.log"
+QMP = "/tmp/oscortex-freeze-qmp.sock"
+for p in (SERIAL, QMP):
+ try: os.remove(p)
+ except OSError: pass
+
+if not os.path.exists(KERNEL):
+ print(f"[repro] kernel not found: {KERNEL} (set OSCORTEX_KERNEL)"); sys.exit(3)
+
+qemu = subprocess.Popen([
+ "qemu-system-aarch64", "-M", "virt,accel=hvf", "-cpu", "host", "-smp", "1", "-m", "4G",
+ "-kernel", KERNEL, "-device", "ramfb",
+ "-device", "virtio-tablet-device", "-device", "virtio-keyboard-device",
+ "-display", "none", "-serial", f"file:{SERIAL}", "-qmp", f"unix:{QMP},server,nowait",
+], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+
+def present():
+ try:
+ with open(SERIAL, "rb") as f: return f.read().count(b"present_callback")
+ except FileNotFoundError: return 0
+
+def qmp_connect(timeout=40):
+ t0 = time.time()
+ while time.time() - t0 < timeout:
+ try:
+ s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); s.connect(QMP); return s
+ except (FileNotFoundError, ConnectionRefusedError): time.sleep(0.3)
+ raise RuntimeError("QMP never came up")
+
+qmp = qmp_connect(); buf = b""
+def rcv():
+ global buf
+ while b"\n" not in buf: buf += qmp.recv(4096)
+ line, buf = buf.split(b"\n", 1); return json.loads(line)
+rcv(); qmp.sendall(b'{"execute":"qmp_capabilities"}\n'); rcv()
+def ev(e): qmp.sendall((json.dumps({"execute":"input-send-event","arguments":{"events":e}})+"\n").encode()); rcv()
+def move(x,y): ev([{"type":"abs","data":{"axis":"x","value":x}},{"type":"abs","data":{"axis":"y","value":y}}])
+def click(x,y):
+ move(x,y); ev([{"type":"btn","data":{"down":True,"button":"left"}}]); time.sleep(0.05); ev([{"type":"btn","data":{"down":False,"button":"left"}}])
+
+print("[repro] waiting for shell...", flush=True)
+t0 = time.time()
+while time.time() - t0 < 90 and present() <= 20: time.sleep(1)
+pc0 = present()
+print(f"[repro] shell present={pc0} after {int(time.time()-t0)}s", flush=True)
+if pc0 <= 20:
+ qemu.send_signal(signal.SIGTERM); print("[repro] shell never rendered"); sys.exit(3)
+
+click(12380, 11880); time.sleep(12)
+launch = present()
+print(f"[repro] post-launch present={launch}", flush=True)
+samples = []
+sweep = [(8000,8000),(14000,8000),(20000,8000),(20000,14000),(14000,14000),(8000,14000)]
+for w in range(6):
+ for (x,y) in sweep: move(x,y); time.sleep(0.15)
+ time.sleep(2); samples.append(present()); print(f"[repro] win{w} present={samples[-1]}", flush=True)
+
+advanced = samples[-1] - launch
+stuck = sum(1 for i in range(1,len(samples)) if samples[i]==samples[i-1])
+ok = advanced > 5 and stuck <= 1
+print(f"[repro] launch={launch} final={samples[-1]} advanced={advanced} stuck={stuck}/{len(samples)-1}")
+print("[repro] RESULT:", "ADVANCING — no freeze" if ok else "FROZEN")
+qmp.close(); qemu.send_signal(signal.SIGTERM)
+try: qemu.wait(timeout=10)
+except subprocess.TimeoutExpired: qemu.kill()
+sys.exit(0 if ok else 1)
diff --git a/dev-tools/test/input-test.py b/dev-tools/test/input-test.py
new file mode 100755
index 0000000..52be668
--- /dev/null
+++ b/dev-tools/test/input-test.py
@@ -0,0 +1,45 @@
+import json,os,socket,subprocess,time,signal
+ISO=""+os.environ.get("OSCORTEX_ISO", (os.environ.get("OSCORTEX_REPO") or ".")+"/oscortex.iso")+""
+SER="/tmp/osc-x86-input.log"; QMP="/tmp/osc-x86-input-qmp.sock"
+for p in (SER,QMP):
+ try: os.remove(p)
+ except OSError: pass
+q=subprocess.Popen(["qemu-system-x86_64",
+ "-drive","if=pflash,format=raw,readonly=on,file=/tmp/ovmf-code.fd",
+ "-drive","if=pflash,format=raw,file=/tmp/ovmf-vars.fd",
+ "-cdrom",ISO,"-m","4096M","-smp","2","-cpu","qemu64","-machine","q35",
+ "-device","virtio-vga","-device","qemu-xhci,id=xhci",
+ "-device","usb-tablet,bus=xhci.0","-device","usb-kbd,bus=xhci.0",
+ "-serial",f"file:{SER}","-display","none","-no-reboot",
+ "-qmp",f"unix:{QMP},server,nowait"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
+def reads():
+ try: return open(SER,'rb').read()
+ except: return b""
+def present(): return reads().count(b"present_callback")
+t0=time.time()
+while time.time()-t0<200 and present()<=30: time.sleep(3)
+print("shell present=",present(),"after",int(time.time()-t0),"s")
+s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)
+for _ in range(60):
+ try: s.connect(QMP); break
+ except: time.sleep(0.5)
+buf=b""
+def rcv():
+ global buf
+ while b"\n" not in buf: buf+=s.recv(4096)
+ l,buf=buf.split(b"\n",1); return json.loads(l)
+rcv(); s.sendall(b'{"execute":"qmp_capabilities"}\n'); rcv()
+def ev(e): s.sendall((json.dumps({"execute":"input-send-event","arguments":{"events":e}})+"\n").encode()); rcv()
+def move(x,y): ev([{"type":"abs","data":{"axis":"x","value":x}},{"type":"abs","data":{"axis":"y","value":y}}])
+base=present()
+# sweep abs pointer — if the kernel processes tablet input, cursor redraws → present advances
+for i in range(8):
+ move(4000+i*3000, 6000+i*2000); time.sleep(0.6)
+time.sleep(3)
+after=present()
+print(f"present base={base} after_sweep={after} delta={after-base}")
+print("tablet detected:", b"absolute pointer (tablet)" in reads())
+print("pointer working (present advanced on moves):", after-base>3)
+s.close(); q.send_signal(signal.SIGTERM)
+try: q.wait(timeout=10)
+except: q.kill()
diff --git a/docs/FEATURE_STATUS.md b/docs/FEATURE_STATUS.md
new file mode 100644
index 0000000..9214928
--- /dev/null
+++ b/docs/FEATURE_STATUS.md
@@ -0,0 +1,32 @@
+# OSCortex Feature Status — honest ledger
+
+States (per [rules.md](../rules.md) Rule 3): **DONE** (verified end-to-end on the real
+artifact) · **UNVERIFIED** (built, not proven on the real artifact) · **STUB**
+(plumbing/UI exists, core behavior faked/missing) · **NOT STARTED**.
+
+> Honesty note: rows marked "(prior session)" were reported DONE in earlier work but
+> have **not been re-verified in the current session**. Per Rule 6 they should be
+> treated as UNVERIFIED until someone re-runs them. This ledger is the start of the
+> stub audit the maintainer asked for — it is itself **incomplete** (see bottom).
+
+| Feature | State | Notes / gap |
+|---|---|---|
+| x86_64 boot → shell render | DONE (prior session) | UEFI ISO boots, renders Flutter shell |
+| aarch64 boot → shell render | DONE (prior session) | HVF/edk2 |
+| Pointer + keyboard input | DONE (prior session) | usb-tablet (x86) / virtio (arm); verified via HUD |
+| **x86 app launch + render (SMP)** | **DONE (this session, headless)** | Verified `-smp 2`: Files renders, 0 panics. **UNVERIFIED on bare metal.** ~70–85% of runs render; intermittent **non-fatal** first-frame stall remains |
+| x86 app launch (single core) | KNOWN-BROKEN, recoverable | Original dart:io crash; OS now survives it (resilience) but app doesn't render. SMP is the fix |
+| **aarch64 app launch** | **KNOWN-BROKEN (freezes); SMP advanced 2026-06-17** | Single-core (shipped run-arm.sh): app launch freezes (same cooperative-scheduler root cause as x86). SMP work this session under HVF (real cores): the AP now runs the app (was idle); fixed a real bug — the AP never called enable_fpu_simd so CNTKCTL_EL1 was unset → the app's CNTVCT_EL0 read trapped (EC=0x18). PRECISE remaining wall: the engine bootstrap needs a pump to finish on the AP; the cooperative hand-off alone stalls, and the only pump (ISR-initiated enter_user_by_pid_noreturn_try) corrupts EL0 thread state → EC=0x24 data abort. Genuine fix = corruption-free per-CPU context switch (Redox-style switch_to) — multi-session. x86 SMP app-launch works (~70–85%); aarch64 does not yet |
+| Crash auto-recovery | DONE (prior session) | kill group → refocus → relaunch |
+| Package pipeline (stream/sign/install) | DONE (prior session) | Ed25519, SHA256, cache |
+| Capability enforcement | DONE (prior session) | conservative (NET gated) |
+| **App networking (TCP/HTTP)** | **UNVERIFIED** | 3-layer API built + compiles; **NOT e2e-verified**; DNS pending |
+| **Web Link / browser** | **STUB** | App shell + webview pipeline (Dart API ↔ embedder ↔ compositor surface) work; engine is a **stub that fills a placeholder surface**. **Servo NOT integrated** — does not load real web pages. Long pole. See [browser-architecture.md](browser-architecture.md) |
+| Canvas app | UNVERIFIED | launches; functional depth not audited this session |
+| Files app | UNVERIFIED | launches + lists `/Applications`; file operations not audited |
+| Boot/splash screen | DONE (prior session) | dot-matrix wordmark |
+
+## This ledger is incomplete (Rule 4 — naming the gap)
+A full per-feature audit of the embedder platform channels, the app suite, and the
+networking stack has **not** been done. Until it is, assume any "(prior session)" or
+"UNVERIFIED" row could hide a stub. Completing this audit is a tracked task.
diff --git a/docs/browser-architecture.md b/docs/browser-architecture.md
new file mode 100644
index 0000000..8795be8
--- /dev/null
+++ b/docs/browser-architecture.md
@@ -0,0 +1,151 @@
+# OSCortex Web Browser — Architecture & Plan
+
+Status: **Phase 0 (foundation + scaffold).** The browser is the project's single
+largest piece — realistically multiple person-months even reusing maximally. This
+doc is the grounded plan from two research passes; it is the authoritative spec
+for the effort.
+
+## Decision: Servo, not Chromium
+
+The directive was "embed prebuilt Chromium so we don't rebuild." That premise does
+not survive contact with a microkernel, and the alternatives were evaluated:
+
+- **Prebuilt Chromium/CEF — impossible.** The Linux binaries are glibc-linked,
+ `dlopen` ~20+ desktop libraries (NSS, dbus, fontconfig, Mesa/GBM, …), require an
+ Ozone X11/Wayland display backend, and assume Chromium's multi-process + zygote +
+ seccomp-bpf sandbox model. OSCortex has none of that; the binary fails at
+ relocation before any code runs. "Don't rebuild" is the part that cannot work.
+- **Chromium from source — not solo-feasible.** Requires a custom Ozone backend +
+ satisfying/stubbing the whole process/sandbox/GPU-process model. Google brought
+ Chromium to Fuchsia (their own from-scratch OS) with a first-party team over
+ ~3 years, and even then it is an embedded WebView, not a desktop browser.
+ Honest solo estimate: 3–5+ person-years, not realistically completable alone.
+- **Cobalt/Starboard — wrong ceiling.** Most designed-for-porting (Blink-lineage,
+ BSD, POSIX-friendly), but it is a YouTube/TV-class HTML5 *app* runtime, not a
+ browser — it cannot browse arbitrary sites.
+- **WPEWebKit — full but heavy.** A full WebKit engine that renders real sites
+ today, but porting onto a glibc-less microkernel drags in a heavy C/C++ tree
+ (ICU, EGL, GStreamer) + a custom libwpe backend. ~1.5–3 person-years.
+- **Servo (chosen).** Rust (matches the kernel + toolchain + FFI), embeds as a
+ *single-process library* (`libservo`) — sidestepping Chromium's entire
+ zygote/sandbox/multi-process burden — and now has a **CPU/software rendering
+ path** (Vello CPU backend) suited to OSCortex's software-composited framebuffer.
+ Renders real sites, though compat is still maturing (Servo is officially a v0.1
+ "tech demo"). Smallest porting surface, best OS-fit. ~9–18 person-months to first
+ real-site render solo; partial-but-improving compat thereafter.
+
+## Architecture: web engine as a userspace service rendering into a compositor surface
+
+The engine does **not** live in the kernel ("lower levels") — that would break the
+microkernel model. It is a **userspace web-engine service** exposed to apps as a
+native webview capability.
+
+**Critical finding: Flutter's external-texture API is GPU-only.** `embedder.h`'s
+`FlutterEngineRegisterExternalTexture` + `TextureFrameCallback` hand back a
+`FlutterOpenGLTexture`; there is no software/pixel-buffer variant, and OSCortex
+runs the software renderer — so external-texture registration is rejected by the
+engine. We do **not** use it.
+
+**Instead, the engine renders into its own OSCortex compositor surface.** OSCortex
+already is a multi-surface compositor (`kernel/src/compositor/mod.rs`: per-surface
+geometry, z-order, clip rect, visibility, owner-PID, `surface_at_point` hit-test).
+That is the OSCortex equivalent of Android's `SurfaceProducer`/texture registry.
+
+```
+Web Link (Flutter app) web-engine service (libservo)
+ ├─ chrome surface (z=N): address bar, └─ renders page → RGBA →
+ │ search, back/fwd, tabs SYS_GPU_SUBMIT_STRIDED(webSurfaceId)
+ └─ OscWebView widget = transparent hit-rect
+ over the web region; forwards input
+ web surface (z=N-1), clipped to
+ oscortex/webview MethodChannel <───────> the viewport rect, composited
+ (control + events) directly beneath the chrome by
+ the OSCortex compositor
+```
+
+- Pixel path: service calls `create_surface_for(owner=WebLink, w, h)` → renders →
+ `gpu_submit_strided_for(surfaceId, rgba, row_bytes)`. Web Link places its chrome
+ surface at z=N and the web surface at z=N−1, clipped to the viewport
+ (`surface_clip_set_for`). No Flutter texture; no engine patch.
+- Input: `OscWebView` wraps a `Listener` + `Focus`; hit-test in Flutter,
+ `globalToLocal` transform to web-local coords, forward via
+ `dispatchInput`/`dispatchScroll`/`dispatchKey`. (Kernel-routed input via
+ `surface_at_point`+owner-PID is a later optimization.)
+
+This mirrors webview_flutter's *texture mode* (off-screen surface + id + Texture
+widget + manual gesture forwarding), substituting the OSCortex compositor surface
+for the Flutter texture registry. It is **engine-agnostic**: the same interface
+serves a stub today and Servo later.
+
+## The `oscortex/webview` contract (MethodChannel, StandardMethodCodec)
+
+Per-instance multiplexing via a `viewId` int in every call/event.
+
+**Methods (app → engine):** `create{viewId,w,h,dpr,initialUrl?}→{surfaceId}`,
+`destroy`, `loadUrl{url,headers?,method?,body?}`, `loadHtml{html,baseUrl?}`,
+`reload`, `stopLoading`, `goBack`, `goForward`, `canGoBack→bool`,
+`canGoForward→bool`, `currentUrl→String?`, `getTitle→String?`,
+`evalJs{source}→Object?`, `resize{w,h,dpr}`, `setViewport{x,y,w,h,scrollDx,scrollDy}`,
+`dispatchInput{type,x,y,button,modifiers}`, `dispatchScroll{x,y,dx,dy}`,
+`dispatchKey{type,keyCode,modifiers,text?}`, `setVisible{visible}`.
+
+**Events (engine → app):** `urlChanged`, `titleChanged`, `loadProgress{0..100}`,
+`loadStarted`, `loadFinished`, `loadError{code,description,url}`,
+`navState{canGoBack,canGoForward}`, `scrollChanged{x,y}`, `frameReady{surfaceId}`.
+
+Input type/button/modifier enums reuse the existing `EV_POINTER`/`EV_KEY`/`EV_SCROLL`
+vocabulary in `kernel/src/embedder/abi.rs`. Transport reuses the platform-channel
+mailbox (`kernel/src/platform_channel/mod.rs`).
+
+## Phased plan
+
+Two tracks. Track B (scaffold) is engine-agnostic and de-risks the integration
+before the expensive Track A (engine) investment; they proceed in parallel.
+
+**Track B — integration scaffold (tractable, weeks):**
+1. `packages/oscortex_webview/` — `OscWebViewController` (the channel API above) +
+ `OscWebView` widget (placeholder + input forwarding) + unit tests. ← *first code*
+2. Wire **Web Link** (`apps/oscortex_web_link`, today a pure mockup) into a real
+ browser chrome: address bar → `loadUrl`, back/fwd buttons gated by `navState`,
+ progress/title from events, the `OscWebView` over the web region.
+3. Embedder handler for `oscortex/webview` (StandardMethodCodec; mirrors the
+ mousecursor/clipboard handlers) routing to the web-engine service.
+4. **Stub web-engine service** — a userspace process that creates the surface and
+ renders a placeholder (URL text / solid fill) so the *entire pipeline*
+ (app → channel → surface → composite → input → events) is provable end-to-end
+ without Servo. This validates the design.
+
+**Track A — Servo bring-up (the long pole, months):**
+1. Cross-compile `libservo` for the OSCortex target with the **Vello CPU / software
+ render path** + WebRender on a software-GL or CPU surface.
+2. Implement the minimal embedder glue: window/surface = the OSCortex compositor
+ surface; input = the forwarded events; reuse the timer/thread/epoll/fs/net shims
+ the Flutter embedder already provides.
+3. **Make-or-break milestone:** render a single static HTML page to the surface,
+ headless, and diff against a reference PNG (the same way the Flutter shell is
+ validated). Everything after is incremental web-compat + interactivity.
+4. Replace the stub service with libservo behind the same `oscortex/webview`
+ contract.
+
+**Final step (already ready):** compile Web Link with the `osx` toolchain
+(`osx build` → universal `.osx`) and install it as the system browser app. This is
+the easy end — it works today; it is gated entirely on the engine underneath.
+
+## Honest effort
+
+Track B: weeks. Track A (Servo to first real-site render): ~9–18 person-months
+solo; genuinely usable compat is multi-year and tracks upstream Servo's own
+maturation. This is the largest single undertaking in the project.
+
+## Sources
+
+Feasibility: Chromium sysroot/glibc baseline & Linux deps, Ozone/GBM, Linux
+sandbox/zygote ([chromium.googlesource.com](https://chromium.googlesource.com/chromium/src/+/main/docs/linux/build_instructions.md));
+Cobalt/Starboard ([github.com/youtube/cobalt](https://github.com/youtube/cobalt));
+Servo `libservo` + Vello CPU backend + servo-shot ([servo.org](https://servo.org/blog/2025/08/22/this-month-in-servo/));
+WPEWebKit ([wpewebkit.org](https://wpewebkit.org/blog/02-overview-of-wpe.html));
+Chromium-on-Fuchsia ([chromium.googlesource.com/.../fuchsia_web](https://chromium.googlesource.com/chromium/src/+/HEAD/fuchsia_web/)).
+Integration: Flutter `embedder.h` external-texture (GPU-only) & compositor/platform-view
+([github.com/flutter/engine](https://github.com/flutter/engine/blob/main/shell/platform/embedder/embedder.h));
+webview_flutter texture-mode vs hybrid-composition & `SurfaceProducer`
+([docs.flutter.dev](https://docs.flutter.dev/release/breaking-changes/android-surface-plugins)).
diff --git a/docs/releases/v0.1.4.md b/docs/releases/v0.1.4.md
new file mode 100644
index 0000000..9257c06
--- /dev/null
+++ b/docs/releases/v0.1.4.md
@@ -0,0 +1,58 @@
+# OSCortex v0.1.4 — x86 app-launch under SMP (the long-open "2nd-engine" blocker is fixed)
+
+## Headline
+Launching a bundled app on x86 no longer takes down the OS. Under multi-core (SMP),
+a launched app (Files, Web Link, Canvas) now **initialises its own Flutter engine and
+renders its full UI on a dedicated application core**, while the shell keeps running on
+the boot core. This was the multi-session blocker (every app launch used to crash the
+shell's dart:io event handler and freeze the machine).
+
+## How it works
+Each launched engine's process group is pinned to its own CPU (**home-core isolation**),
+so it runs on separate per-CPU register state and never shares the cooperative-scheduler
+context that corrupted a second engine on a single core.
+
+## Requirements (important for testing)
+- **This build needs ≥2 CPU cores.** On a real Intel Mac (bare metal) that's automatic.
+ In UTM/QEMU, set the VM to **2+ cores** (`-smp 2` or higher).
+- On a single core, the multi-core isolation is inert and app-launch falls back to the
+ old behaviour (the app crashes) — but the OS now **survives** that crash (see below)
+ rather than hanging.
+
+## Stability
+- **The OS is crash-proof under SMP**: 0 kernel panics / 0 page-table corruption across
+ the headless launch test runs with these fixes. A faulting app is torn down and the
+ shell keeps running.
+- App launch+render succeeds in the large majority of headless runs. An **intermittent
+ first-frame stall** remains (the app or shell occasionally initialises but pumps few
+ frames) — it is **non-fatal**: the machine stays alive; relaunch/reboot renders.
+
+## Web Link (browser) — scaffold, not a working browser yet
+Honest status: **the browser does not browse real web pages yet.** What ships:
+- The **Web Link app launches and renders its chrome** + a placeholder web surface.
+- The **end-to-end webview pipeline is proven**: the app-side Dart API
+ (`oscortex_webview`: loadUrl/back/forward/reload/title/JS/input/resize over the
+ `oscortex/webview` MethodChannel) ↔ the embedder's native webview channel handler ↔
+ an OSCortex compositor surface z-stacked beneath the app chrome. A **stub engine**
+ currently fills that surface and emits URL/progress/nav events, which validates the
+ whole path without a real web engine.
+- **Not integrated:** the actual web rendering engine (**Servo**) — this is the
+ months-long long pole and is the next major effort. Architecture in
+ `docs/browser-architecture.md`.
+
+So: the browser's OS-side plumbing + app shell are done and demonstrable; the web engine
+behind it is not.
+
+## SMP-safety fixes under the hood (latent bugs the single-core kernel had hidden)
+- Per-CPU page-table lock (a global re-entrancy counter let a 2nd core bypass the lock
+ → parallel page-table writes → corruption).
+- Fixed a PTABLE↔window-manager ABBA lock-order deadlock.
+- Reschedule IPI on spawn so an application core wakes immediately.
+- Page-table walks degrade to "not mapped" instead of panicking when crash-recovery has
+ freed an address space another core still references.
+
+## Notes
+- **Single-core builds (the prior default) are byte-unchanged.**
+- Built from `feat/native-engine-port`. **Not yet confirmed on bare-metal hardware** —
+ this release is the vehicle for that test.
+- Assets: `oscortex-x86_64-v0.1.4.iso` (+ `.sha256`).
diff --git a/docs/smp-architecture.md b/docs/smp-architecture.md
new file mode 100644
index 0000000..7aaf270
--- /dev/null
+++ b/docs/smp-architecture.md
@@ -0,0 +1,97 @@
+# OSCortex SMP — Multi-Core Scheduler Architecture & Roadmap
+
+Status: **single-core by default** (the `smp` Cargo feature gates AP bring-up off — see
+`kernel/src/arch/*/smp.rs`). This document is the principled plan to make OSCortex
+*actually scale across cores*, replacing the cooperative single-core scheduler with a
+Redox-style per-CPU preemptive one. It exists because six prior patch-and-pray SMP
+attempts failed for lack of a plan; build to this, incrementally, never breaking the
+single-core path.
+
+## Why single-core today
+
+The post-app-launch freeze is a multi-threaded Dart-VM GC **stop-the-world safepoint**
+that cannot converge on one core (the GC threads need to run *simultaneously* to all
+reach the safepoint). Single-core preemption was tried — it does not fix it. Multi-core
+is the real fix. But naively turning APs loose on the current scheduler corrupted state
+(6 attempts), and AP bring-up itself hung a real Skylake at boot. So: single-core is the
+*stable* config now; SMP is a methodical foundation upgrade.
+
+## Current model (what we're replacing)
+
+- **User scheduling = cooperative hand-off.** `enter_user_by_pid_noreturn(pid)` restores a
+ process's full context and *never returns*; every blocking primitive (futex, cond,
+ mutex, epoll, fd) yields by calling it. Wakers **directly enter the woken thread**.
+- **Preemption = timer-ISR frame rewrite.** The timer ISR saves the full trap frame,
+ calls `timer_preempt_switch_try` to pick the next thread, overwrites the live frame, and
+ returns into it. EL0 preemption is currently **disabled on aarch64** (`preempt_enabled =
+ false`) and quantum-gated on x86.
+- **Global lock.** `PTABLE_LOCK` (recursive, per-CPU holder + recursion counts) guards the
+ process table; ISR paths use `try_lock` to stay deadlock-free.
+- **Sync.** Futex table is `BTreeMap>` with a `get_group_leader` address-space
+ filter (interim cross-process fix). Cond/mutex are cooperative + a `force_wake_all_task_
+ runners` deadlock-breaker that pokes every timerfd.
+- A cooperative kernel `context_switch(old_sp, new_sp)` (callee-saved only) **already
+ exists** on both arches — this is the `switch_to_inner` primitive Redox uses.
+
+## Target model (Redox blueprint, adapted)
+
+1. **Per-CPU scheduler state + affinity run queue** — each CPU has its own `current`/`idle`
+ context; thread selection filters by `sched_affinity`. (We already have sticky
+ `current_cpu` affinity + per-CPU `current_pid`.)
+2. **`CONTEXT_SWITCH_LOCK` + non-preemptable bail-out** — one atomic serializes switches;
+ never switch *away* from a thread that is non-preemptable (holding a kernel lock, or at
+ a non-safepoint). This is the piece that corrupted preempted Dart threads.
+3. **Two-layer `switch_to(prev,next)` / naked `switch_to_inner`** — exact callee-saved +
+ FP/cr3/ttbr save sets. (`context_switch` is the inner half; add the high-level wrapper.)
+4. **Physical-address-keyed futex** — key the waiter table by the *physical* address of the
+ futex word (+ address-space validation), replacing VA+group-scoping. Lock-free vs
+ `PTABLE_LOCK`; correct across cores and across the shell/app shared-VA collision.
+5. **IPI-based wakeups** — wakers `set_state(Running)` + send a reschedule IPI to the
+ target core; they do **not** jump into the woken thread. (x86 `broadcast_resched_ipi`
+ exists; aarch64 needs the GIC SGI.)
+
+## Milestones (each keeps single-core working; AP code gated on the `smp` feature)
+
+- **M0 ✅ AP bring-up → idle.** Secondary cores boot and park (done; gated off by default).
+- **M1 ⏳ Safe-preemption core: `preempt_disable()` + `CONTEXT_SWITCH_LOCK`.** Per-CPU
+ preempt-disable depth (auto-engaged while `PTABLE_LOCK` is held); the timer-preempt path
+ bails when this CPU is non-preemptable, so we never switch away from a global-lock holder.
+ Additive + dormant where preemption is off; real effect on the x86 quantum-preempt path.
+ *Verify: both arches still boot + render single-core, no regression.*
+- **M2 ✅ Physical-address-keyed futex.** `futex_wake_waiters` now scopes by the PHYSICAL
+ address of the futex word (`futex_phys_of` = translate the waiter's VA via its page-table
+ root), the address-space-independent identity; falls back to group-leader scoping only if
+ a translation fails (never drops a wake the old path allowed). Behaviorally identical
+ while apps share the shell's address space (same pml4 → same physaddr → same decision);
+ becomes the correct key once engines are isolated into separate address spaces. *Verified:
+ engine bring-up boots + renders the launcher (present→65, 0 faults).* NOTE: still takes
+ `PTABLE_LOCK` to read each waiter's pml4 (`pml4_phys_of`) — fully lock-free lookup is a
+ later optimization; correctness/keying is the M2 deliverable.
+- **M3 ✅ Reschedule-IPI primitive on both arches.** x86 was already complete
+ (`broadcast_resched_ipi` → `send_resched_ipi` → vector 0x40 → `apic_resched_handler`),
+ and `set_state(Running)` already calls `broadcast_resched_ipi` on a wake. Filled the
+ aarch64 gap: `gic::send_sgi_all_but_self`/`send_sgi` (write GICD_SGIR), `SGI_RESCHED`
+ (SGI 0), `broadcast_resched_ipi` now sends it (guarded on `CPU_COUNT > 1` so single-core
+ never touches the GIC — it's on the hot set_state path), and the IRQ handler recognizes
+ + EOIs SGI 0. *Compile-verified both arches × {default, smp}; provably single-core-neutral
+ (guarded no-op + the SGI-0 branch is unreachable on one core). SGI delivery + the actual
+ rerun-scheduler-on-IPI action are exercised at M5 (APs idle until then).* NOTE: the wake
+ sites still use the cooperative direct-enter on the same core; M3 adds the CROSS-core
+ signal — replacing direct-enter wholesale waits for M4/M5's per-CPU run queues.
+- **M4 Two-layer `switch_to` + per-CPU current/idle contexts.** Build the high-level switch
+ over `context_switch`; give each CPU an idle context. *Verify: BSP uses it with no
+ regression; an AP runs a per-CPU idle loop (heartbeat log) under `--features smp`.*
+- **M5 Turn APs loose + verify the freeze is gone.** APs pull runnable threads (affinity
+ run queue); foreground app's engine threads distribute across cores; GC safepoint
+ converges. *Verify: launch an app under `-smp 2/4`, sweep, no freeze; then UTM + Mac.*
+
+## Hard rules
+- Never break the single-core boot. All AP/SMP-only code is behind `#[cfg(feature="smp")]`
+ or a `CPU_COUNT > 1` runtime check.
+- Verify every milestone in QEMU (both arches) before committing; only verified work ships.
+- `is_preemptable`/non-preemptable bail is the safety net for preempting Dart — do not
+ enable EL0 preemption broadly until safepoint cooperation or the bail covers it.
+
+See also: kernel/src/process/mod.rs (scheduler), kernel/src/arch/*/smp.rs (per-CPU + AP),
+kernel/src/syscall/handlers/futex.rs (futex), kernel/src/arch/*/{idt,apic,vectors}.rs
+(preemption). Memory: the Redox patterns + prior-attempt log live in the agent's notes.
diff --git a/engine-port/artifact.config b/engine-port/artifact.config
index c7a283f..7ebed5d 100644
--- a/engine-port/artifact.config
+++ b/engine-port/artifact.config
@@ -9,7 +9,7 @@
# BUMP `ARTIFACT_VERSION` whenever engine-port/ changes or FLUTTER_REV is bumped,
# so consumers pull a fresh artifact instead of a stale cached one.
-ARTIFACT_VERSION="oscortex-engine-1"
+ARTIFACT_VERSION="oscortex-engine-2"
# Upstream Flutter these artifacts are built from (pinned/frozen — we do not
# track upstream). For reproducibility.
diff --git a/engine-port/patches/0004-switches-allow-gc-flags.patch b/engine-port/patches/0004-switches-allow-gc-flags.patch
new file mode 100644
index 0000000..a9c8d5b
--- /dev/null
+++ b/engine-port/patches/0004-switches-allow-gc-flags.patch
@@ -0,0 +1,22 @@
+diff --git a/engine/src/flutter/shell/common/switches.cc b/engine/src/flutter/shell/common/switches.cc
+index 15d43a5f..e098116f 100644
+--- a/engine/src/flutter/shell/common/switches.cc
++++ b/engine/src/flutter/shell/common/switches.cc
+@@ -42,6 +42,17 @@ struct SwitchDesc {
+ static const std::string kAllowedDartFlags[] = {
+ "--enable-isolate-groups",
+ "--no-enable-isolate-groups",
++ // OSCortex: GC/heap tuning REQUIRED on the cooperative single-core scheduler
++ // (default multi-threaded GC safepoint never converges for a 2nd app engine,
++ // freezing the render thread). Prefix-matched, so "=value" forms are covered.
++ "--old_gen_heap_size",
++ "--new_gen_semi_max_size",
++ "--scavenger_tasks",
++ "--marker_tasks",
++ "--concurrent_mark",
++ "--concurrent_sweep",
++ "--no_concurrent_mark",
++ "--no_concurrent_sweep",
+ };
+ // clang-format on
+
diff --git a/engine-port/publish-engine.sh b/engine-port/publish-engine.sh
index bd17b01..e05b6b0 100755
--- a/engine-port/publish-engine.sh
+++ b/engine-port/publish-engine.sh
@@ -23,8 +23,15 @@ CONTAINER="oscx-engine"
ARCH="${1:-x64}"
MODE="${2:-release}"
NAME="oscortex-${ARCH}-${MODE}"
-OUTDIR="out/oscortex_release"
-[ "$MODE" = "debug" ] && OUTDIR="out/oscortex_debug_unopt"
+# GN out-dir per target. The dir names differ by arch (arm64 was configured as a
+# plain linux release; x64 via the oscortex target), so map explicitly rather
+# than assuming a single dir.
+case "$ARCH-$MODE" in
+ arm64-release) OUTDIR="out/linux_release_arm64" ;;
+ x64-release) OUTDIR="out/oscortex_release_x64" ;;
+ x64-debug) OUTDIR="out/oscortex_debug_unopt_x64" ;;
+ *) OUTDIR="out/oscortex_${MODE}" ;;
+esac
R2_BUCKET="${R2_BUCKET:-oscortex-engine}"
KEY="$ARTIFACT_VERSION/${NAME}.tar.gz"
diff --git a/iso_root/limine.conf b/iso_root/limine.conf
deleted file mode 100644
index a181541..0000000
--- a/iso_root/limine.conf
+++ /dev/null
@@ -1,11 +0,0 @@
-# OSCortex boot configuration
-timeout: 0
-serial: yes
-verbose: yes
-
-/OSCortex AI-First Kernel
- protocol: limine
- path: boot():/boot/kernel
- kaslr: no
- module_path: boot():/boot/libflutter_engine.so
- module_cmdline: libflutter_engine.so
diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml
index 965341b..9ac6236 100644
--- a/kernel/Cargo.toml
+++ b/kernel/Cargo.toml
@@ -26,6 +26,8 @@ smoltcp = { version = "0.12", default-features = false, features = [
"socket-tcp",
"socket-icmp",
"socket-dhcpv4",
+ "socket-dns",
+ "proto-dns",
"medium-ethernet",
"log",
"iface-max-route-count-4",
@@ -33,6 +35,9 @@ smoltcp = { version = "0.12", default-features = false, features = [
"iface-max-addr-count-4",
"iface-max-multicast-group-count-4",
"assembler-max-segment-count-4",
+ "dns-max-name-size-255",
+ "dns-max-result-count-2",
+ "dns-max-server-count-2",
] }
[features]
@@ -47,3 +52,11 @@ limine-boot = []
ai-cortex = []
driver-wasm = []
self-healing = []
+# Multi-core (SMP) AP bring-up. OFF by default: the scheduler isn't fully SMP-safe
+# yet and AP bring-up hung real hardware (a 2016 Skylake Mac) at cortex::smp. The
+# proven-stable config is single-core (serial-GC engine). Enable for SMP dev.
+smp = []
+# Draw an on-screen input debug HUD (bound device + live pointer/key/scroll event
+# counts) over every frame. For diagnosing input on a VM/board with no serial.
+# Off in release ISOs (zero cost). See compositor::input_hud + dev-tools/README.md.
+input-hud = []
diff --git a/kernel/src/app_registry/mod.rs b/kernel/src/app_registry/mod.rs
index bde2985..80367cd 100644
--- a/kernel/src/app_registry/mod.rs
+++ b/kernel/src/app_registry/mod.rs
@@ -485,6 +485,17 @@ pub fn launch(app_id: u32, _flags: u32) -> i64 {
// aot_va=0: the embedder ignores it in APP mode when is_aot=false.
crate::process::set_bootstrap_regs(child_pid, HOST_MODE_APP, app_id as u64, 0);
+ // SMP: the app host was pinned to a dedicated APPLICATION core inside
+ // spawn_with_bootstrap (atomically, before it became Running — see the
+ // home_cpu assignment there). On single-core boots that pin is 0 (the BSP),
+ // so this is purely informational. The home_cpu is inherited by every thread
+ // the host spawns (see spawn_thread), so the whole engine stays on its core,
+ // with its OWN per-CPU GPR scratch — eliminating the cooperative-context
+ // corruption that crashed the shell's dart:io EventHandler when a 2nd engine
+ // launched on a single shared core.
+ log::error!("[APP] app_id={} host_pid={} home_cpu={}",
+ app_id, child_pid, crate::process::home_cpu_of(child_pid));
+
// Track the running instance for crash auto-recovery (see note_thread_exit).
note_running(child_pid, app_id);
diff --git a/kernel/src/arch/aarch64/apic.rs b/kernel/src/arch/aarch64/apic.rs
index ece7eb8..8b7ff35 100644
--- a/kernel/src/arch/aarch64/apic.rs
+++ b/kernel/src/arch/aarch64/apic.rs
@@ -72,6 +72,14 @@ pub fn start_scheduler_tick() {
fn production_irq_handler(f: &mut super::vectors::TrapFrame) {
let intid = super::gic::acknowledge();
+ // Cross-core reschedule IPI (SMP, SGI 0). Taking the interrupt already woke
+ // this core from wfi; EOI and return. Once APs run the scheduler (M5), this is
+ // where the woken core re-picks a runnable thread.
+ if intid == super::gic::SGI_RESCHED {
+ super::gic::eoi(intid);
+ return;
+ }
+
// Non-timer interrupts. virtio-mmio input SPIs (slot i → INTID 48+i) drive the
// pointer/keyboard; drain them into the WM queue, then EOI. Everything else is
// spurious/unhandled: EOI (if real) and return.
@@ -90,6 +98,16 @@ fn production_irq_handler(f: &mut super::vectors::TrapFrame) {
// EOI now so the GIC can deliver the next timer interrupt right after eret.
super::gic::eoi(intid);
+ // SMP: a secondary core that has NOT yet engaged user scheduling does ZERO
+ // work here (just the EOI above, which also wakes it from wfi to re-poll). This
+ // keeps an idling AP from touching PTABLE_LOCK / the wake machinery and thus
+ // from contending with the BSP during the engine's single-core bring-up — the
+ // contention that previously broke app launch under smp>1. The BSP (cpu 0) is
+ // always engaged.
+ if !super::smp::cpu_engaged(super::smp::current_cpu_id()) {
+ return;
+ }
+
// Wake threads whose timerfd deadlines have elapsed, and deliver any pending
// cross-thread wakes — exactly as the x86 APIC-timer ISR does (idt.rs). On
// aarch64 this was missing, so the engine's UI/raster/IO worker threads that
@@ -125,7 +143,11 @@ fn production_irq_handler(f: &mut super::vectors::TrapFrame) {
// engine schedules frames forever (schedule_frame label=idle) but the baton is
// never delivered, so FlutterEngineOnVsync is never called and no frame is
// ever produced or presented. (aarch64 had only a no-op vsync_due() scaffold.)
- if vsync_due() {
+ // GLOBAL per-tick work (vsync baton, WM tick, USB poll) runs on the BSP ONLY —
+ // these are single-driver subsystems; letting a 2nd core drive them too would
+ // double-deliver vsync batons / double-poll USB. APs only do per-CPU scheduling
+ // below.
+ if super::smp::current_cpu_id() == 0 && vsync_due() {
crate::wm::tick();
// Deliver the vsync baton ONLY — do NOT call compositor::tick() here: its
// render_frame() does a full-screen blit/fill that, in the timer ISR under
@@ -160,16 +182,39 @@ fn production_irq_handler(f: &mut super::vectors::TrapFrame) {
let cur = crate::process::current_pid();
let focus = crate::wm::focus_pid();
let target = if focus != 0 { focus } else { 1 };
- if target != 0
+ // HOME-CORE per-core wake-assist (mirrors x86 idt.rs). Only the target's HOME
+ // core re-enters it: the BSP pumps the shell (home 0), an AP pumps the app
+ // pinned to it (home 1+). try_claim_cpu_for_try is home-gated so this can't
+ // steal another core's thread. The ISR-initiated resume below was previously
+ // disabled (// BISECT) as the suspected cause of "pid=2 Dart corruption" — but
+ // that predates the 2026-06-16 per-CPU page-table-lock + ABBA lock-order fixes,
+ // the real corruption source. Re-enabled: the app's engine bootstrap stalls at
+ // FlutterEngineRunInitialized without it (the worker the isolate launch waits
+ // on is parked and only this resume re-enters it), exactly as on x86.
+ let my_cpu = super::smp::current_cpu_id();
+ let target_is_mine = crate::process::home_cpu_of_try(target) == Some(my_cpu);
+ if target != 0 && target_is_mine
&& (cur == 0 || crate::process::is_blocked_try(cur))
&& (crate::wm::input_pending_for(target) > 0
|| crate::wm::embedder_baton_due(target))
&& cur != target
{
- // BISECT: just wake the target; let normal cooperative scheduling enter
- // it (the ISR-initiated resume enter_user_by_pid_noreturn_try is the
- // prime suspect for the residual pid=2 Dart corruption — disabled to test).
- let _ = crate::process::set_state_try(target, crate::process::ProcState::Running);
+ // The cooperative hand-off alone does NOT re-enter the force_wake-released
+ // engine thread on the AP, so the bootstrap stalls without an ISR resume.
+ // The resume corrupts ONLY when it re-enters a thread with no valid saved
+ // FP (build_image zeroes v0–v31 incl. callee-saved v8–v15 → EC=0x24). So:
+ // resume MULTI-CORE only, and ONLY when the target has a valid FP image
+ // (aarch64_fp_valid) — i.e. exactly the case where restoring FP is correct.
+ // No valid FP → wake-only (skip), never enter with garbage FP.
+ if crate::process::set_state_try(target, crate::process::ProcState::Running) {
+ let smp = crate::arch::smp::CPU_COUNT.load(Ordering::Acquire) > 1;
+ if smp
+ && crate::process::aarch64_fp_valid(target)
+ && crate::process::try_claim_cpu_for_try(target, my_cpu)
+ {
+ crate::process::enter_user_by_pid_noreturn_try(target);
+ }
+ }
}
return;
}
@@ -184,12 +229,16 @@ fn production_irq_handler(f: &mut super::vectors::TrapFrame) {
let slice_expired = crate::process::account_tick_try(cur).unwrap_or(false);
let focus = crate::wm::focus_pid();
let target = if focus != 0 { focus } else { 1 };
- // Cooperative-only scheduling (matches x86, which never preempts user threads
- // and renders reliably). The timer ISR still drives vsync/timerfd-wake/kick
- // above; we only suppress the preemptive *thread switch*. Preempting an engine
- // thread mid-execution was the cause of the residual Dart EventHandler crash;
- // now that the syscall-arg3 re-exec corruption is fixed, the engine is driven
- // entirely by cooperative syscall yields and no longer needs preemption.
+ // EL0 preemption stays OFF — empirically it does NOT fix the post-app-launch
+ // freeze and currently can't even fire: in a 45s render test the timer ISR
+ // landed in EL1 (kernel) on EVERY tick (arm-preempt-tick=0), because the threads
+ // spend ~all their time in EL1 syscalls / cooperative-handoff loops. And the
+ // freeze itself is a multi-threaded Dart GC stop-the-world safepoint that can't
+ // converge on ONE core — the deadlocked engine threads are blocked in EL1
+ // cond_wait/epoll, which EL0 preemption can't touch. The real fixes are SMP
+ // (parallel cores → the GC converges) or serial GC (engine allowlist patch); see
+ // [[post-app-launch-freeze]]. Preemption is still wanted for the general
+ // starvation class, but needs EL0 IRQ delivery wired first (separate work).
let preempt_enabled = false;
let should_preempt = preempt_enabled
&& (slice_expired
diff --git a/kernel/src/arch/aarch64/fdt.rs b/kernel/src/arch/aarch64/fdt.rs
index b857a4d..4b00970 100644
--- a/kernel/src/arch/aarch64/fdt.rs
+++ b/kernel/src/arch/aarch64/fdt.rs
@@ -255,6 +255,76 @@ pub unsafe fn parse(dtb_pa: u64) -> Option {
}
}
+/// PSCI call conduit advertised by the DTB `/psci` node's `method` property.
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+pub enum PsciConduit {
+ Hvc,
+ Smc,
+}
+
+/// Read the `/psci` node's `method` ("hvc"/"smc") from the DTB. This is the
+/// authoritative way to know how to issue PSCI calls (CPU_ON for SMP) — guessing
+/// the conduit and getting it wrong traps/hangs. Returns `None` if absent.
+///
+/// # Safety
+/// `dtb_pa` must point at a DTB blob accessible at this address.
+pub unsafe fn psci_method(dtb_pa: u64) -> Option {
+ let hdr = FdtHeader::parse(dtb_pa as *const u8)?;
+ let mut p = hdr.struct_start();
+ let end = hdr.struct_end();
+ let mut depth: i32 = -1;
+ let mut in_psci = false;
+ while p < end {
+ let token = read_be32(p);
+ p = p.add(4);
+ match token {
+ t if t == FDT_BEGIN_NODE => {
+ let name_ptr = p;
+ let mut nlen = 0usize;
+ while *name_ptr.add(nlen) != 0 {
+ nlen += 1;
+ }
+ let name = core::str::from_utf8(core::slice::from_raw_parts(name_ptr, nlen))
+ .unwrap_or("");
+ p = p.add((nlen + 4) & !3);
+ depth += 1;
+ if depth == 1 {
+ in_psci = name == "psci" || name.starts_with("psci@");
+ }
+ }
+ t if t == FDT_END_NODE => {
+ if depth == 1 {
+ in_psci = false;
+ }
+ depth -= 1;
+ }
+ t if t == FDT_PROP => {
+ let len = read_be32(p);
+ let nameoff = read_be32(p.add(4));
+ let val = p.add(8);
+ let pname = hdr.string_at(nameoff);
+ if in_psci && pname == "method" && len > 0 {
+ let s = core::str::from_utf8(core::slice::from_raw_parts(
+ val,
+ (len as usize).saturating_sub(1),
+ ))
+ .unwrap_or("");
+ return Some(if s.starts_with("smc") {
+ PsciConduit::Smc
+ } else {
+ PsciConduit::Hvc
+ });
+ }
+ p = p.add(((len as usize) + 3) & !3);
+ }
+ t if t == FDT_NOP => {}
+ t if t == FDT_END => break,
+ _ => break,
+ }
+ }
+ None
+}
+
/// Scan a physical address range for the FDT magic on an 8-byte stride and
/// return the first address whose blob parses to a valid `/memory` node.
///
diff --git a/kernel/src/arch/aarch64/gic.rs b/kernel/src/arch/aarch64/gic.rs
index 4c7914f..871cd59 100644
--- a/kernel/src/arch/aarch64/gic.rs
+++ b/kernel/src/arch/aarch64/gic.rs
@@ -18,6 +18,11 @@ const GICD_ICENABLER: u64 = 0x180; // Clear-enable
const GICD_IPRIORITYR: u64 = 0x400; // Priority (1 byte/IRQ)
const GICD_ITARGETSR: u64 = 0x800; // CPU targets (1 byte/IRQ, SPIs only)
const GICD_ICFGR: u64 = 0xC00; // Config (2 bits/IRQ: edge/level)
+const GICD_SGIR: u64 = 0xF00; // Software-generated interrupt (send an SGI)
+
+/// SGI id used for the cross-core reschedule IPI (SMP). SGIs are 0..15; nothing
+/// else here uses one.
+pub const SGI_RESCHED: u32 = 0;
// CPU interface registers (offsets from GICC_BASE).
const GICC_CTLR: u64 = 0x00; // CPU interface control
@@ -102,6 +107,22 @@ pub fn route_to_cpu0(intid: u32) {
}
}
+/// Send a Software-Generated Interrupt to **all other** CPU interfaces (the
+/// reschedule IPI). Uses TargetListFilter=0b01 ("all but self"), so on a
+/// single-core system it reaches no one — a safe no-op. `sgi_id` is 0..15.
+pub fn send_sgi_all_but_self(sgi_id: u32) {
+ // GICD_SGIR: [25:24]=TargetListFilter (0b01 = all-but-self),
+ // [23:16]=CPUTargetList (ignored for filter 0b01), [3:0]=SGIINTID.
+ let val = (0b01u32 << 24) | (sgi_id & 0xF);
+ unsafe { gicd_write(GICD_SGIR, val) };
+}
+
+/// Send an SGI to a specific set of CPU interfaces (bitmask, bit N = interface N).
+pub fn send_sgi(target_mask: u8, sgi_id: u32) {
+ let val = ((target_mask as u32) << 16) | (sgi_id & 0xF);
+ unsafe { gicd_write(GICD_SGIR, val) };
+}
+
/// Acknowledge the pending interrupt; returns its INTID (1023 = spurious).
pub fn acknowledge() -> u32 {
unsafe { gicc_read(GICC_IAR) & 0x3FF }
diff --git a/kernel/src/arch/aarch64/smp.rs b/kernel/src/arch/aarch64/smp.rs
index 70ab1be..c5f8b5b 100644
--- a/kernel/src/arch/aarch64/smp.rs
+++ b/kernel/src/arch/aarch64/smp.rs
@@ -21,6 +21,11 @@ pub struct PerCpuData {
pub mpidr: u64,
pub online: AtomicBool,
pub current_pid: AtomicU32,
+ /// An AP is "engaged" once it has joined user scheduling. While NOT engaged
+ /// (online but idling pre-engage), its timer ISR does ZERO scheduler/lock work
+ /// (just EOI) so it can't contend PTABLE_LOCK with the BSP during the engine's
+ /// single-core bring-up. The BSP (cpu 0) is always considered engaged.
+ pub engaged: AtomicBool,
}
const ZERO_CPU: PerCpuData = PerCpuData {
@@ -28,12 +33,27 @@ const ZERO_CPU: PerCpuData = PerCpuData {
mpidr: 0,
online: AtomicBool::new(false),
current_pid: AtomicU32::new(0),
+ engaged: AtomicBool::new(false),
};
static mut PER_CPU_DATA: [PerCpuData; MAX_CPUS] = [ZERO_CPU; MAX_CPUS];
/// Total logical CPUs online (including BSP).
pub static CPU_COUNT: AtomicU32 = AtomicU32::new(1);
+/// Set true by the BSP once it has claimed the init process and is entering
+/// userspace — APs must not run the scheduler before this (else they'd race the
+/// BSP to claim the init thread). Set inside `enter_user_by_pid_noreturn` on CPU 0.
+pub static SMP_SCHED_READY: AtomicBool = AtomicBool::new(false);
+
+#[inline]
+pub fn sched_ready() -> bool {
+ SMP_SCHED_READY.load(Ordering::Acquire)
+}
+#[inline]
+pub fn set_sched_ready() {
+ SMP_SCHED_READY.store(true, Ordering::Release);
+}
+
/// Read this core's MPIDR_EL1 affinity bits.
#[inline]
fn read_mpidr() -> u64 {
@@ -70,10 +90,345 @@ pub fn this_cpu() -> &'static PerCpuData {
unsafe { &(*core::ptr::addr_of!(PER_CPU_DATA))[cpu_idx] }
}
-/// Wake all APs. Currently single-core only.
+// ── SMP Milestone 1: AP bring-up via PSCI CPU_ON ─────────────────────────────
+//
+// Secondary cores park in boot.rs `_start` (wfe). We wake them with PSCI CPU_ON
+// (conduit read from the DTB) pointing at `_ap_trampoline` (a PHYSICAL address —
+// the `-kernel` image is identity-mapped, VA==PA). The trampoline runs MMU-off,
+// replicates the BSP's MMU/system state captured in `capture_boot_state`, turns
+// the MMU on, then calls `ap_main`. Milestone 1: the AP just inits its GIC CPU
+// interface, marks itself online, and idles — it touches NO shared user/scheduler
+// state yet (that's Milestone 2+). The BSP keeps doing all the work; an AP coming
+// online must not perturb the single-core path.
+
+const AP_STACK_SIZE: usize = 64 * 1024;
+const MAX_APS: usize = 3; // cores 1..=3 (core 0 = BSP)
+
+#[repr(C, align(16))]
+struct ApStacks([[u8; AP_STACK_SIZE]; MAX_APS]);
+static mut AP_STACKS: ApStacks = ApStacks([[0; AP_STACK_SIZE]; MAX_APS]);
+
+// BSP MMU/system state the AP must load before it can run kernel C/Rust.
+static mut AP_MAIR: u64 = 0;
+static mut AP_TCR: u64 = 0;
+static mut AP_TTBR0: u64 = 0;
+static mut AP_TTBR1: u64 = 0;
+static mut AP_SCTLR: u64 = 0;
+static mut AP_VBAR: u64 = 0;
+
+/// Snapshot the BSP's live MMU + vector registers so an AP can install identical
+/// translation before touching kernel memory. Call AFTER mmu::enable + vectors.
+#[cfg(feature = "smp")]
+pub fn capture_boot_state() {
+ unsafe {
+ let (mair, tcr, ttbr0, ttbr1, sctlr, vbar): (u64, u64, u64, u64, u64, u64);
+ core::arch::asm!(
+ "mrs {0}, mair_el1",
+ "mrs {1}, tcr_el1",
+ "mrs {2}, ttbr0_el1",
+ "mrs {3}, ttbr1_el1",
+ "mrs {4}, sctlr_el1",
+ "mrs {5}, vbar_el1",
+ out(reg) mair, out(reg) tcr, out(reg) ttbr0,
+ out(reg) ttbr1, out(reg) sctlr, out(reg) vbar,
+ options(nomem, nostack),
+ );
+ AP_MAIR = mair;
+ AP_TCR = tcr;
+ AP_TTBR0 = ttbr0;
+ AP_TTBR1 = ttbr1;
+ AP_SCTLR = sctlr;
+ AP_VBAR = vbar;
+ }
+}
+
+// AP entry trampoline. PSCI starts the core here MMU-off (x0 = context_id = the
+// AP's logical cpu index, which we passed as the PSCI context). It drops EL2→EL1
+// if needed, enables FP, installs the BSP's MMU regs, turns the MMU on, sets VBAR,
+// switches to this AP's stack, then branches to `ap_main(cpu_idx)`.
+core::arch::global_asm!(
+ r#"
+.section .text.ap_boot, "ax"
+.globl _ap_trampoline
+_ap_trampoline:
+ mov x19, x0 // x19 = cpu_idx (PSCI context_id)
+ // Drop EL2 -> EL1h if we came up at EL2.
+ mrs x0, CurrentEL
+ lsr x0, x0, #2
+ cmp x0, #2
+ b.ne 1f
+ mov x0, #(1 << 31)
+ msr hcr_el2, x0
+ mov x0, #0x3c5 // DAIF masked + EL1h
+ msr spsr_el2, x0
+ adr x0, 1f
+ msr elr_el2, x0
+ eret
+1:
+ msr daifset, #0xf
+ // Enable FP/AdvSIMD (CPACR_EL1.FPEN = 0b11).
+ mrs x0, cpacr_el1
+ orr x0, x0, #(3 << 20)
+ msr cpacr_el1, x0
+ isb
+ // Install the BSP's translation regs (MMU still OFF; symbols resolve to PA).
+ adrp x0, {ap_mair}
+ ldr x1, [x0, #:lo12:{ap_mair}]
+ msr mair_el1, x1
+ adrp x0, {ap_tcr}
+ ldr x1, [x0, #:lo12:{ap_tcr}]
+ msr tcr_el1, x1
+ adrp x0, {ap_ttbr0}
+ ldr x1, [x0, #:lo12:{ap_ttbr0}]
+ msr ttbr0_el1, x1
+ adrp x0, {ap_ttbr1}
+ ldr x1, [x0, #:lo12:{ap_ttbr1}]
+ msr ttbr1_el1, x1
+ isb
+ tlbi vmalle1
+ dsb nsh
+ isb
+ // Turn the MMU on (SCTLR with M/C/I as the BSP has it).
+ adrp x0, {ap_sctlr}
+ ldr x1, [x0, #:lo12:{ap_sctlr}]
+ msr sctlr_el1, x1
+ isb
+ // Vector base.
+ adrp x0, {ap_vbar}
+ ldr x1, [x0, #:lo12:{ap_vbar}]
+ msr vbar_el1, x1
+ // Stack: AP_STACKS has slots for cores 1..=MAX_APS (slot = cpu_idx-1). The top
+ // of this AP's slot = base + cpu_idx * AP_STACK_SIZE.
+ adrp x0, {ap_stacks}
+ add x0, x0, :lo12:{ap_stacks}
+ mov x2, {stack_size}
+ mul x2, x2, x19 // x2 = cpu_idx * AP_STACK_SIZE
+ add sp, x0, x2
+ mov x0, x19 // ap_main(cpu_idx)
+ bl {ap_main}
+2: wfe
+ b 2b
+"#,
+ ap_mair = sym AP_MAIR,
+ ap_tcr = sym AP_TCR,
+ ap_ttbr0 = sym AP_TTBR0,
+ ap_ttbr1 = sym AP_TTBR1,
+ ap_sctlr = sym AP_SCTLR,
+ ap_vbar = sym AP_VBAR,
+ ap_stacks = sym AP_STACKS,
+ ap_main = sym ap_main,
+ stack_size = const AP_STACK_SIZE,
+);
+
+#[cfg(feature = "smp")]
+extern "C" {
+ fn _ap_trampoline();
+}
+
+/// First Rust code on a secondary core. MMU + VBAR are live; running on this AP's
+/// own stack. Milestone 1: init the per-core GIC interface, register in the
+/// per-CPU table, mark online, then idle. (No scheduler entry yet.)
+#[no_mangle]
+pub extern "C" fn ap_main(cpu_idx: u64) -> ! {
+ let idx = cpu_idx as usize;
+ unsafe {
+ let table = &mut *core::ptr::addr_of_mut!(PER_CPU_DATA);
+ if idx < MAX_CPUS {
+ table[idx].cpu_id = cpu_idx as u32;
+ table[idx].mpidr = read_mpidr();
+ table[idx].online.store(true, Ordering::Release);
+ }
+ }
+ // Per-core GIC CPU interface (GICv2: banked per-core at the same MMIO base).
+ crate::arch::aarch64::gic::init_cpu_interface();
+ // CRITICAL per-core EL1 setup the AP was MISSING: enable FP/SIMD (CPACR_EL1) AND
+ // EL0 counter access (CNTKCTL_EL1.EL0VCTEN/EL0PCTEN). These are PER-CPU registers
+ // at reset on a secondary core. Without CNTKCTL, the app's inline CNTVCT_EL0 read
+ // (liboscortex_libc monotonic clock, called constantly by the Dart VM) traps to
+ // EL1 with EC=0x18 → report_unhandled → the engine bootstrap deterministically
+ // wedged on the AP (4/4). The x86 AP does this in ap_init; aarch64 ap_main never
+ // did. This is the fix for the aarch64 SMP app-launch freeze.
+ crate::arch::aarch64::cpu::enable_fpu_simd();
+ CPU_COUNT.fetch_add(1, Ordering::AcqRel);
+ log::error!("[SMP] AP cpu={} ONLINE (mpidr={:#x})", cpu_idx, read_mpidr());
+
+ // ENGAGE this AP in HOME-CORE user scheduling (mirrors x86 sched::ap_start).
+ //
+ // History: co-scheduling was attempted 6 ways on 2026-06-14 and froze, blamed on
+ // a "Dart GC safepoint that won't converge" — but those attempts PREDATE the
+ // 2026-06-16 arch-neutral SMP-safety fixes (per-CPU page-table lock depth that
+ // had let a 2nd core bypass the lock → real page-table corruption; the PTABLE↔WM
+ // ABBA lock-order fix). The "pid=2 Dart corruption" chased back then is very
+ // likely those exact bugs, now fixed. The x86 home-core model (pin each WHOLE
+ // engine to one core + foreground-exclusive, so an engine's threads never spread
+ // across cores and there is no cross-core GC safepoint) launches+renders apps on
+ // x86 this session. Apply the same here and let the live HVF test (real parallel
+ // cores) say whether it converges. If it regresses, this is reverted — not
+ // shipped as "done".
+ //
+ // Stay FULLY DORMANT (wfi, engaged=false → timer ISR does nothing per cpu_engaged)
+ // until an app is actually pinned to THIS core. Engaging earlier (e.g. at
+ // flutter_init_ready) makes this core's timer-ISR work contend with the BSP's
+ // timing-sensitive shell first-frame bring-up — observed to stall the shell at
+ // present=1 on HVF. Apps only launch well after the shell is up, so there is
+ // nothing for this core to do before then. spawn_with_bootstrap sets the
+ // CPU_HAS_HOME_WORK flag and broadcasts a reschedule IPI (GIC SGI), which wakes
+ // us from wfi the instant an app is assigned here.
+ while !sched_ready() {
+ core::hint::spin_loop();
+ }
+ // enable_and_halt (not raw wfi): IRQs enabled so the timer/SGI is actually taken
+ // each wake (its handler no-ops here via cpu_engaged), then we re-check the flag.
+ while !crate::process::cpu_has_home_work(cpu_idx as u32) {
+ crate::arch::enable_and_halt();
+ }
+ unsafe {
+ let table = &mut *core::ptr::addr_of_mut!(PER_CPU_DATA);
+ if idx < MAX_CPUS {
+ table[idx].engaged.store(true, Ordering::Release);
+ }
+ }
+ log::error!("[SMP] AP cpu={} ENGAGING home-pinned scheduler (app pinned here)", cpu_idx);
+ loop {
+ // home_cpu filter inside next_runnable_pid restricts the pick to engine(s)
+ // pinned to THIS core, on its own per-CPU state.
+ if let Some(pid) = crate::process::next_runnable_pid(0) {
+ crate::process::enter_user_by_pid_noreturn(pid);
+ }
+ crate::arch::enable_and_halt();
+ }
+}
+
+/// True if the given CPU has engaged user scheduling (BSP/cpu 0 always true).
+#[inline]
+pub fn cpu_engaged(cpu_id: u32) -> bool {
+ if cpu_id == 0 {
+ return true;
+ }
+ let i = cpu_id as usize;
+ if i >= MAX_CPUS {
+ return false;
+ }
+ unsafe { (*core::ptr::addr_of!(PER_CPU_DATA))[i].engaged.load(Ordering::Acquire) }
+}
+
+/// PSCI conduit, latched once from the DTB at wake time.
+#[cfg(feature = "smp")]
+static PSCI_USE_SMC: AtomicBool = AtomicBool::new(false);
+
+/// Raw PSCI/SMCCC call via the selected conduit. Returns x0.
+#[cfg(feature = "smp")]
+unsafe fn psci_raw(use_smc: bool, fn_id: u64, a1: u64, a2: u64, a3: u64) -> i64 {
+ let ret: u64;
+ if use_smc {
+ core::arch::asm!(
+ "smc #0",
+ inout("x0") fn_id => ret,
+ in("x1") a1, in("x2") a2, in("x3") a3,
+ out("x4") _, out("x5") _, out("x6") _, out("x7") _,
+ options(nostack),
+ );
+ } else {
+ core::arch::asm!(
+ "hvc #0",
+ inout("x0") fn_id => ret,
+ in("x1") a1, in("x2") a2, in("x3") a3,
+ out("x4") _, out("x5") _, out("x6") _, out("x7") _,
+ options(nostack),
+ );
+ }
+ ret as i64
+}
+
+/// Probe PSCI_VERSION (0x84000000, side-effect-free) on a conduit. A real PSCI
+/// implementation returns a small positive version word (e.g. 0x10001 for v1.1);
+/// a conduit that isn't wired returns -1 (NOT_SUPPORTED) or garbage.
+#[cfg(feature = "smp")]
+unsafe fn psci_version(use_smc: bool) -> i64 {
+ psci_raw(use_smc, 0x8400_0000, 0, 0, 0)
+}
+
+/// Issue PSCI CPU_ON (SMC64 fn 0xC400_0003) on the selected conduit.
+#[cfg(feature = "smp")]
+unsafe fn psci_cpu_on(target_mpidr: u64, entry_pa: u64, context_id: u64) -> i64 {
+ psci_raw(
+ PSCI_USE_SMC.load(Ordering::Relaxed),
+ 0xC400_0003,
+ target_mpidr,
+ entry_pa,
+ context_id,
+ )
+}
+
+/// Wake secondary cores into `ap_main` (Milestone 1: → idle). Must run after the
+/// BSP's MMU + vectors + GIC are up. Reads the PSCI conduit from the DTB; tries
+/// CPU_ON for cores 1..=MAX_APS and stops at the first absent core.
+#[cfg(feature = "smp")]
+pub fn wake_aps() {
+ let dtb = crate::arch::aarch64::fdt::dtb_ptr();
+ // Determine the PSCI conduit. Prefer the DTB; else probe both with the
+ // side-effect-free PSCI_VERSION and use whichever the platform answers.
+ let conduit = unsafe { crate::arch::aarch64::fdt::psci_method(dtb) };
+ let use_smc = match conduit {
+ Some(crate::arch::aarch64::fdt::PsciConduit::Smc) => true,
+ Some(crate::arch::aarch64::fdt::PsciConduit::Hvc) => false,
+ None => {
+ let smc_v = unsafe { psci_version(true) };
+ let hvc_v = unsafe { psci_version(false) };
+ log::error!("[SMP] PSCI_VERSION probe: smc={:#x} hvc={:#x}", smc_v, hvc_v);
+ // A valid PSCI version is a small positive word (>=0x10000 for v1.x,
+ // 0x2 for v0.2); -1 = NOT_SUPPORTED on that conduit.
+ if smc_v > 0 {
+ true
+ } else if hvc_v > 0 {
+ false
+ } else {
+ log::error!("[SMP] neither conduit answers PSCI — staying single-core");
+ return;
+ }
+ }
+ };
+ PSCI_USE_SMC.store(use_smc, Ordering::Relaxed);
+ log::error!("[SMP] PSCI conduit = {}", if use_smc { "SMC" } else { "HVC" });
+
+ capture_boot_state();
+ let entry = _ap_trampoline as usize as u64;
+ log::error!("[SMP] AP trampoline @ {:#x}; waking secondary cores...", entry);
+
+ for idx in 1..=MAX_APS as u64 {
+ // QEMU virt: core N has MPIDR Aff0 = N (< 16 cores).
+ let target_mpidr = idx;
+ let st = unsafe { psci_cpu_on(target_mpidr, entry, idx) };
+ if st == 0 || st == -4 {
+ // SUCCESS or ALREADY_ON — wait briefly for it to mark online.
+ let mut spun = 0u64;
+ loop {
+ if unsafe { (*core::ptr::addr_of!(PER_CPU_DATA))[idx as usize].online.load(Ordering::Acquire) } {
+ break;
+ }
+ spun += 1;
+ if spun > 200_000_000 {
+ log::error!("[SMP] core {} CPU_ON ok but never came online", idx);
+ break;
+ }
+ core::hint::spin_loop();
+ }
+ } else {
+ // -2 INVALID_PARAMS = core absent → stop probing further cores.
+ log::error!("[SMP] core {} CPU_ON returned {} — no more cores", idx, st);
+ break;
+ }
+ }
+ log::error!("[SMP] online CPU count = {}", CPU_COUNT.load(Ordering::Acquire));
+}
+
+/// Wake all APs. Sets up the BSP's per-CPU entry, then (under `--features smp`)
+/// brings secondary cores online (Milestone 1: into an idle loop).
///
-/// TODO(arm): bring APs online via PSCI `CPU_ON` (SMC/HVC) and have each call
-/// `crate::arch::ap_init()` then `crate::sched::ap_start()`.
+/// Single-core by default — matching x86. Waking cores only idles them today
+/// (`ap_main` parks in `wfi`; co-scheduling doesn't converge the Dart GC
+/// safepoint — see [[smp-bringup]]) and risks a boot hang on real hardware if an
+/// AP faults in the trampoline and the BSP spins waiting for it to come online.
pub fn init(_smp_resp: Option<&'static MpResponse>) {
unsafe {
let table = &mut *core::ptr::addr_of_mut!(PER_CPU_DATA);
@@ -82,10 +437,21 @@ pub fn init(_smp_resp: Option<&'static MpResponse>) {
table[0].online.store(true, Ordering::Release);
}
CPU_COUNT.store(1, Ordering::Release);
- log::info!("[SMP] aarch64 single-core scaffold (AP wake via PSCI not implemented)");
+
+ #[cfg(feature = "smp")]
+ wake_aps();
+ #[cfg(not(feature = "smp"))]
+ log::info!("[SMP] single-core boot (AP bring-up gated behind the `smp` feature)");
}
-/// Broadcast a reschedule IPI to all other online CPUs.
-///
-/// TODO(arm): send a GIC SGI to the target CPU interface(s). No-op (single core).
-pub fn broadcast_resched_ipi() {}
+/// Broadcast a reschedule IPI (GIC SGI) to all other online CPUs, so a thread
+/// made runnable on this core gets promptly picked up by the core it's affined
+/// to. Guarded on CPU_COUNT > 1: single-core never touches the GIC (this is on
+/// the hot `set_state(Running)` path), and the SGI is "all-but-self" so it
+/// reaches no one anyway on one core. The receiving core's IRQ handler EOIs it
+/// and (once APs schedule, M5) reruns its scheduler.
+pub fn broadcast_resched_ipi() {
+ if CPU_COUNT.load(Ordering::Acquire) > 1 {
+ crate::arch::aarch64::gic::send_sgi_all_but_self(crate::arch::aarch64::gic::SGI_RESCHED);
+ }
+}
diff --git a/kernel/src/arch/x86_64/apic.rs b/kernel/src/arch/x86_64/apic.rs
index 8ae3c8b..ab23139 100644
--- a/kernel/src/arch/x86_64/apic.rs
+++ b/kernel/src/arch/x86_64/apic.rs
@@ -60,7 +60,55 @@ fn print_u64_early(val: u64) {
}
}
+/// Upper bound (in TSC cycles) on any legacy-PIT gate spin. The PIT wait is a
+/// fixed ~1–10 ms; this bound is many seconds at any real clock, so it never
+/// trips on working hardware but guarantees we cannot spin forever on firmware
+/// that does not wire the legacy PIT / speaker gate (e.g. Apple Macs), which
+/// otherwise hangs boot at "cortex::starting" with the clock frozen at 0.0s.
+const PIT_SPIN_TIMEOUT_TSC: u64 = 2_000_000_000;
+
+/// TSC frequency (Hz) straight from CPUID — no legacy PIT required. Tries leaf
+/// 0x15 (TSC / core-crystal ratio) then leaf 0x16 (processor base MHz). Returns
+/// 0 when neither is usable (caller falls back to the bounded PIT calibration).
+fn tsc_hz_from_cpuid() -> u64 {
+ let (max_leaf, _, _, _) = crate::arch::x86_64::cpu::cpuid(0, 0);
+ // Leaf 0x15: EAX=denominator, EBX=numerator, ECX=core-crystal Hz.
+ if max_leaf >= 0x15 {
+ let (denom, numer, crystal, _) = crate::arch::x86_64::cpu::cpuid(0x15, 0);
+ if denom != 0 && numer != 0 {
+ if crystal != 0 {
+ return (crystal as u64) * (numer as u64) / (denom as u64);
+ }
+ // Crystal often reads 0 on Skylake/Kaby Lake; the nominal TSC equals
+ // the processor base frequency reported by leaf 0x16.
+ if max_leaf >= 0x16 {
+ let (base_mhz, _, _, _) = crate::arch::x86_64::cpu::cpuid(0x16, 0);
+ if base_mhz != 0 {
+ return base_mhz as u64 * 1_000_000;
+ }
+ }
+ }
+ }
+ // Leaf 0x16: EAX = processor base frequency in MHz.
+ if max_leaf >= 0x16 {
+ let (base_mhz, _, _, _) = crate::arch::x86_64::cpu::cpuid(0x16, 0);
+ if base_mhz != 0 {
+ return base_mhz as u64 * 1_000_000;
+ }
+ }
+ 0
+}
+
fn calibrate_tsc() -> u64 {
+ // Prefer CPUID — it needs no legacy PIT, so it works on firmware (Macs, some
+ // UEFI) that does not provide the i8254 the PIT path below relies on. Guard
+ // against bogus values: QEMU/TCG reports nonsensical CPUID 0x15/0x16 (e.g.
+ // a few MHz), so accept only a plausible CPU clock (0.2–20 GHz) and otherwise
+ // fall through to the (bounded) PIT measurement.
+ let cpuid_hz = tsc_hz_from_cpuid();
+ if (200_000_000..=20_000_000_000).contains(&cpuid_hz) {
+ return cpuid_hz;
+ }
unsafe {
let val = super::port_io::inb(0x61);
super::port_io::outb(0x61, (val & 0xFD) | 1);
@@ -76,16 +124,26 @@ fn calibrate_tsc() -> u64 {
super::port_io::outb(0x61, val2 | 1);
let start = crate::arch::rdtsc();
+ // BOUNDED spin: bail if the PIT gate never fires (missing/idle PIT).
while (super::port_io::inb(0x61) & 0x20) == 0 {
+ if crate::arch::rdtsc().wrapping_sub(start) > PIT_SPIN_TIMEOUT_TSC {
+ break;
+ }
core::hint::spin_loop();
}
let end = crate::arch::rdtsc();
+ let fired = (super::port_io::inb(0x61) & 0x20) != 0;
let elapsed = end.wrapping_sub(start);
-
+
let val3 = super::port_io::inb(0x61);
super::port_io::outb(0x61, val3 & 0xFC);
+ // PIT never fired within the bound → no usable measurement. Return 0 so
+ // the caller uses the conservative default instead of a bogus value.
+ if !fired {
+ return 0;
+ }
elapsed * 100
}
}
@@ -117,7 +175,14 @@ fn calibrate_apic_ticks_per_ms() -> u32 {
}
}
+ // BOUNDED spin (see PIT_SPIN_TIMEOUT_TSC): a missing/idle PIT must not
+ // hang boot. If it never fires, `current` stays the full count → ticks=0
+ // → the caller's 62500 default is used.
+ let spin_start = crate::arch::rdtsc();
while (super::port_io::inb(0x61) & 0x20) == 0 {
+ if crate::arch::rdtsc().wrapping_sub(spin_start) > PIT_SPIN_TIMEOUT_TSC {
+ break;
+ }
core::hint::spin_loop();
}
@@ -331,7 +396,12 @@ pub fn finish_xapic_init() {
}
// Calibrate TSC.
- let tsc_hz = calibrate_tsc();
+ // 0 means neither CPUID nor a working PIT gave a value — keep the safe 2 GHz
+ // default rather than setting a 0 frequency (which would zero the timer period).
+ let tsc_hz = match calibrate_tsc() {
+ 0 => 2_000_000_000,
+ hz => hz,
+ };
set_tsc_hz(tsc_hz);
crate::logger::early_print("[APIC] Calibrated TSC frequency: ");
print_u64_early(tsc_hz);
diff --git a/kernel/src/arch/x86_64/idt.rs b/kernel/src/arch/x86_64/idt.rs
index ef175f6..46adc5f 100644
--- a/kernel/src/arch/x86_64/idt.rs
+++ b/kernel/src/arch/x86_64/idt.rs
@@ -67,7 +67,7 @@ pub fn init() {
IDT.entries[6].set(invalid_opcode_handler as *const () as u64, 0, 0);
IDT.entries[7].set(device_not_avail_handler as *const () as u64, 0, 0);
IDT.entries[8].set(double_fault_handler as *const () as u64, DOUBLE_FAULT_IST, 0);
- IDT.entries[13].set(general_protection_handler as *const () as u64, 0, 0);
+ IDT.entries[13].set(gp_fault_entry as *const () as u64, 0, 0);
IDT.entries[14].set(page_fault_entry as *const () as u64, 0, 0);
IDT.entries[16].set(x87_fp_handler as *const () as u64, 0, 0);
IDT.entries[17].set(alignment_check_handler as *const () as u64, 0, 0);
@@ -131,29 +131,72 @@ extern "x86-interrupt" fn device_not_avail_handler(_frame: InterruptFrame) {}
extern "x86-interrupt" fn double_fault_handler(frame: InterruptFrame, _err: u64) -> ! {
panic!("Double fault! ip={:#x}", frame.ip);
}
-extern "x86-interrupt" fn general_protection_handler(frame: InterruptFrame, err: u64) {
- crate::cortex::interrupt_hook(13, &frame, Some(err));
- let user_mode = (frame.cs & 0x3) == 0x3;
+// #GP frame: same layout as PageFaultFrame (the CPU pushes an error code for #GP too).
+#[repr(C)]
+struct GpFaultFrame {
+ r15: u64, r14: u64, r13: u64, r12: u64,
+ r11: u64, r10: u64, r9: u64, r8: u64,
+ rdi: u64, rsi: u64, rbp: u64, rbx: u64,
+ rdx: u64, rcx: u64, rax: u64,
+ err: u64,
+ rip: u64, cs: u64, rflags: u64, rsp: u64, ss: u64,
+}
+
+#[unsafe(naked)]
+unsafe extern "C" fn gp_fault_entry() {
+ core::arch::naked_asm!(
+ "push rax", "push rcx", "push rdx", "push rbx", "push rbp",
+ "push rsi", "push rdi", "push r8", "push r9", "push r10",
+ "push r11", "push r12", "push r13", "push r14", "push r15",
+ "mov rdi, rsp",
+ "call {handler}",
+ "pop r15", "pop r14", "pop r13", "pop r12", "pop r11",
+ "pop r10", "pop r9", "pop r8", "pop rdi", "pop rsi",
+ "pop rbp", "pop rbx", "pop rdx", "pop rcx", "pop rax",
+ "add rsp, 8",
+ "iretq",
+ handler = sym gp_fault_full_handler,
+ );
+}
+
+extern "C" fn gp_fault_full_handler(frame_ptr: *mut GpFaultFrame) {
+ let f = unsafe { &*frame_ptr };
+ let user_mode = (f.cs & 0x3) == 0x3;
if user_mode {
let pid = crate::process::current_pid();
log::error!(
"[GPF] SIGSEGV: ip={:#x} err={:#x} pid={} cs={:#x} sp={:#x} ss={:#x} rflags={:#x}",
- frame.ip,
- err,
- pid,
- frame.cs,
- frame.sp,
- frame.ss,
- frame.flags
+ f.rip, f.err, pid, f.cs, f.rsp, f.ss, f.rflags
+ );
+ log::error!(
+ "[GPF] regs: rax={:#x} rbx={:#x} rcx={:#x} rdx={:#x} rsi={:#x} rdi={:#x} rbp={:#x}",
+ f.rax, f.rbx, f.rcx, f.rdx, f.rsi, f.rdi, f.rbp
+ );
+ log::error!(
+ "[GPF] regs: r8={:#x} r9={:#x} r10={:#x} r11={:#x} r12={:#x} r13={:#x} r14={:#x} r15={:#x}",
+ f.r8, f.r9, f.r10, f.r11, f.r12, f.r13, f.r14, f.r15
);
crate::syscall::dump_recent_syscalls(24);
+ // RESILIENCE: a userspace fault must NOT halt the whole OS (the old
+ // `halt_forever` turned one app's crash into a dead machine). A user-mode
+ // fault holds no kernel lock, so we can safely tear down the faulting
+ // process — `exit` triggers app crash auto-recovery for the app's group —
+ // and reschedule. The shell and every other process survive one app's
+ // crash. Mirrors sys_exit: die, run the next runnable, never park forever.
if pid != 0 {
- crate::process::kill(pid).ok();
+ crate::process::exit(pid, -1);
}
crate::process::set_current_pid(0);
- crate::arch::halt_forever();
+ loop {
+ crate::process::handle_pending_wakes_try();
+ crate::syscall::check_timerfds_and_wake_try();
+ if let Some(next) = crate::process::next_runnable_pid(0) {
+ crate::process::enter_user_by_pid_noreturn(next);
+ }
+ crate::arch::enable_and_halt();
+ }
}
- panic!("GPF! ip={:#x} err={:#x}", frame.ip, err);
+ panic!("GPF! ip={:#x} err={:#x}", f.rip, f.err);
}
extern "x86-interrupt" fn page_fault_handler(frame: InterruptFrame, err: u64) {
// Kept for any vestigial callers; primary handler is page_fault_entry below.
@@ -382,11 +425,21 @@ extern "C" fn page_fault_full_handler(frame_ptr: *mut PageFaultFrame) {
}
crate::syscall::dump_recent_syscalls(24);
if user_mode {
+ // RESILIENCE (matches the #GP handler): a userspace page fault kills only
+ // the faulting process (triggering app crash auto-recovery) and reschedules
+ // — it must not halt the whole OS. A user-mode fault holds no kernel lock.
if pid != 0 {
- crate::process::kill(pid).ok();
+ crate::process::exit(pid, -1);
}
crate::process::set_current_pid(0);
- crate::arch::halt_forever();
+ loop {
+ crate::process::handle_pending_wakes_try();
+ crate::syscall::check_timerfds_and_wake_try();
+ if let Some(next) = crate::process::next_runnable_pid(0) {
+ crate::process::enter_user_by_pid_noreturn(next);
+ }
+ crate::arch::enable_and_halt();
+ }
}
panic!("Unresolvable kernel page fault: addr={:#x} err={:#x} ip={:#x}", cr2, frame.err, frame.rip);
}
@@ -574,16 +627,27 @@ unsafe extern "C" fn apic_timer_entry() {
}
extern "C" fn apic_timer_handler(frame_ptr: *mut TimerTrapFrame) {
+ let cpu_id = crate::arch::x86_64::smp::this_cpu().cpu_id;
+
+ // AP idle-park during shell bring-up: until the shell's engine is up (and thus
+ // an app could be launched onto an AP), an AP has no home-pinned work. Doing the
+ // timerfd/wake/scheduler work here only contends with the BSP's first-frame
+ // bring-up (a boot stall under TCG SMP). EOI and return — the AP just halts.
+ if cpu_id != 0 && !crate::wm::flutter_init_ready() {
+ crate::arch::apic::eoi();
+ return;
+ }
+
crate::syscall::check_timerfds_and_wake_try();
crate::process::handle_pending_wakes_try();
- let cpu_id = crate::arch::x86_64::smp::this_cpu().cpu_id;
-
// Set a deferred-kick flag every ~500 ms (500 ticks × 1 ms/tick).
// The flag is consumed in syscall context (sys_pthread_mutex_unlock or
// sys_wm_event_wait) where spinlock acquisition is safe. This ISR path
- // only does an atomic store — completely lock-free.
- if cpu_id == 0 {
+ // only does an atomic store — completely lock-free. Fired on ALL cores: a
+ // launched app runs on an AP and needs its own cooperative deadlocks broken
+ // (the global flag is consumed by whichever core's thread next yields).
+ {
static TR_KICK_CTR: core::sync::atomic::AtomicU32 =
core::sync::atomic::AtomicU32::new(0);
let k = TR_KICK_CTR.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
@@ -678,35 +742,36 @@ extern "C" fn apic_timer_handler(frame_ptr: *mut TimerTrapFrame) {
return;
}
- // SMP SAFETY: the scheduler is cooperative and BSP-only — it assumes at most
- // ONE user thread runs at a time. Only CPU 0 may enter/run user threads. If an
- // AP's timer ISR were allowed past here it would claim and enter a user thread
- // (via the wake-assist below), running it CONCURRENTLY with the BSP. Two user
- // threads executing in true parallel break the futex/condvar emulation's
- // single-core assumptions → nondeterministic livelock (the smp>1 failure).
- // Keep APs out: they EOI'd above and now simply return to halt.
- if cpu_id != 0 {
- return;
- }
-
- // Kernel-mode wake assist: when a Flutter runner is sleeping in a syscall
- // hlt loop, queued input can otherwise leave PID 1 runnable-but-unentered.
- // If the interrupt lands in idle (cur=0), PID 1 is still the safe target
- // for post-init WM input. If it lands in PID 1's own wm_event_wait loop,
- // re-entering PID 1 makes it re-execute the syscall and drain the queue.
+ // SMP home-core model: each core runs ONLY threads pinned to its home_cpu, so
+ // the BSP and an AP never enter the SAME user thread concurrently. The old
+ // "BSP-only" gate (which parked every AP) is replaced by a per-target home-core
+ // check inside try_claim_cpu_for_try below: a core's wake-assist can only claim
+ // and enter a target whose home_cpu == this core. The shell (home 0) is woken by
+ // the BSP; a launched app (home 1+) is woken by its AP. On single-core there is
+ // only cpu 0 and home_cpu is always 0, so this is exactly the original path.
+ //
+ // Kernel-mode wake assist: when a Flutter runner is sleeping in a syscall hlt
+ // loop, queued input/baton can leave it runnable-but-unentered. Re-entering the
+ // target makes it re-execute its wm_event_wait and drain the queue (pumping a
+ // frame). This is what pumps a launched app's frames on its application core.
+ let my_cpu = cpu_id;
let cur = crate::process::current_pid();
let focus = crate::wm::focus_pid();
let target = if focus != 0 { focus } else { 1 };
- if target != 0
+ // Only the target's HOME core runs the wake-assist for it. This guards the WHOLE
+ // block (not just the claim): set_state_try has a side effect, so an AP must not
+ // flip the shell's state to Running from core 1 (that races the shell's own
+ // cooperative state machine on core 0 — observed to freeze the shell's bring-up).
+ // On single-core, home_cpu is always 0 == my_cpu, so this is the original path.
+ let target_is_mine = crate::process::home_cpu_of_try(target) == Some(my_cpu);
+ if target != 0 && target_is_mine
&& crate::wm::flutter_init_ready()
&& !crate::wm::flutter_bootstrap_spin_active()
&& (cur == 0 || crate::process::is_blocked_try(cur))
&& (crate::wm::input_pending_for(target) > 0 || crate::wm::embedder_baton_due(target))
{
if crate::process::set_state_try(target, crate::process::ProcState::Running) {
-
if cur != target {
- let my_cpu = crate::arch::smp::this_cpu().cpu_id;
if crate::process::try_claim_cpu_for_try(target, my_cpu) {
crate::process::enter_user_by_pid_noreturn_try(target);
}
diff --git a/kernel/src/arch/x86_64/smp.rs b/kernel/src/arch/x86_64/smp.rs
index fa3ef93..2a2fb2e 100644
--- a/kernel/src/arch/x86_64/smp.rs
+++ b/kernel/src/arch/x86_64/smp.rs
@@ -25,6 +25,21 @@ use limine::request::MpResponse;
/// Maximum CPUs supported.
pub const MAX_CPUS: usize = 64;
+/// Gate: true once CPU 0 has entered the scheduler and claimed the init thread.
+/// APs must not run the scheduler before this (else they'd race the BSP to claim
+/// the init thread). Set inside `enter_user_by_pid_noreturn` on CPU 0. Mirrors
+/// the aarch64 SMP gate so the arch-neutral scheduler code links on both arches.
+pub static SMP_SCHED_READY: AtomicBool = AtomicBool::new(false);
+
+#[inline]
+pub fn sched_ready() -> bool {
+ SMP_SCHED_READY.load(Ordering::Acquire)
+}
+#[inline]
+pub fn set_sched_ready() {
+ SMP_SCHED_READY.store(true, Ordering::Release);
+}
+
// ── Per-CPU data ──────────────────────────────────────────────────────────────
/// Per-CPU mutable state — one entry per logical processor.
@@ -102,56 +117,83 @@ pub fn init(smp_resp: Option<&'static MpResponse>) {
PER_CPU_DATA[0].online.store(true, Ordering::Release);
}
- let resp = match smp_resp {
- Some(r) => r,
- None => {
- log::warn!("[SMP] Limine SMP response missing — single-core only");
- return;
- }
- };
-
- let cpus = resp.cpus();
- let ap_count = cpus.iter().filter(|c| c.lapic_id != bsp_lapic_id).count();
- if ap_count == 0 {
- log::info!("[SMP] No APs — single-core");
- return;
+ // SMP scheduling isn't verified safe on real hardware yet: the scheduler is
+ // not fully SMP-safe, ap_init faulted on a real 2016 Skylake Mac, and the BSP
+ // then ground through a spin-count AP timeout (PAUSE is ~140 cycles on
+ // Skylake, so the old 200x5M budget was ~minutes per AP) — hanging the boot at
+ // cortex::smp. Single-core is the proven-stable config (the freeze is fixed by
+ // the serial-GC engine, not SMP). Boot single-core by default; build with
+ // `--features smp` to develop AP bring-up.
+ #[cfg(not(feature = "smp"))]
+ {
+ let _ = smp_resp;
+ log::info!("[SMP] single-core boot (AP bring-up gated behind the `smp` feature)");
}
- log::info!("[SMP] {} total CPU(s) — waking {} AP(s)", cpus.len(), ap_count);
-
- let mut cpu_idx: u32 = 1;
- for cpu in cpus {
- if cpu.lapic_id == bsp_lapic_id { continue; }
- if cpu_idx as usize >= MAX_CPUS {
- log::warn!("[SMP] MAX_CPUS limit reached — skipping remaining APs");
- break;
+
+ #[cfg(feature = "smp")]
+ {
+ let resp = match smp_resp {
+ Some(r) => r,
+ None => {
+ log::warn!("[SMP] Limine SMP response missing — single-core only");
+ return;
+ }
+ };
+
+ let cpus = resp.cpus();
+ let ap_count = cpus.iter().filter(|c| c.lapic_id != bsp_lapic_id).count();
+ if ap_count == 0 {
+ log::info!("[SMP] No APs — single-core");
+ return;
}
- unsafe {
- PER_CPU_DATA[cpu_idx as usize].cpu_id = cpu_idx;
- PER_CPU_DATA[cpu_idx as usize].lapic_id = cpu.lapic_id;
+ log::info!("[SMP] {} total CPU(s) — waking {} AP(s)", cpus.len(), ap_count);
+
+ let mut cpu_idx: u32 = 1;
+ for cpu in cpus {
+ if cpu.lapic_id == bsp_lapic_id { continue; }
+ if cpu_idx as usize >= MAX_CPUS {
+ log::warn!("[SMP] MAX_CPUS limit reached — skipping remaining APs");
+ break;
+ }
+ unsafe {
+ PER_CPU_DATA[cpu_idx as usize].cpu_id = cpu_idx;
+ PER_CPU_DATA[cpu_idx as usize].lapic_id = cpu.lapic_id;
+ }
+ CPU_COUNT.store(cpu_idx + 1, Ordering::Release);
+ cpu.bootstrap(ap_entry as MpGotoFunction, cpu_idx as u64);
+ cpu_idx += 1;
}
- CPU_COUNT.store(cpu_idx + 1, Ordering::Release);
- cpu.bootstrap(ap_entry as MpGotoFunction, cpu_idx as u64);
- cpu_idx += 1;
- }
- // Wait up to ~2 s per AP to come online.
- let expected = cpu_idx as usize;
- 'outer: for i in 1..expected {
- for _ in 0..200u32 {
- if unsafe { PER_CPU_DATA[i].online.load(Ordering::Acquire) } {
- continue 'outer;
+ // Wait for each AP to come online — bounded in WALL-CLOCK time via the
+ // monotonic clock, NOT a spin count (PAUSE is ~140 cycles on Skylake, so a
+ // spin budget is ~minutes per AP on real hardware when an AP fails to start).
+ let expected = cpu_idx as usize;
+ const AP_TIMEOUT_NS: u64 = 500_000_000; // 0.5 s per AP, then continue
+ for i in 1..expected {
+ let deadline = crate::arch::rdtsc_ns().wrapping_add(AP_TIMEOUT_NS);
+ while !unsafe { PER_CPU_DATA[i].online.load(Ordering::Acquire) } {
+ if crate::arch::rdtsc_ns() >= deadline {
+ log::warn!("[SMP] CPU {} (lapic={}) timed out — continuing",
+ i, unsafe { PER_CPU_DATA[i].lapic_id });
+ break;
+ }
+ core::hint::spin_loop();
}
- for _ in 0..5_000_000usize { core::hint::spin_loop(); }
}
- log::warn!("[SMP] CPU {} (lapic={}) timed out",
- i, unsafe { PER_CPU_DATA[i].lapic_id });
- }
- CPU_COUNT.store(cpu_idx, Ordering::Release);
- let online = (0..expected)
- .filter(|&i| unsafe { PER_CPU_DATA[i].online.load(Ordering::Acquire) })
- .count();
- log::info!("[SMP] {}/{} CPU(s) online", online, expected);
+ // Publish the ONLINE count, not the configured count. The scheduler reads
+ // `CPU_COUNT > 1` to decide whether to take the home-pinned SMP path, and
+ // `pick_home_cpu_for_app` pins apps to cores `1..CPU_COUNT`. If an AP failed
+ // to come online we MUST NOT advertise it — otherwise an app pinned to that
+ // dead core would never be scheduled (starves at a black screen). APs come
+ // online contiguously from index 1 under Limine, so the online count is also
+ // a valid scan range for `current_cpu_id()`.
+ let online = (0..expected)
+ .filter(|&i| unsafe { PER_CPU_DATA[i].online.load(Ordering::Acquire) })
+ .count();
+ CPU_COUNT.store(online as u32, Ordering::Release);
+ log::error!("[SMP] {}/{} CPU(s) online — CPU_COUNT={}", online, expected, online);
+ }
}
/// Broadcast a reschedule IPI to all other online CPUs.
diff --git a/kernel/src/compositor/input_hud.rs b/kernel/src/compositor/input_hud.rs
new file mode 100644
index 0000000..21b8dd9
--- /dev/null
+++ b/kernel/src/compositor/input_hud.rs
@@ -0,0 +1,134 @@
+//! On-screen input debug HUD — opt-in via `--features input-hud`.
+//!
+//! Draws a compact panel each frame showing which input device bound and the
+//! live pointer/key/scroll event counts plus the last pointer position. This is
+//! the "eyes on it" tool for diagnosing input on a real VM or board where there
+//! is no serial console:
+//! * `src=none` → no input device bound at all.
+//! * `p=` never rises → events aren't reaching the kernel while you move/click.
+//! * `xy=` pinned off-screen → the cursor is being clamped or scaled wrong.
+//! * border red → no device; cyan → a device is bound.
+//!
+//! Release builds (without the feature) compile `draw()` to a no-op, so the
+//! shipped ISOs stay clean and pay zero cost.
+
+/// Draw the input HUD over the current back buffer (call just before swap).
+#[cfg(feature = "input-hud")]
+pub fn draw() {
+ use crate::drivers::fb;
+ let (_sw, sh) = match fb::size_px() {
+ Some(s) => s,
+ None => return,
+ };
+
+ let (pc, kc, scnt) = crate::wm::input_counts();
+ let (cx, cy) = crate::wm::cursor_pos();
+ let btn = crate::wm::cursor_buttons();
+ let (key, pressed) = crate::wm::last_key();
+ let src = crate::wm::input_source_name();
+ let bound = crate::wm::input_source() != crate::wm::INPUT_SRC_NONE;
+
+ // line 1: device + per-kind event counters
+ let mut l1 = Line::new();
+ l1.s(b"INPUT src=");
+ l1.s(src.as_bytes());
+ l1.s(b" p=");
+ l1.u(pc as u64);
+ l1.s(b" k=");
+ l1.u(kc as u64);
+ l1.s(b" s=");
+ l1.u(scnt as u64);
+
+ // line 2: last pointer position / buttons / key
+ let mut l2 = Line::new();
+ l2.s(b"xy=");
+ l2.i(cx);
+ l2.b(b',');
+ l2.i(cy);
+ l2.s(b" btn=");
+ l2.u(btn as u64);
+ l2.s(b" key=0x");
+ l2.hex(key as u8);
+ l2.b(if pressed { b'+' } else { b'-' });
+
+ let scale = 2u32;
+ let line_h = (8 * scale + 3) as i32;
+ let pad = 6i32;
+ let tw = fb::text_width(l1.len, scale).max(fb::text_width(l2.len, scale));
+ let panel_w = (tw + pad * 2) as u32;
+ let panel_h = (line_h * 2 + pad * 2) as u32;
+ let x0 = 8i32;
+ let y0 = sh as i32 - panel_h as i32 - 8;
+
+ fb::fill_rect(x0, y0, panel_w, panel_h, 0x00000000);
+ let border = if bound { 0x0038BDF8u32 } else { 0x00FF3030u32 };
+ fb::fill_rect(x0, y0, panel_w, 2, border);
+ fb::fill_rect(x0, y0 + panel_h as i32 - 2, panel_w, 2, border);
+
+ let tx = x0 + pad;
+ fb::draw_text(l1.as_slice(), tx, y0 + pad, scale, 0x0038BDF8);
+ fb::draw_text(l2.as_slice(), tx, y0 + pad + line_h, scale, 0x00FFFFFF);
+}
+
+/// No-op when the HUD feature is disabled (clean release builds).
+#[cfg(not(feature = "input-hud"))]
+#[inline(always)]
+pub fn draw() {}
+
+/// Fixed-capacity ASCII line builder — no heap, no `core::fmt` formatting cost.
+#[cfg(feature = "input-hud")]
+struct Line {
+ buf: [u8; 96],
+ len: usize,
+}
+
+#[cfg(feature = "input-hud")]
+impl Line {
+ fn new() -> Self {
+ Self { buf: [0; 96], len: 0 }
+ }
+ fn as_slice(&self) -> &[u8] {
+ &self.buf[..self.len]
+ }
+ fn b(&mut self, c: u8) {
+ if self.len < self.buf.len() {
+ self.buf[self.len] = c;
+ self.len += 1;
+ }
+ }
+ fn s(&mut self, s: &[u8]) {
+ for &c in s {
+ self.b(c);
+ }
+ }
+ fn u(&mut self, mut n: u64) {
+ if n == 0 {
+ self.b(b'0');
+ return;
+ }
+ let mut tmp = [0u8; 20];
+ let mut i = 0;
+ while n > 0 {
+ tmp[i] = b'0' + (n % 10) as u8;
+ n /= 10;
+ i += 1;
+ }
+ while i > 0 {
+ i -= 1;
+ self.b(tmp[i]);
+ }
+ }
+ fn i(&mut self, n: i32) {
+ if n < 0 {
+ self.b(b'-');
+ self.u((-(n as i64)) as u64);
+ } else {
+ self.u(n as u64);
+ }
+ }
+ fn hex(&mut self, n: u8) {
+ let h = b"0123456789ABCDEF";
+ self.b(h[(n >> 4) as usize]);
+ self.b(h[(n & 0xF) as usize]);
+ }
+}
diff --git a/kernel/src/compositor/mod.rs b/kernel/src/compositor/mod.rs
index 47c276a..eaf18ff 100644
--- a/kernel/src/compositor/mod.rs
+++ b/kernel/src/compositor/mod.rs
@@ -9,6 +9,9 @@ use alloc::vec::Vec;
use spin::Mutex;
use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
+/// On-screen input debug HUD (opt-in via `--features input-hud`; no-op otherwise).
+mod input_hud;
+
/// The mouse-cursor shape the compositor currently draws. Set from userspace
/// via `SYS_CURSOR_SHAPE_SET` when the Flutter framework activates a system
/// cursor (e.g. hovering a button → click/hand, hovering a text field → I-beam).
@@ -954,6 +957,7 @@ pub fn render_frame() {
// verbose log overlay) instead of raw logs. It fills its own black bg.
crate::drivers::bootscreen::render();
draw_software_cursor();
+ input_hud::draw();
crate::drivers::fb::swap_buffers();
crate::wm::push_vsync(frame);
return;
@@ -1020,6 +1024,7 @@ pub fn render_frame() {
drop(c);
draw_software_cursor();
+ input_hud::draw();
crate::drivers::fb::swap_buffers();
let mut c = COMP.lock();
c.frame_counter = c.frame_counter.wrapping_add(1);
diff --git a/kernel/src/drivers/bootscreen.rs b/kernel/src/drivers/bootscreen.rs
index 08a1dc5..09d57fd 100644
--- a/kernel/src/drivers/bootscreen.rs
+++ b/kernel/src/drivers/bootscreen.rs
@@ -11,7 +11,7 @@
//! serial console for developers). Press **F2** to toggle a verbose overlay that
//! renders the recent kernel log ring on demand.
-use core::sync::atomic::{AtomicBool, AtomicU64, Ordering};
+use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use crate::drivers::fb;
use crate::logger;
@@ -29,6 +29,16 @@ const LOG_ERR: u32 = 0x00E0_5A4A; // verbose log error/warn text
static START_NS: AtomicU64 = AtomicU64::new(0);
static VERBOSE: AtomicBool = AtomicBool::new(false);
static DONE: AtomicBool = AtomicBool::new(false);
+/// Explicit boot milestone (0 = none → fall back to the time-derived stage).
+/// Set by `set_phase` at each major init step so the on-screen status names the
+/// LAST step reached — when boot wedges (e.g. on real hardware QEMU can't
+/// reproduce), the frozen splash shows exactly where it stopped.
+static PHASE: AtomicU32 = AtomicU32::new(0);
+
+/// Record the current boot milestone (see PHASE). Phases are listed in `stage()`.
+pub fn set_phase(p: u32) {
+ PHASE.store(p, Ordering::Release);
+}
fn now_ns() -> u64 {
crate::syscall::poll::monotonic_ns()
@@ -87,9 +97,25 @@ fn stage() -> &'static [u8] {
if DONE.load(Ordering::Acquire) {
return b"boot_completed";
}
- match percent() {
- 0..=24 => b"starting",
- 25..=54 => b"init_runtime",
+ // Explicit milestone wins (set by set_phase at each init step). The frozen
+ // splash then names the last step reached if boot wedges on real hardware.
+ match PHASE.load(Ordering::Acquire) {
+ 0 => match percent() {
+ 0..=24 => b"starting",
+ 25..=54 => b"init_runtime",
+ _ => b"warming_engine",
+ },
+ 1 => b"drivers",
+ 2 => b"compositor",
+ 3 => b"scheduler",
+ 4 => b"ipc_wm",
+ 5 => b"filesystem",
+ 6 => b"block_net",
+ 7 => b"packages",
+ 8 => b"cortex",
+ 9 => b"smp",
+ 10 => b"spawn_shell",
+ 11 => b"enter_user",
_ => b"warming_engine",
}
}
diff --git a/kernel/src/drivers/fb.rs b/kernel/src/drivers/fb.rs
index 9483004..8d51e58 100644
--- a/kernel/src/drivers/fb.rs
+++ b/kernel/src/drivers/fb.rs
@@ -626,7 +626,25 @@ pub fn set_double_buffer(active: bool) {
let height = FB_HEIGHT.load(Ordering::Relaxed);
let pitch_px = FB_PITCH_PX.load(Ordering::Relaxed);
let size = pitch_px as usize * height as usize;
- *db = Some(vec![0u32; size]);
+ // A full-fb back buffer is large on high-res panels — a 2880x1800
+ // Retina framebuffer (e.g. a 2016 MacBook Pro) is ~20 MiB. Allocate
+ // it FALLIBLY (and reject an implausible geometry) so a big fb or a
+ // tight heap degrades to single-buffer / direct-to-fb rendering
+ // instead of `vec!` aborting and hanging the boot at
+ // cortex::compositor. Single-buffer is fully supported (every write
+ // path checks DOUBLE_BUFFER_ACTIVE); it just tears.
+ const MAX_DB_PIXELS: usize = 24 * 1024 * 1024; // ~96 MiB sanity ceiling
+ let mut buf: Vec = Vec::new();
+ if size == 0 || size > MAX_DB_PIXELS || buf.try_reserve_exact(size).is_err() {
+ DOUBLE_BUFFER_ACTIVE.store(false, Ordering::SeqCst);
+ log::error!(
+ "[fb] double-buffer unavailable (size={} px) — single-buffer mode",
+ size
+ );
+ } else {
+ buf.resize(size, 0u32);
+ *db = Some(buf);
+ }
}
}
}
diff --git a/kernel/src/drivers/ps2.rs b/kernel/src/drivers/ps2.rs
index 9559cca..1f2d009 100644
--- a/kernel/src/drivers/ps2.rs
+++ b/kernel/src/drivers/ps2.rs
@@ -359,8 +359,21 @@ fn handle_mouse_byte(byte: u8) {
/// IDT vectors are registered (i.e., after `idt::init()`).
pub fn init() {
unsafe {
- // Flush output buffer.
- while in8(PS2_STATUS) & STATUS_OBF != 0 {
+ // Absent i8042 (firmware without a legacy PS/2 controller — e.g. some
+ // UEFI/UTM profiles) reads 0xFF on the status port. 0xFF & STATUS_OBF is
+ // always set, so the flush loops below would spin forever. Detect the
+ // open-bus 0xFF and skip PS/2 entirely (leaving PS2_READY false → the WM
+ // falls back to other input). This is the i8042 analogue of the legacy
+ // PIT hang.
+ if in8(PS2_STATUS) == 0xFF {
+ log::warn!("[PS2] no i8042 controller (status=0xFF) — skipping PS/2 init");
+ return;
+ }
+
+ // Flush output buffer (BOUNDED: a stuck-OBF / open-bus controller must
+ // not spin forever; a real i8042 has at most a couple of bytes queued).
+ for _ in 0..4096 {
+ if in8(PS2_STATUS) & STATUS_OBF == 0 { break; }
let _ = in8(PS2_DATA);
}
@@ -368,8 +381,9 @@ pub fn init() {
wait_write(); out8(PS2_STATUS, 0xAD); // disable port 1
wait_write(); out8(PS2_STATUS, 0xA7); // disable port 2
- // Flush again after disable.
- while in8(PS2_STATUS) & STATUS_OBF != 0 {
+ // Flush again after disable (bounded, same rationale).
+ for _ in 0..4096 {
+ if in8(PS2_STATUS) & STATUS_OBF == 0 { break; }
let _ = in8(PS2_DATA);
}
@@ -411,6 +425,7 @@ pub fn init() {
PS2_DEVICES.store(devs, Ordering::Release);
LAST_ACTIVITY_TSC.store(crate::arch::rdtsc(), Ordering::Relaxed);
PS2_READY.store(true, Ordering::Release);
+ crate::wm::set_input_source(crate::wm::INPUT_SRC_PS2);
}
log::info!(
diff --git a/kernel/src/drivers/virtio_input.rs b/kernel/src/drivers/virtio_input.rs
index 88df836..e2853f4 100644
--- a/kernel/src/drivers/virtio_input.rs
+++ b/kernel/src/drivers/virtio_input.rs
@@ -211,6 +211,7 @@ pub fn init() {
status |= S_DRIVER_OK;
w32(base, STATUS, status);
+ crate::wm::set_input_source(crate::wm::INPUT_SRC_VIRTIO);
// Enable this slot's SPI at the GIC (level-triggered, routed to CPU0).
let intid = SPI_BASE_INTID + slot as u32;
diff --git a/kernel/src/drivers/xhci_runtime.rs b/kernel/src/drivers/xhci_runtime.rs
index 0e3de2f..0241474 100644
--- a/kernel/src/drivers/xhci_runtime.rs
+++ b/kernel/src/drivers/xhci_runtime.rs
@@ -147,6 +147,11 @@ pub struct XhciRuntime {
last_hid: [u8; HID_LEN],
cur_x: i32,
cur_y: i32,
+ // True when the HID interface is an ABSOLUTE pointer (USB tablet) rather than
+ // a boot keyboard/mouse. UTM/QEMU's default pointer is `usb-tablet`, which has
+ // bInterfaceProtocol=0 (no boot protocol) and reports absolute X/Y — without
+ // this it was misrouted to the boot-keyboard handler (no pointer in UTM).
+ hid_is_tablet: bool,
}
static RUNTIME_OK: AtomicBool = AtomicBool::new(false);
@@ -333,6 +338,7 @@ pub fn start(ctrl: &XhciController) -> Result<(), &'static str> {
last_hid: [0; HID_LEN],
cur_x: 64,
cur_y: 64,
+ hid_is_tablet: false,
});
RUNTIME_OK.store(true, Ordering::Release);
log::info!("[USB] XHCI runtime started");
@@ -343,6 +349,7 @@ pub fn start(ctrl: &XhciController) -> Result<(), &'static str> {
if try_enumerate(rt) {
rt.enumerated = true;
log::info!("[USB-HID] device enumerated slot={} protocol={}", rt.slot, rt.hid_protocol);
+ crate::wm::set_input_source(crate::wm::INPUT_SRC_XHCI);
arm_hid_transfer(rt);
break;
}
@@ -546,6 +553,13 @@ fn route_report(report: &[u8], rt: &mut XhciRuntime) {
let old = rt.last_hid;
rt.last_hid.copy_from_slice(report);
+ // Absolute pointer (USB tablet, e.g. UTM's default): parse abs X/Y. Deduped
+ // above (an unchanged abs report is the same position, safe to skip).
+ if rt.hid_is_tablet {
+ route_tablet(report, rt);
+ return;
+ }
+
if rt.hid_protocol == 1 || rt.hid_protocol == 0 {
if let Some((sc, pressed)) = usb_hid::handle_boot_keyboard_report(report) {
crate::wm::push_key(sc, pressed);
@@ -581,6 +595,27 @@ fn route_mouse(report: &[u8], rt: &mut XhciRuntime) {
}
}
+/// USB tablet (absolute) report: byte0 = buttons (b0 L, b1 R, b2 M), bytes1-2 =
+/// absolute X (u16 LE, 0..=32767), bytes3-4 = absolute Y, byte5 = wheel (i8).
+/// This is QEMU/UTM's `usb-tablet` format. Map the absolute range onto the
+/// framebuffer and feed the WM's unified pointer state.
+fn route_tablet(report: &[u8], rt: &mut XhciRuntime) {
+ if report.len() < 5 {
+ return;
+ }
+ let buttons = (report[0] as u32) & 0x7;
+ let ax = u16::from_le_bytes([report[1], report[2]]) as u32;
+ let ay = u16::from_le_bytes([report[3], report[4]]) as u32;
+ let (w, h) = crate::drivers::fb::size_px().unwrap_or((1024, 768));
+ rt.cur_x = ((ax * w.saturating_sub(1)) / 32767) as i32;
+ rt.cur_y = ((ay * h.saturating_sub(1)) / 32767) as i32;
+ crate::wm::push_pointer(rt.cur_x, rt.cur_y, buttons);
+ let wheel = report.get(5).map(|b| *b as i8 as i32).unwrap_or(0);
+ if wheel != 0 {
+ crate::wm::push_scroll(rt.cur_x, rt.cur_y, wheel);
+ }
+}
+
unsafe fn write_input_address_ctx(rt: &XhciRuntime, port: u8, speed: u32) {
let p = p2v(rt.in_phys) as *mut u8;
core::ptr::write_bytes(p, 0, 512);
@@ -735,13 +770,66 @@ unsafe fn try_enumerate(rt: &mut XhciRuntime) -> bool {
return false;
}
+ // For a non-boot HID interface (bInterfaceProtocol=0), read the HID report
+ // descriptor to classify it: UTM/QEMU's default pointer is `usb-tablet`,
+ // which is protocol 0 and reports ABSOLUTE X/Y. Without this it falls through
+ // to the boot-keyboard handler and the pointer never moves.
+ if rt.hid_protocol == 0 {
+ // GET_DESCRIPTOR(HID Report = 0x22, index 0) to interface 0.
+ if control_xfer(rt, 0x81, 0x06, 0x2200, 0, 128, rt.ctrl_buf_phys) {
+ let rd = core::slice::from_raw_parts(p2v(rt.ctrl_buf_phys) as *const u8, 128);
+ rt.hid_is_tablet = report_desc_is_pointer(rd);
+ log::info!(
+ "[USB-HID] protocol=0 → {}",
+ if rt.hid_is_tablet { "absolute pointer (tablet)" } else { "keyboard/other" }
+ );
+ }
+ }
+
// HID class requests: SET_PROTOCOL(boot=0) so reports are boot-format, and
- // SET_IDLE(0) so the device only reports on change. Best-effort.
+ // SET_IDLE(0) so the device only reports on change. Best-effort. (A tablet has
+ // no boot protocol; SET_PROTOCOL is a harmless no-op for it.)
let _ = control_xfer(rt, 0x21, 0x0B, 0, 0, 0, 0); // SET_PROTOCOL boot
let _ = control_xfer(rt, 0x21, 0x0A, 0, 0, 0, 0); // SET_IDLE infinite
true
}
+/// Scan a HID report descriptor for a top-level Generic-Desktop Mouse/Pointer
+/// usage → absolute/relative pointer device (e.g. a USB tablet). Minimal item
+/// walk: item byte = bTag(4)|bType(2)|bSize(2); 0x04 = Usage Page (global),
+/// 0x08 = Usage (local). Generic Desktop page = 0x01; Mouse = 0x02, Pointer =
+/// 0x01, Keyboard = 0x06.
+fn report_desc_is_pointer(rd: &[u8]) -> bool {
+ let mut generic_desktop = false;
+ let mut i = 0usize;
+ while i < rd.len() {
+ let b = rd[i];
+ if b == 0 {
+ break;
+ }
+ let dlen = match b & 0x03 {
+ 3 => 4,
+ n => n as usize,
+ };
+ let tag = b & 0xFC;
+ let data = if dlen >= 1 { rd.get(i + 1).copied().unwrap_or(0) } else { 0 };
+ if tag == 0x04 {
+ // Usage Page
+ generic_desktop = data == 0x01;
+ } else if tag == 0x08 && generic_desktop {
+ // Usage
+ if data == 0x02 || data == 0x01 {
+ return true; // Mouse / Pointer
+ }
+ if data == 0x06 {
+ return false; // Keyboard
+ }
+ }
+ i += 1 + dlen;
+ }
+ false
+}
+
unsafe fn arm_hid_transfer(rt: &mut XhciRuntime) {
put_trb(
rt.ep_ring,
diff --git a/kernel/src/embedder/abi.rs b/kernel/src/embedder/abi.rs
index ade004d..47c7201 100644
--- a/kernel/src/embedder/abi.rs
+++ b/kernel/src/embedder/abi.rs
@@ -197,6 +197,19 @@ pub const SYS_CLIPBOARD_GET: u64 = 0x4B4; // clipboard_get(buf_ptr, buf_len)
pub const SYS_APP_CLOSE_FOREGROUND: u64 = 0x4B5; // app_close_foreground() → 0 (refocus shell, terminate fg app)
pub const SYS_BEEP: u64 = 0x4B6; // beep(freq_hz, duration_ms) → 0 (PC speaker)
+// App networking API — exposes the kernel TCP stack to userspace apps.
+//
+// The TCP socket calls (connect/write/read/close at 0x388-0x38B) are async:
+// `tcp_connect` returns a fd immediately after sending SYN, so an app MUST poll
+// `tcp_status` until the handshake completes before writing. (`net::tcp` doc:
+// this is the correct connect-completion check — a zero-byte read probe does not
+// work because an ESTABLISHED socket reports can_recv()=false until the peer
+// sends.) All are CAP_NET-gated; apps are granted CAP_NET (see process::spawn).
+pub const SYS_TCP_STATUS: u64 = 0x4B7; // tcp_status(fd) → 1 established / 0 connecting / -ECONNREFUSED
+// Reserved for the next networking increment (DNS A-record resolution via a
+// smoltcp DNS socket, reusing the proven TCP stack's ARP/routing):
+pub const SYS_DNS_RESOLVE: u64 = 0x4B8; // dns_resolve(name_ptr, name_len) → ip_be u32 / negative errno
+
/// Mouse-cursor shapes the compositor can draw. Mirrors the subset of Flutter
/// `SystemMouseCursors` kinds OSCortex maps to a distinct on-screen pointer.
pub const CURSOR_SHAPE_BASIC: u32 = 0; // arrow (default)
diff --git a/kernel/src/main.rs b/kernel/src/main.rs
index b17516c..54b5446 100644
--- a/kernel/src/main.rs
+++ b/kernel/src/main.rs
@@ -326,20 +326,42 @@ pub fn kernel_main_arch_limine(map: mm::BootMemMap, hhdm_offset: u64) -> ! {
/// manager, and the logger. This is the part of `kernel_main` that does not
/// touch any boot protocol directly.
fn shared_init_and_run() -> ! {
+ // On-screen boot milestones: set the phase + repaint the splash after each
+ // major step so a hang (e.g. on real hardware QEMU can't reproduce) leaves
+ // the LAST reached step on screen. No-op when the fb isn't ready.
+ macro_rules! bp {
+ ($n:expr) => {
+ if drivers::fb::is_ready() {
+ drivers::bootscreen::set_phase($n);
+ drivers::bootscreen::render();
+ // Present immediately: once the compositor enables double-
+ // buffering, render() draws to the back buffer and nothing
+ // swaps until the engine warm-up loop — which would freeze the
+ // splash at "compositor" and mask a hang in any LATER phase.
+ // Swapping here keeps every milestone actually on screen.
+ drivers::fb::swap_buffers();
+ }
+ };
+ }
+
// ── 4. Platform drivers (input probe) ───────────────────────────────
+ bp!(1);
let qemu_like = arch::cpu::is_qemu_like_hypervisor();
drivers::platform::init_early(qemu_like);
// ── 4b. Compositor scaffold (M13) ────────────────────────────────────
+ bp!(2);
compositor::init();
// ── 5. Scheduler ─────────────────────────────────────────────────────
+ bp!(3);
sched::init();
// ── 6. Security / capabilities ────────────────────────────────────────
security::init();
// ── 7. IPC subsystem ─────────────────────────────────────────────────
+ bp!(4);
ipc::init();
// ── 7a. Window-manager event bridge ──────────────────────────────────
@@ -351,24 +373,30 @@ fn shared_init_and_run() -> ! {
crate::drivers::virtio_input::init();
// ── 7b. Virtual filesystem (initramfs) ───────────────────────────────
+ bp!(5);
fs::init();
// ── 7c. Block, serial, and networking ─────────────────────────────────
+ bp!(6);
drivers::platform::init_block_and_net();
// ── 7d. On-demand package delivery ────────────────────────────────
+ bp!(7);
pkg::init();
// ── 8. AI Cortex ─────────────────────────────────────────────────────
// The Cortex boots last so every kernel subsystem is available to it.
+ bp!(8);
cortex::init();
// ── 9. Signal APs + bring SMP online ────────────────────────────────
+ bp!(9);
KERNEL_INIT_DONE.store(true, Ordering::Release);
#[cfg(target_arch = "x86_64")]
arch::smp_init(SMP_REQUEST.response());
#[cfg(not(target_arch = "x86_64"))]
arch::smp_init(None);
+ bp!(10);
// ── 9b. Spawn init process from initramfs ────────────────────────────
match fs::lookup("/init") {
diff --git a/kernel/src/mm/paging.rs b/kernel/src/mm/paging.rs
index dac7443..1222818 100644
--- a/kernel/src/mm/paging.rs
+++ b/kernel/src/mm/paging.rs
@@ -25,72 +25,91 @@ pub(crate) static PAGE_TABLE_LOCK: Mutex<()> = Mutex::new(());
/// and hand back a guard that releases nothing until depth returns to zero. This
/// is sound because all writers run on one core (the scheduler is cooperative /
/// single-CPU) — there is no second core to race the page-table structure.
-static PAGE_TABLE_LOCK_DEPTH: core::sync::atomic::AtomicU32 =
- core::sync::atomic::AtomicU32::new(0);
+/// PER-CPU re-entrancy depth for the page-table critical section. This MUST be
+/// per-CPU. A single global counter was a critical SMP-safety bug: when core 0 held
+/// the section (depth=1, holding the real lock) and core 1 called lock_page_table(),
+/// core 1's fetch_add saw prev=1 (non-zero), concluded it was a NESTED call, and
+/// BYPASSED the real lock — both cores then mutated page tables in parallel and
+/// corrupted them (observed: KERNEL PANIC "corrupt PTE — a page table was
+/// overwritten"). With a per-CPU counter each core's FIRST acquisition (prev==0)
+/// takes the real cross-core lock and blocks until the other core releases.
+static PAGE_TABLE_LOCK_DEPTH: [core::sync::atomic::AtomicU32; crate::arch::smp::MAX_CPUS] =
+ [const { core::sync::atomic::AtomicU32::new(0) }; crate::arch::smp::MAX_CPUS];
pub(crate) struct PageTableGuard {
outer: Option>,
- /// aarch64 only: DAIF captured by the OUTER acquisition before it masked
- /// IRQs. Restored when the outer guard drops. Unused on nested guards (IRQs
- /// stay masked for the whole outer section).
+ /// CPU that took this guard (index into PAGE_TABLE_LOCK_DEPTH). Stable for the
+ /// section: on aarch64 IRQs are masked (no preemption/migration); on x86 the
+ /// guard is short-lived and the outer drop runs on the same core.
+ cpu: usize,
+ /// aarch64 only: IRQ state captured by the OUTER acquisition before it masked
+ /// IRQs, restored on outer drop. x86 keeps its original non-masking behaviour
+ /// (masking the whole section there starved the app core's timer-ISR frame pump
+ /// during heavy demand-paging → app never presented).
#[cfg(target_arch = "aarch64")]
- saved_daif: u64,
+ saved_irq: u64,
}
impl Drop for PageTableGuard {
fn drop(&mut self) {
if self.outer.is_some() {
- // Outermost guard: clear depth, release the real lock, THEN re-enable
- // IRQs. The lock byte must be released before IRQs come back on, or a
- // freshly-unmasked timer tick could switch to a thread that spins on
- // the still-held lock with IRQs masked → single-core deadlock.
- PAGE_TABLE_LOCK_DEPTH.store(0, core::sync::atomic::Ordering::Release);
- let _ = self.outer.take(); // drop the MutexGuard now (IRQs still masked)
+ // Outermost guard: clear depth, release the real lock, THEN (aarch64)
+ // re-enable IRQs / (x86) re-enable preemption. The lock byte must be
+ // released before preemption/IRQs come back, or a freshly-allowed
+ // switch could land on a thread that spins on the still-held lock.
+ PAGE_TABLE_LOCK_DEPTH[self.cpu].store(0, core::sync::atomic::Ordering::Release);
+ let _ = self.outer.take();
#[cfg(target_arch = "aarch64")]
- crate::arch::aarch64::cpu::interrupts_restore(self.saved_daif);
+ crate::arch::aarch64::cpu::interrupts_restore(self.saved_irq);
} else {
- PAGE_TABLE_LOCK_DEPTH.fetch_sub(1, core::sync::atomic::Ordering::Release);
+ PAGE_TABLE_LOCK_DEPTH[self.cpu].fetch_sub(1, core::sync::atomic::Ordering::Release);
}
+ // x86: balance the preempt_disable() done in every lock_page_table() call
+ // (outer AND nested). Done last so the real lock is already released.
+ #[cfg(not(target_arch = "aarch64"))]
+ crate::process::preempt_enable(self.cpu as u32);
}
}
-/// Acquire the page-table critical section, re-entrant on a single core.
-///
-/// On aarch64 the outer acquisition also MASKS IRQs for the whole critical
-/// section. The reentrant depth counter is only sound if the section cannot be
-/// interleaved by another thread; but syscalls run with IRQs unmasked, so a
-/// generic-timer tick can preempt a lock holder mid-section and switch to a
-/// sibling, which then sees a non-zero depth and proceeds as a bogus "nested"
-/// writer — desynchronising the depth counter from the real lock and eventually
-/// stranding the lock held while depth reads 0 (a later outer acquire then spins
-/// forever with IRQs masked in the demand-abort handler). Masking IRQs around
-/// the outer section makes it genuinely uninterruptible, so nesting only ever
-/// means true same-stack re-entry (e.g. a demand fault during a page-table walk,
-/// which already runs IRQs-masked). x86 keeps its original behaviour.
+/// Acquire the page-table critical section. Re-entrant PER-CPU, SMP-safe across
+/// cores. The FIRST acquisition on each core takes the real cross-core
+/// `PAGE_TABLE_LOCK`, which blocks until any OTHER core holding it releases — the
+/// cross-core serialisation the old GLOBAL depth counter silently skipped (a 2nd
+/// core saw depth>0, treated itself as a bogus "nested" writer, and mutated page
+/// tables in parallel → corruption). aarch64 additionally masks IRQs for the whole
+/// section so its per-CPU depth cannot be desynchronised by a same-core preemption;
+/// x86 keeps its original non-masking behaviour (proven stable, and masking here
+/// starved the app core's frame pump).
#[inline]
pub(crate) fn lock_page_table() -> PageTableGuard {
- // Mask IRQs BEFORE touching the depth counter so the fetch_add → lock
- // sequence cannot be preempted. On a nested call IRQs are already masked
- // (the outer masked them); this is then an idempotent no-op.
#[cfg(target_arch = "aarch64")]
- let saved_daif = crate::arch::aarch64::cpu::interrupts_save_and_disable();
- let prev = PAGE_TABLE_LOCK_DEPTH.fetch_add(1, core::sync::atomic::Ordering::Acquire);
+ let saved_irq = crate::arch::aarch64::cpu::interrupts_save_and_disable();
+ // x86: disable preemption (NOT IRQs) for the section. This pins the thread to
+ // its cpu (timer-preempt bails) so the per-CPU depth counter can't be stranded
+ // by a mid-section migration — which would re-expose the cross-core lock bypass
+ // — while keeping IRQs on so the app core's timer-ISR frame pump still fires.
+ // Returns the pinned cpu; use it for the depth slot so disable/enable balance.
+ #[cfg(not(target_arch = "aarch64"))]
+ let cpu = crate::process::preempt_disable() as usize;
+ #[cfg(target_arch = "aarch64")]
+ let cpu = crate::arch::smp::current_cpu_id() as usize;
+ let prev = PAGE_TABLE_LOCK_DEPTH[cpu].fetch_add(1, core::sync::atomic::Ordering::Acquire);
if prev == 0 {
- // First (outer) acquisition on this core: take the real lock.
+ // First (outer) acquisition on this core: take the real cross-core lock.
let g = PAGE_TABLE_LOCK.lock();
PageTableGuard {
outer: Some(g),
+ cpu,
#[cfg(target_arch = "aarch64")]
- saved_daif,
+ saved_irq,
}
} else {
- // Nested acquisition: the outer guard already holds the lock (and already
- // masked IRQs). `saved_daif` here is "already masked" and is discarded on
- // drop — only the outer guard restores DAIF.
+ // Nested (same-core, same-stack) re-entry: we already hold the lock.
PageTableGuard {
outer: None,
+ cpu,
#[cfg(target_arch = "aarch64")]
- saved_daif,
+ saved_irq,
}
}
}
@@ -177,6 +196,21 @@ mod x86_64_impl {
Some(phys)
}
+ /// Checked page-table-frame → HHDM virt for READ-ONLY WALKS. Returns None if the
+ /// frame is null or out of addressable RAM instead of panicking. A walk can read
+ /// an insane table pointer when a stale pml4 reference (e.g. a futex physaddr
+ /// lookup during an app's crash-recovery teardown on another core) walks a page
+ /// table whose lower levels were freed and reused under SMP. Degrading to "not
+ /// mapped" keeps the kernel alive; the bare phys_to_virt (which panics on garbage,
+ /// a useful invariant for the WRITE paths) is unchanged.
+ #[inline]
+ fn walk_table(phys: u64) -> Option<*const u64> {
+ if phys == 0 || phys >= 0x0000_0004_0000_0000 {
+ return None;
+ }
+ Some(phys.wrapping_add(frame_allocator::hhdm_offset()) as *const u64)
+ }
+
unsafe fn ensure_next_table(entry: *mut u64) -> Option {
let e = unsafe { entry.read_volatile() };
if e & PageFlags::PRESENT.bits() != 0 {
@@ -321,16 +355,16 @@ mod x86_64_impl {
let present = PageFlags::PRESENT.bits();
unsafe {
- let pml4 = phys_to_virt(pml4_phys) as *const u64;
+ let pml4 = walk_table(pml4_phys)?;
let pml4e = pml4.add(pml4_idx).read_volatile();
if pml4e & present == 0 { return None; }
- let pdpt = phys_to_virt(pml4e & PHYS_MASK) as *const u64;
+ let pdpt = walk_table(pml4e & PHYS_MASK)?;
let pdpte = pdpt.add(pdpt_idx).read_volatile();
if pdpte & present == 0 { return None; }
- let pd = phys_to_virt(pdpte & PHYS_MASK) as *const u64;
+ let pd = walk_table(pdpte & PHYS_MASK)?;
let pde = pd.add(pd_idx).read_volatile();
if pde & present == 0 { return None; }
- let pt = phys_to_virt(pde & PHYS_MASK) as *const u64;
+ let pt = walk_table(pde & PHYS_MASK)?;
let pte = pt.add(pt_idx).read_volatile();
if pte & present == 0 { return None; }
Some(pte & PHYS_MASK)
@@ -345,16 +379,16 @@ mod x86_64_impl {
let present = PageFlags::PRESENT.bits();
unsafe {
- let pml4 = phys_to_virt(pml4_phys) as *const u64;
+ let pml4 = walk_table(pml4_phys)?;
let pml4e = pml4.add(pml4_idx).read_volatile();
if pml4e & present == 0 { return None; }
- let pdpt = phys_to_virt(pml4e & PHYS_MASK) as *const u64;
+ let pdpt = walk_table(pml4e & PHYS_MASK)?;
let pdpte = pdpt.add(pdpt_idx).read_volatile();
if pdpte & present == 0 { return None; }
- let pd = phys_to_virt(pdpte & PHYS_MASK) as *const u64;
+ let pd = walk_table(pdpte & PHYS_MASK)?;
let pde = pd.add(pd_idx).read_volatile();
if pde & present == 0 { return None; }
- let pt = phys_to_virt(pde & PHYS_MASK) as *const u64;
+ let pt = walk_table(pde & PHYS_MASK)?;
let pte = pt.add(pt_idx).read_volatile();
if pte & present == 0 { return None; }
let phys = pte & PHYS_MASK;
@@ -404,13 +438,16 @@ mod x86_64_impl {
/// Walk a user PML4 and free every page frame reachable through it
/// (but not the kernel-half entries 256–511, which are shared).
pub fn free_user_pml4(pml4_phys: u64) {
- let pml4 = phys_to_virt(pml4_phys) as *const u64;
+ // Use walk_table (bounds-checked) for every table-pointer deref so a garbage
+ // table pointer (a double-free, or an address space already disturbed by an
+ // SMP race) skips that subtree instead of phys_to_virt-panicking the kernel.
+ let pml4 = match walk_table(pml4_phys) { Some(p) => p, None => return };
// Only walk lower-half entries (0–255).
for i in 0..256usize {
let pml4e = unsafe { pml4.add(i).read_volatile() };
if pml4e & PageFlags::PRESENT.bits() == 0 { continue; }
let pdpt_phys = pml4e & PHYS_MASK;
- let pdpt = phys_to_virt(pdpt_phys) as *const u64;
+ let pdpt = match walk_table(pdpt_phys) { Some(p) => p, None => continue };
for j in 0..512usize {
let pdpte = unsafe { pdpt.add(j).read_volatile() };
if pdpte & PageFlags::PRESENT.bits() == 0 { continue; }
@@ -419,7 +456,7 @@ mod x86_64_impl {
continue;
}
let pd_phys = pdpte & PHYS_MASK;
- let pd = phys_to_virt(pd_phys) as *const u64;
+ let pd = match walk_table(pd_phys) { Some(p) => p, None => continue };
for k in 0..512usize {
let pde = unsafe { pd.add(k).read_volatile() };
if pde & PageFlags::PRESENT.bits() == 0 { continue; }
@@ -428,7 +465,7 @@ mod x86_64_impl {
continue;
}
let pt_phys = pde & PHYS_MASK;
- let pt = phys_to_virt(pt_phys) as *const u64;
+ let pt = match walk_table(pt_phys) { Some(p) => p, None => continue };
for l in 0..512usize {
let pte = unsafe { pt.add(l).read_volatile() };
if pte & PageFlags::PRESENT.bits() != 0 {
@@ -1006,7 +1043,20 @@ pub unsafe fn map_user_page_with_flags(
/// Walk a user PML4 and return the physical frame already mapped at `virt`,
/// or `None` if the page is not yet mapped.
+/// A page-table root is only walkable if it is a plausible physical frame: non-zero
+/// (phys 0 is the reserved/null frame — a freed process clears its pml4_phys to 0)
+/// and within addressable RAM. Walking a freed/null root would deref phys 0 →
+/// `phys_to_virt(0)` = the HHDM base, which is unmapped → unrecoverable kernel page
+/// fault. This is the SMP teardown hazard: crash-recovery frees a dead app's pml4
+/// while another core still holds a stale reference (e.g. a futex physaddr lookup).
+/// Treating an insane root as "nothing mapped" keeps the kernel alive.
+#[inline]
+fn pml4_root_walkable(pml4_phys: u64) -> bool {
+ pml4_phys != 0 && pml4_phys < 0x0000_0004_0000_0000
+}
+
pub fn translate_user_page(pml4_phys: u64, virt: u64) -> Option {
+ if !pml4_root_walkable(pml4_phys) { return None; }
let _lock = lock_page_table();
#[cfg(target_arch = "x86_64")]
{ x86_64_impl::translate_user_page(pml4_phys, virt) }
@@ -1019,6 +1069,7 @@ pub fn translate_user_page(pml4_phys: u64, virt: u64) -> Option {
/// Walk a user PML4 and return the physical frame mapped at `virt` along with
/// its writable and executable flags.
pub fn translate_user_page_flags(pml4_phys: u64, virt: u64) -> Option<(u64, bool, bool)> {
+ if !pml4_root_walkable(pml4_phys) { return None; }
let _lock = lock_page_table();
#[cfg(target_arch = "x86_64")]
{ x86_64_impl::translate_user_page_flags(pml4_phys, virt) }
@@ -1030,6 +1081,9 @@ pub fn translate_user_page_flags(pml4_phys: u64, virt: u64) -> Option<(u64, bool
/// Free all frames in a user PML4 and the PML4 itself.
pub fn free_user_pml4(pml4_phys: u64) {
+ // Never walk/free a null or insane root: a double-free or a stale 0 would
+ // otherwise deref phys 0 and panic the kernel (see pml4_root_walkable).
+ if !pml4_root_walkable(pml4_phys) { return; }
let _lock = lock_page_table();
#[cfg(target_arch = "x86_64")]
x86_64_impl::free_user_pml4(pml4_phys);
@@ -1070,6 +1124,7 @@ pub unsafe fn update_user_page(
/// # Safety
/// `pml4_phys` must be a valid allocated PML4.
pub unsafe fn unmap_user_page(pml4_phys: u64, virt: u64) -> Result {
+ if !pml4_root_walkable(pml4_phys) { return Err("dead/null pml4"); }
let _lock = lock_page_table();
#[cfg(target_arch = "x86_64")]
{
@@ -1092,6 +1147,7 @@ pub static UNMAP_FREED: core::sync::atomic::AtomicU64 = core::sync::atomic::Atom
/// Unmap a range of virtual addresses in a specific PML4 and free their physical frames.
pub fn unmap_user_range(pml4_phys: u64, start_va: u64, size: u64) {
+ if !pml4_root_walkable(pml4_phys) { return; }
if size == 0 { return; }
let start = start_va & !0xFFF;
let end = (start_va + size + 4095) & !0xFFF;
diff --git a/kernel/src/net/tcp.rs b/kernel/src/net/tcp.rs
index d83b1b5..21c6ebf 100644
--- a/kernel/src/net/tcp.rs
+++ b/kernel/src/net/tcp.rs
@@ -26,9 +26,10 @@ use smoltcp::socket::tcp::{self as tcp_sock, Socket as TcpSocket};
use smoltcp::socket::udp::{self as udp_sock, Socket as UdpSocket};
use smoltcp::socket::icmp::{self as icmp_sock, Socket as IcmpSocket};
use smoltcp::socket::dhcpv4::{Socket as DhcpSocket, Event as DhcpEvent};
+use smoltcp::socket::dns::{Socket as DnsSocket, GetQueryResultError};
use smoltcp::time::Instant;
use smoltcp::wire::{
- EthernetAddress, IpAddress, IpCidr, Ipv4Address,
+ DnsQueryType, EthernetAddress, IpAddress, IpCidr, Ipv4Address,
IpEndpoint,
};
@@ -139,6 +140,8 @@ struct TcpStack {
icmp_handle: Option,
// Phase 52: DHCP socket
dhcp_handle: Option,
+ // DNS resolver socket (servers seeded from DHCP; default public resolver).
+ dns_handle: Option,
ip: Ipv4Address,
}
@@ -203,6 +206,14 @@ pub fn init_smoltcp() {
let icmp_socket = IcmpSocket::new(icmp_rx, icmp_tx);
let icmp_handle = sockets.add(icmp_socket);
+ // DNS resolver. Seed with a public resolver so name lookups work even before
+ // DHCP; the DHCP ACK then swaps in the network's own servers (update_servers).
+ let dns_socket = DnsSocket::new(
+ &[IpAddress::Ipv4(Ipv4Address::new(8, 8, 8, 8))],
+ vec![None, None],
+ );
+ let dns_handle = sockets.add(dns_socket);
+
let _g = TCP_LOCK.lock();
let stack = unsafe { &mut *TCP_STACK.0.get() };
*stack = Some(TcpStack {
@@ -211,6 +222,7 @@ pub fn init_smoltcp() {
handles: [None; MAX_TCP_SOCKETS],
icmp_handle: Some(icmp_handle),
dhcp_handle: None,
+ dns_handle: Some(dns_handle),
ip: DEFAULT_IP,
});
@@ -349,6 +361,50 @@ pub fn tcp_close(fd: usize) -> Result<(), i64> {
Ok(())
}
+/// Resolve a hostname to an IPv4 address (big-endian u32) via the DNS socket.
+/// Drives the stack until the query completes or a ~5s timeout. Returns the
+/// first A record, or a negative errno (-22 bad name, -2 failed/no A, -110 timeout).
+pub fn dns_resolve(name: &str) -> Result {
+ let _g = TCP_LOCK.lock();
+ let stack = unsafe { &mut *TCP_STACK.0.get() };
+ let s = stack.as_mut().ok_or(-1i64)?;
+ let dns_h = s.dns_handle.ok_or(-1i64)?;
+
+ // Start an A-record query (disjoint &mut borrows of iface + sockets).
+ let handle = {
+ let cx = s.iface.context();
+ let sock = s.sockets.get_mut::(dns_h);
+ sock.start_query(cx, name, DnsQueryType::A).map_err(|_| -22i64)?
+ };
+
+ // Poll the stack until the resolver answers or we time out.
+ let start = crate::arch::rdtsc_ns();
+ const TIMEOUT_NS: u64 = 5_000_000_000;
+ loop {
+ {
+ let mut dev = VirtioNetDev;
+ s.iface.poll(now(), &mut dev, &mut s.sockets);
+ }
+ let sock = s.sockets.get_mut::(dns_h);
+ match sock.get_query_result(handle) {
+ Ok(addrs) => {
+ for a in addrs.iter() {
+ if let IpAddress::Ipv4(v4) = a {
+ return Ok(u32::from_be_bytes(v4.octets()));
+ }
+ }
+ return Err(-2); // resolved but no A record
+ }
+ Err(GetQueryResultError::Pending) => {}
+ Err(_) => return Err(-2), // failed
+ }
+ if crate::arch::rdtsc_ns().wrapping_sub(start) > TIMEOUT_NS {
+ s.sockets.get_mut::(dns_h).cancel_query(handle);
+ return Err(-110); // ETIMEDOUT
+ }
+ }
+}
+
/// Query current IP address (big-endian u32).
pub fn current_ip_be() -> u32 {
with_stack(|s| {
@@ -396,6 +452,7 @@ pub fn dhcp_discover() -> u32 {
if let Some(s) = stack.as_mut() {
let mut dev = VirtioNetDev;
s.iface.poll(now(), &mut dev, &mut s.sockets);
+ let mut dhcp_dns: Option> = None;
if let Some(h) = s.dhcp_handle {
let sock = s.sockets.get_mut::(h);
if let Some(event) = sock.poll() {
@@ -413,6 +470,13 @@ pub fn dhcp_discover() -> u32 {
s.iface.routes_mut().add_default_ipv4_route(gw).ok();
}
s.ip = ip;
+ if !cfg.dns_servers.is_empty() {
+ let mut v: Vec = Vec::new();
+ for a in cfg.dns_servers.iter() {
+ v.push(IpAddress::Ipv4(*a));
+ }
+ dhcp_dns = Some(v);
+ }
}
DhcpEvent::Deconfigured => {
log::info!("[dhcp] deconfigured");
@@ -420,6 +484,12 @@ pub fn dhcp_discover() -> u32 {
}
}
}
+ // Point the resolver at the network's own DNS servers (the DHCP
+ // socket borrow has ended, so s.sockets is free again).
+ if let (Some(dns_h), Some(servers)) = (s.dns_handle, dhcp_dns) {
+ s.sockets.get_mut::(dns_h).update_servers(&servers);
+ log::info!("[dns] {} resolver(s) from DHCP", servers.len());
+ }
}
}
if let Some(ip) = got_ip { return ip; }
diff --git a/kernel/src/process/mod.rs b/kernel/src/process/mod.rs
index 370443f..e1e2531 100644
--- a/kernel/src/process/mod.rs
+++ b/kernel/src/process/mod.rs
@@ -25,7 +25,7 @@ pub mod elf;
pub mod posix_trampolines;
use alloc::alloc::{alloc, dealloc, Layout};
-use core::sync::atomic::{AtomicU32, Ordering};
+use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use spin::Mutex;
use crate::mm::{frame_allocator, paging};
@@ -175,6 +175,20 @@ pub struct Process {
pub user_stack_size: u64,
/// CPU core index that currently executes this process context (None if idle/blocked).
pub current_cpu: Option,
+ /// SMP/preemption safety (Redox pattern): false while this thread is inside a
+ /// critical region that must not be preempted or migrated to another core
+ /// (e.g. holding a userspace mutex/futex). The timer-preempt path and the
+ /// cross-core scheduler skip a non-preemptable thread instead of switching it
+ /// away — preventing the mid-critical-section corruption that broke preempted
+ /// Dart engine threads. Defaults true. See [[redox-blueprint]].
+ pub is_preemptable: bool,
+ /// SMP: HOME CPU core for this thread's engine (process group). The scheduler
+ /// runs a thread ONLY on its home core, so a whole Flutter engine stays on ONE
+ /// core (cooperative hand-off + per-CPU scratch never split). Different engines
+ /// get different home cores → parallel. 0 = BSP (default; single-core path).
+ /// Threads inherit their creator's home_cpu; an app host is assigned in
+ /// app_registry::launch. Only consulted when CPU_COUNT > 1.
+ pub home_cpu: u32,
/// aarch64 only: full EL0 trap-frame snapshot (x0..x30, SP_EL0, ELR, SPSR,
/// ESR) captured when this thread is timer-preempted at an arbitrary user
/// instruction. The shared `UserRegs` carries only the x86-named subset, so
@@ -271,6 +285,8 @@ impl Process {
user_stack_base: 0,
user_stack_size: 0,
current_cpu: None,
+ is_preemptable: true,
+ home_cpu: 0,
#[cfg(target_arch = "aarch64")]
arch_trapframe: [0u64; 102],
#[cfg(target_arch = "aarch64")]
@@ -393,6 +409,24 @@ pub fn aarch64_take_fp(pid: u32) -> Option<[u64; 66]> {
Some(out)
}
+/// aarch64 ISR-safe (try_lock) check: does `pid` have a VALID saved FP image?
+/// The home-gated timer-ISR wake-assist resume uses this to NEVER re-enter a thread
+/// that lacks a valid FP — resuming with no image zeroes v0–v31 incl. AAPCS64
+/// callee-saved v8–v15, corrupting an already-run engine thread (the EC=0x24 SMP
+/// data abort). When the FP isn't valid the resume is skipped (the thread is left
+/// Running for the cooperative path / a later tick), trading a possible extra stall
+/// for never shipping corrupted FP state. Returns false on lock contention (skip).
+#[cfg(target_arch = "aarch64")]
+pub fn aarch64_fp_valid(pid: u32) -> bool {
+ match PTABLE_LOCK.try_lock() {
+ Some(_g) => {
+ let p = unsafe { &PTABLE[idx_of(pid)] };
+ p.pid == pid && p.arch_fp_valid
+ }
+ None => false,
+ }
+}
+
/// aarch64: read the callee-saved x24–x28 captured for `pid` (used to populate
/// EnterUserRegs so the cooperative/re-exec resume restores them).
#[cfg(target_arch = "aarch64")]
@@ -541,6 +575,7 @@ impl PTableLock {
PTABLE_LOCK_RECURSION[my_cpu as usize] = 1;
PTABLE_LOCK_GUARD = Some(core::mem::transmute(g));
}
+ preempt_disable_cpu(my_cpu); // non-preemptable while holding PTABLE_LOCK
PTableGuard { is_outer: true }
}
}
@@ -559,6 +594,7 @@ impl PTableLock {
PTABLE_LOCK_RECURSION[my_cpu as usize] = 1;
PTABLE_LOCK_GUARD = Some(core::mem::transmute(g));
}
+ preempt_disable_cpu(my_cpu); // non-preemptable while holding PTABLE_LOCK
Some(PTableGuard { is_outer: true })
} else {
None
@@ -576,6 +612,7 @@ impl Drop for PTableGuard {
PTABLE_LOCK.holder.store(0xFFFF_FFFF, Ordering::Release);
PTABLE_LOCK_GUARD = None;
}
+ preempt_enable_cpu(my_cpu); // preemptable again once PTABLE_LOCK is released
} else {
let my_cpu = ptable_cpu_idx();
unsafe {
@@ -592,6 +629,85 @@ static mut PTABLE_LOCK_RECURSION: [u32; 64] = [0; 64];
pub(crate) static PTABLE_LOCK: PTableLock = PTableLock::new();
+// ── SMP M1: per-CPU preempt-disable + non-preemptable bail ───────────────────
+//
+// Redox-blueprint pattern #2 ("the key to safe preemption"): never switch AWAY
+// from a CPU that is in a critical section. Here the critical section is "this
+// CPU holds PTABLE_LOCK" — auto-tracked by incrementing this CPU's preempt depth
+// on the OUTER PTABLE_LOCK acquire and decrementing it on the outer release. The
+// timer-preempt path checks `preempt_disabled()` and bails when set.
+//
+// Why this matters even single-core (x86): the timer ISR's `timer_preempt_switch_
+// try` does a *recursive* PTABLE_LOCK `try_lock`, which SUCCEEDS when the
+// interrupted thread already holds the lock — it could then switch threads while
+// the lock is logically held, leaving `holder` pinned to this CPU and wedging
+// every later PTABLE_LOCK user. Bailing when preempt-disabled closes that hole.
+// One slot per CPU; only the owning CPU touches its slot (+ its own ISR reads it).
+pub static PREEMPT_DEPTH: [AtomicU32; crate::arch::smp::MAX_CPUS] =
+ [const { AtomicU32::new(0) }; crate::arch::smp::MAX_CPUS];
+
+#[inline(always)]
+fn preempt_disable_cpu(cpu: u32) {
+ if (cpu as usize) < crate::arch::smp::MAX_CPUS {
+ PREEMPT_DEPTH[cpu as usize].fetch_add(1, Ordering::AcqRel);
+ }
+}
+#[inline(always)]
+fn preempt_enable_cpu(cpu: u32) {
+ if (cpu as usize) < crate::arch::smp::MAX_CPUS {
+ // Saturating: never wrap below zero on an unbalanced release.
+ let slot = &PREEMPT_DEPTH[cpu as usize];
+ let mut cur = slot.load(Ordering::Acquire);
+ while cur > 0 {
+ match slot.compare_exchange_weak(cur, cur - 1, Ordering::AcqRel, Ordering::Acquire) {
+ Ok(_) => break,
+ Err(v) => cur = v,
+ }
+ }
+ }
+}
+
+/// True if THIS CPU is currently non-preemptable (in a PTABLE_LOCK critical
+/// section, or another future preempt-disabled region). The timer-preempt path
+/// must not switch away when this is set.
+#[inline]
+pub fn preempt_disabled() -> bool {
+ let c = ptable_cpu_idx() as usize;
+ c < crate::arch::smp::MAX_CPUS && PREEMPT_DEPTH[c].load(Ordering::Acquire) > 0
+}
+
+/// Disable preemption on the CURRENT cpu and return that cpu index. The timer-
+/// preempt path bails while PREEMPT_DEPTH>0, so the caller's thread cannot be
+/// switched away or migrated — WITHOUT masking IRQs (timer ticks still fire, so a
+/// frame pump on this core keeps running). Pair with `preempt_enable(cpu)` using
+/// the RETURNED cpu so the dec lands on the same slot even if a migration somehow
+/// occurred in the tiny pre-disable window. Used by the page-table critical section
+/// to keep its per-CPU re-entrancy depth sound on x86 (no IRQ mask there).
+#[inline]
+pub fn preempt_disable() -> u32 {
+ // Briefly mask IRQs so reading the current cpu and bumping its PREEMPT_DEPTH is
+ // ATOMIC — otherwise a timer preemption between the two could migrate the thread
+ // and we'd disable preemption on the WRONG cpu (the thread then runs preemptible
+ // on its new cpu, and a page-table section's per-CPU depth gets stranded on the
+ // old cpu → cross-core lock bypass → page-table corruption, observed ~1/6). Once
+ // PREEMPT_DEPTH is bumped, preemption is disabled, so after we restore IRQs the
+ // thread can no longer migrate; the cpu stays valid for the whole section.
+ let irq = crate::arch::interrupts_save_and_disable();
+ let cpu = ptable_cpu_idx();
+ preempt_disable_cpu(cpu);
+ crate::arch::interrupts_restore(irq);
+ cpu
+}
+#[inline]
+pub fn preempt_enable(cpu: u32) {
+ preempt_enable_cpu(cpu);
+}
+
+/// Global context-switch serialization lock (Redox-blueprint #2). Reserved for the
+/// M4 two-layer `switch_to`; declared now so the foundation is in one place.
+#[allow(dead_code)]
+pub static CONTEXT_SWITCH_LOCK: AtomicBool = AtomicBool::new(false);
+
/// Bitmask of processes with a pending wake-up to be processed lock-free.
pub static PENDING_WAKES: [AtomicU32; 8] = [
AtomicU32::new(0),
@@ -795,8 +911,30 @@ pub fn spawn_with_bootstrap(
p.caps = if bootstrap.rdi == crate::app_registry::HOST_MODE_SHELL {
crate::security::ALL_CAPS
} else {
- crate::security::NO_CAPS
+ // Apps get NET so they can use the userspace TCP/DNS socket API
+ // (the abstraction layer). Everything else stays gated. The security
+ // phase will replace this blanket grant with a per-app declared/
+ // prompted NET permission rather than granting every app by default.
+ crate::security::Capabilities::NET
+ };
+ // SMP home-core assignment — done HERE, atomically before the process
+ // becomes Running, so there is no window where core 0 could schedule a
+ // freshly-spawned app host (home_cpu still defaulting to 0) and leave a
+ // stale `current_cpu` claim that the real home core then can't reclaim.
+ // An app host (HOST_MODE_APP) is pinned to a dedicated application core;
+ // the shell and everything else stays on the BSP (0). On single-core
+ // boots pick_home_cpu_for_app returns 0, so this is a no-op there.
+ p.home_cpu = if bootstrap.rdi == crate::app_registry::HOST_MODE_APP {
+ pick_home_cpu_for_app()
+ } else {
+ 0
};
+ // Wake the dormant application core: it idles (wfi) until it has home-pinned
+ // work. Flag it now (atomic, no lock); the reschedule IPI at the end of this
+ // fn then kicks it out of wfi to engage scheduling.
+ if p.home_cpu != 0 {
+ mark_cpu_has_work(p.home_cpu);
+ }
p.syscall_stack_base = sys_stack_base;
p.syscall_stack_top = sys_stack_top;
p.xstate = XStateBuf::default();
@@ -827,6 +965,13 @@ pub fn spawn_with_bootstrap(
name, pid, entry, bootstrap.rdi, bootstrap.rsi, bootstrap.rdx, bootstrap.parent_pid,
caps_of(pid)
);
+ // SMP: the new process is born Running and may be HOME-PINNED to another core
+ // (an app host → an AP). The spawning core (the shell, on the BSP) must poke the
+ // other cores so the home core wakes from its idle `hlt` and picks the newborn
+ // up immediately, instead of waiting for its next timer tick — which a busy/
+ // starved sibling core (notably under TCG SMP) may delay indefinitely, leaving
+ // the freshly-launched app stuck unscheduled (the "app never ran" freeze).
+ crate::arch::smp::broadcast_resched_ipi();
Ok(pid)
}
@@ -982,6 +1127,83 @@ pub fn kill(pid: u32) -> Result<(), &'static str> {
Ok(())
}
+/// Mark a thread (non-)preemptable (Redox pattern). Set false while it holds a
+/// userspace lock so the timer-preempt path + cross-core scheduler won't switch
+/// it away / migrate it mid-critical-section. ISR/lock-free-safe via try_lock.
+pub fn set_preemptable(pid: u32, value: bool) {
+ if let Some(_g) = PTABLE_LOCK.try_lock() {
+ let p = unsafe { &mut PTABLE[idx_of(pid)] };
+ if p.pid == pid {
+ p.is_preemptable = value;
+ }
+ }
+}
+
+/// SMP: pin `pid` (and its engine, since threads inherit it) to a home CPU core.
+/// The SMP home core a process (and its thread group) is pinned to. 0 = BSP.
+pub fn home_cpu_of(pid: u32) -> u32 {
+ let _g = PTABLE_LOCK.lock();
+ let p = unsafe { &PTABLE[idx_of(pid)] };
+ if p.pid == pid { p.home_cpu } else { 0 }
+}
+
+/// Per-application-core "has home-pinned work" flag. A secondary core (AP) stays
+/// fully dormant — `wfi`, no run-queue polling, its timer ISR does nothing — until
+/// an app is actually pinned to it. Apps only launch well after boot, so before the
+/// first launch an AP has nothing to run, and engaging it early makes its timer-ISR
+/// work contend with the BSP's timing-sensitive shell first-frame bring-up (observed
+/// on aarch64-HVF: engaging at flutter_init_ready stalled the shell at present=1).
+/// Set (once) when an app host is assigned to a core; the spawn's reschedule IPI then
+/// kicks that core out of `wfi` to engage. Never cleared (after the first launch the
+/// AP schedules normally — boot is long over).
+pub static CPU_HAS_HOME_WORK: [core::sync::atomic::AtomicBool; crate::arch::smp::MAX_CPUS] =
+ [const { core::sync::atomic::AtomicBool::new(false) }; crate::arch::smp::MAX_CPUS];
+
+#[inline]
+pub fn mark_cpu_has_work(cpu: u32) {
+ if (cpu as usize) < crate::arch::smp::MAX_CPUS {
+ CPU_HAS_HOME_WORK[cpu as usize].store(true, Ordering::Release);
+ }
+}
+#[inline]
+pub fn cpu_has_home_work(cpu: u32) -> bool {
+ (cpu as usize) < crate::arch::smp::MAX_CPUS
+ && CPU_HAS_HOME_WORK[cpu as usize].load(Ordering::Acquire)
+}
+
+/// ISR-safe (try-lock) read of a process's home core. Returns None if the table
+/// lock is momentarily contended (the caller simply skips this tick). Used by the
+/// per-core timer-ISR wake-assist so a core only ever touches its own home threads.
+pub fn home_cpu_of_try(pid: u32) -> Option {
+ let _g = PTABLE_LOCK.try_lock()?;
+ let p = unsafe { &PTABLE[idx_of(pid)] };
+ Some(if p.pid == pid { p.home_cpu } else { 0 })
+}
+
+/// Round-robin a freshly-launched engine onto a core OTHER than the BSP (0, the
+/// shell's core) when >1 core is online; else the BSP. First app → core 1.
+pub fn pick_home_cpu_for_app() -> u32 {
+ let n = crate::arch::smp::CPU_COUNT.load(Ordering::Acquire);
+ if n <= 1 {
+ return 0;
+ }
+ static NEXT_APP_CPU: AtomicU32 = AtomicU32::new(0);
+ let k = NEXT_APP_CPU.fetch_add(1, Ordering::Relaxed);
+ 1 + (k % (n - 1))
+}
+
+/// Is `pid` currently preemptable? Defaults true for unknown/dead pids so callers
+/// never wedge on a stale id.
+pub fn is_preemptable(pid: u32) -> bool {
+ if let Some(_g) = PTABLE_LOCK.try_lock() {
+ let p = unsafe { &PTABLE[idx_of(pid)] };
+ if p.pid == pid {
+ return p.is_preemptable;
+ }
+ }
+ true
+}
+
/// Set currently active userspace PID (called by exec/scheduler glue).
pub fn set_current_pid(pid: u32) {
crate::arch::smp::this_cpu().current_pid.store(pid, Ordering::Release);
@@ -1131,6 +1353,15 @@ pub fn try_claim_cpu_for_try(pid: u32, my_cpu: u32) -> bool {
let idx = idx_of(pid);
let p = unsafe { &mut PTABLE[idx] };
if p.pid == pid && p.state == ProcState::Running {
+ // SMP home-core: only the target's HOME core may claim it. This is what
+ // lets the per-core timer-ISR wake-assist run on every core safely — the
+ // BSP wakes only home_cpu==0 threads (the shell), an AP wakes only its
+ // own home-pinned threads (a launched app). Two cores never claim the
+ // same user thread. On single-core home_cpu is always 0 == my_cpu.
+ let smp = crate::arch::smp::CPU_COUNT.load(Ordering::Acquire) > 1;
+ if smp && p.home_cpu != my_cpu {
+ return false;
+ }
if p.current_cpu.is_none() || p.current_cpu == Some(my_cpu) {
p.current_cpu = Some(my_cpu);
return true;
@@ -1272,6 +1503,17 @@ pub fn next_runnable_pid_locked(current: u32, my_cpu: u32) -> Option {
}
}
+ // SMP: with >1 core online, an app host is pinned to its own APPLICATION core
+ // (home_cpu, set in spawn_with_bootstrap), so the launched engine runs with its
+ // OWN per-CPU GPR scratch — never sharing the cooperative context that corrupts
+ // a 2nd engine on a single core. We KEEP foreground-exclusive (the shell is
+ // suspended while an app is foreground): letting the shell run concurrently with
+ // the app trips Dart's isolate-confinement check ("Isolate main is owned by os
+ // thread X, failed to schedule from os thread Y" → sys_abort). Exclusive + the
+ // `home_cpu == my_cpu` filter together give: the app runs ALONE on its core, the
+ // shell stays parked on the BSP — separate scratch, no concurrent shell engine.
+ let smp = crate::arch::smp::CPU_COUNT.load(Ordering::Acquire) > 1;
+
let focus = fg;
let input_target = if focus != 0 { focus } else { 1 };
// Only honour the input shortcut when the target belongs to the foreground
@@ -1280,7 +1522,9 @@ pub fn next_runnable_pid_locked(current: u32, my_cpu: u32) -> Option {
if input_ok && input_target != 0 && crate::wm::input_pending_for(input_target) > 0 {
if current != input_target {
let target = unsafe { &mut PTABLE[idx_of(input_target)] };
- if target.pid == input_target && target.state == ProcState::Running {
+ if target.pid == input_target && target.state == ProcState::Running
+ && (!smp || target.home_cpu == my_cpu)
+ {
if target.current_cpu.is_none() || target.current_cpu == Some(my_cpu) {
target.current_cpu = Some(my_cpu);
return Some(input_target);
@@ -1294,7 +1538,9 @@ pub fn next_runnable_pid_locked(current: u32, my_cpu: u32) -> Option {
// the SHELL's own baton triggers this (the app's baton runs the app instead).
if !exclusive && current != 1 && crate::wm::embedder_baton_due(1) {
let embedder = unsafe { &mut PTABLE[idx_of(1)] };
- if embedder.pid == 1 && embedder.state == ProcState::Running {
+ if embedder.pid == 1 && embedder.state == ProcState::Running
+ && (!smp || embedder.home_cpu == my_cpu)
+ {
if embedder.current_cpu.is_none() || embedder.current_cpu == Some(my_cpu) {
embedder.current_cpu = Some(my_cpu);
return Some(1);
@@ -1312,6 +1558,9 @@ pub fn next_runnable_pid_locked(current: u32, my_cpu: u32) -> Option {
let idx = (start + off) % MAX_PROCS;
let p = unsafe { &mut PTABLE[idx] };
if p.pid != 0 && p.state == ProcState::Running {
+ if smp && p.home_cpu != my_cpu {
+ continue; // SMP: this thread's engine is pinned to another core
+ }
if exclusive && get_group_leader_locked(p.pid) != fg_group {
continue; // not in the foreground app's group — skip while it runs
}
@@ -1328,6 +1577,7 @@ pub fn next_runnable_pid_locked(current: u32, my_cpu: u32) -> Option {
if res.is_none() && current != 0 {
let c = unsafe { &mut PTABLE[idx_of(current)] };
if c.pid == current && c.state == ProcState::Running
+ && (!smp || c.home_cpu == my_cpu)
&& (!exclusive || get_group_leader_locked(current) == fg_group)
{
if c.current_cpu.is_none() || c.current_cpu == Some(my_cpu) {
@@ -1372,6 +1622,20 @@ pub fn get_user_context(pid: u32) -> Option {
})
}
+/// Page-table root (PML4 on x86, TTBR0 on aarch64) of any LIVE process — including
+/// Blocked ones (unlike `get_user_context`, which requires Running). Used by the
+/// futex to compute a waiter's PHYSICAL futex address for SMP-correct wake scoping
+/// (a Blocked futex waiter must still be locatable). Returns None for Dead slots.
+pub fn pml4_phys_of(pid: u32) -> Option {
+ let _g = PTABLE_LOCK.lock();
+ let p = unsafe { &PTABLE[idx_of(pid)] };
+ if p.pid == pid && p.state != ProcState::Dead {
+ Some(p.pml4_phys)
+ } else {
+ None
+ }
+}
+
pub fn current_syscall_stack_top() -> Option {
let pid = current_pid();
if pid == 0 {
@@ -1427,14 +1691,14 @@ pub fn spawn_thread(
arg: u64,
stack_size: usize,
) -> Result {
- let (parent_pml4_phys, owning_pid) = {
+ let (parent_pml4_phys, owning_pid, parent_home_cpu) = {
let _g = PTABLE_LOCK.lock();
let p = unsafe { &PTABLE[idx_of(parent_pid)] };
if p.pid != parent_pid || p.state == ProcState::Dead {
log::error!("[spawn_thread] parent not found pid={}", parent_pid);
return Err("parent not found");
}
- (p.pml4_phys, get_group_leader_locked(parent_pid))
+ (p.pml4_phys, get_group_leader_locked(parent_pid), p.home_cpu)
};
let tid = match alloc_pid() {
@@ -1576,6 +1840,7 @@ pub fn spawn_thread(
let p = unsafe { &mut PTABLE[idx_of(tid)] };
p.pid = tid;
p.pml4_phys = parent_pml4_phys; // shared with parent
+ p.home_cpu = parent_home_cpu; // SMP: thread stays on its engine's core
p.regs = regs;
p.state = ProcState::Running;
p.exit_code = 0;
@@ -1707,30 +1972,64 @@ pub fn mark_init_launched() {
PENDING_INIT_PID.store(0, Ordering::Release);
}
-pub fn enter_user_by_pid_noreturn(pid: u32) -> ! {
+pub fn enter_user_by_pid_noreturn(req_pid: u32) -> ! {
let my_cpu = crate::arch::smp::this_cpu().cpu_id;
let old_pid = current_pid();
+ // ── SMP claim resolution ────────────────────────────────────────────────
+ // Release our previous claim, then claim a pid THIS core may run. If the
+ // requested pid is owned by ANOTHER core (a cooperative hand-off targeted a
+ // thread the other core is already running), re-pick a runnable thread for
+ // this core instead of panicking — this is the SMP-correct back-off that
+ // makes every hand-off call site safe without per-site auditing. On a single
+ // core this always resolves to `req_pid` (claim is free), so behaviour there
+ // is unchanged.
+ {
+ let _g = PTABLE_LOCK.lock();
+ // STICKY CPU AFFINITY: do NOT release `old_pid`'s claim here. Releasing it on
+ // every hand-off makes every yielded thread instantly pickable by the other
+ // core, so both cores chase the same hot threads and livelock (rendering
+ // stalls at ~1 frame under smp=2). Keeping the claim sticky pins each thread
+ // to the core that first ran it; unclaimed threads distribute across cores as
+ // they become runnable, and a blocked thread is reclaimed by its own core.
+ // (current_cpu is reset to None when the PCB is freed/reused on exit.)
+ if my_cpu == 0 {
+ crate::arch::smp::set_sched_ready();
+ }
+ }
+ let pid = loop {
+ let got = {
+ let _g = PTABLE_LOCK.lock();
+ let p = unsafe { &mut PTABLE[idx_of(req_pid)] };
+ if p.pid == req_pid
+ && (p.current_cpu.is_none() || p.current_cpu == Some(my_cpu))
+ {
+ p.current_cpu = Some(my_cpu);
+ true
+ } else {
+ false
+ }
+ };
+ if got {
+ break req_pid;
+ }
+ // req_pid is owned by another core — pick a different runnable thread for
+ // this core (next_runnable_pid claims it for my_cpu). If none, idle + retry.
+ if let Some(other) = next_runnable_pid(current_pid()) {
+ if other != req_pid {
+ break other;
+ }
+ }
+ unsafe { crate::arch::enable_and_halt() };
+ };
+
let (pml4_phys, rip, rsp, rflags, rax, rdi, rsi, rdx,
rbx, rbp, r8, r9, r10, r11, r12, r13, r14, r15, rcx,
syscall_stack_top, fs_base, errno_to_deliver, preempted_by_timer) = {
let _g = PTABLE_LOCK.lock();
- if old_pid != 0 && old_pid != pid {
- let old_p = unsafe { &mut PTABLE[idx_of(old_pid)] };
- if old_p.pid == old_pid && old_p.current_cpu == Some(my_cpu) {
- old_p.current_cpu = None;
- }
- }
let p = unsafe { &mut PTABLE[idx_of(pid)] };
assert_eq!(p.pid, pid, "[Process] enter_user_by_pid_noreturn: stale PID");
- if let Some(other_cpu) = p.current_cpu {
- if other_cpu != my_cpu {
- panic!(
- "[Process] enter_user_by_pid_noreturn: PID {} is already running on CPU {}, but CPU {} is trying to enter it!",
- pid, other_cpu, my_cpu
- );
- }
- }
+ // Claim already established above; ensure it's ours (re-pick set it too).
p.current_cpu = Some(my_cpu);
let e = p.errno_to_deliver;
p.errno_to_deliver = 0; // consume it
@@ -2057,6 +2356,11 @@ fn user_launch_task() {
///
/// Returns `None` if there is no other runnable thread (no switch occurs).
pub fn timer_preempt_switch(cur_pid: u32, cur_regs: &UserRegs) -> Option<(u32, UserRegs, u64)> {
+ // SMP M1 (non-preemptable bail): never switch away while this CPU holds
+ // PTABLE_LOCK — doing so would leave the lock pinned to this CPU and wedge it.
+ if preempt_disabled() {
+ return None;
+ }
let my_cpu = crate::arch::smp::current_cpu_id();
let fs = crate::arch::cpu::get_fs_base();
@@ -2122,6 +2426,12 @@ pub fn timer_preempt_switch(cur_pid: u32, cur_regs: &UserRegs) -> Option<(u32, U
/// Try-lock variant of `timer_preempt_switch` to avoid ISR deadlocks.
pub fn timer_preempt_switch_try(cur_pid: u32, cur_regs: &UserRegs) -> Option<(u32, UserRegs, u64)> {
+ // SMP M1 (non-preemptable bail): if the interrupted thread holds PTABLE_LOCK,
+ // a recursive try_lock below would SUCCEED and let us switch threads mid-
+ // critical-section, pinning the lock to this CPU forever. Bail instead.
+ if preempt_disabled() {
+ return None;
+ }
let my_cpu = crate::arch::smp::current_cpu_id();
let fs = crate::arch::cpu::get_fs_base();
diff --git a/kernel/src/sched/mod.rs b/kernel/src/sched/mod.rs
index ccfb8c7..dad5d81 100644
--- a/kernel/src/sched/mod.rs
+++ b/kernel/src/sched/mod.rs
@@ -81,14 +81,44 @@ pub fn init() {
MAX_TASKS, STACK_SIZE / 1024, SCHED_SLICE);
}
-/// AP parking stub.
+/// AP scheduler entry — each application core runs HOME-PINNED user threads.
///
-/// The current scheduler is a single global queue/state machine and is not yet
-/// safe for concurrent scheduling decisions from multiple CPUs. Park APs with
-/// interrupts disabled so only the BSP drives preemption.
+/// SMP design: an engine (process group) is pinned to a `home_cpu` (see
+/// `process::pick_home_cpu_for_app`). `next_runnable_pid` only returns a thread
+/// whose `home_cpu == my_cpu`, so the BSP and each AP run disjoint engines on
+/// disjoint per-CPU GPR scratch — no shared cooperative context to corrupt.
+/// The AP loops: pick a home-pinned runnable thread, enter it; if none, halt
+/// until the next interrupt (timer tick / reschedule IPI) re-checks the queue.
pub fn ap_start(cpu_idx: u32) -> ! {
- log::info!("[Sched] AP cpu={} ready, parked (BSP-only scheduler)", cpu_idx);
- crate::arch::halt_forever();
+ // Wait until the BSP has claimed the init thread and opened SMP scheduling,
+ // so we never race the BSP to claim pid 1 during early bring-up.
+ while !crate::arch::smp::sched_ready() {
+ core::hint::spin_loop();
+ }
+ // Stay fully PARKED (idle hlt, no run-queue polling) until the shell's engine
+ // is up. Apps only launch after that, so the AP has no home-pinned work before
+ // then — and polling next_runnable_pid (PTABLE_LOCK) every timer tick contends
+ // with the BSP during the timing-sensitive shell first-frame bring-up, which
+ // under TCG SMP can stall the shell at boot (present=0). An IPI/timer wakes the
+ // hlt; we just re-check the gate until the shell is ready, then join scheduling.
+ while !crate::wm::flutter_init_ready() {
+ crate::arch::enable_and_halt();
+ }
+ log::error!("[Sched] AP cpu={} entering home-pinned scheduler loop", cpu_idx);
+
+ loop {
+ // current=0: this core has no resident process between entries. The
+ // home_cpu filter inside next_runnable_pid (keyed off this_cpu().cpu_id)
+ // restricts the pick to engines pinned to THIS core.
+ if let Some(pid) = crate::process::next_runnable_pid(0) {
+ // enter_user_by_pid_noreturn never returns to the caller; on the next
+ // syscall/preemption the AP re-enters this loop via its idle path.
+ crate::process::enter_user_by_pid_noreturn(pid);
+ }
+ // Nothing home-pinned to this core right now — wait for an interrupt
+ // (timer tick or reschedule IPI) then re-scan.
+ crate::arch::enable_and_halt();
+ }
}
/// Register the **current** execution context (BSP idle loop) as the idle task.
diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs
index 8d1e7a7..978f994 100644
--- a/kernel/src/syscall/dispatch.rs
+++ b/kernel/src/syscall/dispatch.rs
@@ -50,16 +50,6 @@ pub extern "C" fn dispatch_fast(number: u64, arg0: u64, arg1: u64, arg2: u64, ar
let user_rip = crate::arch::syscall::user_rip();
record_syscall_trace(number, arg0, arg1, arg2, user_rip);
- // Trace the first 32 syscalls made by pid=7 so we can see exactly which
- // call triggers "Poll failed:" and from where.
- {
- static PID7_TRACE: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
- if crate::process::current_pid() == 7 {
- let n = PID7_TRACE.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
- let _ = n;
- }
- }
-
// Post-pid2-exit trace window: log every syscall (any pid) for the first
// POSTEXIT_TRACE_LIMIT calls after PID-2's exit, so we can see exactly
// what pid=1 does once it resumes from pthread_cond_wait.
@@ -501,6 +491,8 @@ pub extern "C" fn dispatch_fast(number: u64, arg0: u64, arg1: u64, arg2: u64, ar
0x38A => sys_tcp_read(arg0, arg1, arg2),
0x38B => sys_tcp_close(arg0),
0x38C => sys_dhcp_discover(),
+ eabi::SYS_TCP_STATUS => sys_tcp_status(arg0),
+ eabi::SYS_DNS_RESOLVE => sys_dns_resolve(arg0, arg1),
// Phase 51 — ext2 read-only filesystem
0x38D => sys_ext2_mount(),
@@ -822,6 +814,7 @@ fn required_cap(number: u64) -> Option {
|| n == eabi::SYS_NET_SEND
|| n == eabi::SYS_NET_RECV => Some(Capabilities::NET),
0x388 | 0x389 | 0x38A | 0x38B | 0x38C => Some(Capabilities::NET),
+ n if n == eabi::SYS_TCP_STATUS || n == eabi::SYS_DNS_RESOLVE => Some(Capabilities::NET),
_ => None,
}
}
diff --git a/kernel/src/syscall/handlers/engine.rs b/kernel/src/syscall/handlers/engine.rs
index 4b23d26..7be0109 100644
--- a/kernel/src/syscall/handlers/engine.rs
+++ b/kernel/src/syscall/handlers/engine.rs
@@ -1325,6 +1325,36 @@ pub(crate) fn sys_tcp_close(fd: u64) -> i64 {
}
}
+/// Poll an outbound TCP connection's state. arg0 = fd.
+/// Returns 1 once the handshake has completed (the socket may send), 0 while
+/// still connecting, or a negative errno (e.g. -111 ECONNREFUSED) if the
+/// connection was refused/reset. This is the correct way to wait for connect:
+/// a zero-byte read probe can't tell "connected, peer silent" from "connecting".
+pub(crate) fn sys_tcp_status(fd: u64) -> i64 {
+ match crate::net::tcp::tcp_is_established(fd as usize) {
+ Ok(true) => 1,
+ Ok(false) => 0,
+ Err(e) => e,
+ }
+}
+
+/// Resolve a hostname to an IPv4 address. arg0=name_ptr, arg1=name_len.
+/// Returns the address as a big-endian u32 (≥0), or a negative errno.
+pub(crate) fn sys_dns_resolve(name_ptr: u64, name_len: u64) -> i64 {
+ let bytes = match unsafe { read_user_bytes(name_ptr, (name_len as usize).min(255)) } {
+ Some(b) => b,
+ None => return -14, // EFAULT
+ };
+ let name = match core::str::from_utf8(bytes) {
+ Ok(s) => s,
+ Err(_) => return -22, // EINVAL
+ };
+ match crate::net::tcp::dns_resolve(name) {
+ Ok(ip) => ip as i64,
+ Err(e) => e,
+ }
+}
+
/// Trigger DHCP DISCOVER. Returns assigned IP as BE u32, or 0 on failure.
pub(crate) fn sys_dhcp_discover() -> i64 {
crate::net::tcp::dhcp_discover() as i64
diff --git a/kernel/src/syscall/handlers/futex.rs b/kernel/src/syscall/handlers/futex.rs
index fb86893..f2be028 100644
--- a/kernel/src/syscall/handlers/futex.rs
+++ b/kernel/src/syscall/handlers/futex.rs
@@ -324,7 +324,36 @@ fn futex_pending_take(addr: u64) -> bool {
false
}
+/// SMP M2: the PHYSICAL address backing a process's futex word at virtual `addr`.
+/// Two contexts share a futex iff this matches — the correct, address-space-
+/// independent identity (Redox blueprint #4). Returns None if the page isn't
+/// mapped in that process (caller then falls back to group-leader scoping).
+fn futex_phys_of(pid: u32, addr: u64) -> Option {
+ let p4 = crate::process::pml4_phys_of(pid)?;
+ let frame = crate::mm::paging::translate_user_page(p4, addr & !0xFFF)?;
+ Some(frame | (addr & 0xFFF))
+}
+
pub(crate) fn futex_wake_waiters(addr: u64, count: u32) -> i64 {
+ // CRITICAL: FUTEX_WAITERS is keyed by the userspace futex/mutex ADDRESS only,
+ // but that address is process-local. The shell (pid 1) and any launched app
+ // (e.g. pid 10) are distinct address spaces that load the same libc / Flutter
+ // engine .so at IDENTICAL virtual addresses, so their internal pthread
+ // mutex/cond addresses COLLIDE in this table. Waking the first `count` waiters
+ // blindly can wake — and remove — the WRONG process's waiter, leaving the
+ // intended one parked forever (the post-app-launch render freeze: a render
+ // thread stuck in pthread_mutex_lock while the unlock woke a sibling-process
+ // waiter instead). Only wake waiters that share the caller's address space.
+ // (Kernel/ISR context, pid 0, has no group and wakes everyone — preserves the
+ // force-wake bring-up path.)
+ let caller = crate::process::current_pid();
+ let caller_grp = if caller == 0 { 0 } else { crate::process::get_group_leader(caller) };
+ // SMP M2: prefer PHYSICAL-address identity over group-leader scoping. The
+ // caller's physical futex address is the authoritative key — two contexts
+ // share the futex iff they map `addr` to the same physical page (correct even
+ // across cores, and across the shell/app shared-VA collision). Kernel/ISR
+ // context (caller==0) still wakes everyone (the bring-up force-wake path).
+ let caller_phys = if caller == 0 { None } else { futex_phys_of(caller, addr) };
let wake_list = {
let mut table = FUTEX_WAITERS.lock();
let Some(waiters) = table.get_mut(&addr) else {
@@ -337,10 +366,26 @@ pub(crate) fn futex_wake_waiters(addr: u64, count: u32) -> i64 {
}
return 0;
};
- let wake_count = waiters.len().min(count as usize);
- let mut woke = Vec::with_capacity(wake_count);
- for _ in 0..wake_count {
- woke.push(waiters.remove(0));
+ let mut woke = Vec::with_capacity((count as usize).min(waiters.len()));
+ let mut i = 0;
+ while i < waiters.len() && woke.len() < count as usize {
+ let w = waiters[i];
+ // Physical-address identity is authoritative when both resolve; fall
+ // back to group-leader scoping only if a translation fails (so we
+ // never DROP a wake the old path would have allowed).
+ let same_space = if caller_grp == 0 {
+ true
+ } else if let (Some(cp), Some(wp)) = (caller_phys, futex_phys_of(w, addr)) {
+ cp == wp
+ } else {
+ crate::process::get_group_leader(w) == caller_grp
+ };
+ if same_space {
+ woke.push(w);
+ waiters.remove(i);
+ } else {
+ i += 1;
+ }
}
if waiters.is_empty() {
table.remove(&addr);
diff --git a/kernel/src/syscall/poll.rs b/kernel/src/syscall/poll.rs
index 1c82b90..271b554 100644
--- a/kernel/src/syscall/poll.rs
+++ b/kernel/src/syscall/poll.rs
@@ -402,7 +402,17 @@ pub fn force_wake_all_task_runners(reason: &str) -> (usize, usize) {
let mut tbl = TIMERFD_TABLE.lock();
for fd in &tfds {
if let Some(t) = tbl.get_mut(fd) {
- t.pending = t.pending.saturating_add(1);
+ // Mark ready WITHOUT accumulating. This is a spurious wake to break
+ // an epoll_wait parking deadlock, NOT a real expiration. The old
+ // `saturating_add(1)` inflated an actively-serviced timerfd's pending
+ // every time force-wake fired (frequent during app bring-up), so the
+ // next timerfd read returned a large BOGUS expiration count — a
+ // prime suspect for corrupting dart:io's timeout processing (the
+ // intermittent app-launch EventHandler #GP). Cap at 1: still reports
+ // EPOLLIN to unpark the waiter, but never piles up fake expirations.
+ if t.pending == 0 {
+ t.pending = 1;
+ }
all_tfds.push(*fd);
}
}
diff --git a/kernel/src/syscall/posix.rs b/kernel/src/syscall/posix.rs
index 7da855c..0bcb147 100644
--- a/kernel/src/syscall/posix.rs
+++ b/kernel/src/syscall/posix.rs
@@ -914,11 +914,19 @@ pub fn sys_pthread_mutex_lock(mutex: u64, sys_nr: u64) -> i64 {
log::warn!("[startup-mutex] after-waiter-add pid={} mutex={:#x}", pid, mutex);
}
- if pid >= 2 && crate::process::get_group_leader(pid) == 1 {
+ // Deadlock breaker for ANY embedder host group (not just the shell, pid 1).
+ // A render thread can contend on a mutex held by a sibling parked in an
+ // infinite epoll_wait on a disarmed timerfd; force-waking the task runners
+ // pokes that timerfd so the holder runs and releases the mutex. This used
+ // to be gated on `group_leader == 1`, so it only ever rescued the SHELL —
+ // every launched APP (group leader = its own host pid, e.g. 10) was skipped
+ // and wedged a few frames after launch. Generalize to the thread's own host.
+ let host = crate::process::get_group_leader(pid);
+ if pid >= 2 && host != pid && host != 0 {
static ENGINE_MUTEX_WAIT_KICK: core::sync::atomic::AtomicU32 =
core::sync::atomic::AtomicU32::new(0);
let kick = ENGINE_MUTEX_WAIT_KICK.fetch_add(1, Ordering::Relaxed);
- crate::process::set_state(1, crate::process::ProcState::Running);
+ crate::process::set_state(host, crate::process::ProcState::Running);
if kick < 32 || (kick & 15) == 0 {
if trace_startup_mutex {
log::warn!("[startup-mutex] before-force-wake pid={} mutex={:#x}", pid, mutex);
@@ -964,7 +972,10 @@ pub fn sys_pthread_mutex_lock(mutex: u64, sys_nr: u64) -> i64 {
let mut mutex_handoff_target = || {
crate::process::next_runnable_pid(pid).or_else(|| {
- if pid >= 2 && crate::process::get_group_leader(pid) == 1 {
+ // Generalized from `group_leader == 1` (shell only) to any embedder
+ // worker thread, so a launched app's render threads also hand off to
+ // their host / siblings instead of stalling.
+ if pid >= 2 && crate::process::get_group_leader(pid) != pid {
super::prefer_embedder_if_baton_due(pid)
.or_else(|| crate::process::next_runnable_sibling_thread(pid))
.or_else(|| super::cooperative_sched_target(pid))
diff --git a/kernel/src/syscall/wait.rs b/kernel/src/syscall/wait.rs
index 8a8baa3..3db30ba 100644
--- a/kernel/src/syscall/wait.rs
+++ b/kernel/src/syscall/wait.rs
@@ -45,8 +45,15 @@ pub(crate) fn futex_waiter_present(addr: u64, pid: u32) -> bool {
}
pub(crate) fn futex_waiter_for(addr: u64, exclude: u32) -> Option {
+ // Only return a waiter in the SAME address space as the caller (`exclude` is
+ // the calling pid). The waiter table is keyed by VA alone, which collides
+ // across processes that load the same .so at the same address — see
+ // futex_wake_waiters for the full rationale.
+ let grp = if exclude == 0 { 0 } else { crate::process::get_group_leader(exclude) };
let table = FUTEX_WAITERS.lock();
table.get(&addr).and_then(|waiters| {
- waiters.iter().copied().find(|&pid| pid != exclude)
+ waiters.iter().copied().find(|&pid| {
+ pid != exclude && (grp == 0 || crate::process::get_group_leader(pid) == grp)
+ })
})
}
diff --git a/kernel/src/wm/mod.rs b/kernel/src/wm/mod.rs
index fe78126..b294c5a 100644
--- a/kernel/src/wm/mod.rs
+++ b/kernel/src/wm/mod.rs
@@ -22,9 +22,16 @@ fn canonical_pid(pid: u32) -> u32 {
}
}
+/// Visibility test between an event's owner and a consumer. BOTH pids MUST be
+/// already canonical (group leaders). This is a RAW comparison that deliberately
+/// does NOT call `canonical_pid` — that would take PTABLE_LOCK, and this runs
+/// inside `with_queue` (WM lock held). Acquiring PTABLE_LOCK under the WM lock
+/// inverts the scheduler's PTABLE→WM order (next_runnable_pid_locked calls
+/// input_pending_for while holding PTABLE_LOCK) → ABBA deadlock under SMP.
+/// Callers canonicalize at the WM-module boundary, before taking the WM lock.
#[inline(always)]
fn owner_visible_to_consumer(owner_pid: u32, consumer_pid: u32) -> bool {
- owner_pid == 0 || owner_pid == canonical_pid(consumer_pid)
+ owner_pid == 0 || owner_pid == consumer_pid
}
struct EventQueue {
@@ -51,7 +58,10 @@ impl EventQueue {
}
fn push(&mut self, mut ev: WmEvent, owner_pid: u32) {
- let owner_pid = canonical_pid(owner_pid);
+ // owner_pid is ALREADY canonical (canonicalized by the caller before the
+ // WM lock was taken). Do NOT call canonical_pid here — it takes PTABLE_LOCK
+ // under the WM lock and deadlocks against the scheduler (see
+ // owner_visible_to_consumer).
ev.seq = self.next_seq;
self.next_seq = self.next_seq.wrapping_add(1).max(1);
@@ -105,7 +115,7 @@ impl EventQueue {
}
fn push_front(&mut self, mut ev: WmEvent, owner_pid: u32) {
- let owner_pid = canonical_pid(owner_pid);
+ // owner_pid is ALREADY canonical (see push() — never take PTABLE_LOCK here).
ev.seq = self.next_seq;
self.next_seq = self.next_seq.wrapping_add(1).max(1);
@@ -524,9 +534,12 @@ pub fn push_event_for(owner_pid: u32, kind: u32, flags: u32, a: u64, b: u64) {
);
});
- // Wake the waiter if they are eligible for this event.
+ // Wake the waiter if they are eligible for this event. We are now OUTSIDE the
+ // WM lock (the with_queue closure closed above), so canonicalizing the raw
+ // waiter pid here is safe — owner_visible_to_consumer itself is raw and must
+ // be fed canonical pids (owner_pid was canonicalized at fn entry).
let waiter = WM_WAITER.load(Ordering::Acquire);
- if waiter != 0 && owner_visible_to_consumer(owner_pid, waiter) {
+ if waiter != 0 && owner_visible_to_consumer(owner_pid, canonical_pid(waiter)) {
WM_WAITER.store(0, Ordering::Release);
crate::process::wake_process(waiter);
}
@@ -636,6 +649,59 @@ static CURSOR_BUTTONS: AtomicU32 = AtomicU32::new(0);
static CURSOR_SEEN: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
static CURSOR_LAST_ACT_NS: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
+// ── Input introspection ──────────────────────────────────────────────────────
+//
+// Lightweight, always-on telemetry: which pointing/key device bound, and how many
+// events each kind has delivered. Cheap (a few relaxed atomics per event) and
+// genuinely useful for answering "is any input device live, and is it sending
+// events?" — the exact question when a board/VM shows no cursor. The optional
+// `input-hud` build feature draws these on-screen (see compositor::input_hud); the
+// counters themselves are unconditional so the data exists even without the HUD.
+
+/// Which input device last bound a pointer/key source.
+pub const INPUT_SRC_NONE: u8 = 0;
+pub const INPUT_SRC_VIRTIO: u8 = 1;
+pub const INPUT_SRC_XHCI: u8 = 2;
+pub const INPUT_SRC_PS2: u8 = 3;
+
+static INPUT_SOURCE: core::sync::atomic::AtomicU8 = core::sync::atomic::AtomicU8::new(INPUT_SRC_NONE);
+static PTR_COUNT: AtomicU32 = AtomicU32::new(0);
+static KEY_COUNT: AtomicU32 = AtomicU32::new(0);
+static SCROLL_COUNT: AtomicU32 = AtomicU32::new(0);
+/// Last key event, packed `(scancode << 1) | pressed`.
+static LAST_KEY: AtomicU32 = AtomicU32::new(0);
+
+/// Record that an input device has bound (called once per device from its driver).
+pub fn set_input_source(src: u8) {
+ INPUT_SOURCE.store(src, Ordering::Relaxed);
+}
+/// The bound input source (`INPUT_SRC_*`), or `INPUT_SRC_NONE` if nothing bound.
+pub fn input_source() -> u8 {
+ INPUT_SOURCE.load(Ordering::Relaxed)
+}
+/// Short name for the bound input source.
+pub fn input_source_name() -> &'static str {
+ match input_source() {
+ INPUT_SRC_VIRTIO => "virtio",
+ INPUT_SRC_XHCI => "xhci",
+ INPUT_SRC_PS2 => "ps2",
+ _ => "none",
+ }
+}
+/// Event counts so far: `(pointer, key, scroll)`.
+pub fn input_counts() -> (u32, u32, u32) {
+ (
+ PTR_COUNT.load(Ordering::Relaxed),
+ KEY_COUNT.load(Ordering::Relaxed),
+ SCROLL_COUNT.load(Ordering::Relaxed),
+ )
+}
+/// Last key event as `(scancode, pressed)`.
+pub fn last_key() -> (u32, bool) {
+ let v = LAST_KEY.load(Ordering::Relaxed);
+ (v >> 1, (v & 1) != 0)
+}
+
/// Current pointer position (absolute, framebuffer pixels).
pub fn cursor_pos() -> (i32, i32) {
(CURSOR_X.load(Ordering::Relaxed), CURSOR_Y.load(Ordering::Relaxed))
@@ -660,6 +726,7 @@ pub fn push_pointer(x: i32, y: i32, buttons: u32) {
CURSOR_BUTTONS.store(buttons, Ordering::Relaxed);
CURSOR_SEEN.store(true, Ordering::Relaxed);
CURSOR_LAST_ACT_NS.store(crate::arch::rdtsc_ns(), Ordering::Relaxed);
+ PTR_COUNT.fetch_add(1, Ordering::Relaxed);
let packed = ((x as u32 as u64) << 32) | (y as u32 as u64);
@@ -696,6 +763,7 @@ pub fn push_pointer(x: i32, y: i32, buttons: u32) {
/// Deliver a mouse scroll-wheel tick. `dz` is the signed wheel delta
/// (negative = scroll toward the user / content down, positive = away / up).
pub fn push_scroll(x: i32, y: i32, dz: i32) {
+ SCROLL_COUNT.fetch_add(1, Ordering::Relaxed);
let packed = ((x as u32 as u64) << 32) | (y as u32 as u64);
let dz_bits = dz as u32 as u64; // preserve sign through i32→u32→u64
@@ -716,6 +784,8 @@ pub fn push_scroll(x: i32, y: i32, dz: i32) {
}
pub fn push_key(scancode: u32, pressed: bool) {
+ KEY_COUNT.fetch_add(1, Ordering::Relaxed);
+ LAST_KEY.store((scancode << 1) | (pressed as u32), Ordering::Relaxed);
// F2 (PS/2 set-1 make 0x3C) toggles the kernel boot screen's verbose log
// overlay. Handled here so it works during the engine warm-up before any app
// is focused; the keypress is still forwarded for normal handling.
diff --git a/packages/oscortex_net/README.md b/packages/oscortex_net/README.md
new file mode 100644
index 0000000..af80bc9
--- /dev/null
+++ b/packages/oscortex_net/README.md
@@ -0,0 +1,45 @@
+# oscortex_net
+
+App networking for OSCortex userspace Flutter apps — a TCP socket plus a minimal
+plaintext HTTP client, over the `oscortex/net` platform channel into the kernel
+TCP stack.
+
+## Usage
+
+```dart
+import 'package:oscortex_net/oscortex_net.dart';
+
+// Raw TCP
+final sock = await OscortexSocket.connect('10.0.2.2', 8080);
+await sock.writeAll('ping'.codeUnits);
+final reply = await sock.read();
+await sock.close();
+
+// Minimal HTTP GET (plaintext / port 80; pass the server IP for now)
+final body = await httpGet('93.184.216.34', 'example.com', '/');
+```
+
+`OscortexSocket.connect` returns only once the (asynchronous) kernel handshake
+has completed, so you can write immediately.
+
+## Status
+
+| Capability | State |
+|---|---|
+| TCP connect / status / send / recv / close | ✅ (kernel syscalls + embedder channel) |
+| Plaintext HTTP/1.0 GET | ✅ helper |
+| DNS by hostname | ⏳ next increment (pass an IP for now) |
+| TLS / HTTPS | ⏳ (smoltcp has no TLS; needs a userspace TLS layer) |
+
+## How it works
+
+```
+Dart (this package)
+ └─ BasicMessageChannel('oscortex/net', BinaryCodec()) compact opcode frames
+ └─ embedder handle_net_channel (tools/flutter-embedder/src/main.rs)
+ └─ TCP syscalls 0x388-0x38B + tcp_status 0x4B7 (CAP_NET)
+ └─ net::tcp (smoltcp) — the same stack the package pipeline uses
+```
+
+Apps are granted `CAP_NET` by the kernel so these calls are permitted (the
+security phase will make that a per-app declared/prompted permission).
diff --git a/packages/oscortex_net/lib/oscortex_net.dart b/packages/oscortex_net/lib/oscortex_net.dart
new file mode 100644
index 0000000..a355d87
--- /dev/null
+++ b/packages/oscortex_net/lib/oscortex_net.dart
@@ -0,0 +1,240 @@
+/// OSCortex app networking.
+///
+/// A thin TCP socket API for userspace Flutter apps, plus a minimal plaintext
+/// HTTP client built on top of it. Everything funnels through the `oscortex/net`
+/// platform channel (a `BasicMessageChannel` with the binary codec, so
+/// TCP byte streams pass through unmangled) into the kernel TCP syscalls.
+///
+/// The kernel `tcp_connect` is asynchronous, so [OscortexSocket.connect] polls
+/// the connection status until the handshake completes before returning.
+///
+/// Not yet supported (follow-up increments): DNS by hostname (pass an IP for
+/// now) and TLS (so HTTP only, not HTTPS).
+library oscortex_net;
+
+import 'dart:async';
+import 'dart:typed_data';
+
+import 'package:flutter/services.dart';
+
+/// Raised when a networking operation fails; [code] is the kernel errno
+/// (negative) or a client-side sentinel.
+class OscortexNetException implements Exception {
+ const OscortexNetException(this.message, this.code);
+ final String message;
+ final int code;
+ @override
+ String toString() => 'OscortexNetException($code): $message';
+}
+
+/// A connected TCP socket backed by a kernel socket fd.
+class OscortexSocket {
+ OscortexSocket._(this.fd);
+
+ /// Kernel socket fd.
+ final int fd;
+
+ static const BasicMessageChannel _channel =
+ BasicMessageChannel('oscortex/net', BinaryCodec());
+
+ // Opcodes (must match the embedder's handle_net_channel).
+ static const int _opConnect = 0x01;
+ static const int _opStatus = 0x02;
+ static const int _opSend = 0x03;
+ static const int _opRecv = 0x04;
+ static const int _opClose = 0x05;
+ static const int _opResolve = 0x06;
+
+ /// Resolve [host] to a dotted-quad IPv4 string, or null on failure.
+ /// (DNS A-record lookup via the kernel resolver.)
+ static Future resolve(String host) async {
+ final List name = host.codeUnits;
+ final Uint8List req = Uint8List(1 + name.length)..[0] = _opResolve;
+ req.setRange(1, 1 + name.length, name);
+ final ByteData? reply = await _channel.send(ByteData.sublistView(req));
+ if (reply == null || reply.lengthInBytes < 4) return null;
+ // BE-order IPv4 as an unsigned u32; 0 = failure sentinel.
+ final int v = reply.getUint32(0, Endian.little);
+ if (v == 0) return null;
+ return '${(v >> 24) & 0xFF}.${(v >> 16) & 0xFF}.${(v >> 8) & 0xFF}.${v & 0xFF}';
+ }
+
+ /// Send a request frame, return the leading little-endian i32 result.
+ static Future _callI32(Uint8List req) async {
+ final ByteData? reply = await _channel.send(ByteData.sublistView(req));
+ if (reply == null || reply.lengthInBytes < 4) return -22; // EINVAL
+ return reply.getInt32(0, Endian.little);
+ }
+
+ static void _putFd(Uint8List req, int fd) {
+ req[1] = fd & 0xFF;
+ req[2] = (fd >> 8) & 0xFF;
+ req[3] = (fd >> 16) & 0xFF;
+ req[4] = (fd >> 24) & 0xFF;
+ }
+
+ static int _parseIpv4(String ip) {
+ final List parts = ip.split('.');
+ if (parts.length != 4) {
+ throw OscortexNetException('not an IPv4 address: $ip', -22);
+ }
+ var v = 0;
+ for (final String p in parts) {
+ final int o = int.tryParse(p) ?? -1;
+ if (o < 0 || o > 255) {
+ throw OscortexNetException('not an IPv4 address: $ip', -22);
+ }
+ v = (v << 8) | o;
+ }
+ return v & 0xFFFFFFFF;
+ }
+
+ /// Open a TCP connection to [ip] (dotted-quad) : [port], returning once the
+ /// handshake has completed. Throws [OscortexNetException] on failure/timeout.
+ static Future connect(
+ String ip,
+ int port, {
+ Duration timeout = const Duration(seconds: 10),
+ }) async {
+ final int ipBe = _parseIpv4(ip);
+ final Uint8List req = Uint8List(7);
+ req[0] = _opConnect;
+ req[1] = (ipBe >> 24) & 0xFF;
+ req[2] = (ipBe >> 16) & 0xFF;
+ req[3] = (ipBe >> 8) & 0xFF;
+ req[4] = ipBe & 0xFF;
+ req[5] = (port >> 8) & 0xFF;
+ req[6] = port & 0xFF;
+
+ final int fd = await _callI32(req);
+ if (fd < 0) throw OscortexNetException('connect failed', fd);
+
+ final OscortexSocket sock = OscortexSocket._(fd);
+ final DateTime deadline = DateTime.now().add(timeout);
+ while (true) {
+ final int st = await sock._status();
+ if (st == 1) return sock; // established
+ if (st < 0) {
+ await sock.close();
+ throw OscortexNetException('connection refused/reset', st);
+ }
+ if (DateTime.now().isAfter(deadline)) {
+ await sock.close();
+ throw const OscortexNetException('connect timed out', -110);
+ }
+ await Future.delayed(const Duration(milliseconds: 20));
+ }
+ }
+
+ Future _status() {
+ final Uint8List req = Uint8List(5)..[0] = _opStatus;
+ _putFd(req, fd);
+ return _callI32(req);
+ }
+
+ /// Write [data], retrying EAGAIN internally. Returns bytes written (which may
+ /// be fewer than `data.length` — use [writeAll] to send everything).
+ Future write(
+ List data, {
+ Duration timeout = const Duration(seconds: 5),
+ }) async {
+ final Uint8List req = Uint8List(5 + data.length);
+ req[0] = _opSend;
+ _putFd(req, fd);
+ req.setRange(5, 5 + data.length, data);
+
+ final DateTime deadline = DateTime.now().add(timeout);
+ while (true) {
+ final int n = await _callI32(req);
+ if (n != -11) {
+ // not EAGAIN
+ if (n < 0) throw OscortexNetException('write failed', n);
+ return n;
+ }
+ if (DateTime.now().isAfter(deadline)) {
+ throw const OscortexNetException('write timed out', -110);
+ }
+ await Future.delayed(const Duration(milliseconds: 10));
+ }
+ }
+
+ /// Write every byte of [data], looping until the buffer is drained.
+ Future writeAll(List data) async {
+ var off = 0;
+ while (off < data.length) {
+ off += await write(data.sublist(off));
+ }
+ }
+
+ /// Read up to [max] bytes. Returns the bytes received, or an empty list if no
+ /// data arrived within [timeout] (e.g. the peer closed or is idle). The caller
+ /// loops until it has what it needs (see [httpGet]).
+ Future read({
+ int max = 8192,
+ Duration timeout = const Duration(seconds: 2),
+ }) async {
+ final Uint8List req = Uint8List(9)..[0] = _opRecv;
+ _putFd(req, fd);
+ req.buffer.asByteData().setUint32(5, max, Endian.little);
+
+ final DateTime deadline = DateTime.now().add(timeout);
+ while (true) {
+ final ByteData? reply = await _channel.send(ByteData.sublistView(req));
+ if (reply == null || reply.lengthInBytes < 4) {
+ throw const OscortexNetException('read failed', -5);
+ }
+ final int n = reply.getInt32(0, Endian.little);
+ if (n > 0) {
+ // Copy out of the channel buffer (don't alias it).
+ return Uint8List.fromList(
+ reply.buffer.asUint8List(reply.offsetInBytes + 4, n),
+ );
+ }
+ if (n < 0 && n != -11) {
+ throw OscortexNetException('read failed', n);
+ }
+ // n == 0 (nothing this poll) or -11 (EAGAIN): wait and retry until deadline.
+ if (DateTime.now().isAfter(deadline)) return Uint8List(0);
+ await Future.delayed(const Duration(milliseconds: 20));
+ }
+ }
+
+ /// Close the socket.
+ Future close() async {
+ final Uint8List req = Uint8List(5)..[0] = _opClose;
+ _putFd(req, fd);
+ await _callI32(req);
+ }
+}
+
+/// Minimal plaintext HTTP/1.0 GET over a raw TCP socket (no TLS yet, so HTTP
+/// only). [ip] is the server's dotted-quad address, [host] the Host header,
+/// [path] the request path. Returns the response body (headers stripped).
+///
+/// Until DNS lands, resolve the host to an IP out-of-band and pass it here.
+Future httpGet(
+ String ip,
+ String host,
+ String path, {
+ int port = 80,
+}) async {
+ final OscortexSocket sock = await OscortexSocket.connect(ip, port);
+ try {
+ final String request =
+ 'GET $path HTTP/1.0\r\nHost: $host\r\nConnection: close\r\n\r\n';
+ await sock.writeAll(request.codeUnits);
+
+ final BytesBuilder body = BytesBuilder();
+ while (true) {
+ final Uint8List chunk = await sock.read();
+ if (chunk.isEmpty) break; // peer closed / idle past the read timeout
+ body.add(chunk);
+ }
+
+ final String text = String.fromCharCodes(body.takeBytes());
+ final int sep = text.indexOf('\r\n\r\n');
+ return sep >= 0 ? text.substring(sep + 4) : text;
+ } finally {
+ await sock.close();
+ }
+}
diff --git a/packages/oscortex_net/pubspec.yaml b/packages/oscortex_net/pubspec.yaml
new file mode 100644
index 0000000..90e659c
--- /dev/null
+++ b/packages/oscortex_net/pubspec.yaml
@@ -0,0 +1,19 @@
+name: oscortex_net
+description: >
+ OSCortex app networking — a TCP socket + minimal HTTP client for userspace
+ Flutter apps, over the `oscortex/net` platform channel into the kernel TCP
+ stack. (DNS by hostname lands in a follow-up; pass an IP for now.)
+version: 0.1.0
+publish_to: none
+
+environment:
+ sdk: ^3.11.0
+ flutter: '>=3.29.0'
+
+dependencies:
+ flutter:
+ sdk: flutter
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
diff --git a/packages/oscortex_net/test/oscortex_net_test.dart b/packages/oscortex_net/test/oscortex_net_test.dart
new file mode 100644
index 0000000..16222b0
--- /dev/null
+++ b/packages/oscortex_net/test/oscortex_net_test.dart
@@ -0,0 +1,197 @@
+import 'dart:typed_data';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:oscortex_net/oscortex_net.dart';
+
+/// Verifies the `oscortex/net` wire protocol from the Dart side: every request
+/// frame OscortexSocket emits must match what the embedder's handle_net_channel
+/// parses, and replies must be decoded correctly. The kernel/embedder is mocked
+/// via the binary messenger, so these run without a device.
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+ const String channelName = 'oscortex/net';
+
+ final List sent = [];
+ // Tunable canned recv reply (overridden per test).
+ late int recvN;
+ late List recvData;
+
+ Uint8List bytesOf(ByteData? m) =>
+ m!.buffer.asUint8List(m.offsetInBytes, m.lengthInBytes);
+ ByteData i32(int v) => ByteData(4)..setInt32(0, v, Endian.little);
+
+ TestDefaultBinaryMessenger messenger() =>
+ TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
+
+ setUp(() {
+ sent.clear();
+ recvN = 2;
+ recvData = 'hi'.codeUnits;
+ messenger().setMockMessageHandler(channelName, (ByteData? message) async {
+ final Uint8List f = bytesOf(message);
+ sent.add(f);
+ switch (f[0]) {
+ case 0x01: // connect → fd 3
+ return i32(3);
+ case 0x02: // status → established
+ return i32(1);
+ case 0x03: // send → bytes written == payload length
+ return i32(f.length - 5);
+ case 0x04: // recv → [i32 n | data]
+ final int body = recvN > 0 ? recvData.length : 0;
+ final ByteData r = ByteData(4 + body);
+ r.setInt32(0, recvN, Endian.little);
+ for (int i = 0; i < body; i++) {
+ r.setUint8(4 + i, recvData[i]);
+ }
+ return r;
+ case 0x05: // close → 0
+ return i32(0);
+ default:
+ return i32(-22);
+ }
+ });
+ });
+
+ tearDown(() => messenger().setMockMessageHandler(channelName, null));
+
+ test('connect sends [op, ip big-endian, port big-endian] and yields the fd',
+ () async {
+ final OscortexSocket sock = await OscortexSocket.connect('10.0.2.2', 8099);
+ expect(sock.fd, 3);
+
+ final Uint8List c = sent.first;
+ expect(c[0], 0x01);
+ expect([c[1], c[2], c[3], c[4]], [10, 0, 2, 2]); // ip BE
+ expect([c[5], c[6]], [(8099 >> 8) & 0xFF, 8099 & 0xFF]); // port BE
+ // connect must poll status (op 0x02) until established before returning.
+ expect(sent.any((Uint8List f) => f[0] == 0x02), isTrue);
+ });
+
+ test('connect throws on a malformed IP (before sending anything)', () async {
+ await expectLater(
+ OscortexSocket.connect('not.an.ip', 80),
+ throwsA(isA()),
+ );
+ expect(sent, isEmpty);
+ });
+
+ test('connect throws when the kernel returns a negative fd', () async {
+ messenger().setMockMessageHandler(channelName, (ByteData? m) async => i32(-23));
+ await expectLater(
+ OscortexSocket.connect('10.0.2.2', 80),
+ throwsA(isA()),
+ );
+ });
+
+ test('write sends [op, fd little-endian, data] and returns bytes written',
+ () async {
+ final OscortexSocket sock = await OscortexSocket.connect('10.0.2.2', 80);
+ sent.clear();
+
+ final int n = await sock.write([1, 2, 3, 4]);
+ expect(n, 4);
+
+ final Uint8List w = sent.single;
+ expect(w[0], 0x03);
+ expect([w[1], w[2], w[3], w[4]], [3, 0, 0, 0]); // fd=3 LE
+ expect(w.sublist(5), [1, 2, 3, 4]);
+ });
+
+ test('read sends [op, fd LE, max LE] and returns the payload bytes', () async {
+ final OscortexSocket sock = await OscortexSocket.connect('10.0.2.2', 80);
+ sent.clear();
+ recvN = 5;
+ recvData = 'hello'.codeUnits;
+
+ final Uint8List data = await sock.read(max: 4096);
+ expect(String.fromCharCodes(data), 'hello');
+
+ final Uint8List r = sent.first;
+ expect(r[0], 0x04);
+ expect([r[1], r[2], r[3], r[4]], [3, 0, 0, 0]); // fd LE
+ final int maxField = r[5] | (r[6] << 8) | (r[7] << 16) | (r[8] << 24);
+ expect(maxField, 4096); // max, little-endian
+ });
+
+ test('read returns empty once the timeout elapses with no data', () async {
+ final OscortexSocket sock = await OscortexSocket.connect('10.0.2.2', 80);
+ recvN = 0; // kernel: nothing available this poll
+ final Uint8List data =
+ await sock.read(timeout: const Duration(milliseconds: 80));
+ expect(data, isEmpty);
+ });
+
+ test('close sends [op, fd little-endian]', () async {
+ final OscortexSocket sock = await OscortexSocket.connect('10.0.2.2', 80);
+ sent.clear();
+ await sock.close();
+
+ final Uint8List f = sent.single;
+ expect(f[0], 0x05);
+ expect([f[1], f[2], f[3], f[4]], [3, 0, 0, 0]);
+ });
+
+ test('httpGet sends a GET request and returns the body with headers stripped',
+ () async {
+ const String response = 'HTTP/1.0 200 OK\r\n'
+ 'Content-Type: text/plain\r\n'
+ 'Content-Length: 5\r\n'
+ '\r\n'
+ 'HELLO';
+ int reads = 0;
+ messenger().setMockMessageHandler(channelName, (ByteData? message) async {
+ final Uint8List f = bytesOf(message);
+ sent.add(f);
+ switch (f[0]) {
+ case 0x01:
+ return i32(7); // connect → fd 7
+ case 0x02:
+ return i32(1); // established
+ case 0x03:
+ return i32(f.length - 5); // wrote the whole request
+ case 0x04:
+ if (reads++ == 0) {
+ final List b = response.codeUnits;
+ final ByteData r = ByteData(4 + b.length);
+ r.setInt32(0, b.length, Endian.little);
+ for (int i = 0; i < b.length; i++) {
+ r.setUint8(4 + i, b[i]);
+ }
+ return r;
+ }
+ return i32(0); // no more data → httpGet's read loop ends on timeout
+ case 0x05:
+ return i32(0);
+ default:
+ return i32(-22);
+ }
+ });
+
+ final String body = await httpGet('93.184.216.34', 'example.com', '/');
+ expect(body, 'HELLO');
+
+ final Uint8List writeFrame = sent.firstWhere((Uint8List f) => f[0] == 0x03);
+ final String reqText = String.fromCharCodes(writeFrame.sublist(5));
+ expect(reqText, startsWith('GET / HTTP/1.0\r\n'));
+ expect(reqText, contains('Host: example.com\r\n'));
+ expect(reqText, contains('Connection: close'));
+ }, timeout: const Timeout(Duration(seconds: 15)));
+
+ test('resolve sends [op, hostname] and parses the u32 IPv4 reply', () async {
+ messenger().setMockMessageHandler(channelName, (ByteData? message) async {
+ final Uint8List f = bytesOf(message);
+ expect(f[0], 0x06);
+ expect(String.fromCharCodes(f.sublist(1)), 'example.com');
+ final int v = (93 << 24) | (184 << 16) | (216 << 8) | 34; // 93.184.216.34
+ return ByteData(4)..setUint32(0, v, Endian.little);
+ });
+ expect(await OscortexSocket.resolve('example.com'), '93.184.216.34');
+ });
+
+ test('resolve returns null on the 0 failure sentinel', () async {
+ messenger().setMockMessageHandler(channelName,
+ (ByteData? m) async => ByteData(4)..setUint32(0, 0, Endian.little));
+ expect(await OscortexSocket.resolve('nope.invalid'), isNull);
+ });
+}
diff --git a/packages/oscortex_ui/.dart_tool/extension_discovery/README.md b/packages/oscortex_ui/.dart_tool/extension_discovery/README.md
deleted file mode 100644
index 9dc6757..0000000
--- a/packages/oscortex_ui/.dart_tool/extension_discovery/README.md
+++ /dev/null
@@ -1,31 +0,0 @@
-Extension Discovery Cache
-=========================
-
-This folder is used by `package:extension_discovery` to cache lists of
-packages that contains extensions for other packages.
-
-DO NOT USE THIS FOLDER
-----------------------
-
- * Do not read (or rely) the contents of this folder.
- * Do write to this folder.
-
-If you're interested in the lists of extensions stored in this folder use the
-API offered by package `extension_discovery` to get this information.
-
-If this package doesn't work for your use-case, then don't try to read the
-contents of this folder. It may change, and will not remain stable.
-
-Use package `extension_discovery`
----------------------------------
-
-If you want to access information from this folder.
-
-Feel free to delete this folder
--------------------------------
-
-Files in this folder act as a cache, and the cache is discarded if the files
-are older than the modification time of `.dart_tool/package_config.json`.
-
-Hence, it should never be necessary to clear this cache manually, if you find a
-need to do please file a bug.
diff --git a/packages/oscortex_ui/.dart_tool/extension_discovery/devtools.json b/packages/oscortex_ui/.dart_tool/extension_discovery/devtools.json
deleted file mode 100644
index 9057020..0000000
--- a/packages/oscortex_ui/.dart_tool/extension_discovery/devtools.json
+++ /dev/null
@@ -1 +0,0 @@
-{"version":2,"entries":[{"package":"oscortex_ui","rootUri":"../","packageUri":"lib/"}]}
\ No newline at end of file
diff --git a/packages/oscortex_ui/.dart_tool/extension_discovery/vs_code.json b/packages/oscortex_ui/.dart_tool/extension_discovery/vs_code.json
deleted file mode 100644
index 9057020..0000000
--- a/packages/oscortex_ui/.dart_tool/extension_discovery/vs_code.json
+++ /dev/null
@@ -1 +0,0 @@
-{"version":2,"entries":[{"package":"oscortex_ui","rootUri":"../","packageUri":"lib/"}]}
\ No newline at end of file
diff --git a/packages/oscortex_ui/.dart_tool/package_config.json b/packages/oscortex_ui/.dart_tool/package_config.json
deleted file mode 100644
index ba0e83e..0000000
--- a/packages/oscortex_ui/.dart_tool/package_config.json
+++ /dev/null
@@ -1,58 +0,0 @@
-{
- "configVersion": 2,
- "packages": [
- {
- "name": "characters",
- "rootUri": "file:///Users/ghostportal/.pub-cache/hosted/pub.dev/characters-1.4.1",
- "packageUri": "lib/",
- "languageVersion": "3.4"
- },
- {
- "name": "collection",
- "rootUri": "file:///Users/ghostportal/.pub-cache/hosted/pub.dev/collection-1.19.1",
- "packageUri": "lib/",
- "languageVersion": "3.4"
- },
- {
- "name": "flutter",
- "rootUri": "file:///opt/homebrew/share/flutter/packages/flutter",
- "packageUri": "lib/",
- "languageVersion": "3.9"
- },
- {
- "name": "material_color_utilities",
- "rootUri": "file:///Users/ghostportal/.pub-cache/hosted/pub.dev/material_color_utilities-0.13.0",
- "packageUri": "lib/",
- "languageVersion": "3.5"
- },
- {
- "name": "meta",
- "rootUri": "file:///Users/ghostportal/.pub-cache/hosted/pub.dev/meta-1.17.0",
- "packageUri": "lib/",
- "languageVersion": "3.5"
- },
- {
- "name": "sky_engine",
- "rootUri": "file:///opt/homebrew/share/flutter/bin/cache/pkg/sky_engine",
- "packageUri": "lib/",
- "languageVersion": "3.9"
- },
- {
- "name": "vector_math",
- "rootUri": "file:///Users/ghostportal/.pub-cache/hosted/pub.dev/vector_math-2.2.0",
- "packageUri": "lib/",
- "languageVersion": "3.1"
- },
- {
- "name": "oscortex_ui",
- "rootUri": "../",
- "packageUri": "lib/",
- "languageVersion": "3.11"
- }
- ],
- "generator": "pub",
- "generatorVersion": "3.11.0",
- "flutterRoot": "file:///opt/homebrew/share/flutter",
- "flutterVersion": "3.41.1",
- "pubCache": "file:///Users/ghostportal/.pub-cache"
-}
diff --git a/packages/oscortex_ui/.dart_tool/package_graph.json b/packages/oscortex_ui/.dart_tool/package_graph.json
deleted file mode 100644
index b56e5e6..0000000
--- a/packages/oscortex_ui/.dart_tool/package_graph.json
+++ /dev/null
@@ -1,60 +0,0 @@
-{
- "roots": [
- "oscortex_ui"
- ],
- "packages": [
- {
- "name": "oscortex_ui",
- "version": "0.1.0",
- "dependencies": [
- "flutter"
- ],
- "devDependencies": []
- },
- {
- "name": "flutter",
- "version": "0.0.0",
- "dependencies": [
- "characters",
- "collection",
- "material_color_utilities",
- "meta",
- "sky_engine",
- "vector_math"
- ]
- },
- {
- "name": "sky_engine",
- "version": "0.0.0",
- "dependencies": []
- },
- {
- "name": "vector_math",
- "version": "2.2.0",
- "dependencies": []
- },
- {
- "name": "meta",
- "version": "1.17.0",
- "dependencies": []
- },
- {
- "name": "material_color_utilities",
- "version": "0.13.0",
- "dependencies": [
- "collection"
- ]
- },
- {
- "name": "collection",
- "version": "1.19.1",
- "dependencies": []
- },
- {
- "name": "characters",
- "version": "1.4.1",
- "dependencies": []
- }
- ],
- "configVersion": 1
-}
\ No newline at end of file
diff --git a/packages/oscortex_ui/.dart_tool/version b/packages/oscortex_ui/.dart_tool/version
deleted file mode 100644
index 050c055..0000000
--- a/packages/oscortex_ui/.dart_tool/version
+++ /dev/null
@@ -1 +0,0 @@
-3.41.1
\ No newline at end of file
diff --git a/packages/oscortex_webview/lib/oscortex_webview.dart b/packages/oscortex_webview/lib/oscortex_webview.dart
new file mode 100644
index 0000000..2c37a5a
--- /dev/null
+++ b/packages/oscortex_webview/lib/oscortex_webview.dart
@@ -0,0 +1,322 @@
+/// OSCortex native webview for userspace Flutter apps.
+///
+/// A userspace web-engine service (Servo) renders a page into its own OSCortex
+/// compositor surface, which the kernel z-stacks directly beneath the app's chrome
+/// and clips to the viewport. This package is the app-side half:
+///
+/// * [OscWebViewController] drives navigation and receives events over the
+/// `oscortex/webview` [MethodChannel].
+/// * [OscWebView] is a transparent placeholder widget that reserves the web
+/// region, reports its on-screen rect (so the kernel can position + clip the
+/// web surface), and forwards pointer/scroll/key input to the engine.
+///
+/// Flutter's external-texture API is GPU-only and OSCortex is software-rendered, so
+/// there is deliberately no texture here — the web pixels are a sibling OS surface
+/// showing through a transparent hole in the Flutter scene. The app must therefore
+/// keep the [OscWebView] region transparent (don't paint an opaque background over
+/// it) so the lower surface is visible.
+///
+/// The contract is engine-agnostic: a stub service or Servo serve it identically.
+library oscortex_webview;
+
+import 'dart:async';
+
+import 'package:flutter/gestures.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+
+/// Pointer/key event `type` codes sent in [OscWebViewController.dispatchInput] /
+/// [OscWebViewController.dispatchKey]. Mirror the kernel's EV_* input vocabulary.
+class OscInput {
+ OscInput._();
+ static const int pointerDown = 0;
+ static const int pointerMove = 1;
+ static const int pointerUp = 2;
+ static const int pointerHover = 3;
+ static const int keyDown = 0;
+ static const int keyUp = 1;
+}
+
+/// Drives one native webview instance over `oscortex/webview`.
+///
+/// Construct with a unique [viewId]; call [create] once the render size is known,
+/// then [loadUrl] / [goBack] / etc. Assign the `on*` callbacks to observe engine
+/// events. Call [dispose] to stop receiving events and free the registry slot.
+class OscWebViewController {
+ OscWebViewController(this.viewId) {
+ _instances[viewId] = this;
+ if (!_handlerInstalled) {
+ _channel.setMethodCallHandler(_dispatch);
+ _handlerInstalled = true;
+ }
+ }
+
+ /// Per-instance id, included in every call/event for multiplexing.
+ final int viewId;
+
+ /// The channel name (shared by all instances; events are routed by `viewId`).
+ static const String channelName = 'oscortex/webview';
+ static const MethodChannel _channel = MethodChannel(channelName);
+
+ static final Map _instances =
+ {};
+ static bool _handlerInstalled = false;
+
+ // ── Engine → app event callbacks ────────────────────────────────────────────
+ void Function(String url)? onUrlChanged;
+ void Function(String title)? onTitleChanged;
+ void Function(int progress)? onProgress; // 0..100
+ void Function(String url)? onLoadStarted;
+ void Function(String url)? onLoadFinished;
+ void Function(int code, String description, String url)? onLoadError;
+ void Function(bool canGoBack, bool canGoForward)? onNavState;
+ void Function(int x, int y)? onScrollChanged;
+
+ Map _a([Map? extra]) =>
+ {'viewId': viewId, ...?extra};
+
+ // ── App → engine methods ────────────────────────────────────────────────────
+
+ /// Invoke a channel method, tolerating the absence of an engine service:
+ /// before the `oscortex/webview` service binds (scaffold phase, or if it
+ /// crashed) `invokeMethod` raises [MissingPluginException]; we treat that as
+ /// "no engine yet" and return null so callers (incl. the widget) stay inert
+ /// rather than throwing. Any other error propagates.
+ Future _invoke(String method, [Map? extra]) async {
+ try {
+ return await _channel.invokeMethod(method, _a(extra));
+ } on MissingPluginException {
+ return null;
+ }
+ }
+
+ /// Create the engine's render surface; returns the OSCortex `surfaceId`
+ /// (or -1 if no engine service is bound yet).
+ Future create({
+ required int width,
+ required int height,
+ double dpr = 1.0,
+ String? initialUrl,
+ }) async {
+ try {
+ final Map? r =
+ await _channel.invokeMapMethod(
+ 'create',
+ _a({
+ 'width': width,
+ 'height': height,
+ 'dpr': dpr,
+ 'initialUrl': initialUrl,
+ }),
+ );
+ return (r?['surfaceId'] as int?) ?? -1;
+ } on MissingPluginException {
+ return -1;
+ }
+ }
+
+ Future destroy() => _invoke('destroy');
+
+ Future loadUrl(String url, {Map? headers}) =>
+ _invoke('loadUrl', {'url': url, 'headers': headers});
+
+ Future loadHtml(String html, {String? baseUrl}) =>
+ _invoke('loadHtml', {'html': html, 'baseUrl': baseUrl});
+
+ Future reload() => _invoke('reload');
+ Future stopLoading() => _invoke('stopLoading');
+ Future goBack() => _invoke('goBack');
+ Future goForward() => _invoke('goForward');
+
+ Future canGoBack() async => (await _invoke('canGoBack')) ?? false;
+ Future canGoForward() async =>
+ (await _invoke('canGoForward')) ?? false;
+
+ Future currentUrl() => _invoke('currentUrl');
+ Future getTitle() => _invoke('getTitle');
+
+ Future