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
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# Auto detect text files and perform LF normalization
* text=auto
toolchains/cross/packs/** -diff
third_party/erika_flutter/native/macos/** -diff
third_party/erika_flutter/native/windows/** -diff
220 changes: 220 additions & 0 deletions .github/workflows/build-cross-target-packs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
name: Build Offline Desktop Cross Target Packs

on:
workflow_dispatch:
inputs:
flutter_version:
description: Exact Flutter version embedded packs must match
required: true
default: '3.44.0'
erika_ref:
description: Erika source revision used by the Flutter plugin
required: true
default: 'main'
erika_source_build:
description: Rebuild Erika C API from source instead of its verified release bundle
required: true
type: boolean
default: false
push:
branches:
- codex/cross-compilation
paths:
- .github/workflows/build-cross-target-packs.yml

permissions:
contents: read

env:
FLUTTER_VERSION: ${{ inputs.flutter_version || '3.44.0' }}
ERIKA_REF: ${{ inputs.erika_ref || 'main' }}
ERIKA_FORCE_SOURCE_BUILD: ${{ inputs.erika_source_build && '1' || '0' }}

jobs:
snapshotter:
name: macOS x64 AOT snapshotter
runs-on: macos-15-intel
timeout-minutes: 40
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: stable
architecture: x64
cache: true
- name: Export snapshotter
shell: bash
run: |
set -euo pipefail
erika_macos=third_party/erika_flutter/native/macos/liberika_capi.dylib
test -f "$erika_macos"
lipo "$erika_macos" -verify_arch arm64 x86_64
flutter precache --macos
flutter_root="$(cd "$(dirname "$(command -v flutter)")/.." && pwd)"
engine_revision="$(tr -d '\r\n' < "$flutter_root/bin/cache/engine.stamp")"
source="$flutter_root/bin/cache/artifacts/engine/darwin-x64-release/gen_snapshot"
test -x "$source"
mkdir -p cross-pack/snapshotter
cp "$source" cross-pack/snapshotter/gen_snapshot
chmod +x cross-pack/snapshotter/gen_snapshot
jq -n \
--arg flutterVersion "$FLUTTER_VERSION" \
--arg engineRevision "$engine_revision" \
'{flutterVersion:$flutterVersion,engineRevision:$engineRevision,flutterSnapshotDirectory:"darwin-x64-release"}' \
> cross-pack/snapshotter/metadata.json
file cross-pack/snapshotter/gen_snapshot
- uses: actions/upload-artifact@v4
with:
name: saki-cross-snapshotter
path: cross-pack/snapshotter
compression-level: 0
retention-days: 14

linux-runner:
name: Linux x64 SakiEngine runner
runs-on: ubuntu-24.04
timeout-minutes: 180
steps:
- uses: actions/checkout@v4
- name: Install Linux build dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
clang cmake ninja-build pkg-config libgtk-3-dev libkeybinder-3.0-dev \
libmpv-dev libasound2-dev libass-dev \
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-libav
- uses: dtolnay/rust-toolchain@stable
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: stable
cache: true
- name: Build native runner and plugins
working-directory: Game/SakiEngine
shell: bash
run: |
set -euo pipefail
# Git does not retain the empty asset directories declared by the
# engine template, while Flutter validates every pubspec entry.
mkdir -p Assets/gui Assets/images/cg/chapter1 Assets/images/items \
Assets/music Assets/sound Assets/voice
flutter config --enable-linux-desktop
flutter pub get
flutter build linux --release
- name: Export runner template
working-directory: Game/SakiEngine
shell: bash
run: |
set -euo pipefail
source_dir=build/linux/x64/release/bundle
runner_path="$(find "$source_dir" -maxdepth 1 -type f -perm -111 -print -quit)"
test -n "$runner_path"
runner_executable="$(basename "$runner_path")"
mkdir -p ../../cross-pack/linux-x64/template
cp -a "$source_dir/." ../../cross-pack/linux-x64/template/
rm -rf ../../cross-pack/linux-x64/template/data/flutter_assets
rm -f ../../cross-pack/linux-x64/template/lib/libapp.so
jq -n \
--arg runnerExecutable "$runner_executable" \
--argjson plugins "$(jq '[.plugins.linux[] | select(.native_build == true and .dev_dependency != true) | .name] | sort' .flutter-plugins-dependencies)" \
'{targetPlatform:"linux-x64",runnerExecutable:$runnerExecutable,nativePlugins:$plugins}' \
> ../../cross-pack/linux-x64/metadata.json
find ../../cross-pack/linux-x64/template -type f -maxdepth 4 -print
- uses: actions/upload-artifact@v4
with:
name: saki-cross-linux-x64
path: cross-pack/linux-x64
compression-level: 0
retention-days: 14

windows-runner:
name: Windows x64 SakiEngine runner
runs-on: windows-2022
timeout-minutes: 240
steps:
- uses: actions/checkout@v4
- name: Checkout Erika package source
if: env.ERIKA_FORCE_SOURCE_BUILD == '1'
shell: bash
run: |
set -euo pipefail
erika_root="$(cd "$GITHUB_WORKSPACE/../.." && pwd)/RustProject/Erika"
mkdir -p "$(dirname "$erika_root")"
git clone --depth 1 --branch "$ERIKA_REF" https://github.com/AimesSoft/Erika.git "$erika_root"
echo "ERIKA_REPO_ROOT=$erika_root" >> "$GITHUB_ENV"
- uses: dtolnay/rust-toolchain@stable
if: env.ERIKA_FORCE_SOURCE_BUILD == '1'
with:
targets: x86_64-pc-windows-msvc
- uses: msys2/setup-msys2@v2
if: env.ERIKA_FORCE_SOURCE_BUILD == '1'
with:
msystem: UCRT64
release: false
update: false
path-type: inherit
install: >-
make
tar
diffutils
coreutils
pkgconf
perl
zip
- name: Install Erika native build tools
if: env.ERIKA_FORCE_SOURCE_BUILD == '1'
shell: pwsh
run: |
choco install -y --no-progress nasm llvm
pip install --quiet meson ninja
"LIBCLANG_PATH=C:\Program Files\LLVM\bin" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
Add-Content -Path $env:GITHUB_PATH -Value "C:\Program Files\LLVM\bin"
Add-Content -Path $env:GITHUB_PATH -Value "C:\Program Files\NASM"
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: stable
cache: true
- name: Build native runner and plugins
working-directory: Game/SakiEngine
shell: bash
env:
ERIKA_FORCE_SOURCE_BUILD: ${{ env.ERIKA_FORCE_SOURCE_BUILD }}
ERIKA_NATIVE_PROFILE: lgpl
ERIKA_NATIVE_TARGET: x86_64-pc-windows-msvc
run: |
set -euo pipefail
# Git does not retain the empty asset directories declared by the
# engine template, while Flutter validates every pubspec entry.
mkdir -p Assets/gui Assets/images/cg/chapter1 Assets/images/items \
Assets/music Assets/sound Assets/voice
flutter config --enable-windows-desktop
flutter pub get
flutter build windows --release
- name: Export runner template
working-directory: Game/SakiEngine
shell: bash
run: |
set -euo pipefail
source_dir=build/windows/x64/runner/Release
runner_path="$(find "$source_dir" -maxdepth 1 -type f -iname '*.exe' -print -quit)"
test -n "$runner_path"
runner_executable="$(basename "$runner_path")"
mkdir -p ../../cross-pack/windows-x64/template
cp -a "$source_dir/." ../../cross-pack/windows-x64/template/
rm -rf ../../cross-pack/windows-x64/template/data/flutter_assets
rm -f ../../cross-pack/windows-x64/template/data/app.so
jq -n \
--arg runnerExecutable "$runner_executable" \
--argjson plugins "$(jq '[.plugins.windows[] | select(.native_build == true and .dev_dependency != true) | .name] | sort' .flutter-plugins-dependencies)" \
'{targetPlatform:"windows-x64",runnerExecutable:$runnerExecutable,nativePlugins:$plugins}' \
> ../../cross-pack/windows-x64/metadata.json
find ../../cross-pack/windows-x64/template -type f -maxdepth 4 -print
- uses: actions/upload-artifact@v4
with:
name: saki-cross-windows-x64
path: cross-pack/windows-x64
compression-level: 0
retention-days: 14
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,7 @@ Engine/.flutter-plugins-dependencies
/Launcher/linux/flutter/ephemeral
/third_party/media_kit_libs_macos_video_hotfix/macos/Frameworks
/third_party/media_kit_libs_macos_video_hotfix/macos/.cache

# SakiEngine 的离线 Erika Windows 运行库必须随仓库分发。
!/third_party/erika_flutter/native/windows/x64/
!/third_party/erika_flutter/native/windows/x64/erika_capi.dll
29 changes: 22 additions & 7 deletions Engine/assets/shaders/dissolve.frag
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
#include <flutter/runtime_effect.glsl>

uniform float progress;
uniform vec2 uSize;
uniform vec2 u_imageFrom_size;
uniform vec2 u_imageFrom_dimensions;
uniform vec2 u_imageTo_size;
uniform vec2 u_imageTo_dimensions;
uniform vec2 uOffset;
uniform vec2 u_imageFrom_offset;
uniform vec2 u_imageTo_offset;
uniform sampler2D imageFrom;
uniform sampler2D imageTo;
uniform float overallAlpha;
Expand All @@ -16,15 +18,28 @@ void main() {
// 采样品质由引擎侧 setImageSampler(filterQuality) 控制:
// - FilterQuality.none → 最近邻(像素风立绘保持锐利)
// - FilterQuality.high → 双线性(平滑过渡)
vec2 uv = (FlutterFragCoord().xy - uOffset) / uSize;
vec2 frag_coord = FlutterFragCoord().xy;
vec2 from_uv = (frag_coord - u_imageFrom_offset) / u_imageFrom_size;
vec2 to_uv = (frag_coord - u_imageTo_offset) / u_imageTo_size;
bool inside_from = all(greaterThanEqual(from_uv, vec2(0.0))) &&
all(lessThanEqual(from_uv, vec2(1.0)));
bool inside_to = all(greaterThanEqual(to_uv, vec2(0.0))) &&
all(lessThanEqual(to_uv, vec2(1.0)));

// 按各图尺寸做半像素内缩,避免采样到边缘外(双线性模式下的边缘渗色)。
vec2 from_halfTexel = 0.5 / max(u_imageFrom_dimensions, vec2(1.0));
vec2 to_halfTexel = 0.5 / max(u_imageTo_dimensions, vec2(1.0));
vec2 from_uv = clamp(uv, from_halfTexel, vec2(1.0) - from_halfTexel);
vec2 to_uv = clamp(uv, to_halfTexel, vec2(1.0) - to_halfTexel);
from_uv = clamp(from_uv, from_halfTexel, vec2(1.0) - from_halfTexel);
to_uv = clamp(to_uv, to_halfTexel, vec2(1.0) - to_halfTexel);

vec4 from_color = texture(imageFrom, from_uv);
vec4 to_color = texture(imageTo, to_uv);
vec4 from_color = vec4(0.0);
vec4 to_color = vec4(0.0);
if (inside_from) {
from_color = texture(imageFrom, from_uv);
}
if (inside_to) {
to_color = texture(imageTo, to_uv);
}

fragColor = mix(from_color, to_color, progress) * overallAlpha;
}
1 change: 1 addition & 0 deletions Engine/lib/sakiengine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export 'src/config/saki_engine_config.dart';
export 'src/core/game_module.dart';
export 'src/core/script_canvas.dart';
export 'src/core/module_registry.dart' show registerProjectModule;
export 'src/utils/asset_path_utils.dart' show isFileSystemAssetPath;
45 changes: 23 additions & 22 deletions Engine/lib/src/app/saki_engine_entry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:just_audio_media_kit/just_audio_media_kit.dart';
import 'package:sakiengine/src/config/runtime_project_config.dart';
import 'package:sakiengine/src/config/saki_engine_config.dart';
import 'package:sakiengine/src/core/game_module.dart';
Expand Down Expand Up @@ -145,12 +144,9 @@ Future<void> _openShowcaseResourceDirectoryIfNeeded() async {
if (Platform.isMacOS) {
opened = await _startDetachedProcess('open', <String>[path]);
} else if (Platform.isWindows) {
opened = await _startDetachedProcess(
'explorer',
<String>[
path,
],
runInShell: true);
opened = await _startDetachedProcess('explorer', <String>[
path,
], runInShell: true);
} else if (Platform.isLinux) {
opened = await _startDetachedProcess('xdg-open', <String>[path]);
if (!opened) {
Expand Down Expand Up @@ -425,7 +421,7 @@ class _GameContainerState extends State<GameContainer> with WindowListener {
final sessionKey = saveSlot == null
? 'new_game:${initialScript.isEmpty ? 'start' : initialScript}'
: 'save:${saveSlot.id}:${saveSlot.currentScript}:'
'${saveSlot.saveTime.microsecondsSinceEpoch}';
'${saveSlot.saveTime.microsecondsSinceEpoch}';
currentScreen = gameModule.createGamePlayScreen(
key: ValueKey(sessionKey),
saveSlotToLoad: saveSlot,
Expand All @@ -449,6 +445,7 @@ Future<void> runSakiEngine({
String? gamePath,
int steamAppId = 3536120,
bool enableSteamworks = true,
bool restoreStartupWindowBounds = true,
bool useNearestNeighborSampling = false,
}) async {
setupDebugLogger();
Expand Down Expand Up @@ -509,14 +506,6 @@ Future<void> runSakiEngine({
]);
}

JustAudioMediaKit.ensureInitialized(
android: false,
iOS: false,
macOS: false,
windows: true,
linux: true,
);

if (!kIsWeb) {
await PlatformWindowManager.ensureInitialized();
await PlatformWindowManager.setPreventClose(true);
Expand All @@ -529,11 +518,9 @@ Future<void> runSakiEngine({

await SakiEngineConfig().loadConfig();
await SettingsManager().init();
if (!kIsWeb) {
await PlatformWindowManager.applyStartupWindowSizeForAspectRatio(
SettingsManager().currentGameWindowAspectRatio,
);
}
final startupWindowAspectRatio = !kIsWeb && restoreStartupWindowBounds
? SettingsManager().currentGameWindowAspectRatio
: null;
frameRateBinding.attachSettingsSync();
await LocalizationManager().init();
await UISoundManager().initialize();
Expand All @@ -558,6 +545,19 @@ Future<void> runSakiEngine({
);

runApp(const SakiEngineApp());
if (startupWindowAspectRatio != null) {
// Resizing the native macOS window before Flutter has submitted its
// first surface frame makes ResizeSynchronizer wait and time out.
// Restore the persisted bounds only after a frame can satisfy the
// resize handshake.
WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(
PlatformWindowManager.applyStartupWindowSizeForAspectRatio(
startupWindowAspectRatio,
),
);
});
}
},
zoneSpecification: ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
Expand Down Expand Up @@ -656,7 +656,8 @@ class _SakiEngineAppState extends State<SakiEngineApp> {
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
theme: customTheme ??
theme:
customTheme ??
ThemeData(
primarySwatch: Colors.blue,
fontFamily: 'SourceHanSansCN',
Expand Down
Loading
Loading