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

internal/graphicsdriver/opengl: remove CGO in opengl for macOS #2091

Merged
merged 15 commits into from
May 21, 2022
Merged

internal/graphicsdriver/opengl: remove CGO in opengl for macOS #2091

merged 15 commits into from
May 21, 2022

Conversation

TotallyGamerJet
Copy link
Contributor

internal/os/dl is an implementation of some of dlfcn.h functions (right now just dlsym) in pure go.

internal/os/syscall contains 4 functions that call a C function handle in pure go. The code is an adaptation of code from runtime/sys_darwin_arm64.s & runtime/sys_darwin_amd64.s. It works by doing the necessary steps required to call C and then calls a psuedo C function written in go assembly instead of a CGO generated C function. This pseudo C function places the arguments in the spot where the C function expects them and then calls the C functions.

This is the beginning of converting the darwin portion of ebiten to a pure Go implementation. The benefits of doing so is a smaller executable (because there is no C function for every C call), faster compilation, and eventually cross-compilation. This also lays down the foundation to remove CGO from other unix platforms that have dlfcn.h. All that would need to be done would be to add internal/dl/dl_GOOS.go that links into the unix's libc implementation and then write an implementation of the SyscallX functions for each GOOS and GOARCH.

The only downside to the way this is accomplished is that it uses pragmas such as //go:linkname and //go:cgo_import_dynamic which are used extensively in the go runtime and cgo respectively. There is also a need to maintain the assembly code but that should be easy to match up with the runtime.

…remove CGO for darwin in opengl

internal/os/dl is an implementation of some of dlfcn.h functions (right now just dlsym) in pure go.

internal/os/syscall contains 4 functions that call a C function handle in pure go. The code is an adaptation of code from runtime/sys_darwin_arm64.s & runtime/sys_darwin_amd64.s. It works by doing the necessary steps required to call C and then calls a psuedo C function written in go assembly instead of a CGO generated C function. This pseudo C function places the arguments in the spot where the C function expects them and then calls the C functions.
…remove CGO for darwin in opengl

internal/os/dl is an implementation of some of dlfcn.h functions (right now just dlsym) in pure go.

internal/os/syscall contains 4 functions that call a C function handle in pure go. The code is an adaptation of code from runtime/sys_darwin_arm64.s & runtime/sys_darwin_amd64.s. It works by doing the necessary steps required to call C and then calls a psuedo C function written in go assembly instead of a CGO generated C function. This pseudo C function places the arguments in the spot where the C function expects them and then calls the C functions.
@TotallyGamerJet
Copy link
Contributor Author

It made be advisable to make SyscallX, and Syscall6 a wrapper of SyscallX9 so that there is less assembly to maintain. Also, if it is really necessary to remove the go:linkname pragmas a short assembly function could probably replace those three functions. It'd likely have the signature func call(fn, args unsafe.Pointer).

these functions are not meant to be called so don't have parameters as if they should be
@hajimehoshi
Copy link
Owner

hajimehoshi commented May 13, 2022

Hi, thank you for the nice PR!

  • Have you confirmed the examples worked on actual devices with your PR?
  • We also have an independent package github.com/hajimehoshi/oto/v2. Would it be nicer to have an independent package for your syscall and dl?

@hajimehoshi
Copy link
Owner

hajimehoshi commented May 13, 2022

Another question:

This is the beginning of converting the darwin portion of ebiten to a pure Go implementation. The benefits of doing so is a smaller executable (because there is no C function for every C call), faster compilation, and eventually cross-compilation. This also lays down the foundation to remove CGO from other unix platforms that have dlfcn.h. All that would need to be done would be to add internal/dl/dl_GOOS.go that links into the unix's libc implementation and then write an implementation of the SyscallX functions for each GOOS and GOARCH.

