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

Allocated refactor #906

Merged
merged 2 commits into from
Mar 11, 2024
Merged

Conversation

jherico
Copy link
Contributor

@jherico jherico commented Feb 11, 2024

Description

This PR is currently built on top of #907 and #901 and should not be merged until they're approved and merged.

Migrate the code common to the objects using the VMA library to a single location, mostly the new allocated.h header and template types. The four classes, vkb::core::Buffer, vkb::core::Image, vkb::core::HPPBuffer and ``vkb::core::HPPImage` are now all implemented in terms of common base classes.

The new classes prefer the use of a builder pattern for construction, with reasonable defaults for some values set in the image builder types, reducing the cognitive load of using them as well as the complexity of the image constructors. In a future PR the existing ctors will be marked deprecated and all the samples referencing them will be updated to use the builder pattern.

The buffer types have new static methods intended to be used to create short-lived staging buffers containing data. These static methods include convenience signatures to make it easy to upload containers full of data... so for instance, this code

core::Buffer stage_buffer{device,
                          vertex_data.size() * sizeof(Vertex),
                          VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
                          VMA_MEMORY_USAGE_CPU_ONLY};

stage_buffer.update(vertex_data.data(), vertex_data.size() * sizeof(Vertex));

reduces to

core::Buffer stage_buffer = core::Buffer::create_staging_buffer(device, aligned_vertex_data);

Fixes #900

I'm struggling to 100% test the samples as fragment_shading_rate_dynamic currently crashes on my desktop, even on the main branch and the hlsl shader examples both produce validation warnings.

Fixed #941

Partial fix the the example noted in this bug and at least one other place, but there should probably be an assert if unmap is called without flush ever having been called (regardless of whether the memory is coherent).

General Checklist:

Please ensure the following points are checked:

  • My code follows the coding style
  • I have reviewed file licenses
  • I have commented any added functions (in line with Doxygen)
  • I have commented any code that could be hard to understand
  • My changes do not add any new compiler warnings
  • My changes do not add any new validation layer errors or warnings
  • I have used existing framework/helper functions where possible
  • My changes do not add any regressions
  • I have tested every sample to ensure everything runs correctly
  • This PR describes the scope and expected impact of the changes I am making

Note: The Samples CI runs a number of checks including:

  • I have updated the header Copyright to reflect the current year (CI build will fail if Copyright is out of date)
  • My changes build on Windows, Linux, macOS and Android. Otherwise I have documented any exceptions

Copy link
Contributor

@gary-sweet gary-sweet left a comment

Choose a reason for hiding this comment

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

I'm seeing these validation errors multiple times when I run virtually any sample, followed by a driver crash.

[error] [framework/core/instance.cpp:50] 357932245 - VUID-VkImageSubresourceRange-levelCount-01720: Validation Error: [ VUID-VkImageSubresourceRange-levelCount-01720 ] Object 0: handle = 0xa000000000a, type = VK_OBJECT_TYPE_IMAGE; | MessageID = 0x15559cd5 | vkCreateImageView(): pCreateInfo->subresourceRange.levelCount is zero. The Vulkan spec states: If levelCount is not VK_REMAINING_MIP_LEVELS, it must be greater than 0 (https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/vkspec.html#VUID-VkImageSubresourceRange-levelCount-01720)

[error] [framework/core/instance.cpp:50] 1022674482 - VUID-VkImageSubresourceRange-layerCount-01721: Validation Error: [ VUID-VkImageSubresourceRange-layerCount-01721 ] Object 0: handle = 0xa000000000a, type = VK_OBJECT_TYPE_IMAGE; | MessageID = 0x3cf4c632 | vkCreateImageView(): pCreateInfo->subresourceRange.layerCount is zero. The Vulkan spec states: If layerCount is not VK_REMAINING_ARRAY_LAYERS, it must be greater than 0 (https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/vkspec.html#VUID-VkImageSubresourceRange-layerCount-01721)

[error] [framework/core/instance.cpp:50] -1968073940 - VUID-VkImageViewCreateInfo-imageViewType-04973: Validation Error: [ VUID-VkImageViewCreateInfo-imageViewType-04973 ] Object 0: handle = 0xa000000000a, type = VK_OBJECT_TYPE_IMAGE; | MessageID = 0x8ab1932c | vkCreateImageView(): pCreateInfo->subresourceRange.layerCount (0) must be 1 when using viewType VK_IMAGE_VIEW_TYPE_2D (try looking into VK_IMAGE_VIEW_TYPE_*_ARRAY). The Vulkan spec states: If viewType is VK_IMAGE_VIEW_TYPE_1D, VK_IMAGE_VIEW_TYPE_2D, or VK_IMAGE_VIEW_TYPE_3D; and subresourceRange.layerCount is not VK_REMAINING_ARRAY_LAYERS, then subresourceRange.layerCount must be 1 (https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/vkspec.html#VUID-VkImageViewCreateInfo-imageViewType-04973)

@SaschaWillems
Copy link
Collaborator

SaschaWillems commented Feb 14, 2024

This sadly doesn't build for me, so I can't test it:

image

I'm on Windows with VS2022 17.8.7

@jherico
Copy link
Contributor Author

jherico commented Feb 15, 2024

@SaschaWillems @gary-sweet should be ready for testing and review.

Copy link
Collaborator

@SaschaWillems SaschaWillems left a comment

Choose a reason for hiding this comment

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

Builds fine for me now, but it breaks some samples. E.g. the ray tracing extended and reflection samples now crashes for me following this error:

[error] [framework\core\instance.cpp:50] -330527817 - VUID-vkMapMemory-memory-00682: Validation Error: [ VUID-vkMapMemory-memory-00682 ] Object 0: handle = 0x84785e00000001b9, type = VK_OBJECT_TYPE_DEVICE_MEMORY; | MessageID = 0xec4c8bb7 | vkMapMemory():  Mapping memory without VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT set. Memory has type 0 which has properties VkMemoryPropertyFlags(0). The Vulkan spec states: memory must have been created with a memory type that reports VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT (https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/vkspec.html#VUID-vkMapMemory-memory-00682)
[error] [framework\core/allocated.h:207] Detected Vulkan error: ERROR_MEMORY_MAP_FAILED

Probably need to to test all samples and also on different platforms to make sure we don't break anything with this.

@jherico
Copy link
Contributor Author

jherico commented Feb 15, 2024

@SaschaWillems I've fixed the ray tracing examples to use the builder pattern but retain the usage flags used in the old code.

Regarding the buffers in question raygen_shader_binding_table, miss_shader_binding_table, hit_shader_binding_table, they're currently created, and populated in a way that requires them to be host visible, but they're only written to once. The current logic uses the deprecated VMA usage enum of VMA_MEMORY_USAGE_CPU_TO_GPU, which I believe tries to allocate memory that is device local but host visible if the card provides such a type, but will revert to host visible otherwise. That's typical for things like uniform buffers that get written every frame, but seems strange here.

I guess my question is, since these buffers are only written once, why not just use straight up DEVICE_LOCAL memory and a staging buffer to populate them?

Probably need to to test all samples and also on different platforms to make sure we don't break anything with this.

I've now tested all samples and verified that they all produce output and no new validation errors as far as I can tell (the HLSL examples produce validation errors on main, and the fragment_shader_rate_dynamic sample crashes on main). However, I only really have my main system of Windows 10 and an nVidia card to test on.

@jherico jherico force-pushed the allocated-refactor branch 3 times, most recently from e757747 to 3370698 Compare February 19, 2024 18:26
@gary-sweet
Copy link
Contributor

This all seems to work ok on my platform now, but I notice there is a clang format check failure so I can't approve it yet.

Copy link
Collaborator

@SaschaWillems SaschaWillems left a comment

Choose a reason for hiding this comment

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

I added some comments to one of the samples I did. In general I'm fine with refactoring to make things better/easier, but such refactors shouldn't do fundamental changes to existing samples mainly for the sake of simplifying/unifying things. With many of the samples I write I'm deliberately explicit so people don't need to poke around the framework to see how things are done. And this PR undoes at least some of that, which I'm not too happy with.

color_image_view.subresourceRange.layerCount = 1;
color_image_view.image = storage_image.image;
VK_CHECK(vkCreateImageView(get_device().get_handle(), &color_image_view, nullptr, &storage_image.view));
vkb::core::ImageBuilder image_builder{{width, height, 1}};
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd prefer to roll this back. The whole reason I made this (sample) so explicit, was so that people could easily adopt that code. These changes add a new layer of abstraction (on top of the framework) that would make it harder for people to follow/adopt this sample.

/*
Gets the device address from a buffer that's needed in many places during the ray tracing setup
*/
uint64_t RaytracingBasic::get_buffer_device_address(VkBuffer buffer)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same as above. There is a reason I made all of this very explicit.

@SaschaWillems
Copy link
Collaborator

SaschaWillems commented Feb 22, 2024

And while I'm in general a fan of the builder pattern, I'm kinda skeptical. Afaik we don't use it elsewhere and it kinda feels odd to introduce (yet) another pattern for an already convoluted and hard to maintain framework. Not sure on this, what do the other reviewers think of that approach?

const VkExtent3D image_extent = {width, height, 1};
linked_list_head_image = std::make_unique<vkb::core::Image>(get_device(), image_extent, VK_FORMAT_R32_UINT, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, VMA_MEMORY_USAGE_GPU_ONLY, VK_SAMPLE_COUNT_1_BIT);
linked_list_head_image_view = std::make_unique<vkb::core::ImageView>(*linked_list_head_image, VK_IMAGE_VIEW_TYPE_2D, VK_FORMAT_R32_UINT);
vkb::core::ImageBuilder builder{width, height};
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't see why or how this is better than before? I personally prefer the way it was.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The current image constructor looks like this:

	Image(
	    Device const         &device,
	    const VkExtent3D     &extent,
	    VkFormat              format,
	    VkImageUsageFlags     image_usage,
	    VmaMemoryUsage        memory_usage       = VMA_MEMORY_USAGE_AUTO,
	    VkSampleCountFlagBits sample_count       = VK_SAMPLE_COUNT_1_BIT,
	    uint32_t              mip_levels         = 1,
	    uint32_t              array_layers       = 1,
	    VkImageTiling         tiling             = VK_IMAGE_TILING_OPTIMAL,
	    VkImageCreateFlags    flags              = 0,
	    uint32_t              num_queue_families = 0,
	    const uint32_t       *queue_families     = nullptr);

Even if you ignore the Device argument as being standard to pretty much all the VK resource ctors, that's still a dozen parameters with more than half of them being defaulted. Most style checkers I know start flagging at anything more than 7 ctor paramers.

if you consider Image(device, extent, format, VK_IMAGE_USAGE_SAMPLED_BIT, VMA_MEMORY_USAGE_AUTO, VK_SAMPLE_COUNT_1_BIT, 1, 6), it's not immediately obvious to a downstream consumer of these examples looking at the code what 1 and 6 mean in this context, or why the sample count is set when it hasn't been in prior image creation code. It's only by already knowing the layout of the constructor that you can understand these values.

The builder pattern allows...

  • reasonable defaults for values that are infrequently changed, such as the tiling or sample count
  • access to ALL of the possible values you might want to modify, which the current ctor does not
    • In particular, the VmaMemoryUsage flag is the only part of VmaAllocationCreateInfo that can be customized through this parameter, while VmaAllocationCreateFlags, which is likely something consumers would want to change far more often, is not
  • The builder pattern makes it perfectly clear to readers of the examples what the meaning of the values being set are, while constructor param doesn't unless you're looking at the constructor definition.

I have no doubt that you can find places in the code where the builder pattern ends up appearing "just as complex" or even consumes more lines of code as the existing constructor based approach, but IMO, in places like this we're sacrificing brevity for readability and clarity to consumers of the example code as a learning tool.

Granted I made this change before I added the build methods to the builder and the with_vma_usage call is almost certainly superfluous, so this could be re-written as

		linked_list_head_image = vkb::core::ImageBuilder{width, height}
		    .with_format(VK_FORMAT_R32_UINT)
		    .with_usage(VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT)
		    .build_unique(device);

Which would be slightly more compact and avoid the temporary named builder variable. Would that make you happier?

image_usage,
VMA_MEMORY_USAGE_GPU_ONLY,
VK_SAMPLE_COUNT_1_BIT);
vkb::core::ImageBuilder builder{image_extent};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here. At least for me, the builder pattern seems out of place in here.

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 could replace the entire block with

		shading_rate_image = vkb::core::ImageBuilder{image_extent}
			    .with_format(VK_FORMAT_R8_UINT)
			    .with_usage(VK_IMAGE_USAGE_FRAGMENT_SHADING_RATE_ATTACHMENT_BIT_KHR | VK_IMAGE_USAGE_TRANSFER_DST_BIT)
			    .build_unique(*device);
		    
		shading_rate_image_compute = vkb::core::ImageBuilder{image_extent}
			    .with_format(VK_FORMAT_R8_UINT)
			    .with_usage(VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT)
			    .build_unique(*device);

and eliminate the need for a lambda completely, making the code shorter overall. Is that easier to follow?

@jherico jherico force-pushed the allocated-refactor branch 3 times, most recently from 3d409c5 to c27ac1b Compare February 24, 2024 22:25
@jherico
Copy link
Contributor Author

jherico commented Feb 24, 2024

@SaschaWillems I have removed all use of the builder pattern from the code (except the internal usage in the image and buffer classes to simplify constructors. I have left, and expanded the use of convenience functions for creating staging buffers, as well as noticed and corrected some minor bugs in populating staging buffers where they fail to flush before unmapping.

gary-sweet
gary-sweet previously approved these changes Feb 26, 2024
@SaschaWillems
Copy link
Collaborator

Any idea why the profiles sample cpp is displayed as completely changed (all lines) in this PR? It only looks like the copyright has changed, but I'm not able to see any other change due to the whole file being displayed as different.

@SaschaWillems
Copy link
Collaborator

SaschaWillems commented Feb 26, 2024

It's the same for a lot of other files too, which makes it nigh impossible to review these changes:

image

image

image

image

@jherico
Copy link
Contributor Author

jherico commented Feb 26, 2024

@SaschaWillems It looks like some of the files got converted from LF to CRLF. Most of the files in the repository are LF, but a few are CRLF (prior to this change). I've updated the commit to revert the line ending changes. I must have misconfigured something in git, because I thought I had my system configured to checkout and commit LF, always (autocrlf=false). I'll review my settings and try to figure out how this happened.

@tomadamatkinson might be worthwhile to add to the pre-commit hook something that would verify that none of the line-endings have changed for existing files. These are the only 5 files on main that are currently crlf:

i/crlf  w/crlf  attr/                   framework/scene_graph/components/orthographic_camera.cpp
i/crlf  w/crlf  attr/                   framework/vulkan_sample.h
i/crlf  w/crlf  attr/                   samples/performance/multithreading_render_passes/CMakeLists.txt
i/crlf  w/crlf  attr/                   samples/performance/multithreading_render_passes/multithreading_render_passes.cpp
i/crlf  w/crlf  attr/                   samples/performance/multithreading_render_passes/multithreading_render_passes.h

gary-sweet
gary-sweet previously approved these changes Feb 27, 2024
@marty-johnson59 marty-johnson59 merged commit 002fa05 into KhronosGroup:main Mar 11, 2024
25 checks passed
@kanavin
Copy link
Contributor

kanavin commented Mar 12, 2024

@jherico I'm a bit baffled, but this seems to have regressed 32 bit builds (both arm and x86), where 64 bit build without issues:

| In file included from /srv/storage/alex/yocto/build-arm-32-alt/tmp/work/cortexa15t2hf-neon-poky-linux-gnueabi/vulkan-samples/git/git/framework/core/allocated.h:22,
|                  from /srv/storage/alex/yocto/build-arm-32-alt/tmp/work/cortexa15t2hf-neon-poky-linux-gnueabi/vulkan-samples/git/git/framework/core/buffer.h:22,
|                  from /srv/storage/alex/yocto/build-arm-32-alt/tmp/work/cortexa15t2hf-neon-poky-linux-gnueabi/vulkan-samples/git/git/framework/api_vulkan_sample.h:36,
|                  from /srv/storage/alex/yocto/build-arm-32-alt/tmp/work/cortexa15t2hf-neon-poky-linux-gnueabi/vulkan-samples/git/git/framework/gltf_loader.cpp:32:
| /srv/storage/alex/yocto/build-arm-32-alt/tmp/work/cortexa15t2hf-neon-poky-linux-gnueabi/vulkan-samples/git/git/framework/core/vulkan_resource.h:50:24: error: redefinition of 'constexpr VkObjectType vkb::core::get_object_type(const HandleType&) [with HandleType = long long unsigned int; VkObjectType = VkObjectType]'
|    50 | constexpr VkObjectType get_object_type(const VkImageView &handle)
|       |                        ^~~~~~~~~~~~~~~
| /srv/storage/alex/yocto/build-arm-32-alt/tmp/work/cortexa15t2hf-neon-poky-linux-gnueabi/vulkan-samples/git/git/framework/core/vulkan_resource.h:44:24: note: 'constexpr VkObjectType vkb::core::get_object_type(const HandleType&) [with HandleType = long long unsigned int; VkObjectType = VkObjectType]' previously declared here
|    44 | constexpr VkObjectType get_object_type(const VkImage &handle)
|       |                        ^~~~~~~~~~~~~~~
| /srv/storage/alex/yocto/build-arm-32-alt/tmp/work/cortexa15t2hf-neon-poky-linux-gnueabi/vulkan-samples/git/git/framework/core/vulkan_resource.h:56:24: error: redefinition of 'constexpr VkObjectType vkb::core::get_object_type(const HandleType&) [with HandleType = long long unsigned int; VkObjectType = VkObjectType]'
|    56 | constexpr VkObjectType get_object_type(const VkRenderPass &handle)
|       |                        ^~~~~~~~~~~~~~~
| /srv/storage/alex/yocto/build-arm-32-alt/tmp/work/cortexa15t2hf-neon-poky-linux-gnueabi/vulkan-samples/git/git/framework/core/vulkan_resource.h:44:24: note: 'constexpr VkObjectType vkb::core::get_object_type(const HandleType&) [with HandleType = long long unsigned int; VkObjectType = VkObjectType]' previously declared here
|    44 | constexpr VkObjectType get_object_type(const VkImage &handle)
|       |                        ^~~~~~~~~~~~~~~
| /srv/storage/alex/yocto/build-arm-32-alt/tmp/work/cortexa15t2hf-neon-poky-linux-gnueabi/vulkan-samples/git/git/framework/core/vulkan_resource.h:62:24: error: redefinition of 'constexpr VkObjectType vkb::core::get_object_type(const HandleType&) [with HandleType = long long unsigned int; VkObjectType = VkObjectType]'
|    62 | constexpr VkObjectType get_object_type(const VkSampler &handle)
|       |                        ^~~~~~~~~~~~~~~
| /srv/storage/alex/yocto/build-arm-32-alt/tmp/work/cortexa15t2hf-neon-poky-linux-gnueabi/vulkan-samples/git/git/framework/core/vulkan_resource.h:44:24: note: 'constexpr VkObjectType vkb::core::get_object_type(const HandleType&) [with HandleType = long long unsigned int; VkObjectType = VkObjectType]' previously declared here
|    44 | constexpr VkObjectType get_object_type(const VkImage &handle)
|       |                        ^~~~~~~~~~~~~~~
| /srv/storage/alex/yocto/build-arm-32-alt/tmp/work/cortexa15t2hf-neon-poky-linux-gnueabi/vulkan-samples/git/git/framework/core/vulkan_resource.h:68:24: error: redefinition of 'constexpr VkObjectType vkb::core::get_object_type(const HandleType&) [with HandleType = long long unsigned int; VkObjectType = VkObjectType]'
|    68 | constexpr VkObjectType get_object_type(const VkBuffer &handle)
|       |                        ^~~~~~~~~~~~~~~
| /srv/storage/alex/yocto/build-arm-32-alt/tmp/work/cortexa15t2hf-neon-poky-linux-gnueabi/vulkan-samples/git/git/framework/core/vulkan_resource.h:44:24: note: 'constexpr VkObjectType vkb::core::get_object_type(const HandleType&) [with HandleType = long long unsigned int; VkObjectType = VkObjectType]' previously declared here
|    44 | constexpr VkObjectType get_object_type(const VkImage &handle)
|       |                        ^~~~~~~~~~~~~~~
| ninja: build stopped: subcommand failed.

@jherico
Copy link
Contributor Author

jherico commented Mar 12, 2024

@kanavin I'll try to locally reproduce and find a fix today.

@jherico
Copy link
Contributor Author

jherico commented Mar 12, 2024

ok, so the problem here is that on 32 bit architectures the handles still need to be 64 bit. So while on 64 bit architectures VkImage resolves to struct objectVkImage_T *, on 32 bit platforms it resolves to uint64_t.

#ifndef VK_DEFINE_NON_DISPATCHABLE_HANDLE
    #if (VK_USE_64_BIT_PTR_DEFINES==1)
        #define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef struct object##_T *object;
    #else
        #define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef uint64_t object;
    #endif
#endif

This means that all the template specializations like this:

template <>
constexpr VkObjectType get_object_type(const VkImage &handle)
{ ... }
template <>
constexpr VkObjectType get_object_type(const VkImageView &handle)
{ ... }

have the exact same signature and can't be differentiated by the compiler the way they can in 64-bit land.

@tomadamatkinson should a 32-bit build be added to the CI configs? Two distinct PR's of mine appear to have caused regressions for cmake -A Win32.

@jherico
Copy link
Contributor Author

jherico commented Mar 12, 2024

So, when I run cmake -A Win32 and build the framework I get a LOT of errors, many of which appear way outside the scope of the changes in this PR and in the ASTC PR. For instance, I get a lot of errors building KTX, which hasn't been touched in 3 years.

Looking at the readme, I'm unclear on if 32-bit builds are supported since all of the instructions seem to explicitly specify 64-bit arch.

I'm working on a branch that resolves some of the 32-bit issues I've seen so far, but I'm not touching the third-party build issues other than for astc.

@jherico jherico deleted the allocated-refactor branch March 12, 2024 20:26
@kanavin
Copy link
Contributor

kanavin commented Mar 12, 2024

So, when I run cmake -A Win32 and build the framework I get a LOT of errors, many of which appear way outside the scope of the changes in this PR and in the ASTC PR. For instance, I get a lot of errors building KTX, which hasn't been touched in 3 years.

Looking at the readme, I'm unclear on if 32-bit builds are supported since all of the instructions seem to explicitly specify 64-bit arch.

I'm working on a branch that resolves some of the 32-bit issues I've seen so far, but I'm not touching the third-party build issues other than for astc.

I was worried that in the absence of 32 bit CI that gates all changes, fixing one issue will uncover another (with no clear end in sight). Please do fix what's in your 'jurisdiction' and I'll see what can be done about the rest (my interest is primarily 32 bit linux, especially arm and risc-v which are still used to ship products).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
6 participants