view semicongine/engine.nim @ 1168:73eaec7e1690

add: function to get max supported sampling count
author sam <sam@basx.dev>
date Tue, 25 Jun 2024 21:20:15 +0700
parents cdf7ec8d04d2
children
line wrap: on
line source

import std/compilesettings
import std/algorithm
import std/monotimes
import std/options
import std/strformat
import std/sequtils
import std/logging
import std/os

import ./platform/window

import ./core
import ./vulkan/instance
import ./vulkan/device
import ./vulkan/physicaldevice
import ./vulkan/shader

import ./scene
import ./material
import ./renderer
import ./audio
import ./input
import ./text
import ./panel

import ./steam

const COUNT_N_RENDERTIMES = 199

type
  EngineState* = enum
    Starting
    Running
    Shutdown
  Engine* = object
    applicationName: string
    showFps: bool
    device: Device
    debugger: Debugger
    instance: Instance
    window: NativeWindow
    renderer: Option[Renderer]
    fullscreen: bool
    lastNRenderTimes: array[COUNT_N_RENDERTIMES, int64]
    currentRenderTimeI: int = 0

# forward declarations
func GetAspectRatio*(engine: Engine): float32

proc Destroy*(engine: var Engine) =
  checkVkResult engine.device.vk.vkDeviceWaitIdle()
  if engine.renderer.isSome:
    engine.renderer.get.Destroy()
  engine.device.Destroy()
  if engine.debugger.messenger.Valid:
    engine.debugger.Destroy()
  engine.window.Destroy()
  engine.instance.Destroy()
  if SteamAvailable():
    SteamShutdown()


proc InitEngine*(
  applicationName = querySetting(projectName),
  showFps = DEBUG,
  vulkanVersion = VK_MAKE_API_VERSION(0, 1, 3, 0),
  vulkanLayers: openArray[string] = [],
): Engine =
  echo "Set log level to ", ENGINE_LOGLEVEL
  setLogFilter(ENGINE_LOGLEVEL)

  TrySteamInit()
  if SteamAvailable():
    echo "Starting with Steam"
  else:
    echo "Starting without Steam"

  result.applicationName = applicationName
  result.showFps = showFps
  result.window = CreateWindow(result.applicationName)

  var
    layers = @vulkanLayers
    instanceExtensions: seq[string]

  if DEBUG:
    instanceExtensions.add "VK_EXT_debug_utils"
    layers.add "VK_LAYER_KHRONOS_validation"
    # This stuff might be usefull if we one day to smart GPU memory allocation,
    # but right now it just clobbers up the console log:
    # putEnv("VK_LAYER_ENABLES", "VK_VALIDATION_FEATURE_ENABLE_BEST_PRACTICES_EXT")
    putEnv("VK_LAYER_ENABLES", "VALIDATION_CHECK_ENABLE_VENDOR_SPECIFIC_AMD,VALIDATION_CHECK_ENABLE_VENDOR_SPECIFIC_NVIDIA,VK_VALIDATION_FEATURE_ENABLE_SYNCHRONIZATION_VALIDATION_EXTVK_VALIDATION_FEATURE_ENABLE_GPU_ASSISTED_EXT,VK_VALIDATION_FEATURE_ENABLE_SYNCHRONIZATION_VALIDATION_EXT")

  result.instance = result.window.CreateInstance(
    vulkanVersion = vulkanVersion,
    instanceExtensions = instanceExtensions,
    layers = layers.deduplicate(),
  )
  if DEBUG:
    result.debugger = result.instance.CreateDebugMessenger()
  # create devices
  let selectedPhysicalDevice = result.instance.GetPhysicalDevices().FilterBestGraphics()
  result.device = result.instance.CreateDevice(
    selectedPhysicalDevice,
    enabledExtensions = @[],
    selectedPhysicalDevice.FilterForGraphicsPresentationQueues()
  )
  StartMixerThread()

proc InitRenderer*(
  engine: var Engine,
  shaders: openArray[(MaterialType, ShaderConfiguration)],
  clearColor = NewVec4f(0, 0, 0, 0),
  backFaceCulling = true,
  vSync = false,
  inFlightFrames = 2,
  samples = VK_SAMPLE_COUNT_1_BIT,
) =

  assert not engine.renderer.isSome
  var allShaders = @shaders
  if not shaders.mapIt(it[0]).contains(EMPTY_MATERIAL):
    allShaders.add (EMPTY_MATERIAL, EMPTY_SHADER)
  if not shaders.mapIt(it[0]).contains(PANEL_MATERIAL_TYPE):
    allShaders.add (PANEL_MATERIAL_TYPE, PANEL_SHADER)
  if not shaders.mapIt(it[0]).contains(TEXT_MATERIAL_TYPE):
    allShaders.add (TEXT_MATERIAL_TYPE, TEXT_SHADER)
  engine.renderer = some(engine.device.InitRenderer(
    shaders = allShaders,
    clearColor = clearColor,
    backFaceCulling = backFaceCulling,
    vSync = vSync,
    inFlightFrames = inFlightFrames,
    samples = samples,
  ))