So, even if we can remove Cgo from internal/graphics/opengl, we still require Cgo for other internal packages. Some packages use Objective-C directly. One tricky thing is that there is original Objective-C classes (e.g. https://github.com/hajimehoshi/ebiten/blob/main/internal/ui/ui_glfw_darwin.go). Do you think it would be feasible to remove Cgo usages from Ebiten for darwin?

@TotallyGamerJet
Copy link
Contributor Author

  • Have you confirmed the examples worked on actual devices with your PR?

This PR only affects macOS. I have confirmed that all the tests passed on macOS arm64&amd64. I also ran all the examples on arm64 and most on amd64. All the examples I tried functioned properly.

  • We also have an independent package github.com/hajimehoshi/oto/v2. Would it be nicer to have an independent package for your syscall and dl?

I think it would be. The code in those two packages are useful for any package that wants to replace CGO with pure go. I didn't know what your policy was on external dependencies so I thought to just put it in ebiten to prove that it worked. Would you separate it out into hajimehoshi/syscall & dl or would you want me to create a separate repo?

So, even if we can remove Cgo from internal/graphics/opengl, we still require Cgo for other internal packages. Some packages use Objective-C directly. One tricky thing is that there is original Objective-C classes (e.g. https://github.com/hajimehoshi/ebiten/blob/main/internal/ui/ui_glfw_darwin.go). Do you think it would be feasible to remove Cgo usages from Ebiten for darwin?

For macOS, yes. I'm assuming iOS might be able to be built with pure go but Apple still requires a lot of signing stuff so shrug. Oto appears to be simple to port to pure Go. For glfw we'd do the same thing you did on windows (dll) by making a dylib (dynamic library) and then linking to it with dlopen and dlysym (which are pure go :D). Later, it may be nice to port glfw to pure go but that'd be a HUGE task as there is a lot of finicky Objective-C code (see option 2 below for a solution). As for the original Objective-C class, that one's a little bit trickier. There are three possible ways of solving this that I can think of:

  1. Write the the Objective-C code in Go by writing the runtime code by hand
  2. Write an "Objective-Go" to Go transpiler that outputs pure go objective-c calls
  3. Contribute to macdriver by porting it to pure go

The first option is more error-prone but might give a faster and more iterative result. This may be the best for ebiten since there is only 100 lines that actually needs to be implemented as a class and the rest are functions that (at first glance) appeared simple enough to write by hand.

The second option would likely take longer but it would allow for writing code similar to Objective-C. This'll help with maintainability. It could also be used to port other non-ebiten related projects to go.

The third option would be nice because it supports a project meant to work with macOS in Go. The benefit of doing so it that it already has support for creating classes in Go with a decently nice API. However, this project aims to call a lot more Objective-C code than ebiten needs to support so it'd be a big effort to get it to pure go. It also appears slow on adding new functionality based on the open pull requests and the lack of new commits since December.

Ultimately, it's up to you what you think would be best for ebiten as a whole. I will support you in whichever path you decide to take.

@hajimehoshi
Copy link
Owner

hajimehoshi commented May 13, 2022

This PR only affects macOS. I have confirmed that all the tests passed on macOS arm64&amd64. I also ran all the examples on arm64 and most on amd64. All the examples I tried functioned properly.

Thanks. Doesn't this PR affect iOS? (EDIT: You don't have to test iOS. I'll do later if needed)

I think it would be. The code in those two packages are useful for any package that wants to replace CGO with pure go. I didn't know what your policy was on external dependencies so I thought to just put it in ebiten to prove that it worked. Would you separate it out into hajimehoshi/syscall & dl or would you want me to create a separate repo?

Actually I prefer less external dependencies for most cases, but unfortunately Oto is a different package. Having duplicated code for syscall and dl doesn't sound right for their code amount. There are two options:

  1. Create syscall and dl packages under the github.com/ebiten organization
  2. Create syscall and dl packages under the github.com/TotallyGamerJet account

I'm fine with both (I think creating repositories under github.com/hajimehoshi is not an option). I slightly prefer 1, but it is up to you (2. would be easier to manage to you, right?). I can review the code in both cases. What do you think?

For macOS, yes. I'm assuming iOS might be able to be built with pure go but Apple still requires a lot of signing stuff so shrug.

Another thing is that ebitenmobile creates a C shared library for iOS so removing Cgo is impossible :-), we can reduce Cgo though.

Oto appears to be simple to port to pure Go. For glfw we'd do the same thing you did on windows (dll) by making a dylib (dynamic library) and then linking to it with dlopen and dlysym (which are pure go :D). Later, it may be nice to port glfw to pure go but that'd be a HUGE task as there is a lot of finicky Objective-C code (see option 2 below for a solution).

I'm actually working on porting GLFW to Go for some reasons (https://github.com/hajimehoshi/ebiten/tree/issue-1764-windows-go). Yeah this is a pretty hard work. Let's revisit GLFW for macOS later.

As for the original Objective-C class, that one's a little bit trickier. There are three possible ways of solving this that I can think of:

  1. Write the the Objective-C code in Go by writing the runtime code by hand
  2. Write an "Objective-Go" to Go transpiler that outputs pure go objective-c calls
  3. Contribute to macdriver by porting it to pure go

The first option is more error-prone but might give a faster and more iterative result. This may be the best for ebiten since there is only 100 lines that actually needs to be implemented as a class and the rest are functions that (at first glance) appeared simple enough to write by hand.

Is it really possible to dynamically define an Objective-C class in Go?

The second option would likely take longer but it would allow for writing code similar to Objective-C. This'll help with maintainability. It could also be used to port other non-ebiten related projects to go.

Well, I don't think it is feasible to create a transpiler from Objective-C to Go. The code amount is not so huge anyway.

The third option would be nice because it supports a project meant to work with macOS in Go. The benefit of doing so it that it already has support for creating classes in Go with a decently nice API. However, this project aims to call a lot more Objective-C code than ebiten needs to support so it'd be a big effort to get it to pure go. It also appears slow on adding new functionality based on the open pull requests and the lack of new commits since December.

OK so macdriver is not in pure Go (yet), right? I also think that macdriver is general so we should create our own minimal foundation.

Ultimately, it's up to you what you think would be best for ebiten as a whole. I will support you in whichever path you decide to take.

I prefer the first item so far.

@TotallyGamerJet
Copy link
Contributor Author

This PR only affects macOS. I have confirmed that all the tests passed on macOS arm64&amd64. I also ran all the examples on arm64 and most on amd64. All the examples I tried functioned properly.

Thanks. Doesn't this PR affect iOS? (EDIT: You don't have to test iOS. I'll do later if needed)

I just realized that iOS/arm64 is just a pseudo tag for darwin/arm64. The dl and sys call packages probably work on iOS but I haven't tested them. I only removed cgo from the opengl/gl package. That package is only used by macOS, right? If I'm correct iOS still depends on GLES which is still using cgo. However, looking at opengl/gles it should be simple enough to port as well. I think that should be a separate pull request though. This PR should probably be renamed to make it clear it only makes changes to opengl/gl on macOS

Actually I prefer less external dependencies for most cases, but unfortunately Oto is a different package. Having duplicated code for syscall and dl doesn't sound right for their code amount. There are two options:

  1. Create syscall and dl packages under the github.com/ebiten organization
  2. Create syscall and dl packages under the github.com/TotallyGamerJet account

I'm fine with both (I think creating repositories under github.com/hajimehoshi is not an option). I slightly prefer 1, but it is up to you (2. would be easier to manage to you, right?). I can review the code in both cases. What do you think?

I'd prefer having it put under github.com/ebiten organization.

For macOS, yes. I'm assuming iOS might be able to be built with pure go but Apple still requires a lot of signing stuff so shrug.

Another thing is that ebitenmobile creates a C shared library for iOS so removing Cgo is impossible :-), we can reduce Cgo though.

