reflect(d3d12): fakesign dxil blobs to avoid needing dxil.dll
This commit is contained in:
parent
e8eee02bfb
commit
50aa582fa8
45
Cargo.lock
generated
45
Cargo.lock
generated
|
@ -37,9 +37,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.7"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01"
|
||||
checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
|
@ -1224,7 +1224,7 @@ version = "0.13.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
|
||||
dependencies = [
|
||||
"ahash 0.8.7",
|
||||
"ahash 0.8.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1233,7 +1233,7 @@ version = "0.14.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
||||
dependencies = [
|
||||
"ahash 0.8.7",
|
||||
"ahash 0.8.8",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
|
@ -1654,6 +1654,7 @@ dependencies = [
|
|||
"librashader-presets",
|
||||
"librashader-reflect",
|
||||
"librashader-runtime",
|
||||
"mach-siegbert-vogt-dxcsa",
|
||||
"parking_lot",
|
||||
"rayon",
|
||||
"thiserror",
|
||||
|
@ -1806,6 +1807,15 @@ version = "0.4.20"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||
|
||||
[[package]]
|
||||
name = "mach-siegbert-vogt-dxcsa"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d3e62358869047ad84e507d5bcd47e7f3917629947ba34ac0b3e5969db00a7b"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
|
@ -2271,9 +2281,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.11"
|
||||
version = "0.17.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f6c3c3e617595665b8ea2ff95a86066be38fb121ff920a9c0eb282abcd1da5a"
|
||||
checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crc32fast",
|
||||
|
@ -2284,9 +2294,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.4.0"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30054e72317ab98eddd8561db0f6524df3367636884b7b21b703e4b280a84a14"
|
||||
checksum = "24f040dee2588b4963afb4e420540439d126f73fdacf4a9c486a96d840bac3c9"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
|
@ -2358,9 +2368,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "profiling"
|
||||
version = "1.0.14"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f0f7f43585c34e4fdd7497d746bc32e14458cf11c69341cc0587b1d825dde42"
|
||||
checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58"
|
||||
|
||||
[[package]]
|
||||
name = "qoi"
|
||||
|
@ -2617,9 +2627,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
|
|||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.16"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
|
||||
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
|
@ -2802,11 +2812,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "spirv-to-dxil"
|
||||
version = "0.4.6"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aa1b4b592a3c01a5a443b0d80200f1ae0cf4706928e3d61e03ae570e4085d06"
|
||||
checksum = "5a3fb4188c288f0bcf2d6e18a74647a6346ce974c7751ca075de89e4949d4b0e"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"mach-siegbert-vogt-dxcsa",
|
||||
"spirv-to-dxil-sys",
|
||||
"thiserror",
|
||||
]
|
||||
|
@ -2897,9 +2908,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.0"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
|
||||
checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
|
@ -3672,7 +3683,7 @@ version = "0.29.10"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c824f11941eeae66ec71111cc2674373c772f482b58939bb4066b642aa2ffcf"
|
||||
dependencies = [
|
||||
"ahash 0.8.7",
|
||||
"ahash 0.8.8",
|
||||
"android-activity",
|
||||
"atomic-waker",
|
||||
"bitflags 2.4.2",
|
||||
|
|
|
@ -166,7 +166,7 @@ Please report an issue if you run into a shader that works in RetroArch, but not
|
|||
which was released in late 2018.
|
||||
* For maximum compatibility with shaders, a shader compile pipeline based on [`spirv-to-dxil`](https://github.com/SnowflakePowered/spirv-to-dxil-rs) is used, with the SPIRV-Cross HLSL pipeline used as a fallback.
|
||||
This brings shader compatibility beyond what the RetroArch Direct3D 12 driver provides. The HLSL pipeline fallback may be removed in the future as `spirv-to-dxil` improves.
|
||||
* The Direct3D 12 runtime requires `dxil.dll` and `dxcompiler.dll` from the [DirectX Shader Compiler](https://github.com/microsoft/DirectXShaderCompiler).
|
||||
* The Direct3D 12 runtime requires `dxcompiler.dll` from the [DirectX Shader Compiler](https://github.com/microsoft/DirectXShaderCompiler), which may already be installed as part of Direct3D12. `dxil.dll` is not required.
|
||||
* Metal
|
||||
* The Metal runtime uses the same VBOs as the other runtimes as well as the identity matrix MVP for intermediate passes. RetroArch's Metal driver uses only the final VBO.
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ matches = { version = "0.1.10", features = [] }
|
|||
rustc-hash = "1.1.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies.spirv-to-dxil]
|
||||
version = "0.4"
|
||||
version = "0.4.7"
|
||||
optional = true
|
||||
|
||||
[features]
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
use crate::back::spirv::WriteSpirV;
|
||||
use crate::back::{CompileShader, CompilerBackend, FromCompilation, ShaderCompilerOutput};
|
||||
pub use spirv_to_dxil::DxilObject;
|
||||
pub use spirv_to_dxil::ShaderModel;
|
||||
use spirv_to_dxil::{
|
||||
PushConstantBufferConfig, RuntimeConfig, RuntimeDataBufferConfig, ShaderStage, ValidatorVersion,
|
||||
};
|
||||
|
||||
use crate::back::targets::{OutputTarget, DXIL};
|
||||
use crate::back::{CompileShader, CompilerBackend, FromCompilation, ShaderCompilerOutput};
|
||||
use crate::error::{ShaderCompileError, ShaderReflectError};
|
||||
use crate::front::SpirvCompilation;
|
||||
use crate::reflect::cross::glsl::GlslReflect;
|
||||
use crate::reflect::cross::SpirvCross;
|
||||
use crate::reflect::ReflectShader;
|
||||
pub use spirv_to_dxil::DxilObject;
|
||||
pub use spirv_to_dxil::ShaderModel;
|
||||
use spirv_to_dxil::{
|
||||
PushConstantBufferConfig, RuntimeConfig, RuntimeDataBufferConfig, ShaderStage, ValidatorVersion,
|
||||
};
|
||||
|
||||
impl OutputTarget for DXIL {
|
||||
type Output = DxilObject;
|
||||
|
@ -28,14 +27,12 @@ impl FromCompilation<SpirvCompilation, SpirvCross> for DXIL {
|
|||
compile: SpirvCompilation,
|
||||
) -> Result<CompilerBackend<Self::Output>, ShaderReflectError> {
|
||||
let reflect = GlslReflect::try_from(&compile)?;
|
||||
let vertex = compile.vertex;
|
||||
let fragment = compile.fragment;
|
||||
Ok(CompilerBackend {
|
||||
// we can just reuse WriteSpirV as the backend.
|
||||
backend: WriteSpirV {
|
||||
reflect,
|
||||
vertex,
|
||||
fragment,
|
||||
vertex: compile.vertex,
|
||||
fragment: compile.fragment,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -110,6 +110,7 @@ mod test {
|
|||
use crate::reflect::semantics::{Semantic, ShaderSemantics, UniformSemantic, UniqueSemantics};
|
||||
use crate::reflect::ReflectShader;
|
||||
use bitflags::Flags;
|
||||
use librashader_common::map::FastHashMap;
|
||||
use librashader_preprocess::ShaderSource;
|
||||
use rustc_hash::FxHashMap;
|
||||
use spirv_cross::msl;
|
||||
|
@ -121,7 +122,7 @@ mod test {
|
|||
// let result = ShaderSource::load("../test/shaders_slang/crt/shaders/crt-royale/src/crt-royale-scanlines-horizontal-apply-mask.slang").unwrap();
|
||||
let result = ShaderSource::load("../test/basic.slang").unwrap();
|
||||
|
||||
let mut uniform_semantics: FxHashMap<String, UniformSemantic> = Default::default();
|
||||
let mut uniform_semantics: FastHashMap<String, UniformSemantic> = Default::default();
|
||||
|
||||
for (_index, param) in result.parameters.iter().enumerate() {
|
||||
uniform_semantics.insert(
|
||||
|
|
|
@ -13,7 +13,7 @@ use librashader_runtime_d3d11::options::FilterChainOptionsD3D11;
|
|||
// "../test/Mega_Bezel_Packs/Duimon-Mega-Bezel/Presets/Advanced/Nintendo_GBA_SP/GBA_SP-[ADV]-[LCD-GRID].slangp";
|
||||
|
||||
const FILTER_PATH: &str =
|
||||
"../test/shaders-slang/bezel/Mega_Bezel/Presets/MBZ__0__SMOOTH-ADV.slangp";
|
||||
"../test/shaders_slang/bezel/Mega_Bezel/Presets/MBZ__0__SMOOTH-ADV.slangp";
|
||||
|
||||
// const FILTER_PATH: &str = "../test/slang-shaders/test/history.slangp";
|
||||
// const FILTER_PATH: &str = "../test/slang-shaders/test/feedback.slangp";
|
||||
|
|
|
@ -27,6 +27,7 @@ array-init = "2.1.0"
|
|||
bitvec = "1.0.1"
|
||||
widestring = "1.0.2"
|
||||
array-concat = "0.5.2"
|
||||
mach-siegbert-vogt-dxcsa = "0.1.3"
|
||||
|
||||
rayon = "1.6.1"
|
||||
|
||||
|
|
BIN
librashader-runtime-d3d12/shader/mipmap.dxil
Normal file
BIN
librashader-runtime-d3d12/shader/mipmap.dxil
Normal file
Binary file not shown.
42
librashader-runtime-d3d12/shader/mipmap.hlsl
Normal file
42
librashader-runtime-d3d12/shader/mipmap.hlsl
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
// http://go.microsoft.com/fwlink/?LinkID=615561
|
||||
|
||||
#define GenerateMipsRS
|
||||
"RootFlags ( DENY_VERTEX_SHADER_ROOT_ACCESS |"
|
||||
" DENY_DOMAIN_SHADER_ROOT_ACCESS |"
|
||||
" DENY_GEOMETRY_SHADER_ROOT_ACCESS |"
|
||||
" DENY_HULL_SHADER_ROOT_ACCESS |"
|
||||
" DENY_PIXEL_SHADER_ROOT_ACCESS ),"
|
||||
"DescriptorTable ( SRV(t0, flags=DATA_VOLATILE|DESCRIPTORS_VOLATILE) ),"
|
||||
"DescriptorTable ( UAV(u0, flags=DATA_VOLATILE|DESCRIPTORS_VOLATILE) ),"
|
||||
"RootConstants(num32BitConstants=3, b0),"
|
||||
"StaticSampler(s0,"
|
||||
" filter = FILTER_MIN_MAG_LINEAR_MIP_POINT,"
|
||||
" addressU = TEXTURE_ADDRESS_CLAMP,"
|
||||
" addressV = TEXTURE_ADDRESS_CLAMP,"
|
||||
" addressW = TEXTURE_ADDRESS_CLAMP )"
|
||||
|
||||
SamplerState Sampler : register(s0);
|
||||
Texture2D<float4> SrcMip : register(t0);
|
||||
RWTexture2D<float4> OutMip : register(u0);
|
||||
|
||||
cbuffer MipConstants : register(b0)
|
||||
{
|
||||
float2 InvOutTexelSize; // texel size for OutMip (NOT SrcMip)
|
||||
uint SrcMipIndex;
|
||||
}
|
||||
|
||||
float4 Mip(uint2 coord)
|
||||
{
|
||||
float2 uv = (coord.xy + 0.5) * InvOutTexelSize;
|
||||
return SrcMip.SampleLevel(Sampler, uv, SrcMipIndex);
|
||||
}
|
||||
|
||||
[RootSignature(GenerateMipsRS)]
|
||||
[numthreads(8, 8, 1)]
|
||||
void main(uint3 DTid : SV_DispatchThreadID)
|
||||
{
|
||||
OutMip[DTid.xy] = Mip(DTid.xy);
|
||||
}
|
|
@ -489,8 +489,7 @@ impl FilterChainD3D12 {
|
|||
.into();
|
||||
|
||||
// incredibly cursed.
|
||||
let (reflection, graphics_pipeline) = if !force_hlsl
|
||||
&& let Ok(graphics_pipeline) = D3D12GraphicsPipeline::new_from_dxil(
|
||||
let (reflection, graphics_pipeline) = if let Ok(graphics_pipeline) = D3D12GraphicsPipeline::new_from_dxil(
|
||||
device,
|
||||
library,
|
||||
validator,
|
||||
|
@ -498,7 +497,7 @@ impl FilterChainD3D12 {
|
|||
root_signature,
|
||||
render_format,
|
||||
disable_cache,
|
||||
) {
|
||||
) && !force_hlsl {
|
||||
(dxil_reflection, graphics_pipeline)
|
||||
} else {
|
||||
let hlsl_reflection = hlsl.reflect(index, semantics)?;
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
use crate::descriptor_heap::{D3D12DescriptorHeap, D3D12DescriptorHeapSlot, ResourceWorkHeap};
|
||||
use crate::util::dxc_compile_shader;
|
||||
use crate::util::dxc_validate_shader;
|
||||
use crate::{error, util};
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use librashader_common::Size;
|
||||
use librashader_runtime::scaling::MipmapSize;
|
||||
use std::mem::ManuallyDrop;
|
||||
use std::ops::Deref;
|
||||
use widestring::u16cstr;
|
||||
use windows::Win32::Graphics::Direct3D::Dxc::{
|
||||
CLSID_DxcCompiler, CLSID_DxcLibrary, DxcCreateInstance,
|
||||
};
|
||||
use windows::Win32::Graphics::Direct3D::Dxc::{CLSID_DxcLibrary, CLSID_DxcValidator, DxcCreateInstance};
|
||||
use windows::Win32::Graphics::Direct3D12::{
|
||||
ID3D12DescriptorHeap, ID3D12Device, ID3D12GraphicsCommandList, ID3D12PipelineState,
|
||||
ID3D12Resource, ID3D12RootSignature, D3D12_COMPUTE_PIPELINE_STATE_DESC,
|
||||
|
@ -22,49 +19,7 @@ use windows::Win32::Graphics::Direct3D12::{
|
|||
};
|
||||
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT;
|
||||
|
||||
static GENERATE_MIPS_SRC: &[u8] = b"
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
// http://go.microsoft.com/fwlink/?LinkID=615561
|
||||
|
||||
#define GenerateMipsRS \\
|
||||
\"RootFlags ( DENY_VERTEX_SHADER_ROOT_ACCESS |\" \\
|
||||
\" DENY_DOMAIN_SHADER_ROOT_ACCESS |\" \\
|
||||
\" DENY_GEOMETRY_SHADER_ROOT_ACCESS |\" \\
|
||||
\" DENY_HULL_SHADER_ROOT_ACCESS |\" \\
|
||||
\" DENY_PIXEL_SHADER_ROOT_ACCESS ),\" \\
|
||||
\"DescriptorTable ( SRV(t0, flags=DATA_VOLATILE|DESCRIPTORS_VOLATILE) ),\" \\
|
||||
\"DescriptorTable ( UAV(u0, flags=DATA_VOLATILE|DESCRIPTORS_VOLATILE) ),\" \\
|
||||
\"RootConstants(num32BitConstants=3, b0),\" \\
|
||||
\"StaticSampler(s0,\"\\
|
||||
\" filter = FILTER_MIN_MAG_LINEAR_MIP_POINT,\"\\
|
||||
\" addressU = TEXTURE_ADDRESS_CLAMP,\"\\
|
||||
\" addressV = TEXTURE_ADDRESS_CLAMP,\"\\
|
||||
\" addressW = TEXTURE_ADDRESS_CLAMP )\"
|
||||
|
||||
SamplerState Sampler : register(s0);
|
||||
Texture2D<float4> SrcMip : register(t0);
|
||||
RWTexture2D<float4> OutMip : register(u0);
|
||||
|
||||
cbuffer MipConstants : register(b0)
|
||||
{
|
||||
float2 InvOutTexelSize; // texel size for OutMip (NOT SrcMip)
|
||||
uint SrcMipIndex;
|
||||
}
|
||||
|
||||
float4 Mip(uint2 coord)
|
||||
{
|
||||
float2 uv = (coord.xy + 0.5) * InvOutTexelSize;
|
||||
return SrcMip.SampleLevel(Sampler, uv, SrcMipIndex);
|
||||
}
|
||||
|
||||
[RootSignature(GenerateMipsRS)]
|
||||
[numthreads(8, 8, 1)]
|
||||
void main(uint3 DTid : SV_DispatchThreadID)
|
||||
{
|
||||
OutMip[DTid.xy] = Mip(DTid.xy);
|
||||
}\0";
|
||||
const GENERATE_MIPMAPS_CS: &[u8] = include_bytes!("../shader/mipmap.dxil");
|
||||
|
||||
pub struct D3D12MipmapGen {
|
||||
device: ID3D12Device,
|
||||
|
@ -137,12 +92,14 @@ impl D3D12MipmapGen {
|
|||
pub fn new(device: &ID3D12Device, own_heaps: bool) -> error::Result<D3D12MipmapGen> {
|
||||
unsafe {
|
||||
let library = DxcCreateInstance(&CLSID_DxcLibrary)?;
|
||||
let compiler = DxcCreateInstance(&CLSID_DxcCompiler)?;
|
||||
let validator = DxcCreateInstance(&CLSID_DxcValidator)?;
|
||||
|
||||
let blob =
|
||||
dxc_compile_shader(&library, &compiler, GENERATE_MIPS_SRC, u16cstr!("cs_6_0"))?;
|
||||
dxc_validate_shader(&library, &validator, GENERATE_MIPMAPS_CS)?;
|
||||
|
||||
let blob =
|
||||
std::slice::from_raw_parts(blob.GetBufferPointer().cast(), blob.GetBufferSize());
|
||||
|
||||
let root_signature: ID3D12RootSignature = device.CreateRootSignature(0, blob)?;
|
||||
|
||||
let desc = D3D12_COMPUTE_PIPELINE_STATE_DESC {
|
||||
|
|
|
@ -153,6 +153,12 @@ pub fn dxc_compile_shader(
|
|||
)?;
|
||||
|
||||
let result = result.GetResult()?;
|
||||
|
||||
{
|
||||
let blob = std::slice::from_raw_parts_mut(result.GetBufferPointer() as *mut u8, result.GetBufferSize());
|
||||
mach_siegbert_vogt_dxcsa::sign_in_place(blob);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -290,7 +290,12 @@ pub mod d3d12_hello_triangle {
|
|||
}
|
||||
|
||||
let filter =
|
||||
unsafe { FilterChainD3D12::load_from_path(filter, &device, None).unwrap() };
|
||||
unsafe { FilterChainD3D12::load_from_path(filter, &device, Some(&librashader_runtime_d3d12::options::FilterChainOptionsD3D12 {
|
||||
disable_cache: true,
|
||||
force_hlsl_pipeline: false,
|
||||
force_no_mipmaps: false,
|
||||
..Default::default()
|
||||
})).unwrap() };
|
||||
|
||||
Ok(Sample {
|
||||
dxgi_factory,
|
||||
|
|
|
@ -5,6 +5,8 @@ use crate::hello_triangle::{DXSample, SampleCommandLine};
|
|||
#[test]
|
||||
fn triangle_d3d12() {
|
||||
let sample = hello_triangle::d3d12_hello_triangle::Sample::new(
|
||||
"../test/shaders_slang/bezel/Mega_Bezel/Presets/MBZ__0__SMOOTH-ADV.slangp",
|
||||
|
||||
// "../test/shaders_slang/crt/crt-lottes.slangp",
|
||||
// "../test/basic.slangp",
|
||||
// "../test/shaders_slang/handheld/console-border/gbc-lcd-grid-v2.slangp",
|
||||
|
|
Loading…
Reference in a new issue