Working towards 3D rendering
This commit is contained in:
parent
71d3cd400b
commit
84020a922b
22 changed files with 486 additions and 42 deletions
|
|
@ -16,6 +16,7 @@ import net.halbear.Terrain4J.EngineCore.Vulkan.Structure.DisplayToScreen.SwapCha
|
|||
import net.halbear.Terrain4J.EngineCore.Vulkan.Util.VulkanUtils;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
import org.lwjgl.vulkan.VkCommandBufferSubmitInfo;
|
||||
import org.lwjgl.vulkan.VkExtent2D;
|
||||
import org.lwjgl.vulkan.VkSemaphoreSubmitInfo;
|
||||
import org.tinylog.Logger;
|
||||
|
||||
|
|
@ -39,6 +40,7 @@ public class Render {
|
|||
private final SceneRender sceneRender;
|
||||
private int CurrentFrame;
|
||||
private final VulkanContext RendererContext;
|
||||
private boolean Resize = false;
|
||||
|
||||
public Render(EngineInstance engineInstance) {
|
||||
RendererContext = new VulkanContext(engineInstance.window());
|
||||
|
|
@ -62,6 +64,7 @@ public class Render {
|
|||
}
|
||||
sceneRender = new SceneRender(RendererContext);
|
||||
modelsCache = new ModelsCache();
|
||||
Resize = false;
|
||||
}
|
||||
|
||||
public void Initialise(InitData initData){
|
||||
|
|
@ -105,16 +108,41 @@ public class Render {
|
|||
var CommandPool = CommandPools[CurrentFrame];
|
||||
var CommandBuffer = CommandBuffers[CurrentFrame];
|
||||
RecordingStart(CommandPool, CommandBuffer);
|
||||
int ImageIndex = swapChain.FetchNextImage(RendererContext.GetDevice(), PresentCompleteSemaphores[CurrentFrame]);
|
||||
if (ImageIndex < 0){
|
||||
int ImageIndex;// = swapChain.FetchNextImage(RendererContext.GetDevice(), PresentCompleteSemaphores[CurrentFrame]);
|
||||
if (Resize || (ImageIndex = swapChain.FetchNextImage(RendererContext.GetDevice(), PresentCompleteSemaphores[CurrentFrame])) < 0){
|
||||
resize(engineInstance);
|
||||
return;
|
||||
}
|
||||
sceneRender.Render(RendererContext,CommandBuffer, modelsCache,ImageIndex);
|
||||
sceneRender.Render(engineInstance,RendererContext,CommandBuffer, modelsCache,ImageIndex);
|
||||
RecordingStop(CommandBuffer);
|
||||
Submit(CommandBuffer, CurrentFrame, ImageIndex);
|
||||
swapChain.PresentImage(PresentQueue, RenderCompleteSemaphores[ImageIndex],ImageIndex);
|
||||
Resize = swapChain.PresentImage(PresentQueue, RenderCompleteSemaphores[ImageIndex],ImageIndex);
|
||||
CurrentFrame = (CurrentFrame + 1) % VulkanUtils.MAX_IN_FLIGHT;
|
||||
}
|
||||
|
||||
private void resize(EngineInstance engineInstance){
|
||||
Window window = engineInstance.window();
|
||||
if(window.getWidth() == 0 && window.getHeight() == 0){
|
||||
return;
|
||||
}
|
||||
Resize = false;
|
||||
RendererContext.GetDevice().waitIdle();
|
||||
RendererContext.Resize(window);
|
||||
|
||||
Arrays.asList(RenderCompleteSemaphores).forEach(i->i.cleanup(RendererContext));
|
||||
Arrays.asList(PresentCompleteSemaphores).forEach(i->i.cleanup(RendererContext));
|
||||
for(int i = 0; i < VulkanUtils.MAX_IN_FLIGHT; i++){
|
||||
PresentCompleteSemaphores[i] = new Semaphore(RendererContext);
|
||||
}
|
||||
for(int i = 0; i < RendererContext.GetSwapChain().GetImageCount(); i++){
|
||||
RenderCompleteSemaphores[i] = new Semaphore(RendererContext);
|
||||
}
|
||||
|
||||
VkExtent2D extend = RendererContext.GetSwapChain().GetSwapChainExtent();
|
||||
engineInstance.scene().GetProjection().Resize(extend.width(),extend.height());
|
||||
sceneRender.Resize(RendererContext);
|
||||
}
|
||||
|
||||
private void ResetFence(int currentFrame){
|
||||
var fence = Fences[currentFrame];
|
||||
fence.Reset(RendererContext);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import java.nio.file.Paths;
|
|||
import java.util.Properties;
|
||||
|
||||
public class EngineConfig {
|
||||
public static final double DegToRad = Math.PI/180.0;
|
||||
public static final double RadToDeg = 180.0/Math.PI;
|
||||
private static final long DEFAULT_ACCURACY = 0;
|
||||
private static final int DEFAULT_TICKRATE = 60;
|
||||
private static final int DEFAULT_WIDTH = 640;
|
||||
|
|
@ -43,6 +45,10 @@ public class EngineConfig {
|
|||
private int RequestedImages;
|
||||
private String IconPath = "/WindowResources/Icon/";
|
||||
private String IconName = "ProgramIcon.png";
|
||||
private float FOV = 60f;
|
||||
private float zFarPlane;
|
||||
private float zNearPlane;
|
||||
|
||||
|
||||
private EngineConfig() {
|
||||
var EngineConfigVar = new Properties();
|
||||
|
|
@ -102,6 +108,9 @@ public class EngineConfig {
|
|||
RequestedImages = Integer.parseInt(EngineConfigVar.getOrDefault("RequestedImages", 3).toString());
|
||||
ShaderDebug =Boolean.parseBoolean(EngineConfigVar.getOrDefault("Debug_Shaders", false).toString());
|
||||
RecompileShaders =Boolean.parseBoolean(EngineConfigVar.getOrDefault("ShaderRecompiling", true).toString());
|
||||
FOV = (float)(DegToRad * Float.parseFloat(EngineConfigVar.getOrDefault("field_of_view", 60.0f).toString()));
|
||||
zNearPlane = (Float.parseFloat(EngineConfigVar.getOrDefault("z_near_plane", 1.0f).toString()));
|
||||
zFarPlane = (Float.parseFloat(EngineConfigVar.getOrDefault("z_far_plane", 100.0f).toString()));
|
||||
Logger.debug("\n\nsuccessfully loaded configuration: \n{}\n\n",EngineConfigVar.toString());
|
||||
}
|
||||
else if(!ConfigFile.exists()){
|
||||
|
|
@ -125,6 +134,9 @@ public class EngineConfig {
|
|||
EngineConfigVar.setProperty("ShaderRecompiling","true");
|
||||
EngineConfigVar.setProperty("throttle_accuracy","100000");
|
||||
EngineConfigVar.setProperty("throttle_on_unfocus","true");
|
||||
EngineConfigVar.setProperty("field_of_view","60");
|
||||
EngineConfigVar.setProperty("z_near_plane","1");
|
||||
EngineConfigVar.setProperty("z_far_plane","100");
|
||||
EngineConfigVar.store(new FileWriter(path.toAbsolutePath().toString() + "/" + FILENAME), "created new properties file");
|
||||
Logger.debug("Wrote New Config File [{}]", ConfigFile.getAbsolutePath());
|
||||
} catch (IOException excp2) {
|
||||
|
|
@ -158,7 +170,9 @@ public class EngineConfig {
|
|||
public boolean RecompileShaders(){
|
||||
return RecompileShaders;
|
||||
}
|
||||
|
||||
public float GetFOV(){return FOV;}
|
||||
public float GetZFarPlane(){return zFarPlane;}
|
||||
public float GetZNearPlane(){return zNearPlane;}
|
||||
public boolean VkValidated(){
|
||||
return vkValidated;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package net.halbear.Terrain4J.EngineCore.Logic;
|
||||
|
||||
import net.halbear.Terrain4J.EngineCore.Main.Scene;
|
||||
import net.halbear.Terrain4J.EngineCore.Main.Scene.Scene;
|
||||
import net.halbear.Terrain4J.EngineCore.Display.Window;
|
||||
|
||||
public record EngineInstance(Window window, Scene scene) {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,13 @@ public class VulkanContext {
|
|||
VulkanLayersOpen+=5;
|
||||
VkPipelineCache = new PipelineCache(device);
|
||||
}
|
||||
|
||||
public void Resize(Window window){
|
||||
swapChain.cleanup(device);
|
||||
surface.cleanup(Instance);
|
||||
var EngCfg = EngineConfig.getInstance();
|
||||
surface = new Surface(Instance,PhysDevice,window);
|
||||
swapChain = new SwapChain(window, device, surface, EngCfg.GetRequestedImages(),EngCfg.GetVSYNC());
|
||||
}
|
||||
public void cleanup(){
|
||||
swapChain.cleanup(device);
|
||||
surface.cleanup(Instance);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package net.halbear.Terrain4J.EngineCore.Main;
|
|||
|
||||
import net.halbear.Terrain4J.EngineCore.Display.Window;
|
||||
import net.halbear.Terrain4J.EngineCore.Logic.*;
|
||||
import net.halbear.Terrain4J.EngineCore.Main.Scene.Scene;
|
||||
import net.halbear.Terrain4J.EngineCore.Threads.FastTickThread;
|
||||
import net.halbear.Terrain4J.EngineCore.Threads.MainThread;
|
||||
import net.halbear.Terrain4J.EngineCore.Threads.RenderThread;
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
package net.halbear.Terrain4J.EngineCore.Main;
|
||||
|
||||
import net.halbear.Terrain4J.EngineCore.Display.Window;
|
||||
|
||||
public class Scene {
|
||||
public Scene(Window window) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package net.halbear.Terrain4J.EngineCore.Main.Scene;
|
||||
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Quaternionf;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
public class Actor{
|
||||
private final String ID;
|
||||
private final String ModelID;
|
||||
private final Matrix4f ModelMatrix;
|
||||
private final Vector3f Position;
|
||||
private final Quaternionf Rotation;
|
||||
private float Scale;
|
||||
|
||||
public Actor(String ID, String ModelID, Vector3f Position){
|
||||
this.ID = ID;
|
||||
this.ModelID = ModelID;
|
||||
this.Position = Position;
|
||||
Scale = 1f;
|
||||
Rotation = new Quaternionf();
|
||||
ModelMatrix = new Matrix4f();
|
||||
UpdateModelMatrix();
|
||||
}
|
||||
|
||||
public void ResetRotation(){
|
||||
Rotation.x = 0;
|
||||
Rotation.y = 0;
|
||||
Rotation.z = 0;
|
||||
Rotation.w = 1.0f;
|
||||
}
|
||||
|
||||
public final void SetPosition(float x, float y, float z){
|
||||
Position.x = x;
|
||||
Position.y = y;
|
||||
Position.z = z;
|
||||
UpdateModelMatrix();
|
||||
}
|
||||
|
||||
public void SetScale(float Scale){
|
||||
this.Scale = Scale;
|
||||
UpdateModelMatrix();
|
||||
}
|
||||
|
||||
public void UpdateModelMatrix(){
|
||||
ModelMatrix.translationRotateScale(Position, Rotation, Scale);
|
||||
}
|
||||
|
||||
public String GetID(){return ID;}
|
||||
public String GetModelID(){return ModelID;}
|
||||
public Matrix4f GetModelMatrix(){return ModelMatrix;}
|
||||
public Vector3f GetPosition(){ return Position;}
|
||||
public Quaternionf GetRotation(){return Rotation;}
|
||||
public float GetScale(){return Scale;}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package net.halbear.Terrain4J.EngineCore.Main.Scene;
|
||||
|
||||
import net.halbear.Terrain4J.EngineCore.Display.Window;
|
||||
import net.halbear.Terrain4J.EngineCore.Logic.*;
|
||||
import net.halbear.Terrain4J.EngineCore.Vulkan.Rendering.Projection.Project3D;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Scene {
|
||||
|
||||
private final List<Actor> Actors;
|
||||
private final Project3D Projection;
|
||||
|
||||
public Scene(Window window) {
|
||||
Actors = new ArrayList<>();
|
||||
var EngConfig = EngineConfig.getInstance();
|
||||
Projection = new Project3D(EngConfig.GetFOV(),EngConfig.GetZNearPlane(), EngConfig.GetZFarPlane(),
|
||||
window.getWidth(), window.getHeight());
|
||||
}
|
||||
|
||||
public void AddActor(Actor NewActor){Actors.add(NewActor);}
|
||||
public List<Actor> GetActors(){return Actors;}
|
||||
public Project3D GetProjection(){return Projection;}
|
||||
public void RemoveAllActors(){Actors.clear();}
|
||||
public void RemoveActor(Actor actor){Actors.removeIf(Actor->Actor.GetID().equals(actor.GetID()));}
|
||||
}
|
||||
|
|
@ -1,12 +1,9 @@
|
|||
package net.halbear.Terrain4J.EngineCore.Threads;
|
||||
|
||||
import net.halbear.Terrain4J.EngineCore.Display.Window;
|
||||
import net.halbear.Terrain4J.EngineCore.Logic.EngineConfig;
|
||||
import net.halbear.Terrain4J.EngineCore.Logic.EngineInstance;
|
||||
import net.halbear.Terrain4J.EngineCore.Logic.GameLogic;
|
||||
import net.halbear.Terrain4J.EngineCore.Logic.InitData;
|
||||
import net.halbear.Terrain4J.EngineCore.Main.PrimaryRuntime;
|
||||
import net.halbear.Terrain4J.EngineCore.Main.Scene;
|
||||
import org.tinylog.Logger;
|
||||
|
||||
public class MainThread extends EngineThread {
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
package net.halbear.Terrain4J.EngineCore.Vulkan.Rendering;
|
||||
|
||||
public class Image {
|
||||
|
||||
public static class ImageData{
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package net.halbear.Terrain4J.EngineCore.Vulkan.Rendering.Pipeline.Images;
|
||||
|
||||
import net.halbear.Terrain4J.EngineCore.Logic.VulkanContext;
|
||||
import net.halbear.Terrain4J.EngineCore.Vulkan.Structure.DisplayToScreen.ImageView;
|
||||
|
||||
import static org.lwjgl.vulkan.VK13.*;
|
||||
public class Attachment {
|
||||
private final Image VkImage;
|
||||
private final ImageView VkImageView;
|
||||
private boolean DepthAttachment;
|
||||
|
||||
public Attachment(VulkanContext VkCtx, int Width, int Height, int Format, int Usage){
|
||||
var ImageData = new Image.ImageData().Width(Width).Height(Height).Format(Format)
|
||||
.Usage(Usage | VK_IMAGE_USAGE_SAMPLED_BIT);
|
||||
VkImage = new Image(VkCtx, ImageData);
|
||||
|
||||
int AspectMask = 0;
|
||||
if((Usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) > 0){
|
||||
AspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
DepthAttachment = false;
|
||||
}
|
||||
if((Usage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) > 0){
|
||||
AspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
DepthAttachment = true;
|
||||
}
|
||||
var ImageViewData = new ImageView.ImageViewData().Format(VkImage.GetFormat()).AspectMask(AspectMask);
|
||||
VkImageView = new ImageView(VkCtx.GetDevice(), VkImage.getVulkanImage(), ImageViewData);
|
||||
}
|
||||
|
||||
public Image GetVkImage(){return VkImage;}
|
||||
public ImageView GetVkImageView(){return VkImageView;}
|
||||
public boolean IsDepthAttached(){return DepthAttachment;}
|
||||
|
||||
public void CleanUp(VulkanContext VkCtx){
|
||||
VkImageView.cleanup(VkCtx.GetDevice());
|
||||
VkImage.CleanUp(VkCtx);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
package net.halbear.Terrain4J.EngineCore.Vulkan.Rendering.Pipeline.Images;
|
||||
|
||||
import net.halbear.Terrain4J.EngineCore.Logic.VulkanContext;
|
||||
import net.halbear.Terrain4J.EngineCore.Vulkan.Structure.DeviceLayers.Device;
|
||||
import net.halbear.Terrain4J.EngineCore.Vulkan.Util.VulkanUtils;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
import org.lwjgl.vulkan.VkImageCreateInfo;
|
||||
import org.lwjgl.vulkan.VkMemoryAllocateInfo;
|
||||
import org.lwjgl.vulkan.VkMemoryRequirements;
|
||||
|
||||
import java.nio.LongBuffer;
|
||||
|
||||
import static org.lwjgl.vulkan.VK10.*;
|
||||
import static org.lwjgl.vulkan.VK13.VK_FORMAT_R8G8B8A8_SRGB;
|
||||
|
||||
public class Image {
|
||||
|
||||
private final int Format;
|
||||
private final int MipLevels;
|
||||
private final long VulkanImage;
|
||||
private final long VulkanMemory;
|
||||
|
||||
public Image(VulkanContext VkCtx, ImageData imageData){
|
||||
try(var MemStack = MemoryStack.stackPush()){
|
||||
this.Format = imageData.Format;
|
||||
this.MipLevels = imageData.MipMapLevels;
|
||||
|
||||
VkImageCreateInfo vkImageCreateInfo = VkImageCreateInfo.calloc(MemStack)
|
||||
.sType$Default()
|
||||
.imageType(VK_IMAGE_TYPE_2D)
|
||||
.format(Format)
|
||||
.extent(it -> it
|
||||
.width(imageData.Width)
|
||||
.height(imageData.Height)
|
||||
.depth(1))
|
||||
.mipLevels(MipLevels)
|
||||
.arrayLayers(imageData.ArrayLayers)
|
||||
.samples(imageData.SampleCount)
|
||||
.initialLayout(VK_IMAGE_LAYOUT_UNDEFINED)
|
||||
.sharingMode(VK_SHARING_MODE_EXCLUSIVE)
|
||||
.tiling(VK_IMAGE_TILING_OPTIMAL)
|
||||
.usage(imageData.Usage);
|
||||
Device device = VkCtx.GetDevice();
|
||||
LongBuffer LongPtr = MemStack.mallocLong(1);
|
||||
VulkanUtils.vkCheck(vkCreateImage(device.FetchVulkanDevice(),vkImageCreateInfo,null,LongPtr),
|
||||
"Failed to create Vulkan Image");
|
||||
VulkanImage = LongPtr.get(0);
|
||||
VkMemoryRequirements memoryRequirements = VkMemoryRequirements.calloc(MemStack);
|
||||
vkGetImageMemoryRequirements(device.FetchVulkanDevice(), VulkanImage, memoryRequirements);
|
||||
|
||||
var MemoryAllocation = VkMemoryAllocateInfo.calloc(MemStack)
|
||||
.sType$Default()
|
||||
.allocationSize(memoryRequirements.size())
|
||||
.memoryTypeIndex(VulkanUtils.MemoryTypeFromProperties(VkCtx, memoryRequirements.memoryTypeBits(),0));
|
||||
//allocate
|
||||
VulkanUtils.vkCheck(vkAllocateMemory(device.FetchVulkanDevice(),MemoryAllocation,null,LongPtr)
|
||||
,"Failed to allocate Memory for Vulkan Image");
|
||||
VulkanMemory = LongPtr.get(0);
|
||||
//bind
|
||||
VulkanUtils.vkCheck(vkBindImageMemory(device.FetchVulkanDevice(),VulkanImage,VulkanMemory,0)
|
||||
,"Failed to bind memory for Vulkan Image");
|
||||
}
|
||||
}
|
||||
public void CleanUp(VulkanContext VkCtx){
|
||||
vkDestroyImage(VkCtx.GetDevice().FetchVulkanDevice(), VulkanImage,null);
|
||||
vkFreeMemory(VkCtx.GetDevice().FetchVulkanDevice(), VulkanMemory,null);
|
||||
}
|
||||
public int GetFormat(){return Format;}
|
||||
public int GetMipLevels(){return MipLevels;}
|
||||
public long getVulkanImage(){return VulkanImage;}
|
||||
|
||||
public static class ImageData{
|
||||
private int ArrayLayers;
|
||||
private int Format;
|
||||
private int Height;
|
||||
private int Width;
|
||||
private int MipMapLevels;
|
||||
private int SampleCount;
|
||||
private int Usage;
|
||||
|
||||
public ImageData(){
|
||||
Format = VK_FORMAT_R8G8B8A8_SRGB;
|
||||
MipMapLevels = 1;
|
||||
SampleCount = 1;
|
||||
ArrayLayers = 1;
|
||||
}
|
||||
public ImageData ArrayLayers(int ArrayLayers){
|
||||
this.ArrayLayers = ArrayLayers;
|
||||
return this;
|
||||
}
|
||||
public ImageData Format(int Format){
|
||||
this.Format = Format;
|
||||
return this;
|
||||
}
|
||||
public ImageData Height(int Height){
|
||||
this.Height = Height;
|
||||
return this;
|
||||
}
|
||||
public ImageData Width(int Width){
|
||||
this.Width = Width;
|
||||
return this;
|
||||
}
|
||||
public ImageData MipMapLevels(int MipLevel){
|
||||
this.MipMapLevels = MipLevel;
|
||||
return this;
|
||||
}
|
||||
public ImageData SampleCount(int Samples){
|
||||
this.SampleCount = Samples;
|
||||
return this;
|
||||
}
|
||||
public ImageData Usage(int Usage){
|
||||
this.Usage = Usage;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -67,12 +67,42 @@ public class Pipeline {
|
|||
.pAttachments(BlendAttributeState);
|
||||
IntBuffer ColourFormats = MemStack.mallocInt(1);
|
||||
ColourFormats.put(0,BuildInfo.GetColourFormat());
|
||||
|
||||
VkPipelineDepthStencilStateCreateInfo DepthStencil = null;
|
||||
if(BuildInfo.GetDepthFormat() != VK_FORMAT_UNDEFINED){
|
||||
DepthStencil = VkPipelineDepthStencilStateCreateInfo.calloc(MemStack)
|
||||
.sType$Default()
|
||||
.depthTestEnable(true)
|
||||
.depthWriteEnable(true)
|
||||
.depthCompareOp(VK_COMPARE_OP_LESS_OR_EQUAL)
|
||||
.depthBoundsTestEnable(false)
|
||||
.stencilTestEnable(false);
|
||||
}
|
||||
|
||||
var RendererCreateInfo = VkPipelineRenderingCreateInfo.calloc(MemStack)
|
||||
.sType$Default()
|
||||
.colorAttachmentCount(1)
|
||||
.pColorAttachmentFormats(ColourFormats);
|
||||
|
||||
if(DepthStencil != null){RendererCreateInfo.depthAttachmentFormat(BuildInfo.GetDepthFormat());}
|
||||
|
||||
VkPushConstantRange.Buffer VkPushConstRangeBuffer = null;
|
||||
PushConstantsRange[] PushConstRanges = BuildInfo.GetPushConstantRanges();
|
||||
int PushConstCount = PushConstRanges.length != 0 ? PushConstRanges.length : 0;
|
||||
if(PushConstCount > 0){
|
||||
VkPushConstRangeBuffer = VkPushConstantRange.calloc(PushConstCount,MemStack);
|
||||
for(int i = 0; i < PushConstCount; i++){
|
||||
PushConstantsRange pushConstantsRange = PushConstRanges[i];
|
||||
VkPushConstRangeBuffer.get(i)
|
||||
.stageFlags(pushConstantsRange.Stage())
|
||||
.offset(pushConstantsRange.Offset())
|
||||
.size(pushConstantsRange.Size());
|
||||
}
|
||||
}
|
||||
var PipelineLayoutCreateInfoPtr = VkPipelineLayoutCreateInfo.calloc(MemStack)
|
||||
.sType$Default();
|
||||
.sType$Default()
|
||||
.pPushConstantRanges(VkPushConstRangeBuffer);
|
||||
|
||||
VulkanUtils.vkCheck(vkCreatePipelineLayout(device.FetchVulkanDevice(), PipelineLayoutCreateInfoPtr,null,longPtr)
|
||||
,"Unable to create new pipeline layout");
|
||||
VulkanPipelineLayout = longPtr.get(0);
|
||||
|
|
@ -90,6 +120,9 @@ public class Pipeline {
|
|||
.pDynamicState(VulkanPipelineDynamicStateCreateInfo)
|
||||
.layout(VulkanPipelineLayout)
|
||||
.pNext(RendererCreateInfo);
|
||||
|
||||
if(DepthStencil != null){PipelineCreateInfo.pDepthStencilState(DepthStencil);}
|
||||
|
||||
VulkanUtils.vkCheck(vkCreateGraphicsPipelines(device.FetchVulkanDevice(),
|
||||
VkCtx.GetVkPipelineCache().GetVkPipelineCache(), PipelineCreateInfo,
|
||||
null, longPtr),"Could not create new pipeline");
|
||||
|
|
|
|||
|
|
@ -2,16 +2,35 @@ package net.halbear.Terrain4J.EngineCore.Vulkan.Rendering.Pipeline;
|
|||
|
||||
import net.halbear.Terrain4J.EngineCore.Vulkan.Rendering.Shader.ShaderModule;
|
||||
import org.lwjgl.vulkan.VkPipelineVertexInputStateCreateInfo;
|
||||
import org.lwjgl.vulkan.VkPushConstantRange;
|
||||
|
||||
import static org.lwjgl.vulkan.VK10.VK_FORMAT_UNDEFINED;
|
||||
|
||||
public class PipelineBuildInfo {
|
||||
private final int ColourFormat;
|
||||
private final ShaderModule[] ShaderModules;
|
||||
private final VkPipelineVertexInputStateCreateInfo VertexInput;
|
||||
private int DepthFormat;
|
||||
private PushConstantsRange[] PushConstRanges;
|
||||
|
||||
public PipelineBuildInfo(ShaderModule[] shaderModules, VkPipelineVertexInputStateCreateInfo VertexInput, int ColourFormat){
|
||||
this.ColourFormat = ColourFormat;
|
||||
this.ShaderModules = shaderModules;
|
||||
this.VertexInput = VertexInput;
|
||||
DepthFormat = VK_FORMAT_UNDEFINED;
|
||||
}
|
||||
|
||||
public PipelineBuildInfo SetDepthFormat(int DepthFormat){
|
||||
this.DepthFormat = DepthFormat;
|
||||
return this;
|
||||
}
|
||||
public PipelineBuildInfo SetPushConstantRanges(PushConstantsRange[] PushConstRanges){
|
||||
this.PushConstRanges = PushConstRanges;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PushConstantsRange[] GetPushConstantRanges(){return PushConstRanges;}
|
||||
public int GetDepthFormat(){return DepthFormat;}
|
||||
public int GetColourFormat(){return ColourFormat;}
|
||||
public ShaderModule[] GetShaderModules(){return ShaderModules;}
|
||||
public VkPipelineVertexInputStateCreateInfo GetVertexInputStateCreateInfo(){return VertexInput;}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
package net.halbear.Terrain4J.EngineCore.Vulkan.Rendering.Pipeline;
|
||||
|
||||
public record PushConstantsRange(int Stage, int Offset, int Size) {
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package net.halbear.Terrain4J.EngineCore.Vulkan.Rendering.Projection;
|
||||
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
public class Project3D {
|
||||
private final float FOV; // IT IS IN RADIANS!! REMEEEEMMMBER RADIANS!!!! PI/180!!!!!
|
||||
// there is a universal DegToRad conversion variable one can use in EngineConfig if they need to convert it
|
||||
private final Matrix4f ProjectionMatrix;
|
||||
private final float ZFarPlane;
|
||||
private final float ZNearPlane;
|
||||
|
||||
public Project3D(float FOV, float zNear, float zFar, int Width, int Height){
|
||||
this.FOV = FOV;
|
||||
this.ZFarPlane = zFar;
|
||||
this.ZNearPlane = zNear;
|
||||
ProjectionMatrix = new Matrix4f();
|
||||
Resize(Width, Height);
|
||||
}
|
||||
|
||||
public float GetFOV(){
|
||||
return FOV;
|
||||
}
|
||||
public float GetFarZ(){
|
||||
return ZFarPlane;
|
||||
}
|
||||
public float GetNearZ(){
|
||||
return ZNearPlane;
|
||||
}
|
||||
public void Resize(int Width, int Height){
|
||||
ProjectionMatrix.identity();
|
||||
ProjectionMatrix.perspective(FOV, (float)Width/(float)Height, ZNearPlane, ZFarPlane, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,8 @@ import static org.lwjgl.vulkan.VK13.VK_FORMAT_R32G32B32_SFLOAT;
|
|||
import static org.lwjgl.vulkan.VK13.VK_VERTEX_INPUT_RATE_VERTEX;
|
||||
|
||||
public class VertexBufferStructure {
|
||||
private final int NUMBER_OF_ATTRIBUTES = 1;
|
||||
public static final int TEXT_COORD_COMPONENTS = 2;
|
||||
private static final int NUMBER_OF_ATTRIBUTES = 2;
|
||||
private final int POSITION_COMPONENTS = 3;
|
||||
|
||||
private final VkPipelineVertexInputStateCreateInfo VertexInput;
|
||||
|
|
@ -23,14 +24,25 @@ public class VertexBufferStructure {
|
|||
|
||||
int i =0;
|
||||
int Offset = 0;
|
||||
//vertex position
|
||||
VertexInputAttributes.get(i)
|
||||
.binding(0)
|
||||
.location(i)
|
||||
.format(VK_FORMAT_R32G32B32_SFLOAT)
|
||||
.offset(Offset);
|
||||
i++;
|
||||
Offset += POSITION_COMPONENTS * VulkanUtils.FLOAT_SIZE;
|
||||
//texture coordinates
|
||||
VertexInputAttributes.get(i)
|
||||
.binding(0)
|
||||
.location(i)
|
||||
.format(VK_FORMAT_R32G32B32_SFLOAT)
|
||||
.offset(Offset);
|
||||
|
||||
int Stride = Offset + TEXT_COORD_COMPONENTS * VulkanUtils.FLOAT_SIZE;
|
||||
VertexInputBindings.get(0)
|
||||
.binding(0)
|
||||
.stride(POSITION_COMPONENTS * VulkanUtils.FLOAT_SIZE)
|
||||
.stride(Stride)
|
||||
.inputRate(VK_VERTEX_INPUT_RATE_VERTEX);
|
||||
VertexInput
|
||||
.sType$Default()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package net.halbear.Terrain4J.EngineCore.Vulkan.Rendering.VkModel;
|
||||
|
||||
public record MeshData(String ID, float[] Positions, int[] Indices) {
|
||||
public record MeshData(String ID, float[] Positions, float[] textureCoords, int[] Indices) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,11 @@ public class ModelsCache {
|
|||
|
||||
private static TransferBuffer CreateVerticesBuffer(VulkanContext VkCtx, MeshData meshData){
|
||||
float[] Positions = meshData.Positions();
|
||||
int ElementCount = Positions.length;
|
||||
float[] TextureCoords = meshData.textureCoords();
|
||||
if(TextureCoords == null || TextureCoords.length == 0){
|
||||
TextureCoords = new float[(Positions.length/3) * 2];
|
||||
}
|
||||
int ElementCount = Positions.length + TextureCoords.length;
|
||||
int BufferSize = ElementCount * VulkanUtils.FLOAT_SIZE;
|
||||
var SrcBuffer = new VulkanBuffer(VkCtx,BufferSize,
|
||||
VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
||||
|
|
@ -66,9 +70,12 @@ public class ModelsCache {
|
|||
int Rows = Positions.length / 3;
|
||||
for(int Row = 0; Row < Rows; Row++){
|
||||
int StartPos = Row * 3;
|
||||
int StartTexCoordPos = Row * 2;
|
||||
data.put(Positions[StartPos]);
|
||||
data.put(Positions[StartPos+1]);
|
||||
data.put(Positions[StartPos+2]);
|
||||
data.put(TextureCoords[StartTexCoordPos]);
|
||||
data.put(TextureCoords[StartTexCoordPos + 1]);
|
||||
}
|
||||
SrcBuffer.UnMapMemory(VkCtx);
|
||||
return new TransferBuffer(SrcBuffer,DstBuffer);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package net.halbear.Terrain4J.EngineCore.Vulkan.Structure.DisplayToScreen;
|
||||
|
||||
import net.halbear.Terrain4J.EngineCore.Display.Render;
|
||||
import net.halbear.Terrain4J.EngineCore.Logic.EngineConfig;
|
||||
import net.halbear.Terrain4J.EngineCore.Logic.VulkanContext;
|
||||
import net.halbear.Terrain4J.EngineCore.Vulkan.Rendering.Pipeline.Images.Attachment;
|
||||
import net.halbear.Terrain4J.EngineCore.Vulkan.Rendering.Pipeline.Pipeline;
|
||||
import net.halbear.Terrain4J.EngineCore.Vulkan.Rendering.Pipeline.PipelineBuildInfo;
|
||||
import net.halbear.Terrain4J.EngineCore.Vulkan.Rendering.Pipeline.PipelineCache;
|
||||
import net.halbear.Terrain4J.EngineCore.Vulkan.Rendering.Pipeline.PushConstantsRange;
|
||||
import net.halbear.Terrain4J.EngineCore.Vulkan.Rendering.Shader.ShaderCompiler;
|
||||
import net.halbear.Terrain4J.EngineCore.Vulkan.Rendering.Shader.ShaderModule;
|
||||
import net.halbear.Terrain4J.EngineCore.Vulkan.Rendering.VertexBufferStructure;
|
||||
|
|
@ -15,10 +15,11 @@ import net.halbear.Terrain4J.EngineCore.Vulkan.Rendering.VkModel.VulkanModel;
|
|||
import net.halbear.Terrain4J.EngineCore.Vulkan.Structure.DeviceLayers.CommandBuffer;
|
||||
import net.halbear.Terrain4J.EngineCore.Vulkan.Util.VulkanUtils;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
import org.lwjgl.util.shaderc.Shaderc;
|
||||
import org.lwjgl.vulkan.*;
|
||||
import org.tinylog.Logger;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.LongBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
|
|
@ -26,7 +27,12 @@ import static org.lwjgl.vulkan.KHRSwapchain.VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
|||
import static org.lwjgl.vulkan.KHRSynchronization2.VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL_KHR;
|
||||
import static org.lwjgl.vulkan.VK13.*;
|
||||
|
||||
public class SceneRender { //dynamic rendering;
|
||||
public class SceneRender {//dynamic rendering
|
||||
private static final int DEPTH_FORMAT = VK_FORMAT_D16_UNORM;
|
||||
private final VkClearValue ClearValueDepth;
|
||||
private final ByteBuffer PushConstBuffer;
|
||||
private Attachment[] AttDepth;
|
||||
private VkRenderingAttachmentInfo[] AttInfoDepth;
|
||||
private VkClearValue ClearValueColour;
|
||||
private VkRenderingAttachmentInfo.Buffer[] AttachmentInfoColour;
|
||||
private VkRenderingInfo[] RenderInfo;
|
||||
|
|
@ -44,14 +50,47 @@ public class SceneRender { //dynamic rendering;
|
|||
private final Pipeline VkPipeline;
|
||||
|
||||
public SceneRender(VulkanContext vulkanContext){
|
||||
ClearValueDepth = VkClearValue.calloc().color(c->c.float32(0,1.0f));
|
||||
AttDepth = createDepthAttachments(vulkanContext);
|
||||
AttInfoDepth = createDepthAttachmentsInfo(vulkanContext, AttDepth, ClearValueDepth);
|
||||
ClearValueColour = VkClearValue.calloc().color(c->c.float32(0,R).float32(1,G).float32(2,B).float32(3,1.0f));
|
||||
AttachmentInfoColour = CreateColourAttachmentsInfo(vulkanContext,ClearValueColour);
|
||||
RenderInfo = CreateRenderInfo(vulkanContext, AttachmentInfoColour);
|
||||
|
||||
RenderInfo = CreateRenderInfo(vulkanContext, AttachmentInfoColour, AttInfoDepth);
|
||||
PushConstBuffer = MemoryUtil.memAlloc(VulkanUtils.MATRIX4X4_SIZE * 2);
|
||||
ShaderModule[] shaderModules = SceneRender.CreateShaderModules(vulkanContext);
|
||||
VkPipeline = CreatePipeline(vulkanContext, shaderModules);
|
||||
Arrays.asList(shaderModules).forEach(shaderModule -> shaderModule.CleanUp(vulkanContext));
|
||||
}
|
||||
|
||||
private static Attachment[] createDepthAttachments(VulkanContext VkCtx){
|
||||
SwapChain swapChain = VkCtx.GetSwapChain();
|
||||
int ImageCount = swapChain.GetImageCount();
|
||||
VkExtent2D swapChainExtent = swapChain.GetSwapChainExtent();
|
||||
Attachment[] DepthAttachments = new Attachment[ImageCount];
|
||||
for(int i = 0; i < ImageCount; i++){
|
||||
DepthAttachments[i] = new Attachment(VkCtx, swapChainExtent.width(), swapChainExtent.height(),
|
||||
DEPTH_FORMAT,VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT);
|
||||
}
|
||||
return DepthAttachments;
|
||||
}
|
||||
|
||||
private static VkRenderingAttachmentInfo[] createDepthAttachmentsInfo(VulkanContext VkCtx, Attachment[] DepthAttachments, VkClearValue clearValue){
|
||||
SwapChain swapChain = VkCtx.GetSwapChain();
|
||||
int ImageCount = swapChain.GetImageCount();
|
||||
var Result = new VkRenderingAttachmentInfo[ImageCount];
|
||||
for(int i = 0; i < ImageCount; i++){
|
||||
var Attachments = VkRenderingAttachmentInfo.calloc()
|
||||
.sType$Default()
|
||||
.imageView(DepthAttachments[i].GetVkImageView().GetVulkanImageView())
|
||||
.imageLayout(VK_IMAGE_LAYOUT_STENCIL_ATTACHMENT_OPTIMAL)
|
||||
.loadOp(VK_ATTACHMENT_LOAD_OP_CLEAR)
|
||||
.storeOp(VK_ATTACHMENT_STORE_OP_DONT_CARE)
|
||||
.clearValue(clearValue);
|
||||
Result[i] = Attachments;
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
private static ShaderModule[] CreateShaderModules(VulkanContext VkCtx){
|
||||
if(EngineConfig.getInstance().RecompileShaders()){
|
||||
ShaderCompiler.CompileGLSLShaderOnChange(VERTEX_SHADER_FILE_GLSL, Shaderc.shaderc_glsl_vertex_shader);
|
||||
|
|
@ -62,14 +101,21 @@ public class SceneRender { //dynamic rendering;
|
|||
new ShaderModule(VkCtx, VK_SHADER_STAGE_FRAGMENT_BIT, FRAGMENT_SHADER_FILE_SPV)
|
||||
};
|
||||
}
|
||||
|
||||
private static Pipeline CreatePipeline(VulkanContext VkCtx, ShaderModule[] ShaderModules){
|
||||
var vertexBufferStructure = new VertexBufferStructure();
|
||||
var BuildInfo = new PipelineBuildInfo(ShaderModules,vertexBufferStructure.getVertexInput(),
|
||||
VkCtx.GetSurface().GetSurfaceFormat().ImageFormat());
|
||||
VkCtx.GetSurface().GetSurfaceFormat().ImageFormat())
|
||||
.SetDepthFormat(DEPTH_FORMAT)
|
||||
.SetPushConstantRanges(
|
||||
new PushConstantsRange[]{
|
||||
new PushConstantsRange(VK_SHADER_STAGE_VERTEX_BIT,0,VulkanUtils.MATRIX4X4_SIZE * 2)
|
||||
});
|
||||
var pipeline = new Pipeline(VkCtx, BuildInfo);
|
||||
vertexBufferStructure.cleanup();
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
public void UpdateColours(VulkanContext vulkanContext){
|
||||
if (Gb){
|
||||
if (G < 1.0f && Math.random() <0.5) G+=0.0005f;
|
||||
|
|
@ -94,7 +140,7 @@ public class SceneRender { //dynamic rendering;
|
|||
}
|
||||
ClearValueColour = VkClearValue.calloc().color(c->c.float32(0,R).float32(1,G).float32(2,B).float32(3,1.0f));
|
||||
AttachmentInfoColour = CreateColourAttachmentsInfo(vulkanContext,ClearValueColour);
|
||||
RenderInfo = CreateRenderInfo(vulkanContext, AttachmentInfoColour);
|
||||
RenderInfo = CreateRenderInfo(vulkanContext, AttachmentInfoColour, AttInfoDepth);
|
||||
}
|
||||
|
||||
public void Render(VulkanContext vulkanContext, CommandBuffer commandBuffer,ModelsCache modelsCache, int ImageIndex){
|
||||
|
|
@ -108,7 +154,15 @@ public class SceneRender { //dynamic rendering;
|
|||
VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
VK_ACCESS_2_NONE, VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT, VK_IMAGE_ASPECT_COLOR_BIT);
|
||||
VulkanUtils.ImageBarrier(MemStack, CommandHandle, AttDepth[ImageIndex].GetVkImage().getVulkanImage(),
|
||||
VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL,
|
||||
VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT,
|
||||
VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT,
|
||||
VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
|
||||
VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
|
||||
vkCmdBeginRendering(CommandHandle, RenderInfo[ImageIndex]);
|
||||
|
||||
vkCmdBindPipeline(CommandHandle, VK_PIPELINE_BIND_POINT_GRAPHICS, VkPipeline.GetVulkanPipeline());
|
||||
|
||||
VkExtent2D swapChainExtent = swapChain.GetSwapChainExtent();
|
||||
|
|
@ -129,8 +183,11 @@ public class SceneRender { //dynamic rendering;
|
|||
|
||||
LongBuffer offsets = MemStack.mallocLong(1).put(0,0L);
|
||||
LongBuffer vertexBuffer = MemStack.mallocLong(1);
|
||||
|
||||
//Fetch Actor Code to be written Later
|
||||
var VulkanModels = modelsCache.GetModelMap().values();
|
||||
for(VulkanModel VkModel : VulkanModels){
|
||||
|
||||
for(VulkanMesh mesh : VkModel.GetVkMeshList()){
|
||||
vertexBuffer.put(0,mesh.VerticesBuffer().GetBuffer());
|
||||
vkCmdBindVertexBuffers(CommandHandle,0,vertexBuffer,offsets);
|
||||
|
|
@ -148,19 +205,22 @@ public class SceneRender { //dynamic rendering;
|
|||
}
|
||||
}
|
||||
|
||||
private static VkRenderingInfo[] CreateRenderInfo(VulkanContext vulkanContext, VkRenderingAttachmentInfo.Buffer[] ColourAttachments){
|
||||
private static VkRenderingInfo[] CreateRenderInfo(VulkanContext vulkanContext, VkRenderingAttachmentInfo.Buffer[] ColourAttachments, VkRenderingAttachmentInfo[] DepthAttachments){
|
||||
SwapChain swapChain = vulkanContext.GetSwapChain();
|
||||
int ImageCount = swapChain.GetImageCount();
|
||||
var Result = new VkRenderingInfo[ImageCount];
|
||||
|
||||
try(var MemStack = MemoryStack.stackPush()){
|
||||
VkExtent2D Extent = swapChain.GetSwapChainExtent();
|
||||
var RenderArea = VkRect2D.calloc(MemStack).extent(Extent);
|
||||
|
||||
for(int i = 0; i < ImageCount; i++){
|
||||
var RenderInfo = VkRenderingInfo.calloc()
|
||||
.sType$Default()
|
||||
.renderArea(RenderArea)
|
||||
.layerCount(1)
|
||||
.pColorAttachments(ColourAttachments[i]);
|
||||
.pColorAttachments(ColourAttachments[i])
|
||||
.pDepthAttachment(DepthAttachments[i]);
|
||||
Result[i] = RenderInfo;
|
||||
}
|
||||
}
|
||||
|
|
@ -185,6 +245,11 @@ public class SceneRender { //dynamic rendering;
|
|||
VkPipeline.CleanUp(VkCtx);
|
||||
Arrays.asList(RenderInfo).forEach(VkRenderingInfo::free);
|
||||
Arrays.asList(AttachmentInfoColour).forEach(VkRenderingAttachmentInfo.Buffer::free);
|
||||
Arrays.asList(AttInfoDepth).forEach(VkRenderingAttachmentInfo::free);
|
||||
Arrays.asList(AttachmentInfoColour).forEach(VkRenderingAttachmentInfo.Buffer::free);
|
||||
Arrays.asList(AttDepth).forEach(Attachment->Attachment.CleanUp(VkCtx));
|
||||
MemoryUtil.memFree(PushConstBuffer);
|
||||
ClearValueDepth.free();
|
||||
ClearValueColour.free();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ public class VulkanUtils {
|
|||
|
||||
public static final int FLOAT_SIZE = 4;
|
||||
public static final int INT_SIZE = 4;
|
||||
public static final int MATRIX4X4_SIZE = 16 * FLOAT_SIZE;
|
||||
|
||||
public static int MemoryTypeFromProperties(VulkanContext vulkanContext, int TypeBits, int ReqMask){
|
||||
int result = -1;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue