Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NUI.Scene3D] Add capture to Scene3D.SceneView #6271

Merged
merged 1 commit into from
Aug 22, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ internal static partial class SceneView

[global::System.Runtime.InteropServices.DllImport(Libraries.Scene3D, EntryPoint = "CSharp_Dali_SceneView_CameraTransitionFinishedSignal_Disconnect")]
public static extern void CameraTransitionFinishedDisconnect(global::System.Runtime.InteropServices.HandleRef actor, global::System.Runtime.InteropServices.HandleRef handler);

[global::System.Runtime.InteropServices.DllImport(Libraries.Scene3D, EntryPoint = "CSharp_Dali_SceneView_Capture")]
public static extern int Capture(global::System.Runtime.InteropServices.HandleRef sceneView, global::System.Runtime.InteropServices.HandleRef camera, global::System.Runtime.InteropServices.HandleRef size);

[global::System.Runtime.InteropServices.DllImport(Libraries.Scene3D, EntryPoint = "CSharp_Dali_SceneView_CaptureFinishedSignal_Connect")]
public static extern void CaptureFinishedConnect(global::System.Runtime.InteropServices.HandleRef actor, global::System.Runtime.InteropServices.HandleRef handler);

[global::System.Runtime.InteropServices.DllImport(Libraries.Scene3D, EntryPoint = "CSharp_Dali_SceneView_CaptureFinishedSignal_Disconnect")]
public static extern void CaptureFinishedDisconnect(global::System.Runtime.InteropServices.HandleRef actor, global::System.Runtime.InteropServices.HandleRef handler);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright(c) 2024 Samsung Electronics Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

using System;
using System.ComponentModel;

namespace Tizen.NUI.Scene3D
{
/// <summary>
/// Event arguments of SceneView capture finished event.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public class CaptureFinishedEventArgs : EventArgs
{
/// <summary>
/// Integer ID of the capture request.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public int CaptureId
{
get; set;
}

/// <summary>
/// ImageUrl of the captured result
/// If the capture is failed, it is null.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public ImageUrl CapturedImageUrl
{
get; set;
}
}
}
176 changes: 176 additions & 0 deletions src/Tizen.NUI.Scene3D/src/public/Controls/SceneView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
using Tizen.NUI;
using Tizen.NUI.Binding;
using Tizen.NUI.BaseComponents;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Diagnostics;

namespace Tizen.NUI.Scene3D
{
Expand Down Expand Up @@ -68,14 +71,24 @@ namespace Tizen.NUI.Scene3D
/// <since_tizen> 10 </since_tizen>
public class SceneView : View
{
private Dictionary<int, TaskCompletionSource<ImageUrl>> asyncCaptureIds = new();
private string skyboxUrl;

// CameraTransitionFinishedEvent
private EventHandler cameraTransitionFinishedEventHandler;
private CameraTransitionFinishedEventCallbackType cameraTransitionFinishedEventCallback;

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void CameraTransitionFinishedEventCallbackType(IntPtr data);

// CaptureFinishedEvent
private EventHandler<CaptureFinishedEventArgs> captureFinishedEventHandler;
private EventHandler<CaptureFinishedEventArgs> asyncCaptureFinishedEventHandler;
private CaptureFinishedEventCallbackType captureFinishedEventCallback;

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void CaptureFinishedEventCallbackType(IntPtr data, int captureId, IntPtr capturedImageUrl);

internal SceneView(global::System.IntPtr cPtr, bool cMemoryOwn) : this(cPtr, cMemoryOwn, cMemoryOwn)
{
}
Expand Down Expand Up @@ -104,6 +117,15 @@ protected override void Dispose(DisposeTypes type)
cameraTransitionFinishedEventCallback = null;
}

if (captureFinishedEventCallback != null)
{
NUILog.Debug($"[Dispose] captureFinishedEventCallback");

Interop.SceneView.CaptureFinishedDisconnect(GetBaseHandleCPtrHandleRef, captureFinishedEventCallback.ToHandleRef(this));
NDalicPINVOKE.ThrowExceptionIfExistsDebug();
captureFinishedEventCallback = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SceneView CaptureFinished 이벤트가 오기 전에 SceneView 자기 자신이 Dispose() 될 수 있으니, 이 경우 CaptureAsync 를 기다리는 유저한테 실패정보를 알려주기 위해서 TaskCompletionSource<ImageUrl> 을 private 변수로 만들고, 이 Dispose 타이밍에 SetResult() 를 해줘야 할 것 같습니다.

CaptureFinished 콜백 형태로만 제공한 경우에는 지금처럼 Dispose 된 경우에 별도로 앱한테 DIspose 여부를 알려줄 필요가 없었을텐데...Task<> 로 제공해버린 경우 SetResult 가 오기 전까지 await 를 걸어버린 함수가 계속 남아있을 것이기 때문에 (아마도?) 강제로 SetResult 를 해줘야만 한다..라고 이해를 하고있는데요..

이렇게 Dispose 가 되는 도중 (~= Disposed 가 아직 false 이지만 기본적인 리소스들이 한창 해제가 되고있는 와중에) 유저가 추가적으로 무언가 작업을 할 수 있도록 하는 여지를 만드는게 과연 좋은 일인지 모르겠습니다.

유저가 이 Dispose 로 인해서 Capture 가 취소된 경우를 별도로 처리할 수 있게 하기 위해서, 결국 Capture Result 의 종류를 3가지 이상 (성공 / 실패 / Dispose 로 인한 취소 등) 을 만들어야하고, 특히 Dispose 로 인한 취소가 결과물로 넘어간 경우에는 SceneView 에 접근하는 어떤 동작도 구현해서는 안된다는 제약사항을 주석으로만 남겨야하는데... 앱 개발자들이 이 동작을 잘 납득하고 그대로 따라줄지는 모르겠습니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

특히 이 "Dispose 로 인한 취소" 같은 경우에는 CaptureFinished 콜백을 사용하는 경우에는 필요없는 정보이기 때문에, CaptureAsync 에서만 사용되게 될텐데... 앞으로 Task<> 형태로 return 하는 API 를 계속해서 추가할 때마다 '실제 return 정보 + Dispose 로 인한 취소 여부' 정보를 포함한 struct 를 새로 정의해서 Task 의 return 타입으로 만들어나가면 되는건지.. 의견을 구합니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capture Finished전에 SceneView SceneOff하는 부분에 대해서 Fail 올리는 것으로 하겠습니다.

이 문제 겸사겸사 찝찝했던 것을 해결하는 겸사겸사 SceneOff 타이밍이나 SceneView 소멸 시점에 실패 콜백을 올려주도록 수정 할 계획입니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인해보니 SceneOff때는 이미 남은 캡쳐 요청들에 대해서 실패 콜백을 올려주고 있었습니다.

}

LayoutCount = 0;

base.Dispose(type);
Expand Down Expand Up @@ -170,6 +192,64 @@ public event EventHandler CameraTransitionFinished
}
}

/// <summary>
/// An event emitted when Capture is finished.
/// If Capture is successed, CaptureFinishedEventArgs includes finished capture ID and ImageUrl of the captured image.
/// If Capture is failed, the ImageUrl is null.
/// </summary>
// This will be public opened after ACR done. (Before ACR, need to be hidden as Inhouse API)
[EditorBrowsable(EditorBrowsableState.Never)]
public event EventHandler<CaptureFinishedEventArgs> CaptureFinished
{
add
{
if (captureFinishedEventHandler == null)
{
captureFinishedEventCallback = OnCaptureFinished;
Interop.SceneView.CaptureFinishedConnect(SwigCPtr, captureFinishedEventCallback.ToHandleRef(this));
NDalicPINVOKE.ThrowExceptionIfExists();
}
captureFinishedEventHandler += value;
}

remove
{
captureFinishedEventHandler -= value;
if (captureFinishedEventHandler == null && captureFinishedEventCallback != null)
{
Interop.SceneView.CaptureFinishedDisconnect(SwigCPtr, captureFinishedEventCallback.ToHandleRef(this));
NDalicPINVOKE.ThrowExceptionIfExists();
captureFinishedEventCallback = null;
}
}
}

/// <summary>
/// An event emitted when CaptureAsync is finished.
/// </summary>
private event EventHandler<CaptureFinishedEventArgs> AsyncCaptureFinished
{
add
{
if (asyncCaptureFinishedEventHandler == null)
{
CaptureFinished += dummy;
}
asyncCaptureFinishedEventHandler += value;
}

remove
{
asyncCaptureFinishedEventHandler -= value;
if (asyncCaptureFinishedEventHandler == null)
{
CaptureFinished += dummy;
}
}
}

