Closed GavinRay97 closed 2 years ago
Sounds like a great use case. Do you think it should support drawing only or window events too? E.g. mouse move or window resize?
Sounds like a great use case. Do you think it should support drawing only or window events too? E.g. mouse move or window resize?
IMO the ability to respond to those would be really useful -- particularly the mouse movement (a lot of FX use cursor-movement based UI updates), but I'd be grateful for whatever is possible.
Many audio plugins don't handle resizing 😅
Here's a video example of "Responds to mouse movement in UI, but you're SOL if you want to resize the thing"
Looking at the ctor of Window
, it looks like it just takes a pointer?
Is this pointer just a void*
to whatever the platform representation of a Window handle is?
So from the looks of it, it seems like MAYBE this should already work if there were just a secondary constructor on each platform-specific Window
implementation from the Java side, like this:
public class WindowWin32 extends Window {
@ApiStatus.Internal
public WindowWin32() {
// Make a new window
super(_nMake());
}
public WindowWin32(long HWND) {
// Attach to an existing window, using platform-specific pointer
super(HWND);
}
// Or maybe a static method, probably better to be put on "Window" but whatever
public static Window fromPlatformSpecificWindowPointer(long ptr) {
super(ptr);
}
}
And then, I don't really know C++ very well (or Java for that matter lmao 😅) and I've never used JNI, but I think usage would look roughly something like this?
package com.acme;
import org.jetbrains.jwm.*;
class ExampleApp {
public static void initFromNativeHWND(long HWND) {
Window window = new Window(HWND);
// or
Window window = new Window.fromPlatformSpecificWindowPointer(HWND);
// Now, write the rest of the app ;^)
// "Step 2. Draw the rest of the owl"
}
}
// Stuff to init JVM here
bool initJVM() {}
// Invoke our JWN app, handing it the HWND/window handle (pointer) we've already allocated
bool invokeJWMAppWithHWND() {
jclass cls = env->FindClass("com/acme/ExampleApp");
jclass kCls = static_cast<jclass>(env->NewGlobalRef(cls));
// public static void initFromNativeHWND(long HWND)
jmethodID kInitFromNativeHWND = env->GetStaticMethodID(kCls, "initFromNativeHWND", "Ljava/lang/void");
return true;
}
int main() {
// Register the window class.
const wchar_t CLASS_NAME[] = L"Sample Window Class";
WNDCLASS wc = { };
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
RegisterClass(&wc);
// Create the window
HWND hwnd = CreateWindowEx(
0, // Optional window styles.
CLASS_NAME, // Window class
L"Learn to Program Windows", // Window text
WS_OVERLAPPEDWINDOW, // Window style
// Size and position
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, // Parent window
NULL, // Menu
hInstance, // Instance handle
NULL // Additional application data
);
if (hwnd == NULL)
return 0;
// ====================================
// Initialize JVM & environment
// then call "ExampleApp.initFromNativeHWND()" and give it our new window's HWND
if (initJVM()) {
invokeJWMAppWithHWND(hwnd);
return 1;
}
return 0;
}
Also @tonsky, may as well mention it: I'm a big fan of your work =) Thoroughly enjoy reading your content on The Orange Website 🟧 Didn't know you worked for JB, neato
Though I should prolly clarify -- I don't intend to create a JVM in native code (C/C++/Rust etc) and call it this way. (I will if I have to and it's the only way to get it to run in a .dll
/.so
😅)
If this functionality is added, I would try to work on getting GraalVM to compile JWM so that it's useable with native binaries and shared-libs, or Kotlin Native (if that's an option).
And I would use either Graal's @CFunction
or Kotlin Native's equivalent, to create the underlying native window pointer
Interesting! I see some obstacles on your plan:
May I ask, what kind of features do you plan to get access to by using JWM instead of native Win32 APIs? Especially if you are planning to write the rest of your plugin in native code (if I understand you correctly)?
Re: Java Window ctor, it takes a pointer to our own native C++ Window class, e.g. WindowWin32
Re: Java Window ctor, it takes a pointer to our own native C++ Window class, e.g. WindowWin32
Ahh, well it would not be so simple then! Nothing ever is, aye? 😅
May I ask, what kind of features do you plan to get access to by using JWM instead of native Win32 APIs? Especially if you are planning to write the rest of your plugin in native code (if I understand you correctly)?
I want to open up audio plugin development to the JVM, as a platform.
Personally I am not a fan of C++, and I can imagine how nice it would be to be able to write plugins in a "nicer" (sorry) language like Kotlin or Java.
There's no getting around the fact that you need native code to do this -- but that native code doesn't ACTUALLY have to be C/C++.
Implementing Native Methods in Java with Native Image
What I mean by that is, for example there's someone who has ported the VST C++ API's to a C FFI wrapper and then done Kotlin Native on top of that.
Here's a Kotlin Native binding to the EditController
C++ class of the VST SDK:
class EditController(
override val ptr: CPointer<IEditController>
) : PluginBase() {
private val log by logger()
private val this2: VstInterface<CPointer<IEditController2>> = queryVstInterface(IEditController2_iid)
private val thisPtr2: CPointer<IEditController2> get() = this2.ptr.reinterpret()
So my intention was to:
@CFunction
to write bindings in Java to the native code, making it consumable from Java
.dll
/.so
/.dylib
Compiling to GraalVM might get tricky
Yeah definitely -- I see there are issues with it currently and have checked out your notes and build scripts.
I have some familiarity with it, and had planned to troubleshoot getting it working + reach out to the Graal devs on Slack for ideas (I've done this a good number of times and they're really helpful) if I absolutely couldn't make headway on it.
It's possible that:
Which would suck, but that's a "cross that bridge when I get to it -- with a positive attitude" sort of thing haha.
Window events would not probably work as nice when using only part of the window (we didn’t planned for this)
Just to double check, could you elaborate more on this?
I don't actually know very much about windowing API's, does this mean that child windows are more like partial windows rather than regular whole windows?
JWM was designed to control rendering itself (buffer swaps, vsync, see Layer interface). It might get in the way if your host app is doing it for you
I think the host app essentially hands a "blank canvas" window, where you're free to do to the window what you wish. Someone had asked about using VSTGUI
(A UI framework for VST plugins) with OpenGL custom rendering, and the VST devs answered: "Just give VST the HWND
and then have your OpenGL app do the rendering on the window":
After speaking with some folks who are more experienced at Win32 dev, it seems the expected design pattern is to simply re-parent windows.
So if you are given a HWND
to attach to, it's expected you do this by calling SetParent(hWndChild, hWndNewParent);
"Yes, you can reparent literally any window in the system to yourself." "If you're given an HWND, reparenting is basically the expected usage pattern, as you can't do much with that HWND except reparenting to it."
So maybe something roughly like this is an easier implementation?
interface Reparentable {
public void reparentWindowTo(long parentPlatformSpecificWindowHandle);
}
abstract class Window implements Reparentable {
private long platformSpecificWindowHandle; // HWND, XWindowID, NSView, etc
}
class Win32Window extends Window {
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setparent
private native SetParent(long hWndChild, long hWndNewParent);
@Override
public void reparentWindowTo(long parentPlatformSpecificWindowHandle) {
this.SetParent(this.platformSpecificWindowHandle, parentPlatformSpecificWindowHandle);
}
}
And then trying to get JWM built as a native resource with Graal or Kotlin Native should be the only barrier left if re-parenting is also available (and an acceptable design pattern) on other platforms I think?
Window events would not probably work as nice when using only part of the window (we didn’t planned for this)
Just to double check, could you elaborate more on this? I don't actually know very much about windowing API's, does this mean that child windows are more like partial windows rather than regular whole windows?
Me neither :)
The host app essentially hands a "blank canvas" window, where you're free to do to the window what you wish
Just to reiterate: host app creates an empty window for you, and your plugin is running in a different process? And from that process you want to reparent the original window to yourself? So we need something like Window::makeFromHWND(long hwnd)
that would do reparenting internally. Does that sounds like what you need?
host app creates an empty window for you, and your plugin is running in a different process?
Yeah, correct. The architecture of the audio plugin is:
Digital Audio Workstation process
|
VST3 Interfaces (C++)
|
MyPlugin.dll
The DAW host process will allocate an OS-specific empty window, and then call:
IPlugView::attached(void* windowHandle, enum kPlatformName)
And then the plugin is responsible for taking the window handle and figuring out how to draw itself on that window.
Which I guess apparently means:
"Set this window as your app's parent/container window and then spawn your own window as it's child."
On Windows that's HWND
and SetParent()
and on Linux, I believe that's an XID ("Window"
) and XReparentWindow()
:
XReparentWindow(display, w, parent, x, y)
Display *display;
Window w;
Window parent;
int x, y;
And from that process you want to reparent the original window to yourself? So we need something like
Window::makeFromHWND(long hwnd)
that would do reparenting internally. Does that sounds like what you need?
Yeah that sounds fantastic 💯
Ok, I think it’s getting more clearer now. Before, I though that SetParent() has something to with process separation. Now I see they are all in the same process, and SetParent actually requires two windows: parent and child. So it’s a way to put one window inside another.
In light of that, I think the original proposal of Window::setParent(long hwnd)
would serve this purpose better. I am not sure how would one window inside another look or work, but we can start with this method only and you can report what else do you need.
See https://github.com/HumbleUI/JWM/commit/8417f29175c11568a5ec8531e0336bcb0a4f43f9, let me know if it works
Awesome -- thank you!
Going to build this and then try to make a dummy Win32 app that spawns one Window, and then fires up JVM and sends across the HWND
to a Java class method that calls this new function 👍
First time I've hosted the JVM in native code so may take me a couple of hours, hopefully this works and then I can start seeing if there's any headway I can make on getting JWM
to work with Graal. Because at that point, it's just a matter of writing (roughly) this:
@CContext(Main.Directives.class)
class JwmNativeExample {
interface Callback extends CFunctionPointer {
@InvokeCFunctionPointer void invoke(long hwnd);
}
// Exposes "void myEntrypoint(long hwnd);" as a C function when built as static/shared lib w/ Graal
@CEntryPoint
@CEntryPointOptions(prologue = CEntryPointSetup.EnterCreateIsolatePrologue.class,
epilogue = CEntryPointSetup.LeaveTearDownIsolateEpilogue.class)
private static void myEntrypoint(long hwnd) {
}
private static final CEntryPointLiteral<JwmNativeExample.Callback> myEntrypointCallback =
CEntryPointLiteral.create(Main.class, "myEntrypoint");
}
And then now in CXX you can do:
int main()
{
auto jwmLibrary = LoadLibrary("my-jwm-library.dll");
using myEntrypointFn = void(*)(long hwnd);
auto myEntrypoint = (myEntrypointFn)GetProcAddress(hModule, "myEntrypoint");
// pass HWND here
myEntrypoint(hwnd);
}
Of course the whole point of this is to avoid writing any C/C++, so the only thing that has to be done is to expose the proper @CEntryPoint
names (pluginFactory
) from Java/Kotlin/Clojure whatever, and then the VST3 framework will automatically invoke them for you when it imports your plugin library =)
The net result is that you can write audio plugins that export and interface with, native C(++) methods, without writing any C(++) yourself -- able to use Graal to implement the native handlers!
@tonsky It works! Thanks a ton!! ❤️
Check it out: how neat is this? 🙌
Mind-blowing!
Hello, first I'd like to thank the contributors of this library -- it opens up very exciting possibilities and is sorely needed in the ecosystem. So thank you all 😃
My question:
I've been browsing the source, trying to understand if the following scenario is possible:
ParentApp
) and launches it's own window/UIParentApp
creates a new window + handle, let's call thisChildWindowHandle
, and passes it to ourJWM
programChildWindowHandle
pointer and use it to render/attach ourJWM
app UI toHWND
XWindowID
NSView
orHIView
If this sounds strange to you/you can't imagine why you would want to do this:
.dll
/.so
/.dylib
)ChildWindowHandle
to render intoA popular example (my usecase): VST Audio Plugins for DAW's
It would be amazing to be able to author audio plugins (or at least the UI's) in say, Kotlin. General availability to JVM languages would be revolutionary.
For a code short example, essentially what I'm asking is if something like this is possible (minus the low-level X11 stuff, just the bit about receiving and using the
parentWindowHandle
to render):Curious if there's any way to accomplish this currently? Thank you
Boring Contextual Details Below
The way audio plugins work, is that the host application (generally a DAW) allocates a window handle and gives the plugin the window to render into:
https://steinbergmedia.github.io/vst3_doc/base/classSteinberg_1_1IPlugView.html#a9fbc345c1e87f7e6210a8a49fdb96793
https://steinbergmedia.github.io/vst3_doc/base/group__platformUIType.html