What are the benefits of just reducing cgo? Would it be possible to make ebitenmobile output a pure go binary for iOS in addition to a shared object? This probably should be discussed separately.

I'm actually working on porting GLFW to Go for some reasons (https://github.com/hajimehoshi/ebiten/tree/issue-1764-windows-go). Yeah this is a pretty hard work. Let's revisit GLFW for macOS later.

So hold off on any work on GLFW even dynamically linking to it?

The first option is more error-prone but might give a faster and more iterative result. This may be the best for ebiten since there is only 100 lines that actually needs to be implemented as a class and the rest are functions that (at first glance) appeared simple enough to write by hand.

Is it really possible to dynamically define an Objective-C class in Go?

Yes! Objective-C is just C with a special preprocessor. You can write an entire cocoa application in C calling the Objective-C runtime. An example of it in C can be found here. Going with this method would look like that but in Go.

The second option would likely take longer but it would allow for writing code similar to Objective-C. This'll help with maintainability. It could also be used to port other non-ebiten related projects to go.

Well, I don't think it is feasible to create a transpiler from Objective-C to Go. The code amount is not so huge anyway.

I wasn't trying to describe a transpiler from Objective-C to Go but from a new pseudo Go called Objective-Go that could be compiled to normal Go. But I agree it's a lot of work.