void dummy(object sender, CaptureFinishedEventArgs e) {}

/// <summary>
/// Set/Get the ImageBasedLight ScaleFactor.
/// Scale factor controls light source intensity in [0.0f, 1.0f]
Expand Down Expand Up @@ -573,6 +653,82 @@ public void ResetResolution()
if (NDalicPINVOKE.SWIGPendingException.Pending) throw NDalicPINVOKE.SWIGPendingException.Retrieve();
}

/// <summary>
/// Requests to capture this SceneView with the Camera.
/// When the capture is finished, CaptureFinished Event is emited.
/// <see cref="CaptureFinishedEventArgs"/> includes <see cref="CaptureFinishedEventArgs.CaptureId"/> and <see cref="CaptureFinishedEventArgs.CapturedImageUrl"/>.
/// If the capture is successful, the <see cref="CaptureFinishedEventArgs.CapturedImageUrl"/> contains url of captured image.
/// If the capture fails, the <see cref="CaptureFinishedEventArgs.CapturedImageUrl"/> is null.
/// </summary>
/// <param name="camera">Camera to be used for capture.</param>
/// <param name="size">captured size.</param>
/// <remarks>
/// The input camera should not be used for any other purpose during Capture.
/// (Simultaneous usage elsewhere may result in incorrect rendering.)
/// The camera is required to be added in this SceneView. (Not need to be a selected camera)
/// If the SceneView is disconnected from Scene, the left capture requests are canceled with fail.
/// </remarks>
/// <returns> capture id that id unique value to distinguish each requiest.</returns>
// This will be public opened after ACR done. (Before ACR, need to be hidden as Inhouse API)
[EditorBrowsable(EditorBrowsableState.Never)]
public int Capture(Scene3D.Camera camera, Vector2 size)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dali 구조와 상관 없이 순수 API사용자 관점에서 Capture API의 위치는 camera객체가 갖고 있어야 하는게 아닐까요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

특정 행위에 대한 결과물이 비동기적으로 전달되는 경우에는
C#에서는 이벤트보다는 Task를 사용하는 것이 좋기는 합니다.

Suggested change
public int Capture(Scene3D.Camera camera, Vector2 size)
public Task<ImageUrl> Capture(Scene3D.Camera camera, Vector2 size)

이런 API가 제공되면 앱 개발자는 이렇게 단순하게 코드 작성이 가능해 집니다.

 var url = await sceneView.Caputre(camea, new Vector2(100, 100));
 imageView.ResourceUrl = url.ToString();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ImageUrl class 는 UI Thread 에서만 생성되어야하는 class 이기 때문에 Task 로 만들 수 없습니다. ImageUrl class 는 외부 요인에 의해 생성된 이미지요소(ex : Texture, NativeImage 등,)의 lifecycle을 관리하기위한 class 이기 때문입니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scene3D 개발자 관점에서, 카메라를 가져오고 설정하는 API 는 모두 SceneView에 있는데 Capture 만 Camera API 로 빠져있는 것이 더 어색해 보입니다.

하나의 Camera 가 동시에 여러 SceneView 에 속해있을 수도 없고, 또한 Camera 자기 자신은 자기가 속해있는 SceneView 도 모르는 상황으로, 정말 Camera 관련 속성들만 제어/관리 하고 있습니다. 이런 상태에서 SceneView 에 의존성이 생기는 API 하나를 추가하는 것은 별로 좋은 구조로 보이지 않습니다.

View.SetRenderEffect(BackgroundBlurEffect) 처럼 RenderEffect 에 대한 구현을 Vjew 가 아니라 RenderEffect 에서 설정하는 것처럼 느껴집니다.

Copy link
Contributor Author

@bshsqa bshsqa Aug 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dali 구조와 상관 없이 순수 API사용자 관점에서 Capture API의 위치는 camera객체가 갖고 있어야 하는게 아닐까요?

말씀하신 형태도 API관점에서 좋은 방법이라고 생각합니다만, 몇가지 문제가 있는데요.