proc InitRenderer*(engine: var Engine, clearColor = NewVec4f(0, 0, 0, 0), vSync = false) =
  checkVkResult engine.device.vk.vkDeviceWaitIdle()
  engine.InitRenderer(@[], clearColor, vSync = vSync)
  checkVkResult engine.device.vk.vkDeviceWaitIdle()

proc LoadScene*(engine: var Engine, scene: var Scene) =
  debug &"start loading scene '{scene.name}'"
  assert engine.renderer.isSome
  assert not scene.loaded
  checkVkResult engine.device.vk.vkDeviceWaitIdle()
  scene.AddShaderGlobal(ASPECT_RATIO_ATTRIBUTE, engine.GetAspectRatio)
  engine.renderer.get.SetupDrawableBuffers(scene)
  engine.renderer.get.UpdateMeshData(scene, forceAll = true)
  engine.renderer.get.UpdateUniformData(scene, forceAll = true)
  checkVkResult engine.device.vk.vkDeviceWaitIdle()
  debug &"done loading scene '{scene.name}'"

proc UnloadScene*(engine: var Engine, scene: Scene) =
  debug &"unload scene '{scene.name}'"
  engine.renderer.get.Destroy(scene)

proc RenderScene*(engine: var Engine, scene: var Scene) =
  if WindowIsMinimized():
    return
  assert engine.renderer.isSome, "Renderer has not yet been initialized, call 'engine.InitRenderer' first"
  assert engine.renderer.get.HasScene(scene), &"Scene '{scene.name}' has not been loaded yet"
  let t0 = getMonoTime()

  if engine.renderer.get.StartNewFrame():
    scene.SetShaderGlobal(ASPECT_RATIO_ATTRIBUTE, engine.GetAspectRatio)
    engine.renderer.get.UpdateMeshData(scene)
    engine.renderer.get.UpdateUniformData(scene)
    engine.renderer.get.Render(scene)

  if engine.showFps:
    let nanoSecs = getMonoTime().ticks - t0.ticks
    engine.lastNRenderTimes[engine.currentRenderTimeI] = nanoSecs
    inc engine.currentRenderTimeI
    if engine.currentRenderTimeI >= engine.lastNRenderTimes.len:
      engine.currentRenderTimeI = 0
      engine.lastNRenderTimes.sort
      let
        min = float(engine.lastNRenderTimes[0]) / 1_000_000
        median = float(engine.lastNRenderTimes[engine.lastNRenderTimes.len div 2]) / 1_000_000
        max = float(engine.lastNRenderTimes[^1]) / 1_000_000
      engine.window.SetTitle(&"{engine.applicationName} ({min:.2}, {median:.2}, {max:.2})")


# wrappers for internal things
func GpuDevice*(engine: Engine): Device = engine.device
func GetWindow*(engine: Engine): auto = engine.window
func GetAspectRatio*(engine: Engine): float32 = engine.GetWindow().Size[0] / engine.GetWindow().Size[1]
proc ShowSystemCursor*(engine: Engine) = engine.window.ShowSystemCursor()
proc HideSystemCursor*(engine: Engine) = engine.window.HideSystemCursor()
func Fullscreen*(engine: Engine): bool = engine.fullscreen
proc `Fullscreen=`*(engine: var Engine, enable: bool) =
  if enable != engine.fullscreen:
    engine.fullscreen = enable
    engine.window.Fullscreen(engine.fullscreen)

func Limits*(engine: Engine): VkPhysicalDeviceLimits =
  engine.device.physicalDevice.properties.limits

func MaxFramebufferSampleCount*(engine: Engine, maxSamples = VK_SAMPLE_COUNT_8_BIT): VkSampleCountFlagBits =
  let available = VkSampleCountFlags(
    engine.Limits.framebufferColorSampleCounts.uint32 and engine.Limits.framebufferDepthSampleCounts.uint32
  ).toEnums
  return min(max(available), maxSamples)

proc UpdateInputs*(engine: Engine): bool =
  UpdateInputs(engine.window.PendingEvents())

proc ProcessEvents*(engine: Engine, panel: var Panel) =
  let hasMouseNow = panel.Contains(MousePositionNormalized(engine.window.Size), engine.GetAspectRatio)

  # enter/leave events
  if hasMouseNow:
    if panel.hasMouse:
      if panel.onMouseMove != nil: panel.onMouseMove(panel)
    else:
      if panel.onMouseEnter != nil: panel.onMouseEnter(panel)
  else:
    if panel.hasMouse:
      if panel.onMouseLeave != nil: panel.onMouseLeave(panel)

  # button events
  if hasMouseNow:
    if MouseWasPressed():
      if panel.onMouseDown != nil: panel.onMouseDown(panel, MousePressedButtons())
    if MouseWasReleased():
      if panel.onMouseUp != nil: panel.onMouseUp(panel, MouseReleasedButtons())

  panel.hasMouse = hasMouseNow