Skip to content
Open
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
94 changes: 94 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,97 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

publish-linux-appimage:
name: Linux AppImage x86_64
needs: publish-tauri
permissions:
contents: write
runs-on: ubuntu-22.04
steps:
- name: Проверка исходного кода
uses: actions/checkout@v4

- name: Установка системных зависимостей
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev \
patchelf libmpv-dev mpv ffmpeg mediainfo libfuse2 curl git cmake ninja-build \
meson python3-dev cython3 nasm libvulkan-dev glslang-tools libavcodec-dev \
libavformat-dev libavutil-dev libswresample-dev libswscale-dev libass-dev \
libplacebo-dev libpipewire-0.3-dev libpulse-dev libx11-dev libwayland-dev \
wayland-protocols libegl1-mesa-dev libzimg-dev

- name: Настройка Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Настройка Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Кэширование сборок Rust
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'

- name: Синхронизация версии
shell: bash
run: |
tag='${{ github.ref_name }}'
version="${tag#v}"
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
version="$(node -p "require('./package.json').version")"
fi
npm version "$version" --no-git-tag-version
sed -i -E "0,/^version = \"[^\"]+\"/s//version = \"$version\"/" src-tauri/Cargo.toml
echo "VERSION=$version" >> "$GITHUB_ENV"

- name: Сборка самодостаточного AppImage
run: bash scripts/build-linux-appimage.sh

- name: Публикация AppImage
run: |
tag='${{ github.ref_name }}'
[[ "$tag" =~ ^v[0-9] ]] || tag="v$VERSION"
gh release upload "$tag" dist-linux/*.AppImage --clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

publish-arch-linux:
name: Arch Linux package x86_64
needs: publish-tauri
permissions:
contents: write
runs-on: ubuntu-latest
container:
image: archlinux:base-devel
steps:
- name: Установка инструментов CI
run: pacman -Syu --noconfirm git github-cli nodejs npm rust webkit2gtk-4.1 gtk3 ffmpeg mediainfo cmake ninja meson python cython vulkan-headers glslang libplacebo libass zimg patchelf

- name: Проверка исходного кода
uses: actions/checkout@v4

- name: Сборка pacman-пакета
shell: bash
run: |
tag='${{ github.ref_name }}'
version="${tag#v}"
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
version="$(node -p "require('./package.json').version")"
fi
sed -i "s/^pkgver=.*/pkgver=$version/" packaging/arch/PKGBUILD
useradd -m builder
chown -R builder:builder "$GITHUB_WORKSPACE"
su builder -c "cd '$GITHUB_WORKSPACE/packaging/arch' && makepkg --printsrcinfo > .SRCINFO && makepkg --noconfirm --cleanbuild --nodeps"
echo "VERSION=$version" >> "$GITHUB_ENV"

