diff --git a/binding/HarfBuzzSharp.NativeAssets.Tizen/HarfBuzzSharp.NativeAssets.Tizen.csproj b/binding/HarfBuzzSharp.NativeAssets.Tizen/HarfBuzzSharp.NativeAssets.Tizen.csproj index dd508ffaaed..ed07d6530e2 100644 --- a/binding/HarfBuzzSharp.NativeAssets.Tizen/HarfBuzzSharp.NativeAssets.Tizen.csproj +++ b/binding/HarfBuzzSharp.NativeAssets.Tizen/HarfBuzzSharp.NativeAssets.Tizen.csproj @@ -1,13 +1,12 @@  - $(TFMPrevious)-tizen$(TPVTizenPrevious);$(TFMCurrent)-tizen$(TPVTizenCurrent) + $(TFMPrevious)-tizen$(TPVTizenPrevious);$(TFMCurrent)-tizen$(TPVTizenCurrent);$(TFMMinimum)-tizen$(TPVTizenMinimum) HarfBuzzSharp $(PackagingGroup) - Native Assets for Tizen - diff --git a/binding/IncludeNativeAssets.HarfBuzzSharp.targets b/binding/IncludeNativeAssets.HarfBuzzSharp.targets index 6910ecf96ac..2f2e5d0f370 100644 --- a/binding/IncludeNativeAssets.HarfBuzzSharp.targets +++ b/binding/IncludeNativeAssets.HarfBuzzSharp.targets @@ -48,7 +48,7 @@ false - bin\runtimes\linux-x86\native\ + bin\runtimes\tizen-x86\native\ libHarfBuzzSharp.so @@ -58,7 +58,7 @@ - + diff --git a/binding/IncludeNativeAssets.SkiaSharp.targets b/binding/IncludeNativeAssets.SkiaSharp.targets index b8ea70d9274..f24bf18cb3d 100644 --- a/binding/IncludeNativeAssets.SkiaSharp.targets +++ b/binding/IncludeNativeAssets.SkiaSharp.targets @@ -48,7 +48,7 @@ false - bin\runtimes\linux-x86\native\ + bin\runtimes\tizen-x86\native\ libSkiaSharp.so diff --git a/binding/SkiaSharp.NativeAssets.Tizen/SkiaSharp.NativeAssets.Tizen.csproj b/binding/SkiaSharp.NativeAssets.Tizen/SkiaSharp.NativeAssets.Tizen.csproj index 5f1439c56d7..30af474560a 100644 --- a/binding/SkiaSharp.NativeAssets.Tizen/SkiaSharp.NativeAssets.Tizen.csproj +++ b/binding/SkiaSharp.NativeAssets.Tizen/SkiaSharp.NativeAssets.Tizen.csproj @@ -1,13 +1,12 @@  - $(TFMPrevious)-tizen$(TPVTizenPrevious);$(TFMCurrent)-tizen$(TPVTizenCurrent) + $(TFMPrevious)-tizen$(TPVTizenPrevious);$(TFMCurrent)-tizen$(TPVTizenCurrent);$(TFMMinimum)-tizen$(TPVTizenMinimum) SkiaSharp $(PackagingGroup) - Native Assets for Tizen - diff --git a/binding/SkiaSharp/GRGlInterface.cs b/binding/SkiaSharp/GRGlInterface.cs index 7518a696737..d0a023812f6 100644 --- a/binding/SkiaSharp/GRGlInterface.cs +++ b/binding/SkiaSharp/GRGlInterface.cs @@ -210,6 +210,7 @@ private partial class EvasGlLoader private readonly IntPtr glEvas; private readonly EvasGlApi api; +#if NET7_0_OR_GREATER [LibraryImport (libevas)] internal static partial IntPtr evas_gl_api_get (IntPtr evas_gl); @@ -221,6 +222,19 @@ private partial class EvasGlLoader [LibraryImport(libevas)] internal static partial IntPtr evas_gl_proc_address_get (IntPtr evas_gl, [MarshalAs (UnmanagedType.LPStr)] string name); +#else + [DllImport (libevas)] + internal static extern IntPtr evas_gl_api_get (IntPtr evas_gl); + + [DllImport (libevas)] + internal static extern IntPtr evas_gl_context_api_get (IntPtr evas_gl, IntPtr ctx); + + [DllImport (libevas)] + internal static extern IntPtr evas_gl_current_context_get (IntPtr evas_gl); + + [DllImport (libevas)] + internal static extern IntPtr evas_gl_proc_address_get (IntPtr evas_gl, [MarshalAs (UnmanagedType.LPStr)] string name); +#endif static EvasGlLoader () { diff --git a/build.cake b/build.cake index e4c6916eee1..d103f7e7e19 100644 --- a/build.cake +++ b/build.cake @@ -175,9 +175,10 @@ Information($" {"GIT_URL".PadRight(30)} {{0}}", GIT_URL); #load "./scripts/cake/msbuild.cake" #load "./scripts/cake/UtilsManaged.cake" +#load "./scripts/cake/samples.cake" #load "./scripts/cake/externals.cake" #load "./scripts/cake/UpdateDocs.cake" -#load "./scripts/cake/samples.cake" + Task ("__________________________________") .Description ("__________________________________________________"); @@ -465,10 +466,6 @@ Task ("tests-wasm") } }); -//////////////////////////////////////////////////////////////////////////////////////////////////// -// SAMPLES - the demo apps showing off the work (tasks defined in scripts/cake/samples.cake) -//////////////////////////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////////////////////////// // NUGET - building the package for NuGet.org //////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/samples/Basic/Tizen/README.md b/samples/Basic/Tizen/README.md new file mode 100644 index 00000000000..258222e528b --- /dev/null +++ b/samples/Basic/Tizen/README.md @@ -0,0 +1,53 @@ +# SkiaSharp Tizen Sample + +Demonstrates SkiaSharp views in a Tizen NUI app with a `TabView` for navigating between CPU rendering, GPU shader animation, and interactive drawing. + +## Sample Pages + +This sample uses `Tizen.NUI.Components.TabView` with three tabs, each demonstrating a different SkiaSharp rendering approach. + +### CPU + +A static scene rendered on the CPU — a radial gradient background overlaid with semi-transparent colored circles and centered "SkiaSharp" text. + +**Features:** + +- **`SKCanvasView`** — Software-rendered canvas integrated into the NUI view tree. +- **`SKShader`** — Radial gradient background created with `SKShader.CreateRadialGradient`. +- **`SKCanvas.DrawCircle`** — Semi-transparent colored circles composited over the gradient. +- **`SKCanvas.DrawText`** — Centered "SkiaSharp" text rendered with measured alignment. + +### GPU + +A real-time animated shader running at full frame rate via the NUI GPU surface, with touch interaction. + +**Features:** + +- **`SKGLSurfaceView`** — GPU-composited canvas using `NativeImageQueue` double-buffering through the NUI rendering pipeline. +- **`SKRuntimeEffect`** — SkSL metaball "lava lamp" shader compiled at runtime with `SKRuntimeEffect.BuildShader`. +- **Render loop** — Continuous animation driven by a `Timer` at ~60 fps with an FPS counter overlay. +- **Touch interaction** — Touch position is passed as a shader uniform (`iTouchPos`) to create an interactive white blob. + +### Drawing + +A freehand drawing canvas with a color palette toolbar and touch-based input. + +**Features:** + +- **`SKCanvasView`** — Software-rendered canvas invalidated on demand after each stroke or clear. +- **`SKPath`** — Freehand strokes captured as paths with `MoveTo` and `LineTo` from NUI touch events. +- **Color palette** — Six selectable color swatches with visual selection indicator. +- **Clear button** — Removes all strokes and resets the canvas. + +## Requirements + +- [.NET 10 SDK](https://dotnet.microsoft.com/download) or later +- Tizen workload: `dotnet workload install tizen` + +## Running the Sample + +Build: + +```bash +dotnet build -f net10.0-tizen +``` diff --git a/samples/Basic/Tizen/SkiaSharpSample/App.cs b/samples/Basic/Tizen/SkiaSharpSample/App.cs index 955878bcfda..79cf57e1b97 100644 --- a/samples/Basic/Tizen/SkiaSharpSample/App.cs +++ b/samples/Basic/Tizen/SkiaSharpSample/App.cs @@ -1,72 +1,45 @@ -using System; -using ElmSharp; -using Tizen.Applications; +using Tizen.NUI; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Components; -using SkiaSharp; -using SkiaSharp.Views.Tizen; +namespace SkiaSharpSample; -namespace SkiaSharpSample +public class App : NUIApplication { - public class App : CoreUIApplication - { - public static void Main(string[] args) - { - Elementary.Initialize(); - Elementary.ThemeOverlay(); + public static SamplePage DefaultPage { get; set; } = SamplePage.Cpu; - var app = new App(); - app.Run(args); - } + private GpuPage? gpuPage; - protected override void OnCreate() - { - base.OnCreate(); + public static void Main(string[] args) => new App().Run(args); - Initialize(); - } + protected override void OnCreate() + { + base.OnCreate(); - private void Initialize() - { - var window = new Window("SkiaSharp"); - window.BackButtonPressed += OnBackButtonPressed; - window.AvailableRotations = DisplayRotation.Degree_0 | DisplayRotation.Degree_180 | DisplayRotation.Degree_270 | DisplayRotation.Degree_90; - window.Show(); + GetDefaultWindow().KeyEvent += OnKeyEvent; - var skiaView = new SKCanvasView(window); - skiaView.IgnorePixelScaling = true; - skiaView.PaintSurface += OnPaintSurface; - skiaView.Show(); + var tabView = new TabView + { + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.MatchParent, + }; - var conformant = new Conformant(window); - conformant.Show(); - conformant.SetContent(skiaView); - } + tabView.AddTab(new TabButton { Text = "CPU" }, new CpuPage()); - private void OnPaintSurface(object sender, SKPaintSurfaceEventArgs e) - { - // the the canvas and properties - var canvas = e.Surface.Canvas; + gpuPage = new GpuPage(); + tabView.AddTab(new TabButton { Text = "GPU" }, gpuPage); - // make sure the canvas is blank - canvas.Clear(SKColors.White); + tabView.AddTab(new TabButton { Text = "Drawing" }, new DrawingPage()); - // draw some text - using var paint = new SKPaint - { - Color = SKColors.Black, - IsAntialias = true, - Style = SKPaintStyle.Fill - }; - using var font = new SKFont - { - Size = 24 - }; - var coord = new SKPoint(e.Info.Width / 2, (e.Info.Height + font.Size) / 2); - canvas.DrawText("SkiaSharp", coord, SKTextAlign.Center, font, paint); - } + GetDefaultWindow().Add(tabView); + } - private void OnBackButtonPressed(object sender, EventArgs e) + void OnKeyEvent(object? sender, Window.KeyEventArgs e) + { + if (e.Key.State == Key.StateType.Down && + (e.Key.KeyPressedName == "XF86Back" || e.Key.KeyPressedName == "Escape")) { + gpuPage?.StopAnimation(); Exit(); } } diff --git a/samples/Basic/Tizen/SkiaSharpSample/CpuPage.cs b/samples/Basic/Tizen/SkiaSharpSample/CpuPage.cs new file mode 100644 index 00000000000..e06b402dc99 --- /dev/null +++ b/samples/Basic/Tizen/SkiaSharpSample/CpuPage.cs @@ -0,0 +1,93 @@ +using System; +using Tizen.NUI; +using Tizen.NUI.BaseComponents; + +using SkiaSharp; +using SkiaSharp.Views.Tizen; +using SKCanvasView = SkiaSharp.Views.Tizen.NUI.SKCanvasView; + +namespace SkiaSharpSample; + +public class CpuPage : View +{ + static readonly (float X, float Y, float R, SKColor Color)[] circles = + { + (0.20f, 0.30f, 0.10f, new SKColor(0xFF, 0x4D, 0x66, 0xCC)), + (0.75f, 0.25f, 0.08f, new SKColor(0x4D, 0xB3, 0xFF, 0xCC)), + (0.15f, 0.70f, 0.07f, new SKColor(0xFF, 0x99, 0x1A, 0xCC)), + (0.80f, 0.70f, 0.12f, new SKColor(0x66, 0xFF, 0xB3, 0xCC)), + (0.50f, 0.15f, 0.06f, new SKColor(0xB3, 0x4D, 0xFF, 0xCC)), + (0.40f, 0.80f, 0.09f, new SKColor(0xFF, 0xE6, 0x33, 0xCC)), + }; + + static readonly SKColor[] gradientColors = + { + new SKColor(0x44, 0x88, 0xFF), + new SKColor(0x88, 0x33, 0xCC), + }; + + readonly FpsCounter fpsCounter = new(); + readonly TextLabel fpsLabel; + readonly SKCanvasView skiaView; + + public CpuPage() + { + WidthSpecification = LayoutParamPolicies.MatchParent; + HeightSpecification = LayoutParamPolicies.MatchParent; + + skiaView = new SKCanvasView + { + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.MatchParent, + }; + skiaView.PaintSurface += OnPaintSurface; + + fpsLabel = new TextLabel + { + Text = "FPS: --", + TextColor = Tizen.NUI.Color.White, + PointSize = 10, + Position = new Position(10, 10), + }; + + Add(skiaView); + Add(fpsLabel); + + fpsCounter.Start(); + } + + void OnPaintSurface(object? sender, SKPaintSurfaceEventArgs e) + { + var canvas = e.Surface.Canvas; + var width = e.Info.Width; + var height = e.Info.Height; + var center = new SKPoint(width / 2f, height / 2f); + var radius = Math.Max(width, height) / 2f; + + canvas.Clear(SKColors.White); + + // Gradient background + using var shader = SKShader.CreateRadialGradient( + center, radius, gradientColors, SKShaderTileMode.Clamp); + using var bgPaint = new SKPaint { IsAntialias = true, Shader = shader }; + canvas.DrawRect(0, 0, width, height, bgPaint); + + // Translucent circles + using var circlePaint = new SKPaint { IsAntialias = true, Style = SKPaintStyle.Fill }; + foreach (var (x, y, r, color) in circles) + { + circlePaint.Color = color; + canvas.DrawCircle(x * width, y * height, r * Math.Min(width, height), circlePaint); + } + + // Centered title text + using var textPaint = new SKPaint { Color = SKColors.White, IsAntialias = true }; + using var font = new SKFont { Size = width * 0.12f }; + canvas.DrawText("SkiaSharp", center.X, center.Y + font.Size / 3f, + SKTextAlign.Center, font, textPaint); + + // FPS + if (fpsCounter.Tick() is double fps) + fpsLabel.Text = $"FPS: {fps:F0}"; + } +} diff --git a/samples/Basic/Tizen/SkiaSharpSample/DrawingPage.cs b/samples/Basic/Tizen/SkiaSharpSample/DrawingPage.cs new file mode 100644 index 00000000000..dcc90600ee6 --- /dev/null +++ b/samples/Basic/Tizen/SkiaSharpSample/DrawingPage.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using Tizen.NUI; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Components; + +using SkiaSharp; +using SkiaSharp.Views.Tizen; +using SKCanvasView = SkiaSharp.Views.Tizen.NUI.SKCanvasView; + +namespace SkiaSharpSample; + +public class DrawingPage : View +{ + record struct Stroke(SKPath Path, SKColor Color, float Width); + + static readonly SKColor[] palette = + { + SKColors.Black, + new SKColor(0xE5, 0x39, 0x35), // red + new SKColor(0x1E, 0x88, 0xE5), // blue + new SKColor(0x43, 0xA0, 0x47), // green + new SKColor(0xFB, 0x8C, 0x00), // orange + new SKColor(0x8E, 0x24, 0xAA), // purple + }; + + readonly List strokes = new(); + readonly SKCanvasView skiaView; + readonly FpsCounter fpsCounter = new(); + readonly TextLabel fpsLabel; + + SKPath? currentPath; + SKColor currentColor = SKColors.Black; + float brushSize = 6f; + bool isDrawing; + View? selectedSwatch; + + public DrawingPage() + { + WidthSpecification = LayoutParamPolicies.MatchParent; + HeightSpecification = LayoutParamPolicies.MatchParent; + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Vertical, + }; + + // Canvas area (fills remaining space) + var canvasContainer = new View + { + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = 0, + Weight = 1f, + }; + + skiaView = new SKCanvasView + { + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.MatchParent, + }; + skiaView.PaintSurface += OnPaintSurface; + skiaView.TouchEvent += OnDrawingTouch; + + fpsLabel = new TextLabel + { + Text = "FPS: --", + TextColor = new Tizen.NUI.Color(0.3f, 0.3f, 0.3f, 1f), + PointSize = 10, + Position = new Position(10, 10), + }; + + canvasContainer.Add(skiaView); + canvasContainer.Add(fpsLabel); + + Add(canvasContainer); + Add(CreateToolbar()); + + fpsCounter.Start(); + } + + View CreateToolbar() + { + var toolbar = new View + { + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = 60, + BackgroundColor = new Tizen.NUI.Color(0.95f, 0.95f, 0.95f, 1f), + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Horizontal, + CellPadding = new Size2D(8, 0), + LinearAlignment = LinearLayout.Alignment.CenterVertical, + Padding = new Extents(12, 12, 0, 0), + }, + }; + + bool first = true; + foreach (var color in palette) + { + var swatch = new View + { + Size = new Size(36, 36), + BackgroundColor = new Tizen.NUI.Color( + color.Red / 255f, color.Green / 255f, color.Blue / 255f, 1f), + CornerRadius = 18f, + BorderlineWidth = first ? 3f : 0f, + BorderlineColor = new Tizen.NUI.Color(0.2f, 0.5f, 1f, 1f), + }; + + if (first) + { + selectedSwatch = swatch; + first = false; + } + + var capturedColor = color; + swatch.TouchEvent += (s, e) => + { + if (e.Touch.GetState(0) != PointStateType.Down) + return true; + + currentColor = capturedColor; + + if (selectedSwatch != null) + selectedSwatch.BorderlineWidth = 0f; + if (s is View v) + { + v.BorderlineWidth = 3f; + v.BorderlineColor = new Tizen.NUI.Color(0.2f, 0.5f, 1f, 1f); + selectedSwatch = v; + } + return true; + }; + + toolbar.Add(swatch); + } + + // Spacer + toolbar.Add(new View { WidthSpecification = 0, Weight = 1f, HeightSpecification = 1 }); + + // Clear button + var clearBtn = new Button { Text = "Clear", Size = new Size(80, 40) }; + clearBtn.Clicked += (s, e) => + { + foreach (var stroke in strokes) + stroke.Path.Dispose(); + strokes.Clear(); + currentPath?.Dispose(); + currentPath = null; + isDrawing = false; + skiaView.Invalidate(); + }; + toolbar.Add(clearBtn); + + return toolbar; + } + + void OnPaintSurface(object? sender, SKPaintSurfaceEventArgs e) + { + var canvas = e.Surface.Canvas; + canvas.Clear(SKColors.White); + + using var paint = new SKPaint + { + IsAntialias = true, + Style = SKPaintStyle.Stroke, + StrokeCap = SKStrokeCap.Round, + StrokeJoin = SKStrokeJoin.Round, + }; + + foreach (var stroke in strokes) + { + paint.Color = stroke.Color; + paint.StrokeWidth = stroke.Width; + canvas.DrawPath(stroke.Path, paint); + } + + if (currentPath != null) + { + paint.Color = currentColor; + paint.StrokeWidth = brushSize; + canvas.DrawPath(currentPath, paint); + } + + if (fpsCounter.Tick() is double fps) + fpsLabel.Text = $"FPS: {fps:F0}"; + } + + bool OnDrawingTouch(object sender, View.TouchEventArgs e) + { + var touch = e.Touch; + if (touch.GetPointCount() < 1) + return true; + + var state = touch.GetState(0); + var pos = touch.GetLocalPosition(0); + + switch (state) + { + case PointStateType.Down: + isDrawing = true; + currentPath = new SKPath(); + currentPath.MoveTo(pos.X, pos.Y); + break; + + case PointStateType.Motion: + if (isDrawing && currentPath != null) + currentPath.LineTo(pos.X, pos.Y); + break; + + case PointStateType.Up: + case PointStateType.Leave: + if (isDrawing && currentPath != null) + { + strokes.Add(new Stroke(currentPath, currentColor, brushSize)); + currentPath = null; + isDrawing = false; + } + break; + } + + skiaView.Invalidate(); + return true; + } +} diff --git a/samples/Basic/Tizen/SkiaSharpSample/FpsCounter.cs b/samples/Basic/Tizen/SkiaSharpSample/FpsCounter.cs new file mode 100644 index 00000000000..136fcdccef6 --- /dev/null +++ b/samples/Basic/Tizen/SkiaSharpSample/FpsCounter.cs @@ -0,0 +1,50 @@ +using System.Diagnostics; + +namespace SkiaSharpSample; + +/// +/// Lightweight frame-rate counter. Call once per frame; +/// it returns the measured FPS each time the sampling interval elapses. +/// Also exposes so GPU pages can feed shader time +/// from the same clock without a separate . +/// +public sealed class FpsCounter +{ + private readonly Stopwatch stopwatch = new(); + + private int frameCount; + private double lastSampleTime; + + /// + /// Seconds between FPS samples (default 0.5 s). + /// + public double Interval { get; set; } = 0.5; + + /// + /// Wall-clock seconds since was called. + /// Useful as a shader iTime uniform. + /// + public float ElapsedSeconds => (float)stopwatch.Elapsed.TotalSeconds; + + public void Start() => stopwatch.Start(); + + public void Stop() => stopwatch.Stop(); + + /// + /// Record one frame. Returns the measured FPS when the sampling + /// interval has elapsed, or null otherwise. + /// + public double? Tick() + { + frameCount++; + var now = stopwatch.Elapsed.TotalSeconds; + var delta = now - lastSampleTime; + if (delta < Interval) + return null; + + var fps = frameCount / delta; + frameCount = 0; + lastSampleTime = now; + return fps; + } +} diff --git a/samples/Basic/Tizen/SkiaSharpSample/GpuPage.cs b/samples/Basic/Tizen/SkiaSharpSample/GpuPage.cs new file mode 100644 index 00000000000..6fe9e22e19c --- /dev/null +++ b/samples/Basic/Tizen/SkiaSharpSample/GpuPage.cs @@ -0,0 +1,201 @@ +using System; +using Tizen.NUI; +using Tizen.NUI.BaseComponents; + +using SkiaSharp; +using SkiaSharp.Views.Tizen; +using SKCanvasView = SkiaSharp.Views.Tizen.NUI.SKCanvasView; + +namespace SkiaSharpSample; + +public class GpuPage : View +{ + const string SkslSource = @" +uniform float iTime; +uniform float2 iResolution; +uniform float2 iTouchPos; +uniform float iTouchActive; +uniform float3 iColors[6]; + +half4 main(float2 fragCoord) { + float2 uv = fragCoord / iResolution; + float aspect = iResolution.x / iResolution.y; + float2 st = float2(uv.x * aspect, uv.y); + float t = iTime; + float field = 0.0; + float3 weighted = float3(0.0); + for (int i = 0; i < 6; i++) { + float fi = float(i); + float phase = fi * 1.047; + float speed = 0.3 + fi * 0.07; + float2 center = float2( + aspect * 0.5 + 0.4 * sin(t * speed + phase) * cos(t * speed * 0.6 + fi), + 0.5 + 0.4 * cos(t * speed * 0.8 + phase * 1.3) * sin(t * speed * 0.4 + fi * 0.7) + ); + float2 d = st - center; + float r = length(d); + float strength = 0.030 / (r * r + 0.002); + field += strength; + weighted += iColors[i] * strength; + } + if (iTouchActive > 0.5) { + float2 touchSt = float2(iTouchPos.x * aspect, iTouchPos.y); + float2 d = st - touchSt; + float r = length(d); + float strength = 0.050 / (r * r + 0.002); + field += strength; + weighted += float3(1.0, 0.95, 0.9) * strength; + } + float3 blobColor = weighted / max(field, 0.001); + float edge = smoothstep(5.0, 8.0, field); + float innerGlow = smoothstep(8.0, 20.0, field) * 0.3; + float3 bg = float3(0.03, 0.02, 0.08); + bg += float3(0.02, 0.01, 0.03) * sin(t * 0.2 + uv.y * 3.0); + float halo = smoothstep(3.0, 5.0, field) * (1.0 - edge); + float3 result = bg; + result += blobColor * halo * 0.4; + result = mix(result, blobColor * (1.0 + innerGlow), edge); + float2 vc = uv - 0.5; + float vignette = 1.0 - dot(vc, vc) * 0.8; + result *= vignette; + return half4(clamp(result, 0.0, 1.0), 1.0); +} +"; + + static readonly float[] blobColors = + { + 1.0f, 0.3f, 0.4f, // rose + 0.3f, 0.7f, 1.0f, // sky blue + 1.0f, 0.6f, 0.1f, // amber + 0.4f, 1.0f, 0.7f, // mint + 0.7f, 0.3f, 1.0f, // violet + 1.0f, 0.9f, 0.2f, // yellow + }; + + readonly FpsCounter fpsCounter = new(); + readonly SKPaint shaderPaint = new(); + readonly TextLabel fpsLabel; + readonly SKCanvasView skiaView; + + SKRuntimeShaderBuilder? shaderBuilder; + Timer? renderTimer; + SKPoint touchPos; + bool touchActive; + + public GpuPage() + { + WidthSpecification = LayoutParamPolicies.MatchParent; + HeightSpecification = LayoutParamPolicies.MatchParent; + + skiaView = new SKCanvasView + { + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.MatchParent, + }; + skiaView.PaintSurface += OnPaintSurface; + skiaView.TouchEvent += OnTouch; + + fpsLabel = new TextLabel + { + Text = "FPS: --", + TextColor = Tizen.NUI.Color.White, + PointSize = 10, + Position = new Position(10, 10), + }; + + Add(skiaView); + Add(fpsLabel); + + // Build the SkSL shader (may fail on some devices) + try + { + shaderBuilder = SKRuntimeEffect.BuildShader(SkslSource); + } + catch + { + // Shader compilation not supported — will show fallback + } + + StartAnimation(); + } + + public void StartAnimation() + { + renderTimer?.Stop(); + fpsCounter.Start(); + fpsLabel.Text = "FPS: --"; + renderTimer = new Timer(16); // ~60 fps + renderTimer.Tick += (s, e) => + { + skiaView.Invalidate(); + return true; + }; + renderTimer.Start(); + } + + public void StopAnimation() + { + renderTimer?.Stop(); + renderTimer = null; + fpsCounter.Stop(); + } + + void OnPaintSurface(object? sender, SKPaintSurfaceEventArgs e) + { + var canvas = e.Surface.Canvas; + var width = e.Info.Width; + var height = e.Info.Height; + + if (shaderBuilder == null) + { + // Fallback when SkSL is not available + canvas.Clear(new SKColor(0x08, 0x05, 0x14)); + using var paint = new SKPaint { Color = SKColors.White, IsAntialias = true }; + using var font = new SKFont { Size = width * 0.06f }; + canvas.DrawText("SkSL shader not supported", + width / 2f, height / 2f, SKTextAlign.Center, font, paint); + return; + } + + shaderBuilder.Uniforms["iTime"] = fpsCounter.ElapsedSeconds; + shaderBuilder.Uniforms["iResolution"] = new float[] { width, height }; + shaderBuilder.Uniforms["iTouchPos"] = new float[] { touchPos.X, touchPos.Y }; + shaderBuilder.Uniforms["iTouchActive"] = touchActive ? 1f : 0f; + shaderBuilder.Uniforms["iColors"] = blobColors; + + using var shader = shaderBuilder.Build(); + shaderPaint.Shader = shader; + canvas.DrawRect(0, 0, width, height, shaderPaint); + + if (fpsCounter.Tick() is double fps) + fpsLabel.Text = $"FPS: {fps:F0}"; + } + + bool OnTouch(object sender, View.TouchEventArgs e) + { + var touch = e.Touch; + if (touch.GetPointCount() < 1) + return true; + + var state = touch.GetState(0); + var pos = touch.GetLocalPosition(0); + var w = skiaView.Size.Width; + var h = skiaView.Size.Height; + + switch (state) + { + case PointStateType.Down: + case PointStateType.Motion: + touchActive = true; + if (w > 0 && h > 0) + touchPos = new SKPoint(pos.X / w, pos.Y / h); + break; + case PointStateType.Up: + case PointStateType.Leave: + touchActive = false; + break; + } + + return true; + } +} diff --git a/samples/Basic/Tizen/SkiaSharpSample/SamplePage.cs b/samples/Basic/Tizen/SkiaSharpSample/SamplePage.cs new file mode 100644 index 00000000000..db1e7ea945c --- /dev/null +++ b/samples/Basic/Tizen/SkiaSharpSample/SamplePage.cs @@ -0,0 +1,8 @@ +namespace SkiaSharpSample; + +public enum SamplePage +{ + Cpu, + Gpu, + Drawing, +} diff --git a/samples/Basic/Tizen/SkiaSharpSample/SkiaSharpSample.csproj b/samples/Basic/Tizen/SkiaSharpSample/SkiaSharpSample.csproj index e95eefac9f5..c17ebcda141 100644 --- a/samples/Basic/Tizen/SkiaSharpSample/SkiaSharpSample.csproj +++ b/samples/Basic/Tizen/SkiaSharpSample/SkiaSharpSample.csproj @@ -1,8 +1,8 @@ - + - net10.0-tizen Exe + net10.0-tizen diff --git a/samples/Basic/Tizen/SkiaSharpSample/tizen-manifest.xml b/samples/Basic/Tizen/SkiaSharpSample/tizen-manifest.xml index c29cd21f032..6bc731d28eb 100644 --- a/samples/Basic/Tizen/SkiaSharpSample/tizen-manifest.xml +++ b/samples/Basic/Tizen/SkiaSharpSample/tizen-manifest.xml @@ -1,10 +1,15 @@ - - - - - - SkiaSharpSample.png - - - + + + + + + SkiaSharpSample.png + + diff --git a/samples/Basic/Tizen/SkiaSharpSample/tizen_dotnet_project.yaml b/samples/Basic/Tizen/SkiaSharpSample/tizen_dotnet_project.yaml new file mode 100644 index 00000000000..eb0b38b0214 --- /dev/null +++ b/samples/Basic/Tizen/SkiaSharpSample/tizen_dotnet_project.yaml @@ -0,0 +1,26 @@ +# csproj file path +csproj_file: SkiaSharpSample.csproj + +# Default profile, Tizen API version +profile: tizen +api_version: "10.0" + +# Build type [Debug/ Release/ Test] +build_type: Debug + +# Signing profile to be used for Tizen package signing +# If value is empty: "", active signing profile will be used +# Else If value is ".", default signing profile will be used +signing_profile: "" + +# files monitored for dirty/modified status +files: + - SkiaSharpSample.csproj + - App.cs + - FpsCounter.cs + - SamplePage.cs + - tizen-manifest.xml + - shared/res/SkiaSharpSample.png + +# project dependencies +deps: [] diff --git a/scripts/install-tizen.ps1 b/scripts/install-tizen.ps1 index f2fb8a74022..619e5130f01 100644 --- a/scripts/install-tizen.ps1 +++ b/scripts/install-tizen.ps1 @@ -3,7 +3,7 @@ # - https://developercommunity.visualstudio.com/content/problem/661596/the-updated-path-doesnt-kick-in.html Param( - [string] $Version = "5.6", + [string] $Version = "6.1", [string] $InstallDestination = $null, [boolean] $UpgradeLLVM = $true ) @@ -33,7 +33,7 @@ Write-Host "Install destination is '$ts'..." $tsTemp = Join-Path "$HOME_DIR" "tizen-temp" $install = Join-Path "$tsTemp" "tizen-install.$ext" -$packages = "MOBILE-6.0-NativeAppDevelopment" +$packages = "MOBILE-6.0-NativeAppDevelopment,TIZEN-8.0-NativeAppDevelopment" # download Write-Host "Downloading SDK to '$install'..." @@ -46,6 +46,11 @@ Write-Host "JAVA_HOME is: $env:JAVA_HOME" Write-Host "PATH contains JAVA_HOME: $($env:PATH.Contains("$env:JAVA_HOME"))" & "java" -version +# warn if there is an existing install (the 6.1 installer may fail if the directory already exists) +if (Test-Path "$ts") { + Write-Host "##[warning]Existing Tizen Studio installation found at '$ts'. The installer may fail if this directory already exists." +} + # install Write-Host "Installing SDK to '$ts'..." if ($IsMacOS -or $IsLinux) { diff --git a/source/SkiaSharp.Build.props b/source/SkiaSharp.Build.props index cda85145b9c..ae278d52546 100644 --- a/source/SkiaSharp.Build.props +++ b/source/SkiaSharp.Build.props @@ -64,6 +64,8 @@ net9.0 net10.0 + + net6.0 @@ -84,6 +86,8 @@ 26.2 10.0 10.0.19041.0 + + 8.0 @@ -115,9 +119,13 @@ $(TFMBase) + + $(TFMMinimum) + $(TFMMinimum)-tizen$(TPVTizenMinimum) + - $(BasicTargetFrameworksCurrent);$(BasicTargetFrameworksPrevious);$(BasicTargetFrameworksBase) - $(PlatformTargetFrameworksCurrent);$(PlatformTargetFrameworksPrevious) + $(BasicTargetFrameworksCurrent);$(BasicTargetFrameworksPrevious);$(BasicTargetFrameworksBase);$(BasicTargetFrameworksMinimum) + $(PlatformTargetFrameworksCurrent);$(PlatformTargetFrameworksPrevious);$(PlatformTargetFrameworksMinimum) $(BasicTargetFrameworks);$(PlatformTargetFrameworks)