예를들어, Camera가 화면을 촬영하기 위해서 필요한 몇가지 속성 중에 종횡비가 있습니다.
다른 3D 엔진의 경우에는 Window종횡비가 곧 3D Scene의 종횡비이기 때문에 Camera는 대부분의 경우 Window 데이터에만 접근할 수 있으면 되는데요.
NUI 의 SceneView는 2D Content에 들어가는 Component이므로 하나의 Scene에 여러개의 SceneView를 추가할 수 있으며, 여러 SceneView가 하나의 Camera를 돌려 쓸 수 있습니다.
SceneView별로 카메라를 위한 속성이 다르므로, 위에서 말씀드린 종횡비를 포함하여 Capture를 위해 필요한 속성들이 SceneView에 종속적으로 동작하기 때문에, SceneView에서 Capture를 컨트롤 하는 것이 적합합니다.

그 외에도 차치하신 DALi 구조적인 문제로서도 Camera에서 Capture를 제공하기 위해서는 다양한 고통이 필요한데요.
NUI.Scene3D에서는 대부분의 씬을 위한 기능이 SceneView를 통해 제공되고 있기도 하고, 어쨌거나 해당 SceneView에 포함된 Scene의 렌더링 결과를 얻어내고자 하는 API가 SceneView에 있는 것이 큰 문제가 되지는 않을 것이라고 생각됩니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

int ret = DummyAsync().Result;
이걸 쓰셨으니까 그렇습니다. 저 속성은 동기적으로 (thread를 블럭하여) 결과를 기다립니다.

제대로된 사용법으로 사용하면 블럭이 안됩니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await를 통한 대기는 thread 를 블럭시키지 않습니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

가이드 주신대로 여러가지 시도를 해본 결과 OnTouch 는 정상적으로 끝나면서 main thread 에서 발생한 이벤트도 정상적으로 받는 것을 확인했습니다.

        private bool OnTouch(object s, View.TouchEventArgs e)
        {
            if (e.Touch.GetState(0) == PointStateType.Down)
            {
                Tizen.Log.Error("NUI",$"Before ASDF\n");
                ASDF();
                Tizen.Log.Error("NUI",$"End ASDF\n");
            }
            return true;
        }

        private async void ASDF()
        {
            Tizen.Log.Error("NUI",$"Before await\n");
            var task = DummyAsync();
            int ret = await task;
            Tizen.Log.Error("NUI",$"End await {ret}\n");
        }

결국은 async 라는 태그가 달린 별도의 함수를 앱에서 알아서 잘 구현해야만 하고, await 이후 ImageUrl 을 받은 사용자가 무엇을 어떻게 할지는 저희가 아직은 신경 안써도 된다고 이해했습니다.
그렇다면 말씀해주신대로 이후의 작업은 C# 언어 개발자의 숙련도 문제이지, Framework 의 문제가 아닌 것으로 이해되었습니다.

if (e.Success)
{
    ret.SetResult(e.CapturedImageUrl);
}

소소하게 이 부분에 대해서, 우선 e.CapturedImageUrl 은 cMemOwned 가 false 인 상태로 만들어졌기 때문에, 이 상태 그대로 SetResult 에 넣으면 안되고, Registry 에 등록을 한번 해야지 정상적인 사용이 가능할 것으로 보입니다. 승호님께서 구현하실 때 참고바랍니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그런데 다른 질문이 있는데요,

예를 들어 FileIO 에 걸려있는 아무개 Task 라고 하더라도 유저가 원한다면 main loop 에 block 을 걸어서 FileIO 가 끝날때 까지 강제로 기다리게 하는 방법들이 잘 고려가 되어있을 것으로 예상되는데요...

지금의 Capture 처럼 main loop 에 dependency 가 있는 Task 의 경우에는 앱에서 동기적으로 task complete 결과를 기다리는 코드를 사용한 경우 무조건 dead lock 에 걸리게 될 것인데

이러한 사항이 일반적인 것이어서 따로 주석을 달지 않아도 되는건지, 아니면 함수에서 주석으로 적어놓기만 하면 되는건지 궁금합니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저희가... event로 뭔가를 제공할때도..
main loop에서 Sleep, 또는 while로 점유한체 대기하면 event가 오지 않을 것인데
이러한 사항을 remarks에 적지 않는것과 동일한게 아닐까요?

