Added clock time
This commit is contained in:
@@ -18,6 +18,16 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Verify Visual Studio ATL
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
$atlHeaders = @(Get-ChildItem -Path "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC" -Filter atlbase.h -Recurse -ErrorAction SilentlyContinue)
|
||||||
|
if ($atlHeaders.Count -eq 0) {
|
||||||
|
Write-Error "Visual Studio Build Tools is missing ATL. Install the 'C++ ATL for latest v143 build tools (x86 & x64)' component, component ID Microsoft.VisualStudio.Component.VC.ATL, then restart the runner service."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host "Found ATL header: $($atlHeaders[0].FullName)"
|
||||||
|
|
||||||
- name: Configure Debug
|
- name: Configure Debug
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
@@ -92,6 +102,16 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Verify Visual Studio ATL
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
$atlHeaders = @(Get-ChildItem -Path "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC" -Filter atlbase.h -Recurse -ErrorAction SilentlyContinue)
|
||||||
|
if ($atlHeaders.Count -eq 0) {
|
||||||
|
Write-Error "Visual Studio Build Tools is missing ATL. Install the 'C++ ATL for latest v143 build tools (x86 & x64)' component, component ID Microsoft.VisualStudio.Component.VC.ATL, then restart the runner service."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host "Found ATL header: $($atlHeaders[0].FullName)"
|
||||||
|
|
||||||
- name: Build UI
|
- name: Build UI
|
||||||
shell: powershell
|
shell: powershell
|
||||||
working-directory: ui
|
working-directory: ui
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ set(APP_SOURCES
|
|||||||
"${APP_DIR}/resource.h"
|
"${APP_DIR}/resource.h"
|
||||||
"${APP_DIR}/runtime/RuntimeHost.cpp"
|
"${APP_DIR}/runtime/RuntimeHost.cpp"
|
||||||
"${APP_DIR}/runtime/RuntimeHost.h"
|
"${APP_DIR}/runtime/RuntimeHost.h"
|
||||||
|
"${APP_DIR}/runtime/RuntimeClock.cpp"
|
||||||
|
"${APP_DIR}/runtime/RuntimeClock.h"
|
||||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||||
"${APP_DIR}/runtime/RuntimeJson.h"
|
"${APP_DIR}/runtime/RuntimeJson.h"
|
||||||
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
|
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
|
||||||
@@ -158,6 +160,22 @@ endif()
|
|||||||
enable_testing()
|
enable_testing()
|
||||||
add_test(NAME RuntimeJsonTests COMMAND RuntimeJsonTests)
|
add_test(NAME RuntimeJsonTests COMMAND RuntimeJsonTests)
|
||||||
|
|
||||||
|
add_executable(RuntimeClockTests
|
||||||
|
"${APP_DIR}/runtime/RuntimeClock.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeClockTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RuntimeClockTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RuntimeClockTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME RuntimeClockTests COMMAND RuntimeClockTests)
|
||||||
|
|
||||||
add_executable(RuntimeParameterUtilsTests
|
add_executable(RuntimeParameterUtilsTests
|
||||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||||
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
|
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
|
||||||
|
|||||||
@@ -232,6 +232,8 @@ The Windows jobs validate native third-party dependencies before configuring CMa
|
|||||||
- `GPUDIRECT_DIR`: path to `Blackmagic DeckLink SDK 16.0/Win/Samples/NVIDIA_GPUDirect`.
|
- `GPUDIRECT_DIR`: path to `Blackmagic DeckLink SDK 16.0/Win/Samples/NVIDIA_GPUDirect`.
|
||||||
- `SLANG_ROOT`: path to the Slang binary release folder containing `bin/slangc.exe`.
|
- `SLANG_ROOT`: path to the Slang binary release folder containing `bin/slangc.exe`.
|
||||||
|
|
||||||
|
The Windows runner also needs the Visual Studio ATL component installed. In Visual Studio Build Tools 2022, add `C++ ATL for latest v143 build tools (x86 & x64)`, component ID `Microsoft.VisualStudio.Component.VC.ATL`.
|
||||||
|
|
||||||
Example runner paths:
|
Example runner paths:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -249,8 +251,7 @@ If neither variable is set, the workflow falls back to the repo-local defaults u
|
|||||||
- Genlock.
|
- Genlock.
|
||||||
- Find a better UI library.
|
- Find a better UI library.
|
||||||
- Logs.
|
- Logs.
|
||||||
- Continue source cleanup/refactoring.
|
- Continue source cleanup/refactoring. Pass 1 done
|
||||||
- Display the control URL in the Windows app, ideally clickable, without rendering it on the video output.
|
- Display the control URL in the Windows app, ideally clickable, without rendering it on the video output.
|
||||||
- Support a separate sound shader `.slang` file in shader packages.
|
- Support a separate sound shader `.slang` file in shader packages.
|
||||||
- Add runtime date/time uniforms using UTC and the PC's local offset.
|
|
||||||

|

|
||||||
|
|||||||
@@ -122,6 +122,8 @@ struct ShaderContext
|
|||||||
float2 inputResolution;
|
float2 inputResolution;
|
||||||
float2 outputResolution;
|
float2 outputResolution;
|
||||||
float time;
|
float time;
|
||||||
|
float utcTimeSeconds;
|
||||||
|
float utcOffsetSeconds;
|
||||||
float frameCount;
|
float frameCount;
|
||||||
float mixAmount;
|
float mixAmount;
|
||||||
float bypass;
|
float bypass;
|
||||||
@@ -137,6 +139,8 @@ Fields:
|
|||||||
- `inputResolution`: decoded input video resolution in pixels.
|
- `inputResolution`: decoded input video resolution in pixels.
|
||||||
- `outputResolution`: shader render resolution in pixels. The current pipeline renders the shader stack at input resolution, then scales the final frame to the configured DeckLink output mode.
|
- `outputResolution`: shader render resolution in pixels. The current pipeline renders the shader stack at input resolution, then scales the final frame to the configured DeckLink output mode.
|
||||||
- `time`: elapsed runtime time in seconds.
|
- `time`: elapsed runtime time in seconds.
|
||||||
|
- `utcTimeSeconds`: current UTC time of day from the host PC clock, expressed as seconds since UTC midnight.
|
||||||
|
- `utcOffsetSeconds`: host PC local UTC offset in seconds. Add this to `utcTimeSeconds` and wrap to `0..86400` to get local time of day.
|
||||||
- `frameCount`: incrementing frame counter.
|
- `frameCount`: incrementing frame counter.
|
||||||
- `mixAmount`: runtime mix amount.
|
- `mixAmount`: runtime mix amount.
|
||||||
- `bypass`: `1.0` when the layer is bypassed, otherwise `0.0`.
|
- `bypass`: `1.0` when the layer is bypassed, otherwise `0.0`.
|
||||||
|
|||||||
@@ -209,6 +209,7 @@
|
|||||||
<ClCompile Include="DeckLinkAPI_i.c" />
|
<ClCompile Include="DeckLinkAPI_i.c" />
|
||||||
<ClCompile Include="control\RuntimeServices.cpp" />
|
<ClCompile Include="control\RuntimeServices.cpp" />
|
||||||
<ClCompile Include="decklink\DeckLinkSession.cpp" />
|
<ClCompile Include="decklink\DeckLinkSession.cpp" />
|
||||||
|
<ClCompile Include="runtime\RuntimeClock.cpp" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="gl\GLExtensions.h" />
|
<ClInclude Include="gl\GLExtensions.h" />
|
||||||
@@ -224,6 +225,7 @@
|
|||||||
<ClInclude Include="gl\VideoFrameTransfer.h" />
|
<ClInclude Include="gl\VideoFrameTransfer.h" />
|
||||||
<ClInclude Include="control\RuntimeServices.h" />
|
<ClInclude Include="control\RuntimeServices.h" />
|
||||||
<ClInclude Include="decklink\DeckLinkSession.h" />
|
<ClInclude Include="decklink\DeckLinkSession.h" />
|
||||||
|
<ClInclude Include="runtime\RuntimeClock.h" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Image Include="LoopThroughWithOpenGLCompositing.ico" />
|
<Image Include="LoopThroughWithOpenGLCompositing.ico" />
|
||||||
|
|||||||
@@ -54,6 +54,9 @@
|
|||||||
<ClCompile Include="decklink\DeckLinkSession.cpp">
|
<ClCompile Include="decklink\DeckLinkSession.cpp">
|
||||||
<Filter>Source Files</Filter>
|
<Filter>Source Files</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="runtime\RuntimeClock.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="gl\GLExtensions.h">
|
<ClInclude Include="gl\GLExtensions.h">
|
||||||
@@ -95,6 +98,9 @@
|
|||||||
<ClInclude Include="decklink\DeckLinkSession.h">
|
<ClInclude Include="decklink\DeckLinkSession.h">
|
||||||
<Filter>Header Files</Filter>
|
<Filter>Header Files</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
<ClInclude Include="runtime\RuntimeClock.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Image Include="LoopThroughWithOpenGLCompositing.ico">
|
<Image Include="LoopThroughWithOpenGLCompositing.ico">
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ bool GlobalParamsBuffer::Update(const RuntimeRenderState& state, unsigned availa
|
|||||||
AppendStd140Float(buffer, static_cast<float>(state.timeSeconds));
|
AppendStd140Float(buffer, static_cast<float>(state.timeSeconds));
|
||||||
AppendStd140Vec2(buffer, static_cast<float>(state.inputWidth), static_cast<float>(state.inputHeight));
|
AppendStd140Vec2(buffer, static_cast<float>(state.inputWidth), static_cast<float>(state.inputHeight));
|
||||||
AppendStd140Vec2(buffer, static_cast<float>(state.outputWidth), static_cast<float>(state.outputHeight));
|
AppendStd140Vec2(buffer, static_cast<float>(state.outputWidth), static_cast<float>(state.outputHeight));
|
||||||
|
AppendStd140Float(buffer, static_cast<float>(state.utcTimeSeconds));
|
||||||
|
AppendStd140Float(buffer, static_cast<float>(state.utcOffsetSeconds));
|
||||||
AppendStd140Float(buffer, static_cast<float>(state.frameCount));
|
AppendStd140Float(buffer, static_cast<float>(state.frameCount));
|
||||||
AppendStd140Float(buffer, static_cast<float>(state.mixAmount));
|
AppendStd140Float(buffer, static_cast<float>(state.mixAmount));
|
||||||
AppendStd140Float(buffer, static_cast<float>(state.bypass));
|
AppendStd140Float(buffer, static_cast<float>(state.bypass));
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#include "RuntimeClock.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool ToUtcTime(std::time_t time, std::tm& utcTime)
|
||||||
|
{
|
||||||
|
return gmtime_s(&utcTime, &time) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ToLocalTime(std::time_t time, std::tm& localTime)
|
||||||
|
{
|
||||||
|
return localtime_s(&localTime, &time) == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeClockSnapshot GetRuntimeClockSnapshot()
|
||||||
|
{
|
||||||
|
return MakeRuntimeClockSnapshot(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()));
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeClockSnapshot MakeRuntimeClockSnapshot(std::time_t now)
|
||||||
|
{
|
||||||
|
RuntimeClockSnapshot snapshot;
|
||||||
|
|
||||||
|
std::tm utcTime = {};
|
||||||
|
if (!ToUtcTime(now, utcTime))
|
||||||
|
return snapshot;
|
||||||
|
|
||||||
|
snapshot.utcTimeSeconds =
|
||||||
|
static_cast<double>(utcTime.tm_hour * 3600 + utcTime.tm_min * 60 + utcTime.tm_sec);
|
||||||
|
|
||||||
|
std::tm localTime = {};
|
||||||
|
if (!ToLocalTime(now, localTime))
|
||||||
|
return snapshot;
|
||||||
|
|
||||||
|
utcTime.tm_isdst = localTime.tm_isdst;
|
||||||
|
const std::time_t localAsTime = std::mktime(&localTime);
|
||||||
|
const std::time_t utcAsLocalTime = std::mktime(&utcTime);
|
||||||
|
if (localAsTime != static_cast<std::time_t>(-1) && utcAsLocalTime != static_cast<std::time_t>(-1))
|
||||||
|
snapshot.utcOffsetSeconds = std::difftime(localAsTime, utcAsLocalTime);
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
12
apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeClock.h
Normal file
12
apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeClock.h
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
|
struct RuntimeClockSnapshot
|
||||||
|
{
|
||||||
|
double utcTimeSeconds = 0.0;
|
||||||
|
double utcOffsetSeconds = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
RuntimeClockSnapshot GetRuntimeClockSnapshot();
|
||||||
|
RuntimeClockSnapshot MakeRuntimeClockSnapshot(std::time_t now);
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "stdafx.h"
|
#include "stdafx.h"
|
||||||
#include "RuntimeHost.h"
|
#include "RuntimeHost.h"
|
||||||
|
|
||||||
|
#include "RuntimeClock.h"
|
||||||
#include "RuntimeParameterUtils.h"
|
#include "RuntimeParameterUtils.h"
|
||||||
#include "ShaderCompiler.h"
|
#include "ShaderCompiler.h"
|
||||||
#include "ShaderPackageRegistry.h"
|
#include "ShaderPackageRegistry.h"
|
||||||
@@ -1321,6 +1323,7 @@ bool RuntimeHost::TryGetLayerRenderStates(unsigned outputWidth, unsigned outputH
|
|||||||
|
|
||||||
void RuntimeHost::BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const
|
void RuntimeHost::BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const
|
||||||
{
|
{
|
||||||
|
const RuntimeClockSnapshot clock = GetRuntimeClockSnapshot();
|
||||||
for (const LayerPersistentState& layer : mPersistentState.layers)
|
for (const LayerPersistentState& layer : mPersistentState.layers)
|
||||||
{
|
{
|
||||||
auto shaderIt = mPackagesById.find(layer.shaderId);
|
auto shaderIt = mPackagesById.find(layer.shaderId);
|
||||||
@@ -1331,6 +1334,8 @@ void RuntimeHost::BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned ou
|
|||||||
state.layerId = layer.id;
|
state.layerId = layer.id;
|
||||||
state.shaderId = layer.shaderId;
|
state.shaderId = layer.shaderId;
|
||||||
state.timeSeconds = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
state.timeSeconds = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
||||||
|
state.utcTimeSeconds = clock.utcTimeSeconds;
|
||||||
|
state.utcOffsetSeconds = clock.utcOffsetSeconds;
|
||||||
state.frameCount = static_cast<double>(mFrameCounter);
|
state.frameCount = static_cast<double>(mFrameCounter);
|
||||||
state.mixAmount = 1.0;
|
state.mixAmount = 1.0;
|
||||||
state.bypass = layer.bypass ? 1.0 : 0.0;
|
state.bypass = layer.bypass ? 1.0 : 0.0;
|
||||||
|
|||||||
@@ -102,6 +102,8 @@ struct RuntimeRenderState
|
|||||||
std::vector<ShaderTextureAsset> textureAssets;
|
std::vector<ShaderTextureAsset> textureAssets;
|
||||||
std::vector<ShaderFontAsset> fontAssets;
|
std::vector<ShaderFontAsset> fontAssets;
|
||||||
double timeSeconds = 0.0;
|
double timeSeconds = 0.0;
|
||||||
|
double utcTimeSeconds = 0.0;
|
||||||
|
double utcOffsetSeconds = 0.0;
|
||||||
double frameCount = 0.0;
|
double frameCount = 0.0;
|
||||||
double mixAmount = 1.0;
|
double mixAmount = 1.0;
|
||||||
double bypass = 0.0;
|
double bypass = 0.0;
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ struct ShaderContext
|
|||||||
float2 inputResolution;
|
float2 inputResolution;
|
||||||
float2 outputResolution;
|
float2 outputResolution;
|
||||||
float time;
|
float time;
|
||||||
|
float utcTimeSeconds;
|
||||||
|
float utcOffsetSeconds;
|
||||||
float frameCount;
|
float frameCount;
|
||||||
float mixAmount;
|
float mixAmount;
|
||||||
float bypass;
|
float bypass;
|
||||||
@@ -23,6 +25,8 @@ cbuffer GlobalParams
|
|||||||
float gTime;
|
float gTime;
|
||||||
float2 gInputResolution;
|
float2 gInputResolution;
|
||||||
float2 gOutputResolution;
|
float2 gOutputResolution;
|
||||||
|
float gUtcTimeSeconds;
|
||||||
|
float gUtcOffsetSeconds;
|
||||||
float gFrameCount;
|
float gFrameCount;
|
||||||
float gMixAmount;
|
float gMixAmount;
|
||||||
float gBypass;
|
float gBypass;
|
||||||
@@ -80,6 +84,8 @@ float4 fragmentMain(FragmentInput input) : SV_Target
|
|||||||
context.inputResolution = gInputResolution;
|
context.inputResolution = gInputResolution;
|
||||||
context.outputResolution = gOutputResolution;
|
context.outputResolution = gOutputResolution;
|
||||||
context.time = gTime;
|
context.time = gTime;
|
||||||
|
context.utcTimeSeconds = gUtcTimeSeconds;
|
||||||
|
context.utcOffsetSeconds = gUtcOffsetSeconds;
|
||||||
context.frameCount = gFrameCount;
|
context.frameCount = gFrameCount;
|
||||||
context.mixAmount = gMixAmount;
|
context.mixAmount = gMixAmount;
|
||||||
context.bypass = gBypass;
|
context.bypass = gBypass;
|
||||||
|
|||||||
36
shaders/utc-clock/shader.json
Normal file
36
shaders/utc-clock/shader.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"id": "utc-clock",
|
||||||
|
"name": "UTC Clock",
|
||||||
|
"description": "Shows an analog clock driven by the host PC clock. Uses UTC seconds plus the PC UTC offset exposed to shaders.",
|
||||||
|
"category": "Utility",
|
||||||
|
"entryPoint": "shadeVideo",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "showLocalTime",
|
||||||
|
"label": "Show Local Time",
|
||||||
|
"type": "bool",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "clockScale",
|
||||||
|
"label": "Clock Scale",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.7,
|
||||||
|
"min": 0.25,
|
||||||
|
"max": 0.95,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "faceColor",
|
||||||
|
"label": "Face Color",
|
||||||
|
"type": "color",
|
||||||
|
"default": [0.03, 0.04, 0.05, 0.82]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "accentColor",
|
||||||
|
"label": "Accent Color",
|
||||||
|
"type": "color",
|
||||||
|
"default": [0.1, 0.62, 0.86, 1.0]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
75
shaders/utc-clock/shader.slang
Normal file
75
shaders/utc-clock/shader.slang
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
float secondsOfDay(float seconds)
|
||||||
|
{
|
||||||
|
return seconds - floor(seconds / 86400.0) * 86400.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float lineMask(float2 p, float angle, float innerRadius, float outerRadius, float width)
|
||||||
|
{
|
||||||
|
float2 direction = float2(sin(angle), cos(angle));
|
||||||
|
float along = dot(p, direction);
|
||||||
|
float across = length(p - direction * along);
|
||||||
|
float body = smoothstep(width, width * 0.35, across);
|
||||||
|
float start = smoothstep(innerRadius - width, innerRadius + width, along);
|
||||||
|
float end = 1.0 - smoothstep(outerRadius - width, outerRadius + width, along);
|
||||||
|
return body * start * end;
|
||||||
|
}
|
||||||
|
|
||||||
|
float ringMask(float radius, float target, float width)
|
||||||
|
{
|
||||||
|
return smoothstep(width, 0.0, abs(radius - target));
|
||||||
|
}
|
||||||
|
|
||||||
|
float tickMask(float2 p, int tick)
|
||||||
|
{
|
||||||
|
float angle = (float(tick) / 60.0) * 6.28318530718;
|
||||||
|
float2 direction = float2(sin(angle), cos(angle));
|
||||||
|
float along = dot(p, direction);
|
||||||
|
float across = length(p - direction * along);
|
||||||
|
float major = (tick % 5) == 0 ? 1.0 : 0.0;
|
||||||
|
float inner = lerp(0.82, 0.76, major);
|
||||||
|
float outer = 0.9;
|
||||||
|
float width = lerp(0.006, 0.011, major);
|
||||||
|
float body = smoothstep(width, width * 0.35, across);
|
||||||
|
float start = smoothstep(inner, inner + 0.025, along);
|
||||||
|
float end = 1.0 - smoothstep(outer - 0.025, outer, along);
|
||||||
|
return body * start * end;
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 shadeVideo(ShaderContext context)
|
||||||
|
{
|
||||||
|
float2 aspect = float2(context.outputResolution.x / max(context.outputResolution.y, 1.0), 1.0);
|
||||||
|
float2 p = (context.uv * 2.0 - 1.0) * aspect / max(clockScale, 0.01);
|
||||||
|
float radius = length(p);
|
||||||
|
|
||||||
|
float seconds = showLocalTime
|
||||||
|
? secondsOfDay(context.utcTimeSeconds + context.utcOffsetSeconds)
|
||||||
|
: secondsOfDay(context.utcTimeSeconds);
|
||||||
|
float hour = floor(seconds / 3600.0);
|
||||||
|
float minute = floor((seconds - hour * 3600.0) / 60.0);
|
||||||
|
float second = seconds - hour * 3600.0 - minute * 60.0;
|
||||||
|
|
||||||
|
float secondAngle = second / 60.0 * 6.28318530718;
|
||||||
|
float minuteAngle = (minute + second / 60.0) / 60.0 * 6.28318530718;
|
||||||
|
float hour12 = hour - floor(hour / 12.0) * 12.0;
|
||||||
|
float hourAngle = (hour12 + minute / 60.0) / 12.0 * 6.28318530718;
|
||||||
|
|
||||||
|
float4 color = context.sourceColor;
|
||||||
|
float face = smoothstep(1.0, 0.97, radius);
|
||||||
|
color = lerp(color, faceColor, face * faceColor.a);
|
||||||
|
|
||||||
|
float ring = ringMask(radius, 0.92, 0.01);
|
||||||
|
float ticks = 0.0;
|
||||||
|
for (int i = 0; i < 60; ++i)
|
||||||
|
ticks = max(ticks, tickMask(p, i));
|
||||||
|
|
||||||
|
float hourHand = lineMask(p, hourAngle, -0.05, 0.48, 0.028);
|
||||||
|
float minuteHand = lineMask(p, minuteAngle, -0.07, 0.72, 0.018);
|
||||||
|
float secondHand = lineMask(p, secondAngle, -0.12, 0.8, 0.009);
|
||||||
|
float hub = smoothstep(0.07, 0.0, radius);
|
||||||
|
|
||||||
|
float whiteDetail = max(max(ring, ticks), max(hourHand, minuteHand));
|
||||||
|
color.rgb = lerp(color.rgb, float3(0.92, 0.96, 1.0), whiteDetail);
|
||||||
|
color.rgb = lerp(color.rgb, accentColor.rgb, max(secondHand, hub) * accentColor.a);
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
50
tests/RuntimeClockTests.cpp
Normal file
50
tests/RuntimeClockTests.cpp
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#include "RuntimeClock.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int gFailures = 0;
|
||||||
|
|
||||||
|
void Expect(bool condition, const char* message)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::cerr << "FAIL: " << message << "\n";
|
||||||
|
++gFailures;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestUtcSecondsOfDay()
|
||||||
|
{
|
||||||
|
const RuntimeClockSnapshot midnight = MakeRuntimeClockSnapshot(0);
|
||||||
|
Expect(midnight.utcTimeSeconds == 0.0, "Unix epoch starts at UTC midnight");
|
||||||
|
|
||||||
|
const RuntimeClockSnapshot midday = MakeRuntimeClockSnapshot(12 * 3600 + 34 * 60 + 56);
|
||||||
|
Expect(midday.utcTimeSeconds == 45296.0, "UTC time of day is seconds since midnight");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestOffsetLooksLikeTimezoneOffset()
|
||||||
|
{
|
||||||
|
const RuntimeClockSnapshot snapshot = MakeRuntimeClockSnapshot(12 * 3600);
|
||||||
|
Expect(std::fmod(snapshot.utcOffsetSeconds, 60.0) == 0.0, "UTC offset is minute-aligned");
|
||||||
|
Expect(snapshot.utcOffsetSeconds >= -14.0 * 3600.0 && snapshot.utcOffsetSeconds <= 14.0 * 3600.0,
|
||||||
|
"UTC offset is in the normal timezone range");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
TestUtcSecondsOfDay();
|
||||||
|
TestOffsetLooksLikeTimezoneOffset();
|
||||||
|
|
||||||
|
if (gFailures != 0)
|
||||||
|
{
|
||||||
|
std::cerr << gFailures << " RuntimeClock test failure(s).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "RuntimeClock tests passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -62,6 +62,8 @@ void TestGlobalParamStylePacking()
|
|||||||
AppendStd140Float(buffer, 10.0f); // time
|
AppendStd140Float(buffer, 10.0f); // time
|
||||||
AppendStd140Vec2(buffer, 1920.0f, 1080.0f); // input resolution
|
AppendStd140Vec2(buffer, 1920.0f, 1080.0f); // input resolution
|
||||||
AppendStd140Vec2(buffer, 1280.0f, 720.0f); // output resolution
|
AppendStd140Vec2(buffer, 1280.0f, 720.0f); // output resolution
|
||||||
|
AppendStd140Float(buffer, 45296.0f); // UTC time of day
|
||||||
|
AppendStd140Float(buffer, 36000.0f); // UTC offset
|
||||||
AppendStd140Float(buffer, 42.0f); // frame count
|
AppendStd140Float(buffer, 42.0f); // frame count
|
||||||
AppendStd140Float(buffer, 0.5f); // mix
|
AppendStd140Float(buffer, 0.5f); // mix
|
||||||
AppendStd140Float(buffer, 1.0f); // bypass
|
AppendStd140Float(buffer, 1.0f); // bypass
|
||||||
@@ -78,8 +80,10 @@ void TestGlobalParamStylePacking()
|
|||||||
Expect(ReadFloat(buffer, 0) == 10.0f, "time is at the start of the block");
|
Expect(ReadFloat(buffer, 0) == 10.0f, "time is at the start of the block");
|
||||||
Expect(ReadFloat(buffer, 8) == 1920.0f, "first vec2 aligns after scalar padding");
|
Expect(ReadFloat(buffer, 8) == 1920.0f, "first vec2 aligns after scalar padding");
|
||||||
Expect(ReadFloat(buffer, 16) == 1280.0f, "second vec2 follows first vec2");
|
Expect(ReadFloat(buffer, 16) == 1280.0f, "second vec2 follows first vec2");
|
||||||
Expect(ReadInt(buffer, 36) == 3, "history length scalar remains tightly packed");
|
Expect(ReadFloat(buffer, 24) == 45296.0f, "UTC time follows output resolution");
|
||||||
Expect(ReadFloat(buffer, 48) == 4.0f, "vec2 shader parameter aligns to 8 bytes");
|
Expect(ReadFloat(buffer, 28) == 36000.0f, "UTC offset follows UTC time");
|
||||||
|
Expect(ReadInt(buffer, 44) == 3, "history length scalar remains tightly packed");
|
||||||
|
Expect(ReadFloat(buffer, 56) == 4.0f, "vec2 shader parameter aligns to 8 bytes");
|
||||||
Expect(ReadFloat(buffer, 64) == 0.1f, "color parameter aligns to 16 bytes");
|
Expect(ReadFloat(buffer, 64) == 0.1f, "color parameter aligns to 16 bytes");
|
||||||
Expect(ReadInt(buffer, 80) == 1, "boolean parameter follows vec4");
|
Expect(ReadInt(buffer, 80) == 1, "boolean parameter follows vec4");
|
||||||
Expect(ReadInt(buffer, 84) == 2, "enum parameter follows boolean");
|
Expect(ReadInt(buffer, 84) == 2, "enum parameter follows boolean");
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
--app-radius-sm: 4px;
|
--app-radius-sm: 4px;
|
||||||
--app-space: 1rem;
|
--app-space: 1rem;
|
||||||
--app-container: 980px;
|
--app-container: 980px;
|
||||||
|
--control-height: 42px;
|
||||||
|
--button-min-width: 7.25rem;
|
||||||
--app-font: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
--app-font: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
--app-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
--app-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
background: var(--app-bg);
|
background: var(--app-bg);
|
||||||
@@ -96,12 +98,11 @@ h4 {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
|
||||||
input[type="number"],
|
input[type="number"],
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
select {
|
select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 38px;
|
min-height: var(--control-height);
|
||||||
border: 1px solid var(--app-border);
|
border: 1px solid var(--app-border);
|
||||||
background: var(--app-surface-2);
|
background: var(--app-surface-2);
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@@ -135,6 +136,11 @@ button:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
width: auto;
|
||||||
|
min-width: var(--button-min-width);
|
||||||
|
min-height: var(--control-height);
|
||||||
|
padding: 0.65rem 1rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background: var(--app-primary);
|
background: var(--app-primary);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -201,7 +207,7 @@ pre {
|
|||||||
|
|
||||||
.panel__header button {
|
.panel__header button {
|
||||||
width: auto;
|
width: auto;
|
||||||
min-width: 9rem;
|
min-width: var(--button-min-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-grid,
|
.dashboard-grid,
|
||||||
@@ -459,7 +465,7 @@ pre {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stack-panel__reload {
|
.stack-panel__reload {
|
||||||
min-width: 8.25rem;
|
min-width: 8.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar__group {
|
.toolbar__group {
|
||||||
@@ -469,10 +475,16 @@ pre {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toolbar__inline {
|
.toolbar__inline {
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(7.5rem, 0.28fr);
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: stretch;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbar__inline button {
|
||||||
|
width: auto;
|
||||||
|
min-width: var(--button-min-width);
|
||||||
|
}
|
||||||
|
|
||||||
.layer-stack {
|
.layer-stack {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
@@ -532,17 +544,21 @@ pre {
|
|||||||
|
|
||||||
.layer-card__actions {
|
.layer-card__actions {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-card__actions button,
|
.layer-card__actions button,
|
||||||
.layer-card__subheader button {
|
.layer-card__subheader button {
|
||||||
width: auto;
|
width: auto;
|
||||||
min-width: 5.25rem;
|
min-width: var(--button-min-width);
|
||||||
|
height: var(--control-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-button {
|
.icon-button {
|
||||||
width: 38px;
|
width: var(--control-height);
|
||||||
min-width: 38px;
|
min-width: var(--control-height);
|
||||||
|
height: var(--control-height);
|
||||||
|
min-height: var(--control-height);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -585,6 +601,7 @@ pre {
|
|||||||
width: 34px;
|
width: 34px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
min-width: 34px;
|
min-width: 34px;
|
||||||
|
min-height: 34px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -654,6 +671,7 @@ pre {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.shader-picker__trigger {
|
.shader-picker__trigger {
|
||||||
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -713,6 +731,7 @@ pre {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.shader-picker__option {
|
.shader-picker__option {
|
||||||
|
width: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
min-height: 4.25rem;
|
min-height: 4.25rem;
|
||||||
@@ -1015,8 +1034,7 @@ pre {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-grid,
|
.dashboard-grid,
|
||||||
.stack-panel__grid,
|
.stack-panel__grid {
|
||||||
.toolbar__inline {
|
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1025,10 +1043,17 @@ pre {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel__header button,
|
.panel__header button,
|
||||||
.toolbar__inline button,
|
|
||||||
.stack-panel__reload,
|
.stack-panel__reload,
|
||||||
.layer-card__actions,
|
.layer-card__actions button {
|
||||||
.layer-card__actions button,
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card__actions {
|
||||||
|
width: auto;
|
||||||
|
align-self: flex-end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.layer-card__field select {
|
.layer-card__field select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -1049,8 +1074,12 @@ pre {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbar__inline {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
.parameter__reset {
|
.parameter__reset {
|
||||||
width: 100%;
|
width: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameter__swatch {
|
.parameter__swatch {
|
||||||
|
|||||||
Reference in New Issue
Block a user