Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions APKBUILD
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ makedepends="
cmake
samurai
qt6-qtbase-dev
qt6-qtbase-private-dev
qt6-qtdeclarative-dev
qt6-qtwayland-dev
qt6-qtwebengine-dev
Expand All @@ -57,6 +58,7 @@ makedepends="
git
linux-pam-dev
hunspell-dev
pulseaudio-dev
"
install=""
subpackages="$pkgname-doc"
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ endif()
find_package(Qt6 6.4...6.10.1 REQUIRED COMPONENTS
Core
Gui
GuiPrivate
# GuiPrivate
Widgets
Qml
Quick
Expand Down
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,16 @@ sudo dnf install cmake ninja-build gcc-c++ \
qt6-qtlocation-devel \
qt6-qtpositioning-devel \
qt6-qtsensors-devel \
qt6-qtwebengine-devel \
pam-devel \
hunspell-devel \
hunspell-en-US
hunspell-en-US \
pulseaudio-libs-devel
```

**Ubuntu/Debian:**
```bash
sudo apt install cmake ninja-build g++ \
sudo apt install cmake ninja-build g++ pkg-config \
qt6-base-dev \
qt6-declarative-dev \
qt6-wayland-dev \
Expand All @@ -86,7 +88,10 @@ sudo apt install cmake ninja-build g++ \
qt6-svg-plugins \
qt6-location-dev \
qt6-positioning-dev \
qt6-base-private-dev
qt6-base-private-dev \
qt6-webengine-dev \
qml-module-qtwebengine \
libpulse-dev
```

### Runtime Requirements
Expand All @@ -95,6 +100,7 @@ sudo apt install cmake ninja-build g++ \
- Wayland display server support
- D-Bus session bus
- Linux kernel 5.10 or later
- **Real-Time Kernel (PREEMPT_RT)**: Recommended for optimal performance and touch response. The shell will warn if running on a standard kernel.

### Optional Dependencies

Expand Down Expand Up @@ -203,11 +209,6 @@ sudo cmake --install build-apps
- Native app embedding functional
- All system services available

**macOS (Development Only):**
- Shell runs without Wayland compositor
- UI development and testing only
- Native app embedding not available

## Running

### First-Time System Setup (Required)
Expand Down
53 changes: 53 additions & 0 deletions analyze_perf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import re
import sys

log_file = "shell_perf.log"

frame_times = []
render_times = []
sync_times = []

# Regex for render thread timing
# [DEBUG] [window 0x...][render thread 0x...] syncAndRender: frame rendered in 0ms, sync=0, render=0, swap=0
render_pattern = re.compile(r"frame rendered in (\d+)ms, sync=(\d+), render=(\d+), swap=(\d+)")

# Regex for GUI thread timing (frame interval)
# [DEBUG] [window 0x...][gui thread] polishAndSync: start, elapsed since last call: 5 ms
interval_pattern = re.compile(r"polishAndSync: start, elapsed since last call: (\d+) ms")

intervals = []

with open(log_file, 'r') as f:
for line in f:
render_match = render_pattern.search(line)
if render_match:
total = int(render_match.group(1))
sync = int(render_match.group(2))
render = int(render_match.group(3))
frame_times.append(total)
render_times.append(render)
sync_times.append(sync)
continue

interval_match = interval_pattern.search(line)
if interval_match:
intervals.append(int(interval_match.group(1)))

if not intervals:
print("No frame data found in log.")
sys.exit(1)

avg_interval = sum(intervals) / len(intervals)
avg_fps = 1000 / avg_interval if avg_interval > 0 else 0
min_fps = 1000 / max(intervals) if max(intervals) > 0 else 0
max_fps = 1000 / min(intervals) if min(intervals) > 0 else 0

print(f"--- Performance Summary ---")
print(f"Total Frames Analyzed: {len(intervals)}")
print(f"Average FPS: {avg_fps:.2f}")
print(f"Min FPS: {min_fps:.2f} (Max Interval: {max(intervals)}ms)")
print(f"Max FPS: {max_fps:.2f} (Min Interval: {min(intervals)}ms)")
print(f"---------------------------")
print(f"Average Render Time: {sum(render_times)/len(render_times):.2f}ms")
print(f"Max Render Time: {max(render_times)}ms")
print(f"Average Sync Time: {sum(sync_times)/len(sync_times):.2f}ms")
64 changes: 64 additions & 0 deletions marathon-ui/Containers/MScrollView.qml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import QtQuick
import QtQuick.Controls

Flickable {
id: root
Expand All @@ -11,6 +12,69 @@ Flickable {
flickDeceleration: 5000
maximumFlickVelocity: 2500
boundsBehavior: Flickable.DragAndOvershootBounds

// API for Global Input Handler
readonly property bool isScrollable: true
function scrollBy(pixels) {
flick(0, pixels * 5) // Multiply for better feel
}

// Enable mouse wheel scrolling
WheelHandler {
onWheel: (event) => {
root.flick(0, -event.angleDelta.y * 5);
}
}

// Enable Keyboard Scrolling (Arrow Keys, Page Up/Down)
focus: true
Keys.onUpPressed: root.flick(0, 500)
Keys.onDownPressed: root.flick(0, -500)
Keys.onPressed: (event) => {
if (event.key === Qt.Key_PageUp) {
root.flick(0, 2000);
event.accepted = true;
} else if (event.key === Qt.Key_PageDown) {
root.flick(0, -2000);
event.accepted = true;
}
}

ScrollBar.vertical: ScrollBar {
id: vbar
policy: ScrollBar.AsNeeded
width: 6
active: root.moving || root.flicking || edgeScrollArea.containsMouse
}

// Software Edge Scrolling for Q20 Trackpad
// Allows scrolling by moving cursor along the right edge
MouseArea {
id: edgeScrollArea
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
width: 20 // Active zone width
hoverEnabled: true
preventStealing: true
z: 100 // Ensure it's on top

property real lastY: 0

onEntered: {
lastY = mouseY
vbar.active = true // Show scrollbar when entering edge
}

onPositionChanged: {
var delta = mouseY - lastY
if (Math.abs(delta) > 2) { // Threshold to prevent jitter
// Multiply delta for faster scrolling
root.flick(0, -delta * 100)
lastY = mouseY
}
}
}

Column {
id: contentContainer
Expand Down
41 changes: 41 additions & 0 deletions profile_session.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/bin/bash
echo "Stopping any running instances..."
pkill marathon-shell

echo "Starting Marathon Shell with QML debugger enabled..."
echo "Please wait for the shell to appear, then perform your swipe gestures."
# Start shell in background, wait for debugger connection
# Enable render timing for fallback data
export QSG_RENDER_TIMING=1
./build/shell/marathon-shell-bin -qmljsdebugger=port:10000,host:127.0.0.1 &> shell_perf.log &
SHELL_PID=$!

# Wait for port to be ready
echo "Waiting for debugger port 10000..."
for i in {1..30}; do
if netstat -tuln | grep -q ":10000 "; then
echo "Port 10000 is open!"
break
fi
sleep 0.5
done

echo "Checking if shell is running..."
ps -p $SHELL_PID || echo "Shell is NOT running!"

echo "Checking for listening port..."
netstat -tuln | grep 10000 || echo "Port 10000 not found!"

echo "Attaching QML Profiler..."
echo "RECORDING FOR 20 SECONDS - GO GO GO!"

# Run profiler for 20 seconds.
# We use SIGINT (2) to allow it to gracefully close and save data if possible,
# though qmlprofiler usually saves on exit.
timeout --signal=SIGINT 20s /usr/lib/qt6/bin/qmlprofiler --attach localhost:10000 --output profile_data.qtd

echo "Profiling finished."
echo "Saving data to $(pwd)/profile_data.qtd"

# Optional: Kill shell after profiling
# kill $SHELL_PID
3 changes: 3 additions & 0 deletions scripts/99-hackberry.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ZitaoTech HackBerryPi Q20 Trackpad Configuration
# Force as Touchpad to enable Edge Scrolling
ACTION=="add|change", KERNEL=="event*", ATTRS{idVendor}=="4041", ATTRS{idProduct}=="0001", ENV{ID_INPUT_MOUSE}="0", ENV{ID_INPUT_TOUCHPAD}="1", ENV{LIBINPUT_SCROLL_METHOD}="edge"
23 changes: 23 additions & 0 deletions scripts/install_dependencies.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash
set -e

echo "Installing Marathon Shell dependencies..."

# 1. Install Shared Libraries
echo "Installing libMarathonCore..."
sudo cp -P build/marathon-core/libMarathonCore.so* /usr/local/lib/
sudo ldconfig

# 2. Install QML Modules
echo "Installing MarathonUI modules..."
# Create target directory
sudo mkdir -p /usr/local/lib/qt6/qml/MarathonUI

# Copy MarathonUI modules from build-ui
# We use rsync to copy the contents, excluding intermediate build files if possible,
# but cp -r is safer to ensure we get everything needed (qmldir, plugins, etc.)
# The build-ui/MarathonUI structure should be:
# build-ui/MarathonUI/Theme/qmldir, lib..., etc.
sudo cp -r build-ui/MarathonUI/* /usr/local/lib/qt6/qml/MarathonUI/

echo "Dependencies installed successfully."
32 changes: 32 additions & 0 deletions scripts/install_fonts.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/bin/bash
set -e

FONT_DIR="/usr/local/share/fonts/marathon"
SOURCE_DIR="shell/resources/fonts"

echo "Installing Marathon Shell fonts..."

# Create font directory
sudo mkdir -p "$FONT_DIR"

# Copy fonts
if [ -d "$SOURCE_DIR" ]; then
sudo cp "$SOURCE_DIR"/*.TTF "$FONT_DIR/" 2>/dev/null || sudo cp "$SOURCE_DIR"/*.ttf "$FONT_DIR/"
echo "Fonts copied to $FONT_DIR"
else
echo "Error: Font source directory '$SOURCE_DIR' not found!"
exit 1
fi

# Update font cache
echo "Updating font cache..."
sudo fc-cache -fv

# Verify installation
echo "Verifying 'Slate' font..."
if fc-list : family | grep -q "Slate"; then
echo "✅ Slate font installed successfully!"
else
echo "❌ Slate font NOT found in system cache."
exit 1
fi
33 changes: 33 additions & 0 deletions scripts/marathon-shell-session
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/bin/bash

# Marathon Shell Session Wrapper
# Sets up the environment for running Marathon Shell as a Wayland compositor

# Set Qt platform to eglfs (native KMS/DRM) or xcb (nested on X11)
# eglfs is preferred for a standalone compositor session
export QT_QPA_PLATFORM="eglfs;xcb"

# Set XDG session type
export XDG_SESSION_TYPE=wayland

# Set XDG current desktop
export XDG_CURRENT_DESKTOP=Marathon

# Force Firefox/Mozilla to use Wayland
export MOZ_ENABLE_WAYLAND=1

# Ensure Qt uses the correct QPA theme
# export QT_QPA_PLATFORMTHEME=gtk3

# Add custom QML import path
export QML2_IMPORT_PATH=/usr/local/lib/qt6/qml:$QML2_IMPORT_PATH
echo "QML2_IMPORT_PATH: $QML2_IMPORT_PATH" >&2

# Log input devices to verify udev rules
echo "=== Input Devices ===" >&2
libinput list-devices 2>&1 | grep -E "Device:|Scroll methods:|Scroll button:" >&2
echo "=====================" >&2

# Launch the shell with logging
# We use exec to replace the shell process with the marathon-shell process
exec /usr/local/bin/marathon-shell "$@" > /home/pi/marathon-session.log 2>&1
39 changes: 37 additions & 2 deletions shell/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -321,12 +321,44 @@ endif()

find_package(PkgConfig REQUIRED)
pkg_check_modules(PULSEAUDIO REQUIRED libpulse)
include_directories(${PULSEAUDIO_INCLUDE_DIRS})
target_include_directories(marathon-shell PRIVATE
${PULSEAUDIO_INCLUDE_DIRS}
)

find_package(Qt6 REQUIRED COMPONENTS Gui)

# Check for GuiPrivate or manually add headers
if(TARGET Qt6::GuiPrivate)
target_link_libraries(marathon-shell PRIVATE Qt6::GuiPrivate)
else()
message(STATUS "Qt6::GuiPrivate target not found. Attempting to locate private headers manually.")
# Try to find the private headers manually
# We know it's at /usr/include/aarch64-linux-gnu/qt6/QtGui/6.8.2/QtGui/qpa/qplatformscreen.h
# So we need /usr/include/aarch64-linux-gnu/qt6/QtGui/6.8.2/QtGui

# Get Qt version from Core
if(TARGET Qt6::Core)
get_target_property(QT_CORE_VERSION Qt6::Core VERSION)
endif()

find_path(QT_GUI_PRIVATE_INCLUDE_DIR qpa/qplatformscreen.h
HINTS
/usr/include/${CMAKE_LIBRARY_ARCHITECTURE}/qt6/QtGui/${QT_CORE_VERSION}/QtGui
/usr/include/qt6/QtGui/${QT_CORE_VERSION}/QtGui
/usr/include/aarch64-linux-gnu/qt6/QtGui/6.8.2/QtGui
)

if(QT_GUI_PRIVATE_INCLUDE_DIR)
target_include_directories(marathon-shell PRIVATE ${QT_GUI_PRIVATE_INCLUDE_DIR})
message(STATUS "Found Qt6 Gui Private headers: ${QT_GUI_PRIVATE_INCLUDE_DIR}")
else()
message(WARNING "Qt6 Gui Private headers not found! Build may fail.")
endif()
endif()

target_link_libraries(marathon-shell PRIVATE
Qt6::Core
Qt6::Gui
Qt6::GuiPrivate
Qt6::Widgets
Qt6::Qml
Qt6::Quick
Expand Down Expand Up @@ -374,6 +406,9 @@ if(HAVE_HUNSPELL)
message(STATUS "Linking Hunspell to marathon-shell")
endif()

# Enable QML Debugging/Profiling
target_compile_definitions(marathon-shell PRIVATE QT_QML_DEBUG)

set_target_properties(marathon-shell PROPERTIES
MACOSX_BUNDLE TRUE
WIN32_EXECUTABLE TRUE
Expand Down
Loading
Loading