{
int id = Interop.SceneView.Capture(SwigCPtr, camera.SwigCPtr, Vector2.getCPtr(size));
if (NDalicPINVOKE.SWIGPendingException.Pending) throw NDalicPINVOKE.SWIGPendingException.Retrieve();
return id;
}

/// <summary>
/// Requests to capture this SceneView with the Camera asynchronously.
/// </summary>
/// <param name="camera">Camera to be used for capture.</param>
/// <param name="size">captured size.</param>
/// <remarks>
/// The input camera should not be used for any other purpose during Capture.
/// (Simultaneous usage elsewhere may result in incorrect rendering.)
/// The camera is required to be added in this SceneView. (Not need to be a selected camera)
/// If the SceneView is disconnected from Scene, the left capture requests are canceled with fail.
/// </remarks>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains the URL of the captured image.
/// If the capture is successful, the task result is the ImageURL.
/// If the capture fails, the task will complete with an <see cref="InvalidOperationException"/>
/// </returns>
// This will be public opened after ACR done. (Before ACR, need to be hidden as Inhouse API)
[EditorBrowsable(EditorBrowsableState.Never)]
public Task<ImageUrl> CaptureAsync(Scene3D.Camera camera, Vector2 size)
{
void Handler(object _, CaptureFinishedEventArgs e)
{
if (asyncCaptureIds.TryGetValue(e.CaptureId, out var tcs))
{
try
{
if (e.CapturedImageUrl != null)
{
tcs.SetResult(e.CapturedImageUrl);
}
else
{
tcs.SetException(new InvalidOperationException("Fail to Capture"));
}
}
finally
{
AsyncCaptureFinished -= Handler;
asyncCaptureIds.Remove(e.CaptureId);
}
}
};

AsyncCaptureFinished += Handler;
var captureId = Interop.SceneView.Capture(SwigCPtr, camera.SwigCPtr, Vector2.getCPtr(size));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

모종의 이유로 (ex : SceneView 가 SceneOff 상태) 캡쳐가 실패한 경우 Capture API 가 끝나기 전에 CaptureFinished 콜백이 올라오도록 구현되어 있는 것이 가장 마지막 코드인데요, CaptureFinished 콜백을 Idler 에서 올려보내도록 수정 부탁드립니다.

TaskCompletionSource<ImageUrl> ret = new TaskCompletionSource<ImageUrl>();
asyncCaptureIds.Add(captureId, ret);

return ret.Task;
}

internal void SetUseFramebuffer(bool useFramebuffer)
{
Interop.SceneView.UseFramebuffer(SwigCPtr, useFramebuffer);
Expand Down Expand Up @@ -693,5 +849,25 @@ private void OnCameraTransitionFinished(IntPtr data)
{
cameraTransitionFinishedEventHandler?.Invoke(this, EventArgs.Empty);
}

// Callback for capture finished signal
private void OnCaptureFinished(IntPtr data, int captureId, IntPtr capturedImageUrl)
{
CaptureFinishedEventArgs e = new CaptureFinishedEventArgs();
ImageUrl imageUrl = new ImageUrl(NUI.Interop.ImageUrl.NewImageUrl(new ImageUrl(capturedImageUrl, false).SwigCPtr), true);
NDalicPINVOKE.ThrowExceptionIfExists();

e.CaptureId = captureId;
e.CapturedImageUrl = imageUrl.HasBody() ? imageUrl : null;

if (asyncCaptureIds.ContainsKey(e.CaptureId))
{
asyncCaptureFinishedEventHandler?.Invoke(this, e);
}
else
{
captureFinishedEventHandler?.Invoke(this, e);
}
}
}
}
4 changes: 4 additions & 0 deletions src/Tizen.NUI/src/internal/Interop/Interop.ImageUrl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ internal static partial class Interop
{
internal static partial class ImageUrl
{

[DllImport(NDalicPINVOKE.Lib, EntryPoint = "CSharp_Dali_new_ImageUrl_Copy")]
public static extern global::System.IntPtr NewImageUrl(global::System.Runtime.InteropServices.HandleRef csImageUrl);

[DllImport(NDalicPINVOKE.Lib, EntryPoint = "CSharp_Dali_delete_ImageUrl")]
public static extern void Delete(HandleRef jarg1);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,4 @@ public enum VisibilityChangeType
/// <since_tizen> 3 </since_tizen>
PARENT
}

}
Loading
Loading