The third option would be nice because it supports a project meant to work with macOS in Go. The benefit of doing so it that it already has support for creating classes in Go with a decently nice API. However, this project aims to call a lot more Objective-C code than ebiten needs to support so it'd be a big effort to get it to pure go. It also appears slow on adding new functionality based on the open pull requests and the lack of new commits since December.

OK so macdriver is not in pure Go (yet), right? I also think that macdriver is general so we should create our own minimal foundation.

That is correct. It also still doesn't support the M1 chips so that too would have to be added.

I prefer the first item so far.

I agree, I think that is the best option.

@hajimehoshi
Copy link
Owner

hajimehoshi commented May 13, 2022

I just realized that iOS/arm64 is just a pseudo tag for darwin/arm64. The dl and sys call packages probably work on iOS but I haven't tested them. I only removed cgo from the opengl/gl package. That package is only used by macOS, right? If I'm correct iOS still depends on GLES which is still using cgo. However, looking at opengl/gles it should be simple enough to port as well. I think that should be a separate pull request though. This PR should probably be renamed to make it clear it only makes changes to opengl/gl on macOS

True. internal/graphicsdriver/opengl/gl is not used by iOS, then this PR doesn't affect iOS. Let's revisit this later. Thanks!

I'd prefer having it put under github.com/ebiten organization.

Sure. Let's put necessary things for darwin under github.com/ebiten. What name would be nice? (This might be the most difficult question lol) I assume the package is only for darwin. darwinutil or something?

What are the benefits of just reducing cgo? Would it be possible to make ebitenmobile output a pure go binary for iOS in addition to a shared object? This probably should be discussed separately.

Probably we can reduce compile time. There is no strong benefit. Yeah let's discuss this later.

So hold off on any work on GLFW even dynamically linking to it?

I don't plan to port GLFW to pure Go for macOS so far. This is only for Windows (#1764). So we can start the dynamically-linking work for macOS.

In the long term, I might rewrite GLFW to pure Go for macOS and UNIX-like systems, but this should not be urgent.

Yes! Objective-C is just C with a special preprocessor. You can write an entire cocoa application in C calling the Objective-C runtime. An example of it in C can be found here. Going with this method would look like that but in Go.

I see. Thank you for the link! objc_allocateClassPair is what I wanted to know. Let's go with the first item: "Write the the Objective-C code in Go by writing the runtime code by hand".

@TotallyGamerJet TotallyGamerJet changed the title internal/graphicsdriver/opengl, internal/os/syscall, internal/os/dl: remove CGO in opengl for darwin internal/graphicsdriver/opengl, internal/os/syscall, internal/os/dl: remove CGO in opengl for macOS May 13, 2022
@TotallyGamerJet
Copy link
Contributor Author

I'd prefer having it put under github.com/ebiten organization.

Sure. Let's put necessary things for darwin under github.com/ebiten. What name would be nice? (This might be the most difficult question lol) I assume the package is only for darwin. darwinutil or something?

dl and syscall currently only work on darwin but it should be trivial to implement it for linux. For dl all that is needed for linux is to create a dl_linux.go file that contains //go:cgo_import_dynamic pragmas that link the necessary c functions. The only darwin specific parts of syscall are syscall_SyscallX found in sys_darwin_64.s and obviously the assembly files. However, the syscall_SyscallX could be removed and replaced with the assembly function taken from the runtime. Then the only thing left to do to implement syscall for linux would be writing the assembly functions. The assembly functions should be nearly identical since macOS and linux are both unix systems.

Since this package could be used for platforms other than darwin in the future I don't think it should be named something relating to it. Perhaps something relating to the fact that it is about dynamically linking and calling C? Like callc or linkc?

Although, if it was darwin specific it would be a good place to put Go bindings to the Objective-C runtime. Where should that be placed since it too will be used by Oto.

Naming is always the hardest!