- name: Публикация Arch Linux пакета и PKGBUILD
shell: bash
run: |
tag='${{ github.ref_name }}'
[[ "$tag" =~ ^v[0-9] ]] || tag="v$VERSION"
gh release upload "$tag" packaging/arch/*.pkg.tar.zst packaging/arch/PKGBUILD packaging/arch/.SRCINFO packaging/arch/l-mpv.desktop --clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ Portable-L-MPV*
*.tar.gz
*.rar
scratch/
/dist-linux/
/packaging/arch/*.pkg.tar.zst
/.runtime/



Expand Down Expand Up @@ -98,4 +101,3 @@ scratch/
.agents/rules/
agents/rules/
.codebase-memory/*

195 changes: 150 additions & 45 deletions README.md

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions native/linux-ncnn/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
cmake_minimum_required(VERSION 3.20)
project(lmpv_ncnn LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

find_package(ncnn CONFIG REQUIRED)
find_path(VAPOURSYNTH_INCLUDE_DIR VapourSynth4.h PATH_SUFFIXES vapoursynth REQUIRED)

add_library(lmpv_ncnn SHARED lmpv_ncnn.cpp)
target_include_directories(lmpv_ncnn PRIVATE "${VAPOURSYNTH_INCLUDE_DIR}")
target_link_libraries(lmpv_ncnn PRIVATE ncnn)
set_target_properties(lmpv_ncnn PROPERTIES
PREFIX "lib"
OUTPUT_NAME "lmpv_ncnn"
INSTALL_RPATH "$ORIGIN/../lib"
)

install(TARGETS lmpv_ncnn LIBRARY DESTINATION vapoursynth)
179 changes: 179 additions & 0 deletions native/linux-ncnn/lmpv_ncnn.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// L-MPV NCNN/Vulkan VapourSynth filter.
// Accepts planar RGB float frames and preconverted NCNN .param/.bin models.

#include <VapourSynth4.h>
#include <VSHelper4.h>
#include <gpu.h>
#include <net.h>

#include <algorithm>
#include <cstdint>
#include <memory>
#include <string>

struct FilterData {
VSNode *node{};
VSVideoInfo vi{};
ncnn::Net net;
int input_index{};
int output_index{};
int scale{2};
int tile_size{256};
int overlap{16};
};

static void set_filter_error(VSFrameContext *ctx, const VSAPI *vsapi, const std::string &message) {
vsapi->setFilterError(("L-MPV NCNN: " + message).c_str(), ctx);
}

static const VSFrame *VS_CC get_frame(int n, int activation_reason, void *instance_data,
void **, VSFrameContext *frame_ctx, VSCore *core,
const VSAPI *vsapi) {
auto *d = static_cast<FilterData *>(instance_data);
if (activation_reason == arInitial) {
vsapi->requestFrameFilter(n, d->node, frame_ctx);
return nullptr;
}
if (activation_reason != arAllFramesReady)
return nullptr;

const VSFrame *src = vsapi->getFrameFilter(n, d->node, frame_ctx);
const int src_w = vsapi->getFrameWidth(src, 0);
const int src_h = vsapi->getFrameHeight(src, 0);
const int dst_w = src_w * d->scale;
const int dst_h = src_h * d->scale;
VSFrame *dst = vsapi->newVideoFrame(&d->vi.format, dst_w, dst_h, src, core);

const int tile = std::max(32, d->tile_size);
const int overlap = std::clamp(d->overlap, 0, tile / 3);
const int step = std::max(1, tile - overlap * 2);

for (int top = 0;; top = std::min(top + step, std::max(0, src_h - tile))) {
const int tile_h = std::min(tile, src_h - top);
for (int left = 0;; left = std::min(left + step, std::max(0, src_w - tile))) {
const int tile_w = std::min(tile, src_w - left);
ncnn::Mat input(tile_w, tile_h, 3, sizeof(float), 1);
for (int plane = 0; plane < 3; ++plane) {
const auto *source = reinterpret_cast<const float *>(vsapi->getReadPtr(src, plane));
const ptrdiff_t stride = vsapi->getStride(src, plane) / sizeof(float);
float *target = input.channel(plane);
for (int y = 0; y < tile_h; ++y)
std::copy_n(source + (top + y) * stride + left, tile_w, target + y * tile_w);
}

ncnn::Extractor extractor = d->net.create_extractor();
if (extractor.input(d->input_index, input) != 0) {
vsapi->freeFrame(src);
vsapi->freeFrame(dst);
set_filter_error(frame_ctx, vsapi, "failed to upload an input tile");
return nullptr;
}
ncnn::Mat output;
if (extractor.extract(d->output_index, output) != 0 || output.empty()) {
vsapi->freeFrame(src);
vsapi->freeFrame(dst);
set_filter_error(frame_ctx, vsapi, "Vulkan inference failed");
return nullptr;
}
if (output.w != tile_w * d->scale || output.h != tile_h * d->scale || output.c < 3) {
vsapi->freeFrame(src);
vsapi->freeFrame(dst);
set_filter_error(frame_ctx, vsapi, "model output shape does not match the selected 2x scale");
return nullptr;
}

const int crop_l = left == 0 ? 0 : overlap * d->scale;
const int crop_t = top == 0 ? 0 : overlap * d->scale;
const int crop_r = left + tile_w == src_w ? 0 : overlap * d->scale;
const int crop_b = top + tile_h == src_h ? 0 : overlap * d->scale;
for (int plane = 0; plane < 3; ++plane) {
auto *target = reinterpret_cast<float *>(vsapi->getWritePtr(dst, plane));
const ptrdiff_t stride = vsapi->getStride(dst, plane) / sizeof(float);
const float *source = output.channel(plane);
for (int y = crop_t; y < output.h - crop_b; ++y) {
std::copy_n(source + y * output.w + crop_l, output.w - crop_l - crop_r,
target + (top * d->scale + y) * stride + left * d->scale + crop_l);
}
}

if (left + tile_w == src_w)
break;
}
if (top + tile_h == src_h)
break;
}

vsapi->freeFrame(src);
return dst;
}

static void VS_CC free_filter(void *instance_data, VSCore *, const VSAPI *vsapi) {
auto *d = static_cast<FilterData *>(instance_data);
vsapi->freeNode(d->node);
delete d;
}

static void VS_CC create_filter(const VSMap *in, VSMap *out, void *, VSCore *core,
const VSAPI *vsapi) {
auto d = std::make_unique<FilterData>();
d->node = vsapi->mapGetNode(in, "clip", 0, nullptr);
const VSVideoInfo *input_vi = vsapi->getVideoInfo(d->node);
if (!vsh::isConstantVideoFormat(input_vi) || input_vi->format.colorFamily != cfRGB ||
input_vi->format.sampleType != stFloat || input_vi->format.bitsPerSample != 32) {
vsapi->mapSetError(out, "L-MPV NCNN: input must be constant RGBS");
vsapi->freeNode(d->node);
return;
}

int error = 0;
const char *param_path = vsapi->mapGetData(in, "param_path", 0, &error);
const char *model_path = vsapi->mapGetData(in, "model_path", 0, &error);
d->scale = static_cast<int>(vsapi->mapGetInt(in, "scale", 0, &error));
if (error) d->scale = 2;
d->tile_size = static_cast<int>(vsapi->mapGetInt(in, "tile_size", 0, &error));
if (error) d->tile_size = 256;
d->overlap = static_cast<int>(vsapi->mapGetInt(in, "overlap", 0, &error));
if (error) d->overlap = 16;
const bool fp16 = !!vsapi->mapGetInt(in, "fp16", 0, &error);
int device_error = 0;
const int requested_device = static_cast<int>(vsapi->mapGetInt(in, "device_id", 0, &device_error));

ncnn::create_gpu_instance();
const int device_id = device_error ? ncnn::get_default_gpu_index() : requested_device;
ncnn::VulkanDevice *device = ncnn::get_gpu_device(device_id);
if (!device) {
vsapi->mapSetError(out, "L-MPV NCNN: no Vulkan compute device found");
vsapi->freeNode(d->node);
return;
}
d->net.opt.use_vulkan_compute = true;
d->net.opt.use_fp16_packed = fp16;
d->net.opt.use_fp16_storage = fp16;
d->net.set_vulkan_device(device);
if (d->net.load_param(param_path) != 0 || d->net.load_model(model_path) != 0 ||
d->net.input_indexes().empty() || d->net.output_indexes().empty()) {
vsapi->mapSetError(out, "L-MPV NCNN: failed to load .param/.bin model pair");
vsapi->freeNode(d->node);
return;
}
d->input_index = d->net.input_indexes().front();
d->output_index = d->net.output_indexes().front();
d->vi = *input_vi;
d->vi.width *= d->scale;
d->vi.height *= d->scale;

FilterData *data = d.release();
VSFilterDependency dependency[] = {{data->node, rpStrictSpatial}};
vsapi->createVideoFilter(out, "LmpvNcnn", &data->vi, get_frame, free_filter,
fmParallelRequests, dependency, 1, data, core);
}

VS_EXTERNAL_API(void) VapourSynthPluginInit2(VSPlugin *plugin, const VSPLUGINAPI *vspapi) {
vspapi->configPlugin("io.github.menely.lmpv.ncnn", "lmpvncnn",
"L-MPV NCNN Vulkan Upscaler", VS_MAKE_VERSION(1, 0),
VAPOURSYNTH_API_VERSION, 0, plugin);
vspapi->registerFunction(
"Model",
"clip:vnode;param_path:data;model_path:data;scale:int:opt;tile_size:int:opt;overlap:int:opt;fp16:int:opt;device_id:int:opt;",
"clip:vnode;", create_filter, nullptr, plugin);
}
33 changes: 33 additions & 0 deletions packaging/arch/.SRCINFO
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
pkgbase = l-mpv
pkgdesc = Modern desktop media player powered by Tauri and libmpv
pkgver = 2.0.2
pkgrel = 1
url = https://github.com/Menely/L-MPV
arch = x86_64
license = MIT
makedepends = cargo
makedepends = npm
makedepends = rust
makedepends = git
makedepends = cmake
makedepends = ninja
makedepends = meson
makedepends = cython
makedepends = vulkan-headers
makedepends = glslang
makedepends = patchelf
depends = ffmpeg
depends = mediainfo
depends = webkit2gtk-4.1
depends = gtk3
depends = python
depends = vulkan-icd-loader
depends = libplacebo
depends = libass
depends = zimg
source = l-mpv-2.0.2.tar.gz::https://github.com/Menely/L-MPV/archive/refs/tags/v2.0.2.tar.gz
source = l-mpv.desktop
sha256sums = SKIP
sha256sums = SKIP

pkgname = l-mpv
39 changes: 39 additions & 0 deletions packaging/arch/PKGBUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Maintainer: L-MPV contributors
pkgname=l-mpv
pkgver=2.0.2
pkgrel=1
pkgdesc='Modern desktop media player powered by Tauri and libmpv'
arch=('x86_64')
url='https://github.com/Menely/L-MPV'
license=('MIT')
depends=('ffmpeg' 'mediainfo' 'webkit2gtk-4.1' 'gtk3' 'python' 'vulkan-icd-loader' 'libplacebo' 'libass' 'zimg')
makedepends=('cargo' 'npm' 'rust' 'git' 'cmake' 'ninja' 'meson' 'cython' 'vulkan-headers' 'glslang' 'patchelf')
source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz"
'l-mpv.desktop')
sha256sums=('SKIP' 'SKIP')

prepare() {
cd "L-MPV-$pkgver"
npm version "$pkgver" --no-git-tag-version
sed -i -E "0,/^version = \"[^\"]+\"/s//version = \"$pkgver\"/" src-tauri/Cargo.toml
npm ci
}

build() {
cd "L-MPV-$pkgver"
L_MPV_RUNTIME_BUILD_DIR="$srcdir/runtime-build" \
L_MPV_RUNTIME_PREFIX="$srcdir/runtime" \
bash scripts/build-linux-runtime.sh
npm run build
cargo build --manifest-path src-tauri/Cargo.toml --release
}

package() {
cd "L-MPV-$pkgver"
install -Dm755 src-tauri/target/release/l-mpv "$pkgdir/usr/bin/l-mpv"
install -d "$pkgdir/usr/lib/l-mpv"
cp -a "$srcdir/runtime/." "$pkgdir/usr/lib/l-mpv/"
install -Dm644 src-tauri/icons/128x128.png "$pkgdir/usr/share/icons/hicolor/128x128/apps/l-mpv.png"
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
install -Dm644 "$srcdir/l-mpv.desktop" "$pkgdir/usr/share/applications/l-mpv.desktop"
}
10 changes: 10 additions & 0 deletions packaging/arch/l-mpv.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=L-MPV
Comment=Modern media player powered by libmpv
Exec=l-mpv %U
Icon=l-mpv
Terminal=false
Categories=AudioVideo;Player;
MimeType=video/mp4;video/x-matroska;video/x-msvideo;video/quicktime;video/webm;video/mp2t;video/mpeg;audio/mpeg;audio/flac;audio/x-wav;audio/aac;audio/ogg;audio/mp4;audio/opus;
StartupNotify=true
Loading