I see. Thank you for the link! objc_allocateClassPair is what I wanted to know. Let's go with the first item: "Write the the Objective-C code in Go by writing the runtime code by hand".

Sounds good!

@hajimehoshi
Copy link
Owner

dl and syscall currently only work on darwin but it should be trivial to implement it for linux.

Ooh, is this true? For Linux, I thought it was almost impossible (see golang/go#18296). For macOS, my understanding is that we can assume the existence and the location of a dynamic linker.

And for the Linux case, we should support various architectures not only amd64 and arm64 but also mips, riscv, and so on. I hope this would not be so problematic.

I'll think the new package name tomorrow as I'll go to bed soon.

@TotallyGamerJet
Copy link
Contributor Author

From my understanding of how CGO is actually implemented it should be. According to this comment in that same issue he supposedly did it. I'm not sure it still works but it shows that it is possible.

The function that makes my PR possible is func libcCall(fn, arg unsafe.Pointer) int32 found in runtime/sys_libc.go. This function has a build tag of //go:build darwin || (openbsd && !mips64) which means it's not useable on linux (but is on openbsd). Perhaps we could get the Go team to accept support for that function on other platforms? Otherwise, it seems that linux will in fact be more difficult to implement than I first thought.

@hajimehoshi
Copy link
Owner

Perhaps we could get the Go team to accept support for that function on other platforms? Otherwise, it seems that linux will in fact be more difficult to implement than I first thought.

Yeah, I realized libcCall is not exposed for Linux. (I have modified the runtime by -overlay by the way https://github.com/hajimehoshi/hitsumabushi/blob/main/1.18_linux/runtime/sys_libc.go.patch)

We are not sure we can remove all Cgo from Linux yet, but I agree the new package should not be only for macOS.

So what about these package names? As the package is under the namespace ebiten (organization), we don't have to make it too unique. I don't have a strong opinion here but I prefer callc or purego so far. Would purego sound too general?

  • github.com/ebiten/callc: your suggestion
  • github.com/ebiten/linkc: your suggestion
  • github.com/ebiten/nocgo: inspired by the existing nocgo project
  • github.com/ebiten/nonecgo
  • github.com/ebiten/withoutcgo
  • github.com/ebiten/purego
  • github.com/ebiten/doteki: 動的 ("dynamic") in Japanese
  • github.com/ebiten/rinku: リンク ("link") in Japanese
  • gtihub.com/ebiten/umi: 海 ("sea" from the pronunciation of C) in Japanese
  • github.com/ebiten/junsui: 純粋 ("pure") in Japanese

@TotallyGamerJet
Copy link
Contributor Author

So what about these package names? As the package is under the namespace ebiten (organization), we don't have to make it too unique. I don't have a strong opinion here but I prefer callc or purego so far. Would purego sound too general?

I like purego the best because it describes the goal of the package - to have only go. It also plays nice with purego/dl and purego/syscall for the same reason. As well as other system packages like purego/objc could also be added.

Also, I noticed that the runtime does have a 6 argument SyscallX version called Syscall6X. Should our function names match the runtime? Currently, I wrote SyscallX6. And should we go:linkname to their implementation like we do for SyscallX. We should only do that if we plan on keeping it that way. Doing so will likely make it more difficult to port to other GOOS and GOARCH combinations because that function isn't implemented on other platforms.

@hajimehoshi
Copy link
Owner

OK, I've created the purego repository: https://github.com/ebiten/purego I appreciate if you send a PR to this repository first.

Also, I noticed that the runtime does have a 6 argument SyscallX version called Syscall6X. Should our function names match the runtime? Currently, I wrote SyscallX6.

Yes, let's follow the official runtime's convention if possible. I'm not sure what X means here.

And should we go:linkname to their implementation like we do for SyscallX. We should only do that if we plan on keeping it that way. Doing so will likely make it more difficult to port to other GOOS and GOARCH combinations because that function isn't implemented on other platforms.

So my understanding is that just exposing functions in the runtime by go:linkname will work for macOS rather than implementing them by ourselves, is that correct? As our first milestone is to make the macOS version pure Go, I think just exposing the runtime function is enough. Let's revisit when we want to port the purego package to other platforms. What do you think?

@TotallyGamerJet
Copy link
Contributor Author

So my understanding is that just exposing functions in the runtime by go:linkname will work for macOS rather than implementing them by ourselves, is that correct? As our first milestone is to make the macOS version pure Go, I think just exposing the runtime function is enough. Let's revisit when we want to port the purego package to other platforms. What do you think?

Yes it will work on macOS.

@TotallyGamerJet
Copy link
Contributor Author

Yes, let's follow the official runtime's convention if possible. I'm not sure what X means here.

The X means that it calls into C and not an actual system call where you'd use x80

@hajimehoshi
Copy link
Owner

Error: internal/graphicsdriver/opengl/gl/procaddr_darwin.go:11:9: possible misuse of unsafe.Pointer

Is this fixable?

@TotallyGamerJet
Copy link
Contributor Author

Error: internal/graphicsdriver/opengl/gl/procaddr_darwin.go:11:9: possible misuse of unsafe.Pointer

Is this fixable?

Change the signature of the function purego.Dlsym to return unsafe.Pointer.

@TotallyGamerJet
Copy link
Contributor Author

Oh we could do what you did for windows and change getProcAddress to return a uintptr. But that would require breaking compatibility since InitWithProcAddress take a function that returns unsafe Pointer

@hajimehoshi
Copy link
Owner

Oh we could do what you did for windows and change getProcAddress to return a uintptr. But that would require breaking compatibility since InitWithProcAddress take a function that returns unsafe Pointer

Where is InitWithProcAddress defined?

@TotallyGamerJet
Copy link
Contributor Author

Sorry it’s InitWithProcAddrFunc In package_darwin.go but since it’s in the internal package it probably doesn’t matter that we change the signature

@hajimehoshi
Copy link
Owner

Sorry it’s InitWithProcAddrFunc In package_darwin.go but since it’s in the internal package it probably doesn’t matter that we change the signature

Sure, of course you can change this if necessary.

@changkun
Copy link
Contributor

(Sorry for jumping in...) This looks like great work and legendary change, and I wonder if there are any benchmarks to show how much performance gain could benefit from this change. We know that Cgo is costly, but it would still be very interesting to see how much different we could get if GL calls are done through syscalls directly.

}

func ActiveTexture(texture uint32) {
purego.SyscallN(gpActiveTexture, uintptr(texture))
Copy link
Owner

@hajimehoshi hajimehoshi May 19, 2022

Choose a reason for hiding this comment

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

We might be able to integrate package_windows.go and package_darwin.go by adding a Windows version of purego.SyscallN. (Note that syscall.SyscallN is not available for Ebiten due to the current Go version restriction). Of course you don't have to do this in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that would be a good idea. It should be simple enough: just separate out SyscallN into its own file and then implement syscall_Sycall9 for windows using the windows package version

@TotallyGamerJet
Copy link
Contributor Author

(Sorry for jumping in...) This looks like great work and legendary change, and I wonder if there are any benchmarks to show how much performance gain could benefit from this change. We know that Cgo is costly, but it would still be very interesting to see how much different we could get if GL calls are done through syscalls directly.

I don’t believe there will be much of any difference in performance. The reason cgo is so slow is because it has to tell the runtime that it’s about to be in external code that could block forever. We still do this because it’s necessary. (Look at runtime.entersyscall and runtime.exitsyscall). Other metrics like binary size I’ve seen about 0.7MB smaller and about 4MB less ram. The main benefit of removing cgo is compilation time and cross-compilation. The second of which we won’t see until cgo is removed entirely from ebiten. I haven’t tested for compile time improvements

@hajimehoshi
Copy link
Owner

Please rebase this PR to resolve the conflicts. Thanks!

@TotallyGamerJet TotallyGamerJet changed the title internal/graphicsdriver/opengl, internal/os/syscall, internal/os/dl: remove CGO in opengl for macOS internal/graphicsdriver/opengl: remove CGO in opengl for macOS May 21, 2022
Copy link
Owner

@hajimehoshi hajimehoshi left a comment

Choose a reason for hiding this comment

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

LGTM, thank you very much!

@hajimehoshi hajimehoshi merged commit f7e2198 into hajimehoshi:main May 21, 2022
hajimehoshi added a commit that referenced this pull request May 21, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants